fix: shortcuts back working

This commit is contained in:
2026-04-26 08:38:26 +02:00
parent 60bacd84f0
commit a1456592bd
6 changed files with 100 additions and 6 deletions

View File

@@ -44,6 +44,7 @@ defmodule BDS.Desktop.ShellLive do
socket socket
|> assign(:page_title, ShellData.title()) |> assign(:page_title, ShellData.title())
|> assign(:page_language, ShellData.ui_language()) |> assign(:page_language, ShellData.ui_language())
|> assign(:client_shortcuts, Commands.client_shortcuts())
|> assign(:offline_mode, true) |> assign(:offline_mode, true)
|> assign(:tab_meta, %{}) |> assign(:tab_meta, %{})
|> assign(:project_menu_open, false) |> assign(:project_menu_open, false)
@@ -940,6 +941,8 @@ defmodule BDS.Desktop.ShellLive do
defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale))
defp encoded_shortcuts(shortcuts), do: Jason.encode!(shortcuts)
defp panel_tab_label(:tasks), do: translated("Tasks") defp panel_tab_label(:tasks), do: translated("Tasks")
defp panel_tab_label(:output), do: translated("Output") defp panel_tab_label(:output), do: translated("Output")
defp panel_tab_label(:git_log), do: translated("Git Log") defp panel_tab_label(:git_log), do: translated("Git Log")

View File

@@ -1,4 +1,4 @@
<div class="app" id="bds-shell-app" phx-hook="AppShell" phx-window-keydown="shortcut"> <div class="app" id="bds-shell-app" phx-hook="AppShell" data-shortcuts={encoded_shortcuts(@client_shortcuts)}>
<div class="window-titlebar" data-region="title-bar"> <div class="window-titlebar" data-region="title-bar">
<div class="window-titlebar-menu-bar is-hidden"> <div class="window-titlebar-menu-bar is-hidden">
<button class="window-titlebar-menu-button" type="button">File</button> <button class="window-titlebar-menu-button" type="button">File</button>

View File

@@ -17,7 +17,7 @@ defmodule BDS.UI.Commands do
%{id: :select_all, accelerator: "CTRL+A"}, %{id: :select_all, accelerator: "CTRL+A"},
%{id: :find, accelerator: "CTRL+F"}, %{id: :find, accelerator: "CTRL+F"},
%{id: :replace, accelerator: "CTRL+H"}, %{id: :replace, accelerator: "CTRL+H"},
%{id: :edit_preferences, accelerator: "CTRL+,"}, %{id: :edit_preferences, accelerator: "CTRL+,", key: ",", primary: true},
%{id: :view_posts, accelerator: "CTRL+1", key: "1", primary: true}, %{id: :view_posts, accelerator: "CTRL+1", key: "1", primary: true},
%{id: :view_media, accelerator: "CTRL+2", key: "2", primary: true}, %{id: :view_media, accelerator: "CTRL+2", key: "2", primary: true},
%{id: :toggle_sidebar, accelerator: "CTRL+B", key: "b", primary: true}, %{id: :toggle_sidebar, accelerator: "CTRL+B", key: "b", primary: true},
@@ -37,12 +37,28 @@ defmodule BDS.UI.Commands do
Map.get(shortcut, :meta, Map.get(shortcut, "meta", false)) or Map.get(shortcut, :meta, Map.get(shortcut, "meta", false)) or
Map.get(shortcut, :ctrl, Map.get(shortcut, "ctrl", false)) Map.get(shortcut, :ctrl, Map.get(shortcut, "ctrl", false))
case Enum.find(@menu_shortcuts, &shortcut_match?(&1, key, primary)) do shift = Map.get(shortcut, :shift, Map.get(shortcut, "shift", false))
alt = Map.get(shortcut, :alt, Map.get(shortcut, "alt", false))
case Enum.find(@menu_shortcuts, &shortcut_match?(&1, key, primary, shift, alt)) do
%{id: command_id} -> MenuBar.execute(state, command_id) %{id: command_id} -> MenuBar.execute(state, command_id)
nil -> state nil -> state
end end
end end
def client_shortcuts do
@menu_shortcuts
|> Enum.filter(&Map.has_key?(&1, :key))
|> Enum.map(fn shortcut ->
%{
key: shortcut.key,
primary: Map.get(shortcut, :primary, false),
shift: Map.get(shortcut, :shift, false),
alt: Map.get(shortcut, :alt, false)
}
end)
end
def accelerator_label(command_id) when is_atom(command_id) do def accelerator_label(command_id) when is_atom(command_id) do
case Enum.find(@menu_shortcuts, &(&1.id == command_id)) do case Enum.find(@menu_shortcuts, &(&1.id == command_id)) do
%{accelerator: accelerator} -> accelerator %{accelerator: accelerator} -> accelerator
@@ -50,9 +66,12 @@ defmodule BDS.UI.Commands do
end end
end end
defp shortcut_match?(%{key: expected_key, primary: expected_primary}, key, primary) do defp shortcut_match?(%{key: expected_key} = shortcut, key, primary, shift, alt) do
key == expected_key and primary == expected_primary key == expected_key and
primary == Map.get(shortcut, :primary, false) and
shift == Map.get(shortcut, :shift, false) and
alt == Map.get(shortcut, :alt, false)
end end
defp shortcut_match?(_shortcut, _key, _primary), do: false defp shortcut_match?(_shortcut, _key, _primary, _shift, _alt), do: false
end end

