Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Stale cells after fitting, hiding or reordering a data grid column on a result narrower than the window. (#2446)
- A rebound shortcut silently killing Quit, Minimize, Hide, Settings, Show Toolbar or Enter Full Screen.
- `Ctrl+Cmd+J` accepted in Settings > Keyboard while the SQL editor kept it for Jump to Definition.
- Autocomplete keeping an earlier prefix's ordering after the typed word becomes an exact match. (#2444)
- Whole MySQL and MariaDB result set fetched before a capped query returned its first rows. (#2427)
- KILL sent to a different server when a MySQL or MariaDB connection's host is spelled `localhost`.
Expand Down
17 changes: 13 additions & 4 deletions TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ extension ShortcutAction {
(.special(.space, control: true), String(localized: "Show Completions")),
(.special(.upArrow, option: true), String(localized: "Move Line Up")),
(.special(.downArrow, option: true), String(localized: "Move Line Down")),
(.character("j", command: true, control: true), String(localized: "Jump to Definition")),
(.special(.upArrow, shift: true, option: true), String(localized: "Extend Selection to Previous Statement")),
(.special(.downArrow, shift: true, option: true), String(localized: "Extend Selection to Next Statement"))
]
Expand All @@ -315,13 +316,21 @@ extension ShortcutAction {
(.special(.rightArrow, option: true), String(localized: "Move Word Right"))
]

/// App-level shortcuts that are wired directly in the menu and are not
/// customizable: tab selection (Cmd+1 through Cmd+9) and editor zoom. These
/// fire regardless of focus, so a user binding would silently collide.
/// Every key equivalent a menu builder hardcodes, and so every combo no `ShortcutAction`
/// can be bound to. These fire regardless of focus, and AppKit blanks the loser when two
/// menu items claim one combo, so a binding the recorder let through would silently kill
/// the hardcoded command instead. An entry added to a menu builder belongs here too.
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"))
(.character("-", command: true), String(localized: "Zoom Out")),
(.character(",", command: true), String(localized: "Settings…")),
(.character("h", command: true), String(localized: "Hide TablePro")),
(.character("h", command: true, option: true), String(localized: "Hide Others")),
(.character("q", command: true), String(localized: "Quit TablePro")),
(.character("m", command: true), String(localized: "Minimize")),
(.character("t", command: true, option: true), String(localized: "Show Toolbar")),
(.character("f", command: true, control: true), String(localized: "Enter Full Screen"))
]
for number in 1...9 {
shortcuts.append((
Expand Down
24 changes: 24 additions & 0 deletions TableProTests/Core/Menu/MainMenuBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ struct MainMenuStructureTests {
#expect(duplicates.isEmpty, "AppKit blanks the loser when two items claim one combo: \(duplicates)")
}

/// A menu builder that hardcodes a key equivalent takes that combo off the table for every
/// `ShortcutAction`, and only `reservedAppShortcuts` tells the recorder so. Nothing else
/// forces the two to agree, so a hardcoded item added without a matching entry ships a
/// binding the recorder accepts and AppKit then blanks.
@Test("Every hardcoded menu key equivalent is reserved against user binding")
func hardcodedKeyEquivalentsAreReserved() {
func canonical(_ key: BoundKey) -> String? {
key.menuKeyEquivalent.map { "\(key.modifierFlags.rawValue)-\($0)" }
}

let keyboard = KeyboardSettings()
let customizable = Set(ShortcutAction.allCases.compactMap { keyboard.shortcut(for: $0).flatMap(canonical) })
let reserved = Set(ShortcutAction.reservedAppShortcuts.compactMap { canonical($0.key) })

let hardcoded = flatten(buildMenu())
.filter { !$0.keyEquivalent.isEmpty }
.map { (combo: "\($0.keyEquivalentModifierMask.rawValue)-\($0.keyEquivalent)", title: $0.title) }
.filter { !customizable.contains($0.combo) }
#expect(!hardcoded.isEmpty, "Found no hardcoded menu shortcuts, so this guard would pass vacuously")

let unreserved = hardcoded.filter { !reserved.contains($0.combo) }.map(\.title)
#expect(unreserved.isEmpty, "Hardcoded menu shortcuts missing from reservedAppShortcuts: \(unreserved)")
}

@Test("Every menu item carries an action")
func everyItemHasAnAction() {
let dead = flatten(buildMenu())
Expand Down
Loading