fix: menu in macos now with hotkeys

This commit is contained in:
2026-04-25 09:54:33 +02:00
parent 390c67df9b
commit 26d4954724
3 changed files with 96 additions and 10 deletions

View File

@@ -2,6 +2,7 @@ defmodule BDS.Desktop.MenuBar do
@moduledoc false @moduledoc false
use BDS.Desktop.MenuCompat use BDS.Desktop.MenuCompat
alias BDS.UI.Commands
alias BDS.UI.MenuBar, as: ShellMenuBar alias BDS.UI.MenuBar, as: ShellMenuBar
alias Desktop.OS alias Desktop.OS
alias Desktop.Window alias Desktop.Window
@@ -38,7 +39,7 @@ defmodule BDS.Desktop.MenuBar do
<%= if item.separator do %> <%= if item.separator do %>
<hr /> <hr />
<% else %> <% else %>
<item onclick={Atom.to_string(item.id)}>{item.label}</item> <item onclick={Atom.to_string(item.id)}>{item.native_label}</item>
<% end %> <% end %>
<% end %> <% end %>
</menu> </menu>
@@ -110,9 +111,21 @@ defmodule BDS.Desktop.MenuBar do
defp normalize_item(%{separator: true}), do: %{separator: true} defp normalize_item(%{separator: true}), do: %{separator: true}
defp normalize_item(item) do defp normalize_item(item) do
%{id: item.id, label: item_label(item.id), separator: false} label = item_label(item.id)
shortcut = Commands.accelerator_label(item.id)
%{
id: item.id,
label: label,
native_label: native_label(label, shortcut),
separator: false,
shortcut: shortcut
}
end end
defp native_label(label, nil), do: label
defp native_label(label, shortcut), do: label <> "\t" <> shortcut
defp group_label(:file), do: "File" defp group_label(:file), do: "File"
defp group_label(:edit), do: "Edit" defp group_label(:edit), do: "Edit"
defp group_label(:view), do: "View" defp group_label(:view), do: "View"

View File

@@ -3,18 +3,54 @@ defmodule BDS.UI.Commands do
alias BDS.UI.MenuBar alias BDS.UI.MenuBar
@menu_shortcuts [
%{id: :new_post, accelerator: "CTRL+N"},
%{id: :import_media, accelerator: "CTRL+I"},
%{id: :save, accelerator: "CTRL+S"},
%{id: :close_tab, accelerator: "CTRL+W", key: "w", primary: true},
%{id: :quit, accelerator: "CTRL+Q"},
%{id: :undo, accelerator: "CTRL+Z"},
%{id: :redo, accelerator: "CTRL+Y"},
%{id: :cut, accelerator: "CTRL+X"},
%{id: :copy, accelerator: "CTRL+C"},
%{id: :paste, accelerator: "CTRL+V"},
%{id: :select_all, accelerator: "CTRL+A"},
%{id: :find, accelerator: "CTRL+F"},
%{id: :replace, accelerator: "CTRL+H"},
%{id: :edit_preferences, accelerator: "CTRL+,"},
%{id: :view_posts, accelerator: "CTRL+1", key: "1", primary: true},
%{id: :view_media, accelerator: "CTRL+2", key: "2", primary: true},
%{id: :toggle_sidebar, accelerator: "CTRL+B", key: "b", primary: true},
%{id: :toggle_panel, accelerator: "CTRL+J", key: "j", primary: true},
%{id: :toggle_assistant_sidebar, accelerator: "CTRL+\\", key: "\\", primary: true},
%{id: :toggle_dev_tools, accelerator: "CTRL+SHIFT+I"},
%{id: :publish_selected, accelerator: "CTRL+SHIFT+P"},
%{id: :preview_post, accelerator: "CTRL+SHIFT+V"},
%{id: :generate_sitemap, accelerator: "CTRL+R"},
%{id: :validate_site, accelerator: "CTRL+SHIFT+L"},
%{id: :upload_site, accelerator: "CTRL+SHIFT+U"}
]
def handle_shortcut(state, shortcut) when is_map(shortcut) do def handle_shortcut(state, shortcut) when is_map(shortcut) do
key = shortcut |> Map.get(:key, Map.get(shortcut, "key", "")) |> String.downcase() key = shortcut |> Map.get(:key, Map.get(shortcut, "key", "")) |> String.downcase()
primary = Map.get(shortcut, :meta, false) or Map.get(shortcut, :ctrl, false) primary = Map.get(shortcut, :meta, false) or Map.get(shortcut, :ctrl, false)
cond do case Enum.find(@menu_shortcuts, &shortcut_match?(&1, key, primary)) do
primary and key == "b" -> MenuBar.execute(state, :toggle_sidebar) %{id: command_id} -> MenuBar.execute(state, command_id)
primary and key == "j" -> MenuBar.execute(state, :toggle_panel) nil -> state
primary and key == "1" -> MenuBar.execute(state, :view_posts)
primary and key == "2" -> MenuBar.execute(state, :view_media)
primary and key == "\\" -> MenuBar.execute(state, :toggle_assistant_sidebar)
primary and key == "w" -> MenuBar.execute(state, :close_tab)
true -> state
end end
end end
def accelerator_label(command_id) when is_atom(command_id) do
case Enum.find(@menu_shortcuts, &(&1.id == command_id)) do
%{accelerator: accelerator} -> accelerator
nil -> nil
end
end
defp shortcut_match?(%{key: expected_key, primary: expected_primary}, key, primary) do
key == expected_key and primary == expected_primary
end
defp shortcut_match?(_shortcut, _key, _primary), do: false
end end

