diff --git a/CLAUDE.md b/CLAUDE.md index ba9f63cbb..2888dd1c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,12 +179,31 @@ No custom diffing engines unless unavoidable. ## Keyboard Shortcuts -Implemented using **native command handling**: - -* `Cmd + Enter` → Execute query -* `Cmd + S` → Commit changes -* `Cmd + R` → Refresh data -* `Ctrl + Space` → Trigger autocomplete +All keyboard shortcuts are centralized in `Core/KeyboardShortcuts.swift` following macOS Human Interface Guidelines. + +### Key Files +* `Core/KeyboardShortcuts.swift` - Centralized shortcut definitions and key codes +* `OpenTableApp.swift` - Menu command handlers using KeyboardShortcuts enum + +### Standard macOS Shortcuts (following HIG): +* `⌘N` → New connection +* `⌘T` → New tab +* `⌘W` → Close tab +* `⌘S` → Save changes +* `⌘R` → Refresh data +* `⌘Z` / `⌘⇧Z` → Undo / Redo +* `⌘X` / `⌘C` / `⌘V` → Cut / Copy / Paste +* `⌘A` → Select All +* `⌘F` → Toggle filters +* `Escape` → Clear selection + +### Application-Specific: +* `⌘↩` → Execute query +* `⌘I` → Add row +* `⌘D` → Duplicate row +* `⌘B` → Toggle table browser +* `⌘⇧H` → Toggle history +* `⌃Space` → Trigger autocomplete ## Summary Rule diff --git a/OpenTable/ContentView.swift b/OpenTable/ContentView.swift index ccc7de666..9341c25b7 100644 --- a/OpenTable/ContentView.swift +++ b/OpenTable/ContentView.swift @@ -291,8 +291,8 @@ struct ContentView: View { private func setupEscapeKeyMonitor() { escapeKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - // Escape key code is 53 - if event.keyCode == 53 { + // Escape key - standard macOS cancel/clear shortcut + if event.keyCode == KeyCodes.escape { NotificationCenter.default.post(name: .clearSelection, object: nil) // Return nil to consume the event, or return event to let it propagate return nil diff --git a/OpenTable/Core/KeyboardShortcuts.swift b/OpenTable/Core/KeyboardShortcuts.swift new file mode 100644 index 000000000..abe5de4fb --- /dev/null +++ b/OpenTable/Core/KeyboardShortcuts.swift @@ -0,0 +1,298 @@ +// +// KeyboardShortcuts.swift +// OpenTable +// +// Centralized keyboard shortcut definitions following macOS Human Interface Guidelines. +// Reference: https://developer.apple.com/design/human-interface-guidelines/keyboards +// +// This file serves as the single source of truth for all keyboard shortcuts in the app. +// When adding new shortcuts, check this file first to avoid conflicts. +// + +import SwiftUI + +// MARK: - Keyboard Shortcut Definitions + +/// Centralized keyboard shortcut definitions following macOS conventions. +/// +/// ## macOS Standard Shortcuts (System-defined, should not be overridden): +/// - ⌘Q: Quit application +/// - ⌘H: Hide application +/// - ⌘,: Open Settings/Preferences +/// - ⌘M: Minimize window +/// - ⌘W: Close window/tab +/// - ⌘N: New document/connection +/// - ⌘O: Open document +/// - ⌘S: Save +/// - ⌘Z: Undo +/// - ⌘⇧Z: Redo +/// - ⌘X: Cut +/// - ⌘C: Copy +/// - ⌘V: Paste +/// - ⌘A: Select All +/// - ⌘F: Find +/// +/// ## Application-Specific Shortcuts: +/// These follow macOS conventions where similar functionality exists. +enum KeyboardShortcuts { + + // MARK: - File Menu + + /// New Connection (⌘N) + /// Standard macOS shortcut for creating new items + static let newConnection = KeyboardShortcut("n", modifiers: .command) + + /// New Tab (⌘T) + /// Standard macOS shortcut for new tabs (Safari, Terminal, etc.) + static let newTab = KeyboardShortcut("t", modifiers: .command) + + /// Close Tab (⌘W) + /// Standard macOS shortcut for closing current tab/window + static let closeTab = KeyboardShortcut("w", modifiers: .command) + + /// Save Changes (⌘S) + /// Standard macOS shortcut for saving + static let saveChanges = KeyboardShortcut("s", modifiers: .command) + + /// Refresh (⌘R) + /// Standard shortcut for refresh (Safari, Finder, etc.) + static let refresh = KeyboardShortcut("r", modifiers: .command) + + // MARK: - Edit Menu + + /// Undo (⌘Z) + /// Standard macOS undo shortcut + static let undo = KeyboardShortcut("z", modifiers: .command) + + /// Redo (⌘⇧Z) + /// Standard macOS redo shortcut + static let redo = KeyboardShortcut("z", modifiers: [.command, .shift]) + + /// Cut (⌘X) + /// Standard macOS cut shortcut + static let cut = KeyboardShortcut("x", modifiers: .command) + + /// Copy (⌘C) + /// Standard macOS copy shortcut + static let copy = KeyboardShortcut("c", modifiers: .command) + + /// Paste (⌘V) + /// Standard macOS paste shortcut + static let paste = KeyboardShortcut("v", modifiers: .command) + + /// Select All (⌘A) + /// Standard macOS select all shortcut + static let selectAll = KeyboardShortcut("a", modifiers: .command) + + /// Delete (⌘⌫) + /// Delete selected items - macOS uses Cmd+Delete for moving to Trash + static let delete = KeyboardShortcut(.delete, modifiers: .command) + + /// Clear Selection (Escape) + /// Standard macOS convention for canceling/clearing + static let clearSelection = KeyboardShortcut(.escape, modifiers: []) + + // MARK: - Row Operations (Edit Menu - Custom) + + /// Add Row (⌘I) + /// Insert new row - 'I' for Insert, follows database convention + static let addRow = KeyboardShortcut("i", modifiers: .command) + + /// Duplicate Row (⌘D) + /// Duplicate selected item - follows Finder/Keynote convention + static let duplicateRow = KeyboardShortcut("d", modifiers: .command) + + /// Truncate Table (⌥⌫) + /// Dangerous operation, uses Option modifier for extra confirmation + static let truncateTable = KeyboardShortcut(.delete, modifiers: .option) + + // MARK: - View Menu + + /// Toggle Table Browser/Sidebar (⌘B) + /// Similar to Safari's bookmarks sidebar toggle + static let toggleSidebar = KeyboardShortcut("b", modifiers: .command) + + /// Toggle Inspector (⌘⌥B) + /// Similar to Xcode's inspector toggle + static let toggleInspector = KeyboardShortcut("b", modifiers: [.command, .option]) + + /// Toggle Filters (⌘F) + /// Standard find/filter shortcut + static let toggleFilters = KeyboardShortcut("f", modifiers: .command) + + /// Toggle History Panel (⌘⇧H) + /// History panel toggle - H for History + static let toggleHistory = KeyboardShortcut("h", modifiers: [.command, .shift]) + + // MARK: - Query Editor + + /// Execute Query (⌘↩) + /// Standard shortcut for executing/running in many IDEs + static let executeQuery = KeyboardShortcut(.return, modifiers: .command) + + /// Format Query (⌘⇧L) + /// Format/beautify the SQL query + static let formatQuery = KeyboardShortcut("l", modifiers: [.command, .shift]) + + /// Trigger Autocomplete (⌃Space) + /// Standard IDE autocomplete trigger + static let triggerAutocomplete = KeyboardShortcut(.space, modifiers: .control) + + // MARK: - Navigation + + /// Next Tab (⌘⇧]) + /// Standard macOS tab navigation (Safari, Terminal) + static let nextTab = KeyboardShortcut("]", modifiers: [.command, .shift]) + + /// Previous Tab (⌘⇧[) + /// Standard macOS tab navigation (Safari, Terminal) + static let previousTab = KeyboardShortcut("[", modifiers: [.command, .shift]) + + // MARK: - Window + + /// Minimize (⌘M) + /// Standard macOS minimize - handled by system + static let minimize = KeyboardShortcut("m", modifiers: .command) + + /// Zoom/Full Screen (⌃⌘F) + /// Standard macOS full screen toggle + static let fullScreen = KeyboardShortcut("f", modifiers: [.control, .command]) +} + +// MARK: - Shortcut Documentation + +/// Documentation of all keyboard shortcuts for reference. +/// This can be displayed in a help menu or settings panel. +struct ShortcutDocumentation { + + struct ShortcutInfo { + let keys: String + let description: String + let category: String + } + + static let allShortcuts: [ShortcutInfo] = [ + // File + ShortcutInfo(keys: "⌘N", description: "New Connection", category: "File"), + ShortcutInfo(keys: "⌘T", description: "New Tab", category: "File"), + ShortcutInfo(keys: "⌘W", description: "Close Tab", category: "File"), + ShortcutInfo(keys: "⌘S", description: "Save Changes", category: "File"), + ShortcutInfo(keys: "⌘R", description: "Refresh", category: "File"), + + // Edit + ShortcutInfo(keys: "⌘Z", description: "Undo", category: "Edit"), + ShortcutInfo(keys: "⌘⇧Z", description: "Redo", category: "Edit"), + ShortcutInfo(keys: "⌘X", description: "Cut", category: "Edit"), + ShortcutInfo(keys: "⌘C", description: "Copy", category: "Edit"), + ShortcutInfo(keys: "⌘V", description: "Paste", category: "Edit"), + ShortcutInfo(keys: "⌘A", description: "Select All", category: "Edit"), + ShortcutInfo(keys: "⌘⌫", description: "Delete", category: "Edit"), + ShortcutInfo(keys: "⌘I", description: "Add Row", category: "Edit"), + ShortcutInfo(keys: "⌘D", description: "Duplicate Row", category: "Edit"), + ShortcutInfo(keys: "⌥⌫", description: "Truncate Table", category: "Edit"), + ShortcutInfo(keys: "Escape", description: "Clear Selection", category: "Edit"), + + // View + ShortcutInfo(keys: "⌘B", description: "Toggle Table Browser", category: "View"), + ShortcutInfo(keys: "⌘⌥B", description: "Toggle Inspector", category: "View"), + ShortcutInfo(keys: "⌘F", description: "Toggle Filters", category: "View"), + ShortcutInfo(keys: "⌘⇧H", description: "Toggle History", category: "View"), + + // Query + ShortcutInfo(keys: "⌘↩", description: "Execute Query", category: "Query"), + ShortcutInfo(keys: "⌘⇧L", description: "Format Query", category: "Query"), + ShortcutInfo(keys: "⌃Space", description: "Trigger Autocomplete", category: "Query"), + + // Navigation + ShortcutInfo(keys: "⌘⇧]", description: "Next Tab", category: "Navigation"), + ShortcutInfo(keys: "⌘⇧[", description: "Previous Tab", category: "Navigation"), + + // Data Grid + ShortcutInfo(keys: "↩", description: "Edit Cell", category: "Data Grid"), + ShortcutInfo(keys: "Tab", description: "Next Cell", category: "Data Grid"), + ShortcutInfo(keys: "⇧Tab", description: "Previous Cell", category: "Data Grid"), + ShortcutInfo(keys: "↑↓←→", description: "Navigate Cells", category: "Data Grid"), + ShortcutInfo(keys: "⇧↑↓", description: "Extend Selection", category: "Data Grid"), + ] +} + +// MARK: - Key Code Constants + +/// AppKit key codes for use in NSEvent handling. +/// These are used in performKeyEquivalent and keyDown handlers. +enum KeyCodes { + // Letters + static let a: UInt16 = 0 + static let b: UInt16 = 11 + static let c: UInt16 = 8 + static let d: UInt16 = 2 + static let e: UInt16 = 14 + static let f: UInt16 = 3 + static let g: UInt16 = 5 + static let h: UInt16 = 4 + static let i: UInt16 = 34 + static let j: UInt16 = 38 + static let k: UInt16 = 40 + static let l: UInt16 = 37 + static let m: UInt16 = 46 + static let n: UInt16 = 45 + static let o: UInt16 = 31 + static let p: UInt16 = 35 + static let q: UInt16 = 12 + static let r: UInt16 = 15 + static let s: UInt16 = 1 + static let t: UInt16 = 17 + static let u: UInt16 = 32 + static let v: UInt16 = 9 + static let w: UInt16 = 13 + static let x: UInt16 = 7 + static let y: UInt16 = 16 + static let z: UInt16 = 6 + + // Numbers + static let zero: UInt16 = 29 + static let one: UInt16 = 18 + static let two: UInt16 = 19 + static let three: UInt16 = 20 + static let four: UInt16 = 21 + static let five: UInt16 = 23 + static let six: UInt16 = 22 + static let seven: UInt16 = 26 + static let eight: UInt16 = 28 + static let nine: UInt16 = 25 + + // Special Keys + static let returnKey: UInt16 = 36 + static let tab: UInt16 = 48 + static let space: UInt16 = 49 + static let delete: UInt16 = 51 // Backspace + static let escape: UInt16 = 53 + static let forwardDelete: UInt16 = 117 + + // Arrow Keys + static let leftArrow: UInt16 = 123 + static let rightArrow: UInt16 = 124 + static let downArrow: UInt16 = 125 + static let upArrow: UInt16 = 126 + + // Function Keys + static let f1: UInt16 = 122 + static let f2: UInt16 = 120 + static let f3: UInt16 = 99 + static let f4: UInt16 = 118 + static let f5: UInt16 = 96 + static let f6: UInt16 = 97 + static let f7: UInt16 = 98 + static let f8: UInt16 = 100 + static let f9: UInt16 = 101 + static let f10: UInt16 = 109 + static let f11: UInt16 = 103 + static let f12: UInt16 = 111 + + // Brackets + static let leftBracket: UInt16 = 33 // [ + static let rightBracket: UInt16 = 30 // ] + + // Keypad Enter (different from Return) + static let keypadEnter: UInt16 = 76 +} diff --git a/OpenTable/Extensions/NSViewController+SwiftUI.swift b/OpenTable/Extensions/NSViewController+SwiftUI.swift index 15bd6aa71..1578ef742 100644 --- a/OpenTable/Extensions/NSViewController+SwiftUI.swift +++ b/OpenTable/Extensions/NSViewController+SwiftUI.swift @@ -30,8 +30,8 @@ private class KeyboardHandlingHostingController: NSHostingControl // Check for Command modifier let commandPressed = event.modifierFlags.contains(.command) - // Handle Cmd+Return (Save) - if commandPressed && (event.keyCode == 36 || event.keyCode == 76) { + // Handle Cmd+Return (Save) - standard macOS convention + if commandPressed && (event.keyCode == KeyCodes.returnKey || event.keyCode == KeyCodes.keypadEnter) { onSave?() return true } @@ -41,13 +41,13 @@ private class KeyboardHandlingHostingController: NSHostingControl } override func cancelOperation(_ sender: Any?) { - // Handle Escape key + // Handle Escape key - standard macOS cancel convention onCancel?() } override func keyDown(with event: NSEvent) { // Check for Escape key without modifiers - if event.keyCode == 53 && event.modifierFlags.intersection(.deviceIndependentFlagsMask).isEmpty { + if event.keyCode == KeyCodes.escape && event.modifierFlags.intersection(.deviceIndependentFlagsMask).isEmpty { onCancel?() return } diff --git a/OpenTable/OpenTableApp.swift b/OpenTable/OpenTableApp.swift index c9d53dea0..7bfc67255 100644 --- a/OpenTable/OpenTableApp.swift +++ b/OpenTable/OpenTableApp.swift @@ -2,6 +2,10 @@ // OpenTableApp.swift // OpenTable // +// Main application entry point with menu commands. +// Keyboard shortcuts follow macOS Human Interface Guidelines. +// See KeyboardShortcuts.swift for centralized shortcut definitions. +// // Created by Ngo Quoc Dat on 16/12/25. // @@ -17,6 +21,7 @@ final class AppState: ObservableObject { @Published var hasRowSelection: Bool = false // True when rows are selected in data grid @Published var hasTableSelection: Bool = false // True when tables are selected in sidebar @Published var isHistoryPanelVisible: Bool = false // Global history panel visibility + @Published var isQueryTabActive: Bool = false // True when current tab is a query tab } // MARK: - App @@ -54,19 +59,19 @@ struct OpenTableApp: App { .windowStyle(.automatic) .defaultSize(width: 1200, height: 800) .commands { - // File menu + // MARK: - File Menu CommandGroup(replacing: .newItem) { Button("New Connection...") { NotificationCenter.default.post(name: .newConnection, object: nil) } - .keyboardShortcut("n", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.newConnection) } CommandGroup(after: .newItem) { Button("New Tab") { NotificationCenter.default.post(name: .newTab, object: nil) } - .keyboardShortcut("t", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.newTab) .disabled(!appState.isConnected) Divider() @@ -74,7 +79,7 @@ struct OpenTableApp: App { Button("Save Changes") { NotificationCenter.default.post(name: .saveChanges, object: nil) } - .keyboardShortcut("s", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.saveChanges) .disabled(!appState.isConnected) Button("Close Tab") { @@ -89,36 +94,36 @@ struct OpenTableApp: App { keyWindow?.close() } } - .keyboardShortcut("w", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.closeTab) Divider() Button("Refresh") { NotificationCenter.default.post(name: .refreshData, object: nil) } - .keyboardShortcut("r", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.refresh) .disabled(!appState.isConnected) } - // Edit menu - Undo/Redo (replace the standard undo/redo) + // MARK: - Edit Menu - Undo/Redo CommandGroup(replacing: .undoRedo) { Button("Undo") { NotificationCenter.default.post(name: .undoChange, object: nil) } - .keyboardShortcut("z", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.undo) Button("Redo") { NotificationCenter.default.post(name: .redoChange, object: nil) } - .keyboardShortcut("z", modifiers: [.command, .shift]) + .keyboardShortcut(KeyboardShortcuts.redo) } - // Edit menu - replace pasteboard to add our Delete with shortcut + // MARK: - Edit Menu - Pasteboard CommandGroup(replacing: .pasteboard) { Button("Cut") { NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: nil) } - .keyboardShortcut("x", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.cut) Button("Copy") { if appState.hasRowSelection { @@ -129,12 +134,12 @@ struct OpenTableApp: App { NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: nil) } } - .keyboardShortcut("c", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.copy) Button("Paste") { NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: nil) } - .keyboardShortcut("v", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.paste) Button("Delete") { // Check if first responder is the history panel's table view @@ -153,7 +158,7 @@ struct OpenTableApp: App { // For data grid and other views, use notification for batched undo NotificationCenter.default.post(name: .deleteSelectedRows, object: nil) } - .keyboardShortcut(.delete, modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.delete) .disabled(!appState.isCurrentTabEditable && !appState.hasTableSelection) Divider() @@ -161,28 +166,28 @@ struct OpenTableApp: App { Button("Select All") { NSApp.sendAction(#selector(NSText.selectAll(_:)), to: nil, from: nil) } - .keyboardShortcut("a", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.selectAll) Button("Clear Selection") { NotificationCenter.default.post(name: .clearSelection, object: nil) } - .keyboardShortcut(.escape, modifiers: []) + .keyboardShortcut(KeyboardShortcuts.clearSelection) } - // Edit menu - row operations (after pasteboard) + // MARK: - Edit Menu - Row Operations CommandGroup(after: .pasteboard) { Divider() Button("Add Row") { NotificationCenter.default.post(name: .addNewRow, object: nil) } - .keyboardShortcut("i", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.addRow) .disabled(!appState.isCurrentTabEditable) Button("Duplicate Row") { NotificationCenter.default.post(name: .duplicateRow, object: nil) } - .keyboardShortcut("d", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.duplicateRow) .disabled(!appState.isCurrentTabEditable) Divider() @@ -191,22 +196,22 @@ struct OpenTableApp: App { Button("Truncate Table") { NotificationCenter.default.post(name: .truncateTables, object: nil) } - .keyboardShortcut(.delete, modifiers: .option) + .keyboardShortcut(KeyboardShortcuts.truncateTable) .disabled(!appState.hasTableSelection) } - // View menu + // MARK: - View Menu CommandGroup(after: .sidebar) { Button("Toggle Table Browser") { NotificationCenter.default.post(name: .toggleTableBrowser, object: nil) } - .keyboardShortcut("b", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.toggleSidebar) .disabled(!appState.isConnected) Button("Toggle Inspector") { NotificationCenter.default.post(name: .toggleRightSidebar, object: nil) } - .keyboardShortcut("b", modifiers: [.command, .option]) + .keyboardShortcut(KeyboardShortcuts.toggleInspector) .disabled(!appState.isConnected) Divider() @@ -214,13 +219,35 @@ struct OpenTableApp: App { Button("Toggle Filters") { NotificationCenter.default.post(name: .toggleFilterPanel, object: nil) } - .keyboardShortcut("f", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.toggleFilters) .disabled(!appState.isConnected) Button("Toggle History") { NotificationCenter.default.post(name: .toggleHistoryPanel, object: nil) } - .keyboardShortcut("h", modifiers: [.command, .shift]) + .keyboardShortcut(KeyboardShortcuts.toggleHistory) + .disabled(!appState.isConnected) + } + + // MARK: - Query Menu (Custom) + CommandMenu("Query") { + Button("Execute Query") { + NotificationCenter.default.post(name: .executeQuery, object: nil) + } + .keyboardShortcut(KeyboardShortcuts.executeQuery) + .disabled(!appState.isConnected) + + Button("Format Query") { + NotificationCenter.default.post(name: .formatQuery, object: nil) + } + .keyboardShortcut(KeyboardShortcuts.formatQuery) + .disabled(!appState.isConnected) + + Divider() + + Button("Clear Query") { + NotificationCenter.default.post(name: .clearQuery, object: nil) + } .disabled(!appState.isConnected) } } diff --git a/OpenTable/Views/Editor/EditorTextView.swift b/OpenTable/Views/Editor/EditorTextView.swift index a5ec6327e..7cca0a25e 100644 --- a/OpenTable/Views/Editor/EditorTextView.swift +++ b/OpenTable/Views/Editor/EditorTextView.swift @@ -194,14 +194,14 @@ final class EditorTextView: NSTextView { return } - // Cmd+Enter to execute query - if event.modifierFlags.contains(.command) && event.keyCode == 36 { + // Cmd+Enter to execute query (⌘↩) + if event.modifierFlags.contains(.command) && event.keyCode == KeyCodes.returnKey { onExecute?() return } - // Ctrl+Space to trigger manual completion - if event.modifierFlags.contains(.control) && event.keyCode == 49 { + // Ctrl+Space to trigger manual completion (⌃Space) + if event.modifierFlags.contains(.control) && event.keyCode == KeyCodes.space { onManualCompletion?() return } @@ -234,7 +234,7 @@ final class EditorTextView: NSTextView { } // Handle backspace to delete matching pairs - if event.keyCode == 51 { // Backspace + if event.keyCode == KeyCodes.delete { if shouldDeletePair() { deletePair() return diff --git a/OpenTable/Views/Editor/HistoryListViewController.swift b/OpenTable/Views/Editor/HistoryListViewController.swift index 9d44a70be..33b156e01 100644 --- a/OpenTable/Views/Editor/HistoryListViewController.swift +++ b/OpenTable/Views/Editor/HistoryListViewController.swift @@ -1238,7 +1238,7 @@ private class HistoryTableView: NSTableView, NSMenuItemValidation { let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask) // Return/Enter key - open in new tab - if (event.keyCode == 36 || event.keyCode == 76) && modifiers.isEmpty { + if (event.keyCode == KeyCodes.returnKey || event.keyCode == KeyCodes.keypadEnter) && modifiers.isEmpty { if selectedRow >= 0 { keyboardDelegate?.handleReturnKey() return @@ -1246,7 +1246,7 @@ private class HistoryTableView: NSTableView, NSMenuItemValidation { } // Space key - toggle preview - if event.keyCode == 49 && modifiers.isEmpty { + if event.keyCode == KeyCodes.space && modifiers.isEmpty { if selectedRow >= 0 { keyboardDelegate?.handleSpaceKey() return @@ -1254,19 +1254,19 @@ private class HistoryTableView: NSTableView, NSMenuItemValidation { } // Cmd+E - edit bookmark - if event.keyCode == 14 && modifiers == .command { + if event.keyCode == KeyCodes.e && modifiers == .command { keyboardDelegate?.handleEditBookmark() return } // Escape key - clear search or selection - if event.keyCode == 53 && modifiers.isEmpty { + if event.keyCode == KeyCodes.escape && modifiers.isEmpty { keyboardDelegate?.handleEscapeKey() return } // Delete key (bare, not Cmd+Delete which goes through menu) - if event.keyCode == 51 && modifiers.isEmpty { + if event.keyCode == KeyCodes.delete && modifiers.isEmpty { if selectedRow >= 0 { keyboardDelegate?.handleDeleteKey() return diff --git a/OpenTable/Views/Editor/QueryEditorView.swift b/OpenTable/Views/Editor/QueryEditorView.swift index d27647de1..89b85246e 100644 --- a/OpenTable/Views/Editor/QueryEditorView.swift +++ b/OpenTable/Views/Editor/QueryEditorView.swift @@ -45,15 +45,14 @@ struct QueryEditorView: View { Image(systemName: "trash") } .buttonStyle(.borderless) - .help("Clear Query (⌘+Delete)") - .keyboardShortcut(.delete, modifiers: .command) + .help("Clear Query") // Format button Button(action: formatQuery) { Image(systemName: "text.alignleft") } .buttonStyle(.borderless) - .help("Format Query") + .help("Format Query (⌘⇧L)") Divider() .frame(height: 16) @@ -67,7 +66,7 @@ struct QueryEditorView: View { } .buttonStyle(.borderedProminent) .controlSize(.small) - .keyboardShortcut(.return, modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.executeQuery) } .padding(.horizontal, 12) .padding(.vertical, 8) diff --git a/OpenTable/Views/Editor/QueryTabBar.swift b/OpenTable/Views/Editor/QueryTabBar.swift index 3f260317f..a3351944f 100644 --- a/OpenTable/Views/Editor/QueryTabBar.swift +++ b/OpenTable/Views/Editor/QueryTabBar.swift @@ -41,8 +41,8 @@ struct QueryTabBar: View { .frame(width: 28, height: 28) } .buttonStyle(.borderless) - .help("New Query Tab (⌘+T)") - .keyboardShortcut("t", modifiers: .command) + .help("New Query Tab (⌘T)") + .keyboardShortcut(KeyboardShortcuts.newTab) .padding(.trailing, 8) } .frame(height: 32) diff --git a/OpenTable/Views/Editor/SQLCompletionWindowController.swift b/OpenTable/Views/Editor/SQLCompletionWindowController.swift index 0a5285a92..bdfa85629 100644 --- a/OpenTable/Views/Editor/SQLCompletionWindowController.swift +++ b/OpenTable/Views/Editor/SQLCompletionWindowController.swift @@ -168,23 +168,23 @@ final class SQLCompletionWindowController: NSObject { guard isVisible else { return false } switch event.keyCode { - case 125: // Down arrow + case KeyCodes.downArrow: // Down arrow selectNext() return true - case 126: // Up arrow + case KeyCodes.upArrow: // Up arrow selectPrevious() return true - case 36: // Return + case KeyCodes.returnKey: // Return confirmSelection() return true - case 53: // Escape + case KeyCodes.escape: // Escape dismiss() return true - case 48: // Tab + case KeyCodes.tab: // Tab confirmSelection() return true diff --git a/OpenTable/Views/Results/DataGridView.swift b/OpenTable/Views/Results/DataGridView.swift index df7e9417d..3e7ec08ba 100644 --- a/OpenTable/Views/Results/DataGridView.swift +++ b/OpenTable/Views/Results/DataGridView.swift @@ -1441,8 +1441,8 @@ final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { /// Override to catch Delete/Backspace before menu items can intercept override func performKeyEquivalent(with event: NSEvent) -> Bool { - // Delete (keyCode 51) or Forward Delete (keyCode 117) - if event.keyCode == 51 || event.keyCode == 117 { + // Delete (Backspace) or Forward Delete + if event.keyCode == KeyCodes.delete || event.keyCode == KeyCodes.forwardDelete { let selectedIndices = Set(selectedRowIndexes.map { $0 }) if !selectedIndices.isEmpty && coordinator?.isEditable == true { // Mark rows for deletion @@ -1457,21 +1457,21 @@ final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { override func keyDown(with event: NSEvent) { // Note: Cmd+N is captured by app menu (New Connection) - // Use File > Add Row (Cmd+I) for adding rows + // Use Edit > Add Row (Cmd+I) for adding rows let row = selectedRow let isShiftHeld = event.modifierFlags.contains(.shift) switch event.keyCode { - case 126: // Up arrow - move to previous row (Shift extends selection) + case KeyCodes.upArrow: // Up arrow - move to previous row (Shift extends selection) handleUpArrow(currentRow: row, isShiftHeld: isShiftHeld) return - case 125: // Down arrow - move to next row (Shift extends selection) + case KeyCodes.downArrow: // Down arrow - move to next row (Shift extends selection) handleDownArrow(currentRow: row, isShiftHeld: isShiftHeld) return - case 123: // Left arrow - move to previous column + case KeyCodes.leftArrow: // Left arrow - move to previous column if focusedColumn > 1 { // Skip row number column (index 0) focusedColumn -= 1 if row >= 0 { @@ -1486,7 +1486,7 @@ final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { } return - case 124: // Right arrow - move to next column + case KeyCodes.rightArrow: // Right arrow - move to next column if focusedColumn >= 1 && focusedColumn < numberOfColumns - 1 { focusedColumn += 1 if row >= 0 { @@ -1501,19 +1501,19 @@ final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { } return - case 36: // Enter/Return - edit focused cell + case KeyCodes.returnKey: // Enter/Return - edit focused cell if row >= 0 && focusedColumn >= 1 && coordinator?.isEditable == true { editColumn(focusedColumn, row: row, with: nil, select: true) } return - case 53: // Escape - clear focus and selection + case KeyCodes.escape: // Escape - clear focus and selection focusedRow = -1 focusedColumn = -1 NotificationCenter.default.post(name: .clearSelection, object: nil) return - case 51, 117: // Delete or Backspace key + case KeyCodes.delete, KeyCodes.forwardDelete: // Delete or Backspace key // Post notification to trigger batched deletion in MainContentView // This enables undoing all deletions at once if !selectedRowIndexes.isEmpty { @@ -1521,7 +1521,7 @@ final class KeyHandlingTableView: NSTableView, NSMenuItemValidation { return } - case 48: // Tab - move to next cell + case KeyCodes.tab: // Tab - move to next cell if row >= 0 && focusedColumn >= 1 { var nextColumn = focusedColumn + 1 var nextRow = row diff --git a/OpenTable/Views/Sidebar/SidebarView.swift b/OpenTable/Views/Sidebar/SidebarView.swift index 55e7b8ef0..8d88b3823 100644 --- a/OpenTable/Views/Sidebar/SidebarView.swift +++ b/OpenTable/Views/Sidebar/SidebarView.swift @@ -240,19 +240,19 @@ struct SidebarView: View { NSPasteboard.general.clearContents() NSPasteboard.general.setString(names.joined(separator: ","), forType: .string) } - .keyboardShortcut("c", modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.copy) Divider() Button("Truncate") { batchToggleTruncate() } - .keyboardShortcut(.delete, modifiers: .option) + .keyboardShortcut(KeyboardShortcuts.truncateTable) Button("Delete", role: .destructive) { batchToggleDelete() } - .keyboardShortcut(.delete, modifiers: .command) + .keyboardShortcut(KeyboardShortcuts.delete) } /// Batch toggle truncate for all selected tables diff --git a/README.md b/README.md index 090a4c8fb..6f43b3f57 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,55 @@ A native macOS database client built with SwiftUI. A fast, lightweight alternati ## Keyboard Shortcuts +All keyboard shortcuts follow macOS Human Interface Guidelines for consistency with native apps. + +### File Menu +| Shortcut | Action | +|----------|--------| +| `⌘N` | New connection | +| `⌘T` | New tab | +| `⌘W` | Close tab | +| `⌘S` | Save/commit changes | +| `⌘R` | Refresh data | + +### Edit Menu +| Shortcut | Action | +|----------|--------| +| `⌘Z` | Undo | +| `⌘⇧Z` | Redo | +| `⌘X` | Cut | +| `⌘C` | Copy | +| `⌘V` | Paste | +| `⌘A` | Select All | +| `⌘⌫` | Delete | +| `⌘I` | Add row | +| `⌘D` | Duplicate row | +| `⌥⌫` | Truncate table | +| `Escape` | Clear selection | + +### View Menu +| Shortcut | Action | +|----------|--------| +| `⌘B` | Toggle table browser | +| `⌘⌥B` | Toggle inspector | +| `⌘F` | Toggle filters | +| `⌘⇧H` | Toggle history | + +### Query Menu +| Shortcut | Action | +|----------|--------| +| `⌘↩` | Execute query | +| `⌘⇧L` | Format query | +| `⌃Space` | Trigger autocomplete | + +### Data Grid Navigation | Shortcut | Action | |----------|--------| -| `Cmd+Enter` | Execute query | -| `Cmd+S` | Save/commit changes | -| `Cmd+R` | Refresh data | -| `Cmd+W` | Close tab | -| `Cmd+N` | New connection | -| `Cmd+E` | Export to CSV | -| `Cmd+Shift+E` | Export to JSON | -| `Ctrl+Space` | Trigger autocomplete | +| `↩` | Edit cell | +| `Tab` | Next cell | +| `⇧Tab` | Previous cell | +| `↑↓←→` | Navigate cells | +| `⇧↑↓` | Extend selection | ## Requirements