View File

@@ -7,6 +7,37 @@ document.addEventListener("DOMContentLoaded", () => {
const ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar"; const ASSISTANT_STORAGE_KEY = "bds-panel-assistant-sidebar";
const UI_LANGUAGE_STORAGE_KEY = "bds-ui-language"; const UI_LANGUAGE_STORAGE_KEY = "bds-ui-language";
const parseShortcutConfig = (value) => {
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch (_error) {
return [];
}
};
const normalizeShortcutKey = (key) => String(key || "").toLowerCase();
const shortcutTargetIsEditable = (event) => {
const tag = event.target?.tagName || null;
return event.target?.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(tag);
};
const shortcutMatchesEvent = (shortcut, event) => {
const primary = event.metaKey || event.ctrlKey;
return (
normalizeShortcutKey(event.key) === normalizeShortcutKey(shortcut.key) &&
primary === Boolean(shortcut.primary) &&
event.shiftKey === Boolean(shortcut.shift) &&
event.altKey === Boolean(shortcut.alt)
);
};
const clamp = (value, min, max) => Math.max(min, Math.min(value, max)); const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
const readStoredSize = (key, fallback, min, max) => { const readStoredSize = (key, fallback, min, max) => {
@@ -93,6 +124,7 @@ document.addEventListener("DOMContentLoaded", () => {
const Hooks = { const Hooks = {
AppShell: { AppShell: {
mounted() { mounted() {
this.shortcuts = parseShortcutConfig(this.el.dataset.shortcuts);
this.syncStoredLayout(); this.syncStoredLayout();
this.syncStoredUiLanguage(); this.syncStoredUiLanguage();
this.destroyOverlaySync = syncTitlebarOverlayInsets(); this.destroyOverlaySync = syncTitlebarOverlayInsets();
@@ -159,7 +191,33 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}; };
this.handleShortcutKeyDown = (event) => {
if (shortcutTargetIsEditable(event)) {
return;
}
const shortcut = this.shortcuts.find((candidate) => shortcutMatchesEvent(candidate, event));
if (!shortcut) {
return;
}
event.preventDefault();
event.stopPropagation();
this.pushEvent("shortcut", {
key: normalizeShortcutKey(event.key),
meta: event.metaKey,
ctrl: event.ctrlKey,
alt: event.altKey,
shift: event.shiftKey,
tag: event.target?.tagName || null,
contentEditable: event.target?.isContentEditable || false
});
};
window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction); window.addEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.addEventListener("keydown", this.handleShortcutKeyDown, true);
this.el.addEventListener("change", this.handleChange); this.el.addEventListener("change", this.handleChange);
}, },
@@ -167,6 +225,7 @@ document.addEventListener("DOMContentLoaded", () => {
this.el.removeEventListener("mousedown", this.handleMouseDown); this.el.removeEventListener("mousedown", this.handleMouseDown);
this.el.removeEventListener("change", this.handleChange); this.el.removeEventListener("change", this.handleChange);
window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction); window.removeEventListener("bds:native-menu-action", this.handleNativeMenuAction);
window.removeEventListener("keydown", this.handleShortcutKeyDown, true);
if (this.destroyOverlaySync) { if (this.destroyOverlaySync) {
this.destroyOverlaySync(); this.destroyOverlaySync();
} }

View File

@@ -60,6 +60,11 @@ defmodule BDS.Desktop.AutomationTest do
snapshot = Automation.snapshot(session) snapshot = Automation.snapshot(session)
assert snapshot.panel_visible == false assert snapshot.panel_visible == false
assert :ok = Automation.press(session, "Meta+,")
snapshot = Automation.snapshot(session)
assert snapshot.editor_title == "Settings"
assert :ok = Automation.click(session, "[data-testid='toggle-assistant']") assert :ok = Automation.click(session, "[data-testid='toggle-assistant']")
snapshot = Automation.snapshot(session) snapshot = Automation.snapshot(session)

View File

@@ -79,6 +79,9 @@ defmodule BDS.UI.ShellTest do
state = Commands.handle_shortcut(state, %{meta: true, key: "w"}) state = Commands.handle_shortcut(state, %{meta: true, key: "w"})
assert state.tabs == [] assert state.tabs == []
assert state.editor_route == :dashboard assert state.editor_route == :dashboard
state = Commands.handle_shortcut(state, %{meta: true, key: ","})
assert state.editor_route == :settings
end end
test "resizing is clamped to the shell limits and dirty flags only apply to post tabs" do test "resizing is clamped to the shell limits and dirty flags only apply to post tabs" do
@@ -142,6 +145,11 @@ defmodule BDS.UI.ShellTest do
assert live_js =~ "windowControlsOverlay" assert live_js =~ "windowControlsOverlay"
assert live_js =~ "geometrychange" assert live_js =~ "geometrychange"
assert live_js =~ "--bds-titlebar-overlay-left" assert live_js =~ "--bds-titlebar-overlay-left"
assert live_js =~ "dataset.shortcuts"
assert live_js =~ "addEventListener(\"keydown\", this.handleShortcutKeyDown, true)"
assert live_js =~ "event.preventDefault()"
assert live_js =~ "this.pushEvent(\"shortcut\""
assert template =~ "data-shortcuts={encoded_shortcuts(@client_shortcuts)}"
assert template =~ "tab-actions" assert template =~ "tab-actions"
assert template =~ "tab-dirty-indicator" assert template =~ "tab-dirty-indicator"
end end