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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Restore Previous Values in the Edit menu, taking back a save that already committed. Starter license. (#2107)
- Data Rewind settings in Settings > Data & Results, with an off switch and Clear Saved Changes.
- Restore Previous Values in the toolbar's Table Actions group.
- Rebindable Find shortcut in Settings > Keyboard, for giving `Cmd+F` to the filter bar instead.

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,6 @@ extension TextViewController {
case (commandKey, "]"):
handleIndent()
return nil
case (commandKey, "f"):
_ = self.textView.resignFirstResponder()
self.findViewController?.showFindPanel()
return nil
case ([commandKey, .shift], "D"):
duplicateLine()
return nil
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Core/Menu/EditMenuBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ enum EditMenuBuilder {
MenuItemFactory.item(
String(localized: "Find…"),
action: #selector(MainSplitViewController.performFind(_:)),
keyEquivalent: "f",
modifiers: .command
shortcut: .find,
keyboard: keyboard
),
MenuItemFactory.item(
String(localized: "Find Next"),
Expand Down
14 changes: 10 additions & 4 deletions TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case nextStatement
case runStatementAndAdvance
case previewSQL
case find
case findNext
case findPrevious
case aiExplainQuery
Expand Down Expand Up @@ -149,7 +150,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
.executeQueryWithoutLimit, .cancelQuery, .explainQuery, .formatQuery,
.foldAll, .unfoldAll, .toggleFold,
.previousStatement, .nextStatement, .runStatementAndAdvance,
.previewSQL, .findNext, .findPrevious, .aiExplainQuery, .aiOptimizeQuery:
.previewSQL, .find, .findNext, .findPrevious, .aiExplainQuery, .aiOptimizeQuery:
return .editor
case .undo, .redo, .cut, .copy, .copyRowsExplicit, .copyWithHeaders, .copyAsJson,
.paste, .delete, .selectAll, .clearSelection, .addRow, .duplicateRow,
Expand All @@ -167,8 +168,13 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
}
}

/// `find` is deliberately `.global` rather than `.editor`: it dispatches to whichever
/// surface holds focus, and only a global context overlaps `.dataGrid`, which is what
/// lets it yield `Cmd+F` to a user who binds the filter bar there instead.
var context: ShortcutContext {
switch self {
case .find:
return .global
case .executeQuery, .executeAllStatements, .executeQueryWithoutLimit,
.cancelQuery, .explainQuery, .formatQuery, .foldAll, .unfoldAll,
.toggleFold, .previousStatement, .nextStatement, .runStatementAndAdvance,
Expand Down Expand Up @@ -224,6 +230,7 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case .previousStatement: return String(localized: "Previous Statement")
case .nextStatement: return String(localized: "Next Statement")
case .runStatementAndAdvance: return String(localized: "Run Statement and Advance")
case .find: return String(localized: "Find")
case .findNext: return String(localized: "Find Next")
case .findPrevious: return String(localized: "Find Previous")
case .export: return String(localized: "Export")
Expand Down Expand Up @@ -281,7 +288,6 @@ extension ShortcutAction {
(.character("/", command: true), String(localized: "Toggle Comment")),
(.character("[", command: true), String(localized: "Indent")),
(.character("]", command: true), String(localized: "Outdent")),
(.character("f", command: true), String(localized: "Find")),
(.character("d", command: true, shift: true), String(localized: "Duplicate Line")),
(.character("k", command: true, shift: true), String(localized: "Delete Line")),
(.special(.space, control: true), String(localized: "Show Completions")),
Expand Down Expand Up @@ -315,8 +321,7 @@ extension ShortcutAction {
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("f", command: true), String(localized: "Find"))
(.character("-", command: true), String(localized: "Zoom Out"))
]
for number in 1...9 {
shortcuts.append((
Expand Down Expand Up @@ -517,6 +522,7 @@ struct KeyboardSettings: Codable, Equatable {
.unfoldAll: .special(.rightArrow, command: true, shift: true, option: true),
.toggleFold: .special(.leftArrow, command: true, option: true),
.previewSQL: .character("p", command: true, shift: true),
.find: .character("f", command: true),
.findNext: .character("g", command: true),
.findPrevious: .character("g", command: true, shift: true),
.aiExplainQuery: .character("l", command: true),
Expand Down
4 changes: 3 additions & 1 deletion TablePro/Views/Settings/KeyboardSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ struct KeyboardSettingsView: View {
}
Button(String(localized: "Reassign")) {
if let state = conflictAlert {
settings.clearShortcut(for: state.conflictingAction)
if settings.isCustomized(state.conflictingAction) {
settings.clearShortcut(for: state.conflictingAction)
}
settings.setShortcut(state.combo, for: state.action)
}
conflictAlert = nil
Expand Down
64 changes: 61 additions & 3 deletions TableProTests/Core/Menu/MainMenuBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,72 @@ struct MainMenuShortcutCoverageTests {
#expect(item?.keyEquivalentModifierMask == [.command, .shift])
}

@Test("Find keeps Cmd+F and the filter bar no longer competes for it")
func findOwnsCommandF() {
@Test("Find ships on Cmd+F and the filter bar keeps Cmd+Option+F")
func findAndFilterDefaultsHold() {
#expect(KeyboardSettings.defaultShortcuts[.find] == .character("f", command: true))
#expect(KeyboardSettings.defaultShortcuts[.toggleFilters] == .character("f", command: true, option: true))
#expect(
KeyboardSettings.defaultShortcuts[.focusSidebarSearch]
== .character("f", command: true, option: true, control: true)
)
#expect(ShortcutAction.reservedAppShortcuts.contains { $0.key == .character("f", command: true) })
}

@Test("Cmd+F is customizable rather than reserved")
func commandFIsNoLongerReserved() {
let commandF = BoundKey.character("f", command: true)
#expect(!ShortcutAction.reservedAppShortcuts.contains { $0.key == commandF })
#expect(!ShortcutAction.editorBuiltIns.contains { $0.key == commandF })
#expect(ShortcutAction.reservedConflict(for: commandF, context: .dataGrid) == nil)
#expect(ShortcutAction.reservedConflict(for: commandF, context: .editor) == nil)
}

@Test("Cmd+F for the filter bar is no longer refused outright, only reported against Find")
func recorderReportsFindRatherThanRefusingCommandF() {
let commandF = BoundKey.character("f", command: true)
#expect(ShortcutAction.reservedConflict(for: commandF, context: ShortcutAction.toggleFilters.context) == nil)
#expect(KeyboardSettings.default.findConflict(for: commandF, excluding: .toggleFilters) == .find)
}

@Test("Reassigning Cmd+F leaves Find on its default so it recovers when the filter bar moves back")
func reassigningCommandFDoesNotStrandFind() {
var keyboard = KeyboardSettings()
#expect(!keyboard.isCustomized(.find))

keyboard.setShortcut(.character("f", command: true), for: .toggleFilters)
#expect(keyboard.shortcut(for: .find) == nil)
#expect(!keyboard.isCustomized(.find))

keyboard.setShortcut(.character("f", command: true, option: true), for: .toggleFilters)
#expect(keyboard.shortcut(for: .find) == .character("f", command: true))
}

@Test("Find yields Cmd+F to a user-bound filter bar instead of sharing it")
func findYieldsCommandFToFilterBar() {
var keyboard = KeyboardSettings()
keyboard.setShortcut(.character("f", command: true), for: .toggleFilters)

#expect(keyboard.shortcut(for: .find) == nil)
#expect(keyboard.shortcut(for: .toggleFilters) == .character("f", command: true))

let menu = buildMenu()
MainMenuKeyEquivalentSync.apply(keyboard: keyboard, to: menu)
let claimants = flatten(menu).filter {
$0.keyEquivalent == "f" && $0.keyEquivalentModifierMask == [.command]
}
#expect(claimants.count == 1)
#expect(claimants.first?.identifier == MenuItemFactory.identifier(for: .toggleFilters))
}

@Test("Find… carries no hardcoded key equivalent")
func findMenuItemFollowsKeyboardSettings() {
var keyboard = KeyboardSettings()
keyboard.setShortcut(.character("f", command: true, shift: true), for: .find)

let menu = buildMenu()
MainMenuKeyEquivalentSync.apply(keyboard: keyboard, to: menu)
let item = flatten(menu).first { $0.identifier == MenuItemFactory.identifier(for: .find) }
#expect(item?.keyEquivalent == "f")
#expect(item?.keyEquivalentModifierMask == [.command, .shift])
}
}

Expand Down
15 changes: 13 additions & 2 deletions TableProTests/Models/KeyboardShortcutTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,21 @@ struct ShortcutConflictTests {
#expect(conflict == .refresh)
}

@Test("Editor action does not conflict with the data-grid Cmd+F filter")
func crossContextDoesNotConflict() {
@Test("Cmd+F is held by the global Find action, so an editor binding collides with it")
func commandFConflictsWithFind() {
let settings = KeyboardSettings.default
let conflict = settings.findConflict(for: .character("f", command: true), excluding: .executeQuery)
#expect(conflict == .find)
}

@Test("A data-grid binding does not conflict with an editor-only default")
func crossContextDoesNotConflict() {
let settings = KeyboardSettings.default
#expect(KeyboardSettings.defaultShortcuts[.formatQuery] == .character("l", command: true, shift: true))
let conflict = settings.findConflict(
for: .character("l", command: true, shift: true),
excluding: .previousPage
)
#expect(conflict == nil)
}
}
Expand Down
4 changes: 2 additions & 2 deletions docs/features/keyboard-shortcuts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The grid meanings come back the moment you click into the grid, and `Delete` on
| Find next | `Cmd+G` |
| Find previous | `Cmd+Shift+G` |

Every row except Find Next and Find Previous is built into the editor and cannot be rebound. Standard macOS text navigation and clipboard keys also apply.
Every row except Find, Find Next and Find Previous is built into the editor and cannot be rebound. Standard macOS text navigation and clipboard keys also apply.

## Data Grid

Expand Down Expand Up @@ -273,7 +273,7 @@ A combination already taken by another action in the same context raises a dialo
Menu actions need a modifier (`Cmd`, `Option`, `Ctrl`, or `Shift`); function keys `F1` through `F12` work bare, as do grid actions that read the key directly, like Preview FK Reference (`Space`).

<Info>
Some shortcuts cannot be reassigned: editor built-ins such as `Cmd+/`, tab selection (`Cmd+1` through `Cmd+9`), text size (`Cmd+=`, `Cmd+-`), Find (`Cmd+F`), and macOS system shortcuts, which are read live from System Settings. The recorder warns if you try.
Some shortcuts cannot be reassigned: editor built-ins such as `Cmd+/`, tab selection (`Cmd+1` through `Cmd+9`), text size (`Cmd+=`, `Cmd+-`), and macOS system shortcuts, which are read live from System Settings. The recorder warns if you try.
</Info>

## Outside the app
Expand Down
4 changes: 3 additions & 1 deletion docs/switching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ Set these up on your first day:
| Explain the query | `Cmd+Option+E` |
| Save the query as a favorite | `Cmd+D` |

A few cannot be reassigned, so learn them rather than fight them: editor built-ins such as `Cmd+/` for comment, `Cmd+[` and `Cmd+]` for indent, tab selection `Cmd+1` through `Cmd+9`, text size `Cmd+=` and `Cmd+-`, and `Cmd+F` for find. The recorder tells you when you hit one.
A few cannot be reassigned, so learn them rather than fight them: editor built-ins such as `Cmd+/` for comment, `Cmd+[` and `Cmd+]` for indent, tab selection `Cmd+1` through `Cmd+9`, and text size `Cmd+=` and `Cmd+-`. The recorder tells you when you hit one.

Coming from TablePlus, where `Cmd+F` opens the row filter? Rebind **Toggle Filters** to `Cmd+F` in **Settings > Keyboard** and choose **Reassign** when it asks. Find gives up `Cmd+F` while the filter bar holds it, and takes it back if you move the filter bar off again.

If you use Vim bindings, turn on [Vim Mode](/features/vim-mode) in **Settings > Editor** and most of this section stops mattering.

Expand Down
Loading