Put every shortcut on one shared keymap and make the app keyboard-reachable
The keymap was scattered over ShortcutsService if-chains, the file panel's key switch, macOS-only menu accelerators and hand-typed hints in 16 tooltips x 7 locales, and large parts of the app had no keyboard path at all. src/shared/keymap.ts is now the one table: scopes, per-platform defaults, exact matching, formatting with localized key names, conflicts, reserved combos and native-menu accelerators, proven by keymap.spec.ts. - ShortcutsService resolves against the effective keymap: claim() takes the key (preventDefault, held keys repeat only movement, never mid-IME), and from a text field only chords and F-keys fire. The file panel, App, editor, manager and Settings windows all dispatch through it. - FileActionsService.run is the one file-command dispatcher for the list keys, the row menu, the macOS menu bar and the toolbar. - New keys: the row menu (Shift+F10 / Menu / Ctrl+Return), the source picker (Mod+K), the path bar (Mod+L), transfers (Mod+J), sorting (Alt+1-7), Shift+movement selection, close other tabs, move and disconnect tabs, refresh and edit on Windows/Linux, permissions, copy path, reveal, trash, open in default app, editor find next/previous. - Right-click menus: soft shortcut hints, icons on the everyday verbs (the toolbar's glyphs), section headers, scrolling, item keys while open, a highlighted first item when opened from the keyboard. The panel source picker now renders through the same menu. - Dialogs: role=dialog, a Tab trap via cycleFocus (WKWebView skips buttons on Tab without Keyboard navigation), initial focus, Esc that stops propagating; the chmod dialog takes focus and applies on Enter. - Fixed: Ctrl+Tab also flipped the active panel; Esc in a manager dialog closed the manager; Ctrl-F/T fired Cmd shortcuts on macOS; shortcuts fired under dialogs; keys died after any button click on macOS; the editor's save prompt and find box never got focus; WebView2 reload keys; a window could save default settings before its first load. - Settings closes on its own close-window key (new settingsClose request). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+54
-5
@@ -136,11 +136,18 @@ touching the adjacent code.
|
||||
update-initiated) — a plain `Utils.quit()` there quits without installing.
|
||||
`Utils.quit()` runs the same approval, which is why the guard checks
|
||||
`quitConfirmed` first.
|
||||
- **⌘W in any secondary window arrives as the MAIN window's `close-tab`
|
||||
menu action** (macOS: the app menu intercepts it before the webview sees
|
||||
the key). bun must check the focused secondary window first — the Settings
|
||||
window, then editors — and close THAT; a new window kind that skips this
|
||||
check makes ⌘W close a main-window tab behind it.
|
||||
- **macOS ⌘-keys go to the PAGE first; only unclaimed ones reach the app
|
||||
menu.** WKWebView's `performKeyEquivalent` hands key equivalents to the page
|
||||
while the webview is first responder (WebKit `WebViewImpl.mm`: "lets
|
||||
webpages have a crack at intercepting key-modified keypresses"); a key the
|
||||
page `preventDefault`s never reaches the menu. Old comments here claimed the
|
||||
opposite ("the menu intercepts ⌘W first") — what was really happening is that
|
||||
windows which didn't claim ⌘W let it fall through to the menu's Close Tab.
|
||||
Two consequences: a handled shortcut MUST `preventDefault` (`claim()` does),
|
||||
or the menu runs it a second time; and a ⌘W a secondary window doesn't claim
|
||||
still arrives as the MAIN window's `close-tab` action — bun checks the
|
||||
focused Settings window, then editors, first and closes THAT, so a new
|
||||
window kind must either claim close-window or join that check.
|
||||
- **One rpc instance per window** — BrowserView binding steals the rpc's
|
||||
transport; a shared instance silently sends to the wrong webview.
|
||||
- **RPC before socket open was silently lost on 1.18.1.** 2.x Electroview queues
|
||||
@@ -270,6 +277,44 @@ touching the adjacent code.
|
||||
`libayatana-appindicator is deprecated. Please use libayatana-appindicator-glib`
|
||||
(upstream electrobun's choice, not ours).
|
||||
|
||||
## Keyboard (`src/shared/keymap.ts`, `ui/src/app/core/shortcuts.service.ts`)
|
||||
|
||||
- **WKWebView skips buttons, checkboxes and radios on Tab** unless the user
|
||||
turned on System Settings → Keyboard → Keyboard navigation (or presses ⌥Tab).
|
||||
Dialogs, Settings and the manager were mouse-only on a default Mac. Tab is
|
||||
walked by hand: `cycleFocus(root, event)` (core/focus-cycle.ts) — use it in
|
||||
any new dialog or window.
|
||||
- **WebKit doesn't focus a clicked button — focus drops to `<body>`** (Windows
|
||||
parks it on the button, where Space clicks it again). Every list key died
|
||||
until the list was clicked. `App` `(mousedown)` keeps focus on chrome-button
|
||||
clicks and forwards `<body>` keys to the active list; keep both.
|
||||
- **Match modifiers exactly.** The old panel switch caught every Tab, so
|
||||
Ctrl+Tab flipped the active panel AND switched tabs; `isMod = metaKey ||
|
||||
ctrlKey` made ⌃F (Cocoa's forward-one-character) open the editor's find.
|
||||
`matchesEvent` compares all four modifiers, platform-exact.
|
||||
- **AltGr is Ctrl+Alt on Windows/Linux.** A Ctrl+Alt+letter binding steals
|
||||
"@", "€", "ł"… — never a default there, never fired from a text field, and
|
||||
an event with `getModifierState('AltGraph')` matches nothing.
|
||||
- **Type-to-filter takes every printable key.** A plain-letter binding (vim
|
||||
j/k) must resolve BEFORE it — and then that letter no longer starts a query
|
||||
(Settings warns). While a query is being typed, printable keys (Space too)
|
||||
always extend it.
|
||||
- **A dialog's Esc must `stopPropagation`.** The connection manager closes on
|
||||
a window-level Escape; the dialog cleared its own signal first, so the
|
||||
window handler saw "no dialog open" and closed the whole window.
|
||||
- **An unclaimed key behind a dialog reaches the macOS menu** — ⌘T opened a
|
||||
tab behind the rename prompt. The main window swallows window commands and
|
||||
the menu bar's panel commands while a dialog or menu is up.
|
||||
- **WebView2 reloads the page on unclaimed F5 / Ctrl+R** (default browser
|
||||
accelerators), dropping every session and the editor's unsaved text. Every
|
||||
window root ends with `blockReloadKeys`; a blanket `stopPropagation` (the old
|
||||
path bar) hides keys from it — don't add one.
|
||||
- **Hold-to-repeat** fired ⌘W once per repeat — every tab closed. `claim()`
|
||||
repeats only movement-like commands (`isRepeatable`).
|
||||
- **Focus after a state change needs `afterNextRender({ injector })`.** Under
|
||||
zoneless CD `queueMicrotask` runs before the render that creates an `@if`
|
||||
element (the editor's find box and textarea), so `.focus()` hit nothing.
|
||||
|
||||
## Domain logic
|
||||
|
||||
- **sessionId ≠ filesystem identity.** Two panels on the same server are two
|
||||
@@ -287,6 +332,10 @@ touching the adjacent code.
|
||||
- Settings: the backend spread-merges (`{...DEFAULT_SETTINGS, ...data}`) so
|
||||
new keys auto-default, but the UI SettingsService reads field-by-field —
|
||||
a new setting silently stays at default until wired in all five spots.
|
||||
- **Saves are whole-object: a window must not save before its first
|
||||
`getSettings` landed.** A ⌘+ pressed while an editor opened wrote
|
||||
`DEFAULT_SETTINGS` (no keyboard remaps) over the user's file.
|
||||
`SettingsService.persist()` is a no-op until `applyAll` ran once.
|
||||
- **Every window's RPC must serve `saveSettings`.** The scale shortcuts
|
||||
shipped in editor and manager windows while only the main RPC served it:
|
||||
the request went unanswered, the change applied locally, and main's next
|
||||
|
||||
Reference in New Issue
Block a user