From 26d4954724d6ce9db3c94e0a068ae4df398c2ab8 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 25 Apr 2026 09:54:33 +0200 Subject: [PATCH] fix: menu in macos now with hotkeys --- lib/bds/desktop/menu_bar.ex | 17 ++++++++++-- lib/bds/ui/commands.ex | 52 +++++++++++++++++++++++++++++++------ test/bds/desktop_test.exs | 37 ++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lib/bds/desktop/menu_bar.ex b/lib/bds/desktop/menu_bar.ex index f11a696..db79376 100644 --- a/lib/bds/desktop/menu_bar.ex +++ b/lib/bds/desktop/menu_bar.ex @@ -2,6 +2,7 @@ defmodule BDS.Desktop.MenuBar do @moduledoc false use BDS.Desktop.MenuCompat + alias BDS.UI.Commands alias BDS.UI.MenuBar, as: ShellMenuBar alias Desktop.OS alias Desktop.Window @@ -38,7 +39,7 @@ defmodule BDS.Desktop.MenuBar do <%= if item.separator do %>
<% else %> - {item.label} + {item.native_label} <% end %> <% end %> @@ -110,9 +111,21 @@ defmodule BDS.Desktop.MenuBar do defp normalize_item(%{separator: true}), do: %{separator: true} 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 + 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(:edit), do: "Edit" defp group_label(:view), do: "View" diff --git a/lib/bds/ui/commands.ex b/lib/bds/ui/commands.ex index 14f9b33..980cc40 100644 --- a/lib/bds/ui/commands.ex +++ b/lib/bds/ui/commands.ex @@ -3,18 +3,54 @@ defmodule BDS.UI.Commands do 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 key = shortcut |> Map.get(:key, Map.get(shortcut, "key", "")) |> String.downcase() primary = Map.get(shortcut, :meta, false) or Map.get(shortcut, :ctrl, false) - cond do - primary and key == "b" -> MenuBar.execute(state, :toggle_sidebar) - primary and key == "j" -> MenuBar.execute(state, :toggle_panel) - 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 + case Enum.find(@menu_shortcuts, &shortcut_match?(&1, key, primary)) do + %{id: command_id} -> MenuBar.execute(state, command_id) + nil -> state 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 diff --git a/test/bds/desktop_test.exs b/test/bds/desktop_test.exs index 4e84c52..30c215f 100644 --- a/test/bds/desktop_test.exs +++ b/test/bds/desktop_test.exs @@ -68,6 +68,37 @@ defmodule BDS.DesktopTest do assert :api_documentation in help_actions 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 html = BDS.Desktop.ShellController.index_html() @@ -244,4 +275,10 @@ defmodule BDS.DesktopTest do assert payload["result"]["project_id"] == project.id assert payload["result"]["url"] == "http://127.0.0.1:4123/" end + + defp menu_item(groups, id) do + groups + |> Enum.flat_map(& &1.items) + |> Enum.find(&Map.get(&1, :id) == id) + end end