View File

@@ -68,6 +68,37 @@ defmodule BDS.DesktopTest do
assert :api_documentation in help_actions assert :api_documentation in help_actions
end end
test "desktop menu bar exposes native accelerator labels for system menu items" do
groups = BDS.Desktop.MenuBar.groups(dev_mode?: false)
assert menu_item(groups, :new_post).native_label == "New Post\tCTRL+N"
assert menu_item(groups, :import_media).native_label == "Import Media\tCTRL+I"
assert menu_item(groups, :save).native_label == "Save\tCTRL+S"
assert menu_item(groups, :close_tab).shortcut == "CTRL+W"
assert menu_item(groups, :close_tab).native_label == "Close Tab\tCTRL+W"
assert menu_item(groups, :quit).native_label == "Quit\tCTRL+Q"
assert menu_item(groups, :undo).native_label == "Undo\tCTRL+Z"
assert menu_item(groups, :redo).native_label == "Redo\tCTRL+Y"
assert menu_item(groups, :cut).native_label == "Cut\tCTRL+X"
assert menu_item(groups, :copy).native_label == "Copy\tCTRL+C"
assert menu_item(groups, :paste).native_label == "Paste\tCTRL+V"
assert menu_item(groups, :select_all).native_label == "Select All\tCTRL+A"
assert menu_item(groups, :find).native_label == "Find\tCTRL+F"
assert menu_item(groups, :replace).native_label == "Replace\tCTRL+H"
assert menu_item(groups, :edit_preferences).native_label == "Preferences\tCTRL+,"
assert menu_item(groups, :view_posts).native_label == "Posts\tCTRL+1"
assert menu_item(groups, :view_media).native_label == "Media\tCTRL+2"
assert menu_item(groups, :toggle_sidebar).native_label == "Toggle Sidebar\tCTRL+B"
assert menu_item(groups, :toggle_panel).native_label == "Toggle Panel\tCTRL+J"
assert menu_item(groups, :toggle_assistant_sidebar).native_label == "Toggle Assistant Sidebar\tCTRL+\\"
assert menu_item(groups, :publish_selected).native_label == "Publish Selected\tCTRL+SHIFT+P"
assert menu_item(groups, :preview_post).native_label == "Preview Post\tCTRL+SHIFT+V"
assert menu_item(groups, :generate_sitemap).native_label == "Generate Sitemap\tCTRL+R"
assert menu_item(groups, :validate_site).native_label == "Validate Site\tCTRL+SHIFT+L"
assert menu_item(groups, :upload_site).native_label == "Upload Site\tCTRL+SHIFT+U"
assert menu_item(groups, :metadata_diff).shortcut == nil
end
test "desktop shell html follows the old app frame regions and references bundled assets" do test "desktop shell html follows the old app frame regions and references bundled assets" do
html = BDS.Desktop.ShellController.index_html() html = BDS.Desktop.ShellController.index_html()
@@ -244,4 +275,10 @@ defmodule BDS.DesktopTest do
assert payload["result"]["project_id"] == project.id assert payload["result"]["project_id"] == project.id
assert payload["result"]["url"] == "http://127.0.0.1:4123/" assert payload["result"]["url"] == "http://127.0.0.1:4123/"
end end
defp menu_item(groups, id) do
groups
|> Enum.flat_map(& &1.items)
|> Enum.find(&Map.get(&1, :id) == id)
end
end end