Skip to content

refactor(shortcuts)!: rewrite keyboard handling on a single menu-driven authority with hardware key codes#1556

Merged
datlechin merged 2 commits into
mainfrom
refactor/keyboard-shortcuts
Jun 2, 2026
Merged

refactor(shortcuts)!: rewrite keyboard handling on a single menu-driven authority with hardware key codes#1556
datlechin merged 2 commits into
mainfrom
refactor/keyboard-shortcuts

Conversation

@datlechin
Copy link
Copy Markdown
Member

@datlechin datlechin commented Jun 2, 2026

Summary

Rewrites the keyboard shortcut path so there is one dispatch authority and one binding model. Shortcuts are stored as hardware key codes instead of characters, so custom bindings work on any keyboard layout and shifted symbols record correctly. Conflict detection now knows the full shortcut space (other actions, live macOS system shortcuts, built-in editor commands, and the app's reserved tab/zoom keys).

Root cause

The old system had two uncoordinated layers:

  1. The customizable layer: KeyCombo -> SwiftUI menu key-equivalents, read from KeyboardSettings.
  2. A set of hardcoded NSEvent local monitors and keyDown overrides that fire before menu key-equivalents (per the documented sendEvent order) and never consult KeyboardSettings.

Because layer 2 ran first, rebinding Copy or Cut changed the menu but not the editor, the data-grid Delete ignored its binding, and pagination keys were eaten by the editor. On top of that, KeyCombo stored charactersIgnoringModifiers, which is layout-dependent and records { for Shift+[, so re-recording a default could stop matching.

What changed

  • New BoundKey model stores {keyCode, modifiers}. Matching compares key codes (layout-independent), and display strings are derived through UCKeyTranslate.
  • Single dispatch authority: menu key-equivalents driven by KeyboardSettings, contextualized by focus. A new ShortcutContext (global / editor / dataGrid) lets the same combo mean different things in the editor and the grid, so Cmd+[ pages the grid and indents the editor without a conflict.
  • Deleted the editor's hardcoded Cmd+C / Cmd+X monitor. Copy and Cut now route through the responder chain, so custom bindings apply. The Cmd+X line-cut moved into the text view's cut(_:) so it stays a responder-chain action.
  • Data-grid Delete follows its binding; pagination buttons no longer double-fire; Find Next / Find Previous are wired.
  • Conflict detection uses CopySymbolicHotKeys() for live system shortcuts (replacing a hand-maintained list), a built-in editor registry, and an app-reserved registry (Cmd+1-Cmd+9, zoom), and treats editor vs grid as separate contexts.
  • Settings: grouped by surface, per-action reset button, and DataGrip-style conflict messages.
  • Old KeyCombo settings migrate to BoundKey on load; the override dictionary stays keyed by action id.

Default shortcut changes (breaking)

  • Cmd+N now opens a new connection. Manage Connections keeps its File menu item.
  • Show Tables / Show Favorites sidebars: Ctrl+1 / Ctrl+2 (they switch macOS Spaces) move to Cmd+Option+1 / Cmd+Option+2.
  • First Page / Last Page now default to Cmd+Option+Up / Cmd+Option+Down.
  • Find Next / Find Previous now work (Cmd+G / Cmd+Shift+G).
  • Cmd+[ / Cmd+] and Cmd+F are unchanged; they are now context-scoped.

Testing

  • New and updated unit tests cover keyCode matching (shifted symbols, layout independence), context-scoped conflicts, the reserved registries, no-bare-key menu registration, and KeyCombo -> BoundKey migration.
  • New package test covers the empty-selection line cut.
  • swiftlint lint --strict clean on changed files.

Please verify after building

  • Vim: pressing Escape exits insert mode (Escape is no longer a menu key-equivalent; the Vim monitor handles it).
  • Editor Copy now writes an attributed string (standard NSTextView behavior); plain-text paste is unaffected.

Function keys and #1489

BoundKey now treats function keys (F1-F12) as first-class: the recorder captures them with or without a modifier, they render as F5 and register as menu key-equivalents, and they survive sanitization. There are no function-key defaults yet.

This overlaps with open PR #1489 (F5/F9/F1 shortcuts), which adds a separate FunctionKeyShortcutMonitor. If this PR lands first, #1489 can drop the monitor and add its function-key bindings as ShortcutAction defaults plus menu items, keeping a single dispatch authority. The two PRs also touch the same files (KeyboardShortcutModels, KeyboardSettingsView, tests, docs), so a merge order should be agreed.

@mintlify
Copy link
Copy Markdown

mintlify Bot commented Jun 2, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jun 2, 2026, 10:04 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09f4fcf365

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +84 to +85
} else if legacyKey.count == 1, let character = legacyKey.first {
resolvedKeyCode = KeyboardLayout.keyCode(for: character)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve shifted legacy shortcuts during migration

When decoding old settings that were recorded with Shift (for example Cmd+Shift+[ was persisted by the previous charactersIgnoringModifiers path as key: "{", shift: true), this migration asks KeyboardLayout.keyCode(for: "{"). That lookup only indexes unshifted/base characters, so it returns nil and compactMapValues drops the user's override, silently reverting or clearing customized shifted-symbol shortcuts after upgrade. Please normalize shifted legacy glyphs like {/} back to their base physical keys before dropping the entry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c33b87cf82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +249 to +253
static let reservedAppShortcuts: [(key: BoundKey, name: String)] = {
var shortcuts: [(key: BoundKey, name: String)] = [
(.character("=", command: true), String(localized: "Zoom In")),
(.character("-", command: true), String(localized: "Zoom Out"))
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block standard app menu shortcuts

When a user records built-in app menu equivalents such as ⌘Q, ⌘H, ⌘M, or ⌘,, this reserved list does not catch them, and SystemHotkeyChecker only reads macOS symbolic hotkeys rather than the app’s standard menu items. The resolver therefore accepts those bindings, but AppKit already has those key equivalents in the application menu, so the customized action will either be shadowed by Quit/Hide/Minimize/Settings or create an ambiguous duplicate shortcut.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55f1d6eee6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


if settings.isCustomized(action) {
Button {
settings.resetToDefault(for: action)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-check conflicts before resetting one shortcut

When an action has been cleared or changed because another action took its default shortcut, this reset button restores the default without running the same conflict checks used for recording. For example, after reassigning action B to action A's default and clearing A via the conflict dialog, pressing A's reset button creates two active actions with the same key in overlapping contexts, so one binding is shadowed/ambiguous despite the settings UI normally preventing this.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit 197521e into main Jun 2, 2026
5 checks passed
@datlechin datlechin deleted the refactor/keyboard-shortcuts branch June 2, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant