From fa9d814aea9cf7c21f3c77590befdc6cce4c9679 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 4 May 2026 23:13:53 -0700 Subject: [PATCH 1/2] feat(desktop): add toggle to disable adaptive keyboard-layout mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an opt-in "Adaptive layout mapping" preference on the keyboard settings page. When off (default), shortcuts dispatch and display exactly as bound regardless of the OS input source — i.e. ⌘Z stays ⌘Z on QWERTZ instead of being remapped to physical KeyY. The preference is persisted in localStorage and read by the resolver index, the display hooks (useHotkeyDisplay, useFormatBinding), and the imperative getDispatchChord, all of which pass null for the layout map when adaptive is off. --- .../hotkeys/hooks/useBinding/useBinding.ts | 8 ++--- .../useHotkeyDisplay/useHotkeyDisplay.ts | 13 +++++++-- .../src/renderer/hotkeys/stores/index.ts | 1 + .../stores/keyboardPreferencesStore.ts | 28 ++++++++++++++++++ .../hotkeys/utils/resolveHotkeyFromEvent.ts | 29 +++++++++++++++---- .../_authenticated/settings/keyboard/page.tsx | 29 +++++++++++++++++++ 6 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts diff --git a/apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts b/apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts index f3ba672b9ce..bac3bbb1da0 100644 --- a/apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts +++ b/apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts @@ -1,6 +1,7 @@ import { HOTKEYS, type HotkeyId } from "../../registry"; import { useHotkeyOverridesStore } from "../../stores/hotkeyOverridesStore"; import { useKeyboardLayoutStore } from "../../stores/keyboardLayoutStore"; +import { useKeyboardPreferencesStore } from "../../stores/keyboardPreferencesStore"; import type { ShortcutBinding } from "../../types"; import { bindingToDispatchChord } from "../../utils/binding"; @@ -33,8 +34,7 @@ export function getBinding(id: HotkeyId): ShortcutBinding | null { * the bound handler on non-US layouts. */ export function getDispatchChord(id: HotkeyId): string | null { - return bindingToDispatchChord( - getBinding(id), - useKeyboardLayoutStore.getState().map, - ); + const adaptive = useKeyboardPreferencesStore.getState().adaptiveLayoutEnabled; + const layoutMap = adaptive ? useKeyboardLayoutStore.getState().map : null; + return bindingToDispatchChord(getBinding(id), layoutMap); } diff --git a/apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts b/apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts index 9df55b5c205..22add732053 100644 --- a/apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts +++ b/apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts @@ -2,13 +2,22 @@ import { useMemo } from "react"; import { formatHotkeyDisplay } from "../../display"; import { PLATFORM } from "../../registry"; import { useKeyboardLayoutStore } from "../../stores/keyboardLayoutStore"; +import { useKeyboardPreferencesStore } from "../../stores/keyboardPreferencesStore"; import type { HotkeyDisplay, ShortcutBinding } from "../../types"; import { bindingToDispatchChord } from "../../utils/binding"; import { useBinding } from "../useBinding"; +/** Effective layout map for display: null when the user has disabled + * adaptive layout mapping, so glyphs match the authored chord. */ +function useActiveLayoutMap(): ReadonlyMap | null { + const layoutMap = useKeyboardLayoutStore((s) => s.map); + const adaptive = useKeyboardPreferencesStore((s) => s.adaptiveLayoutEnabled); + return adaptive ? layoutMap : null; +} + export function useHotkeyDisplay(id: string): HotkeyDisplay { const binding = useBinding(id as Parameters[0]); - const layoutMap = useKeyboardLayoutStore((s) => s.map); + const layoutMap = useActiveLayoutMap(); const chord = bindingToDispatchChord(binding, layoutMap); return useMemo( () => formatHotkeyDisplay(chord, PLATFORM, layoutMap), @@ -25,7 +34,7 @@ export function useHotkeyDisplay(id: string): HotkeyDisplay { export function useFormatBinding( binding: ShortcutBinding | null, ): HotkeyDisplay { - const layoutMap = useKeyboardLayoutStore((s) => s.map); + const layoutMap = useActiveLayoutMap(); const chord = bindingToDispatchChord(binding, layoutMap); return useMemo( () => formatHotkeyDisplay(chord, PLATFORM, layoutMap), diff --git a/apps/desktop/src/renderer/hotkeys/stores/index.ts b/apps/desktop/src/renderer/hotkeys/stores/index.ts index 8c5b63a8a45..478d066be0e 100644 --- a/apps/desktop/src/renderer/hotkeys/stores/index.ts +++ b/apps/desktop/src/renderer/hotkeys/stores/index.ts @@ -1 +1,2 @@ export { useHotkeyOverridesStore } from "./hotkeyOverridesStore"; +export { useKeyboardPreferencesStore } from "./keyboardPreferencesStore"; diff --git a/apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts b/apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts new file mode 100644 index 00000000000..abe0aecf6d8 --- /dev/null +++ b/apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts @@ -0,0 +1,28 @@ +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; + +interface KeyboardPreferencesState { + /** Opt-in: when true, logical bindings are translated through the OS + * keyboard layout — e.g. `⌘Z` dispatches to physical KeyY on QWERTZ. + * Defaults to false so bindings dispatch and display as if on US-ANSI + * regardless of the current input source. */ + adaptiveLayoutEnabled: boolean; + setAdaptiveLayoutEnabled: (enabled: boolean) => void; +} + +export const useKeyboardPreferencesStore = create()( + persist( + (set) => ({ + adaptiveLayoutEnabled: false, + setAdaptiveLayoutEnabled: (enabled) => + set({ adaptiveLayoutEnabled: enabled }), + }), + { + name: "keyboard-preferences", + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ + adaptiveLayoutEnabled: state.adaptiveLayoutEnabled, + }), + }, + ), +); diff --git a/apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts b/apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts index 42a4585426c..e5a6c44912d 100644 --- a/apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts +++ b/apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts @@ -1,9 +1,19 @@ import { HOTKEYS, type HotkeyId } from "../registry"; import { useHotkeyOverridesStore } from "../stores/hotkeyOverridesStore"; import { useKeyboardLayoutStore } from "../stores/keyboardLayoutStore"; +import { useKeyboardPreferencesStore } from "../stores/keyboardPreferencesStore"; import type { ShortcutBinding } from "../types"; import { bindingToDispatchChord } from "./binding"; +/** Layout map for the resolver — null when the user has disabled adaptive + * layout mapping, so logical bindings keep their authored chord. */ +function activeLayoutMap(): ReadonlyMap | null { + if (!useKeyboardPreferencesStore.getState().adaptiveLayoutEnabled) { + return null; + } + return useKeyboardLayoutStore.getState().map; +} + /** * KeyboardEvent → registered {@link HotkeyId}, or `null` if unbound. Uses the * same `event.code` normalization as react-hotkeys-hook so the reverse index @@ -134,21 +144,28 @@ function buildRegisteredAppChords( return map; } -// Reassigned on each override OR layout change; `let` is required so the -// subscribe callbacks can replace the reference the resolver reads. +// Reassigned on each override, layout, OR adaptive-layout-toggle change; +// `let` is required so the subscribe callbacks can replace the reference +// the resolver reads. let registeredAppChords = buildRegisteredAppChords( useHotkeyOverridesStore.getState().overrides, - useKeyboardLayoutStore.getState().map, + activeLayoutMap(), ); useHotkeyOverridesStore.subscribe((state) => { registeredAppChords = buildRegisteredAppChords( state.overrides, - useKeyboardLayoutStore.getState().map, + activeLayoutMap(), + ); +}); +useKeyboardLayoutStore.subscribe(() => { + registeredAppChords = buildRegisteredAppChords( + useHotkeyOverridesStore.getState().overrides, + activeLayoutMap(), ); }); -useKeyboardLayoutStore.subscribe((state) => { +useKeyboardPreferencesStore.subscribe(() => { registeredAppChords = buildRegisteredAppChords( useHotkeyOverridesStore.getState().overrides, - state.map, + activeLayoutMap(), ); }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx index debcaa1c331..a8121be44ca 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx @@ -9,7 +9,9 @@ import { import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; import { Kbd, KbdGroup } from "@superset/ui/kbd"; +import { Label } from "@superset/ui/label"; import { toast } from "@superset/ui/sonner"; +import { Switch } from "@superset/ui/switch"; import { cn } from "@superset/ui/utils"; import { createFileRoute } from "@tanstack/react-router"; import { useMemo, useState } from "react"; @@ -24,6 +26,7 @@ import { useHotkeyOverridesStore, useRecordHotkeys, } from "renderer/hotkeys"; +import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores/keyboardPreferencesStore"; const CATEGORY_ORDER: HotkeyCategory[] = [ "Navigation", @@ -137,6 +140,13 @@ function KeyboardShortcutsPage() { const resetAll = useHotkeyOverridesStore((s) => s.resetAll); const setOverride = useHotkeyOverridesStore((s) => s.setOverride); + const adaptiveLayoutEnabled = useKeyboardPreferencesStore( + (s) => s.adaptiveLayoutEnabled, + ); + const setAdaptiveLayoutEnabled = useKeyboardPreferencesStore( + (s) => s.setAdaptiveLayoutEnabled, + ); + useRecordHotkeys(recordingId, { // New printable bindings follow the printed character (matches what the // user sees on their keyboard). F-keys / named keys are forced to @@ -215,6 +225,25 @@ function KeyboardShortcutsPage() { + {/* Preferences */} +
+
+ +

+ Remap shortcuts to your current keyboard layout (e.g. ⌘Z dispatches + to physical KeyY on QWERTZ). When off, shortcuts always match the + keys you bound. +

+
+ +
+ {/* Search */}
From eb30829292e10b4b6d1ebfa0ca9513ba65925175 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 4 May 2026 23:19:02 -0700 Subject: [PATCH 2/2] refactor(hotkeys): export useKeyboardPreferencesStore from top-level barrel Match how useHotkeyOverridesStore is exposed so the keyboard settings page can import both stores from the same module. --- apps/desktop/src/renderer/hotkeys/index.ts | 5 ++++- .../routes/_authenticated/settings/keyboard/page.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/hotkeys/index.ts b/apps/desktop/src/renderer/hotkeys/index.ts index 0cdc8a0ef37..647dc89d3d5 100644 --- a/apps/desktop/src/renderer/hotkeys/index.ts +++ b/apps/desktop/src/renderer/hotkeys/index.ts @@ -10,7 +10,10 @@ export { useRecordHotkeys, } from "./hooks"; export { HOTKEYS, type HotkeyId, PLATFORM } from "./registry"; -export { useHotkeyOverridesStore } from "./stores"; +export { + useHotkeyOverridesStore, + useKeyboardPreferencesStore, +} from "./stores"; export type { BindingMode, HotkeyCategory, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx index a8121be44ca..992d9a59b55 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx @@ -24,9 +24,9 @@ import { useFormatBinding, useHotkeyDisplay, useHotkeyOverridesStore, + useKeyboardPreferencesStore, useRecordHotkeys, } from "renderer/hotkeys"; -import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores/keyboardPreferencesStore"; const CATEGORY_ORDER: HotkeyCategory[] = [ "Navigation",