diff --git a/CHANGELOG.md b/CHANGELOG.md index 2371f1000..0800a9c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,9 +29,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Copy rows writes TSV, HTML table, and plain text to the clipboard for richer paste in spreadsheet apps - Row drag adds TSV and HTML representations alongside the internal drag type - AI provider settings allow manually entering a model name when the provider does not return one +- Edit menu has a Find submenu with Find, Find Next (`Cmd+G`), Find Previous (`Cmd+Shift+G`), Use Selection for Find (`Cmd+E`), and Jump to Selection (`Cmd+J`) +- File menu has a New Window item (`Cmd+Ctrl+N`) that opens a fresh editor window for the active connection ### Changed +- `Cmd+N` now opens the New Connection form. Manage Connections moves to the File menu without a default shortcut. +- `Cmd+D` now duplicates the selected row. Save as Favorite moves to `Cmd+Shift+D`. +- Removed default shortcuts that conflicted with system reservations: `Cmd+Y` (Quick Look), `Cmd+Option+Delete` (Empty Trash), `Cmd+Ctrl+C` (Color Picker), `Cmd+L` (URL bar / Add Link). +- Show History moves from `Cmd+Y` (system Quick Look) to `Cmd+Shift+Y` to free the system shortcut while keeping the Y mnemonic. (`Cmd+Option+H` is reserved by macOS for Hide Others, so we avoid that chord too.) +- Truncate Table no longer has a default shortcut; the action stays in the Edit menu and the sidebar context menu. +- Switch Connection no longer has a default shortcut; the menu entry remains and users can rebind in Settings > Keyboard. +- Explain with AI no longer has a default shortcut; the menu entry remains and users can rebind in Settings > Keyboard. +- File menu "Save Changes" renamed to "Save". +- View menu Show/Hide labels now flip based on panel state (Show Sidebar / Hide Sidebar, Show Inspector / Hide Inspector, Show Filters / Hide Filters, Show History / Hide History, Show Results / Hide Results). - MCP server lazy-starts on first external request. Manual enable in Settings is no longer required - Settings tab renamed from "MCP" to "Integrations" with new sections for connected clients, activity log, and pairing - Integrations settings: rename MCP Server section to Integrations, restructure with searchable activity log, native list with keyboard navigation, accessibility labels, color-blind-safe status icons. diff --git a/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Controller/TextViewController+FindPanelTarget.swift b/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Controller/TextViewController+FindPanelTarget.swift index 9631b3bf7..467c46f3b 100644 --- a/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Controller/TextViewController+FindPanelTarget.swift +++ b/LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Controller/TextViewController+FindPanelTarget.swift @@ -35,4 +35,16 @@ public extension TextViewController { _ = textView.resignFirstResponder() findViewController?.showFindPanel() } + + func findNextMatch() { + findViewController?.viewModel.moveToNextMatch() + } + + func findPreviousMatch() { + findViewController?.viewModel.moveToPreviousMatch() + } + + func setFindText(_ text: String) { + findViewController?.viewModel.findText = text + } } diff --git a/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift b/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift index 5b8b08948..6ecaa259d 100644 --- a/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift +++ b/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift @@ -454,7 +454,7 @@ private struct HistoryToolbarButton: View { } label: { Label("History", systemImage: "clock") } - .help(String(localized: "Toggle Query History (⌘Y)")) + .help(String(localized: "Toggle Query History (⇧⌘Y)")) } } diff --git a/TablePro/Models/UI/KeyboardShortcutModels.swift b/TablePro/Models/UI/KeyboardShortcutModels.swift index caa67dc57..825bd1850 100644 --- a/TablePro/Models/UI/KeyboardShortcutModels.swift +++ b/TablePro/Models/UI/KeyboardShortcutModels.swift @@ -36,6 +36,8 @@ enum ShortcutCategory: String, Codable, CaseIterable, Identifiable { /// All customizable keyboard shortcut actions enum ShortcutAction: String, Codable, CaseIterable, Identifiable { // File + case newConnection + case newWindow case manageConnections case newTab case openDatabase @@ -70,6 +72,11 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable { case delete case selectAll case clearSelection + case find + case findNext + case findPrevious + case useSelectionForFind + case jumpToSelection case addRow case duplicateRow case truncateTable @@ -98,14 +105,15 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable { var category: ShortcutCategory { switch self { - case .manageConnections, .newTab, .openDatabase, .openFile, .switchConnection, - .saveChanges, .saveAs, .previewSQL, .closeTab, .refresh, + case .newConnection, .newWindow, .manageConnections, .newTab, .openDatabase, .openFile, + .switchConnection, .saveChanges, .saveAs, .previewSQL, .closeTab, .refresh, .executeQuery, .explainQuery, .formatQuery, .export, .importData, .quickSwitcher, .previousPage, .nextPage, .saveAsFavorite, .openTerminal: return .file case .undo, .redo, .cut, .copy, .copyWithHeaders, .copyAsJson, .paste, - .delete, .selectAll, .clearSelection, .addRow, - .duplicateRow, .truncateTable, .previewFKReference: + .delete, .selectAll, .clearSelection, + .find, .findNext, .findPrevious, .useSelectionForFind, .jumpToSelection, + .addRow, .duplicateRow, .truncateTable, .previewFKReference: return .edit case .toggleTableBrowser, .toggleInspector, .toggleFilters, .toggleHistory, .toggleResults, .previousResultTab, .nextResultTab, .closeResultTab: @@ -119,13 +127,15 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable { var displayName: String { switch self { + case .newConnection: return String(localized: "New Connection") + case .newWindow: return String(localized: "New Window") case .manageConnections: return String(localized: "Manage Connections") case .executeQuery: return String(localized: "Execute Query") case .newTab: return String(localized: "New Tab") case .openDatabase: return String(localized: "Open Database") case .openFile: return String(localized: "Open File") case .switchConnection: return String(localized: "Switch Connection") - case .saveChanges: return String(localized: "Save Changes") + case .saveChanges: return String(localized: "Save") case .saveAs: return String(localized: "Save As") case .previewSQL: return String(localized: "Preview SQL") case .closeTab: return String(localized: "Close Tab") @@ -148,16 +158,21 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable { case .delete: return String(localized: "Delete") case .selectAll: return String(localized: "Select All") case .clearSelection: return String(localized: "Clear Selection") + case .find: return String(localized: "Find") + case .findNext: return String(localized: "Find Next") + case .findPrevious: return String(localized: "Find Previous") + case .useSelectionForFind: return String(localized: "Use Selection for Find") + case .jumpToSelection: return String(localized: "Jump to Selection") case .addRow: return String(localized: "Add Row") case .duplicateRow: return String(localized: "Duplicate Row") case .truncateTable: return String(localized: "Truncate Table") case .previewFKReference: return String(localized: "Preview FK Reference") case .saveAsFavorite: return String(localized: "Save as Favorite") - case .toggleTableBrowser: return String(localized: "Toggle Table Browser") - case .toggleInspector: return String(localized: "Toggle Inspector") - case .toggleFilters: return String(localized: "Toggle Filters") - case .toggleHistory: return String(localized: "Toggle History") - case .toggleResults: return String(localized: "Toggle Results") + case .toggleTableBrowser: return String(localized: "Show Sidebar") + case .toggleInspector: return String(localized: "Show Inspector") + case .toggleFilters: return String(localized: "Show Filters") + case .toggleHistory: return String(localized: "Show History") + case .toggleResults: return String(localized: "Show Results") case .previousResultTab: return String(localized: "Previous Result") case .nextResultTab: return String(localized: "Next Result") case .closeResultTab: return String(localized: "Close Result Tab") @@ -457,12 +472,12 @@ struct KeyboardSettings: Codable, Equatable { /// Default shortcuts — applied when user has no overrides static let defaultShortcuts: [ShortcutAction: KeyCombo] = [ // File - .manageConnections: KeyCombo(key: "n", command: true), + .newConnection: KeyCombo(key: "n", command: true), + .newWindow: KeyCombo(key: "n", command: true, control: true), .executeQuery: KeyCombo(key: "return", command: true, isSpecialKey: true), .newTab: KeyCombo(key: "t", command: true), .openDatabase: KeyCombo(key: "k", command: true), .openFile: KeyCombo(key: "o", command: true), - .switchConnection: KeyCombo(key: "c", command: true, control: true), .saveChanges: KeyCombo(key: "s", command: true), .saveAs: KeyCombo(key: "s", command: true, shift: true), .previewSQL: KeyCombo(key: "p", command: true, shift: true), @@ -488,17 +503,21 @@ struct KeyboardSettings: Codable, Equatable { .delete: KeyCombo(key: "delete", command: true, isSpecialKey: true), .selectAll: KeyCombo(key: "a", command: true), .clearSelection: KeyCombo(key: "escape", isSpecialKey: true), + .find: KeyCombo(key: "f", command: true), + .findNext: KeyCombo(key: "g", command: true), + .findPrevious: KeyCombo(key: "g", command: true, shift: true), + .useSelectionForFind: KeyCombo(key: "e", command: true), + .jumpToSelection: KeyCombo(key: "j", command: true), .addRow: KeyCombo(key: "n", command: true, shift: true), - .duplicateRow: KeyCombo(key: "d", command: true, shift: true), - .truncateTable: KeyCombo(key: "delete", option: true, isSpecialKey: true), + .duplicateRow: KeyCombo(key: "d", command: true), .previewFKReference: KeyCombo(key: "space", isSpecialKey: true), - .saveAsFavorite: KeyCombo(key: "d", command: true), + .saveAsFavorite: KeyCombo(key: "d", command: true, shift: true), // View .toggleTableBrowser: KeyCombo(key: "0", command: true), .toggleInspector: KeyCombo(key: "i", command: true, option: true), .toggleFilters: KeyCombo(key: "f", command: true, shift: true), - .toggleHistory: KeyCombo(key: "y", command: true), + .toggleHistory: KeyCombo(key: "y", command: true, shift: true), .toggleResults: KeyCombo(key: "r", command: true, option: true), .previousResultTab: KeyCombo(key: "[", command: true, option: true), .nextResultTab: KeyCombo(key: "]", command: true, option: true), @@ -509,7 +528,6 @@ struct KeyboardSettings: Codable, Equatable { .showNextTab: KeyCombo(key: "]", command: true, shift: true), // AI - .aiExplainQuery: KeyCombo(key: "l", command: true), .aiOptimizeQuery: KeyCombo(key: "l", command: true, option: true), ] } diff --git a/TablePro/TableProApp.swift b/TablePro/TableProApp.swift index e56be1bbd..e85e26ad2 100644 --- a/TablePro/TableProApp.swift +++ b/TablePro/TableProApp.swift @@ -122,6 +122,18 @@ struct AppMenuCommands: Commands { settingsManager.keyboard.keyboardShortcut(for: action) } + private func openNewMainWindow() { + let connectionId = actions?.connectionId + ?? NSApp.keyWindow.flatMap { WindowLifecycleMonitor.shared.connectionId(forWindow: $0) } + guard let connectionId else { + WelcomeWindowFactory.openOrFront() + return + } + WindowManager.shared.openTab( + payload: EditorTabPayload(connectionId: connectionId, intent: .newEmptyTab) + ) + } + /// Prefers the focused scene value; falls back to the coordinator back-reference /// so Cmd+W still routes through `closeTab()` (with its unsaved-changes dialog) /// when focus is inside an AppKit subview and `@FocusedValue` has not resolved. @@ -195,10 +207,15 @@ struct AppMenuCommands: Commands { // File menu CommandGroup(replacing: .newItem) { - Button("Manage Connections") { - WelcomeWindowFactory.openOrFront() + Button(String(localized: "New Connection...")) { + ConnectionFormWindowFactory.openOrFront() } - .optionalKeyboardShortcut(shortcut(for: .manageConnections)) + .optionalKeyboardShortcut(shortcut(for: .newConnection)) + + Button(String(localized: "New Window")) { + openNewMainWindow() + } + .optionalKeyboardShortcut(shortcut(for: .newWindow)) } CommandGroup(after: .newItem) { @@ -213,6 +230,11 @@ struct AppMenuCommands: Commands { } .disabled(!(actions?.isConnected ?? false) || actions?.isReadOnly ?? false) + Button(String(localized: "Manage Connections...")) { + WelcomeWindowFactory.openOrFront() + } + .optionalKeyboardShortcut(shortcut(for: .manageConnections)) + Button("Open Database...") { actions?.openDatabaseSwitcher() } @@ -227,7 +249,7 @@ struct AppMenuCommands: Commands { Divider() - Button("Save Changes") { + Button(String(localized: "Save")) { actions?.saveChanges() } .optionalKeyboardShortcut(shortcut(for: .saveChanges)) @@ -423,14 +445,38 @@ struct AppMenuCommands: Commands { // Edit menu - pasteboard commands with FocusedValue support PasteboardCommands(settingsManager: settingsManager) - // Edit menu - Find + row operations (after pasteboard) + // Edit menu - Find submenu + row operations (after pasteboard) CommandGroup(after: .pasteboard) { Divider() - Button(String(localized: "Find...")) { - EditorEventRouter.shared.showFindPanelForKeyWindow() + Menu(String(localized: "Find")) { + Button(String(localized: "Find...")) { + EditorEventRouter.shared.showFindPanelForKeyWindow() + } + .optionalKeyboardShortcut(shortcut(for: .find)) + + Button(String(localized: "Find Next")) { + EditorEventRouter.shared.findNextForKeyWindow() + } + .optionalKeyboardShortcut(shortcut(for: .findNext)) + + Button(String(localized: "Find Previous")) { + EditorEventRouter.shared.findPreviousForKeyWindow() + } + .optionalKeyboardShortcut(shortcut(for: .findPrevious)) + + Divider() + + Button(String(localized: "Use Selection for Find")) { + EditorEventRouter.shared.useSelectionForFindForKeyWindow() + } + .optionalKeyboardShortcut(shortcut(for: .useSelectionForFind)) + + Button(String(localized: "Jump to Selection")) { + EditorEventRouter.shared.jumpToSelectionForKeyWindow() + } + .optionalKeyboardShortcut(shortcut(for: .jumpToSelection)) } - .keyboardShortcut("f", modifiers: .command) Divider() @@ -458,12 +504,16 @@ struct AppMenuCommands: Commands { // View menu CommandGroup(after: .sidebar) { - Button(String(localized: "Toggle Sidebar")) { + Button(actions?.isSidebarVisible == true + ? String(localized: "Hide Sidebar") + : String(localized: "Show Sidebar")) { NSApp.sendAction(#selector(NSSplitViewController.toggleSidebar(_:)), to: nil, from: nil) } .optionalKeyboardShortcut(shortcut(for: .toggleTableBrowser)) - Button("Toggle Inspector") { + Button(actions?.isInspectorVisible == true + ? String(localized: "Hide Inspector") + : String(localized: "Show Inspector")) { actions?.toggleRightSidebar() } .optionalKeyboardShortcut(shortcut(for: .toggleInspector)) @@ -471,13 +521,17 @@ struct AppMenuCommands: Commands { Divider() - Button("Toggle Filters") { + Button(actions?.isFilterPanelVisible == true + ? String(localized: "Hide Filters") + : String(localized: "Show Filters")) { actions?.toggleFilterPanel() } .optionalKeyboardShortcut(shortcut(for: .toggleFilters)) .disabled(!(actions?.isConnected ?? false) || !(actions?.isTableTab ?? false)) - Button("Toggle History") { + Button(actions?.isHistoryPanelVisible == true + ? String(localized: "Hide History") + : String(localized: "Show History")) { actions?.toggleHistoryPanel() } .optionalKeyboardShortcut(shortcut(for: .toggleHistory)) @@ -485,7 +539,9 @@ struct AppMenuCommands: Commands { Divider() - Button("Toggle Results") { + Button(actions?.isResultsVisible == true + ? String(localized: "Hide Results") + : String(localized: "Show Results")) { actions?.toggleResults() } .optionalKeyboardShortcut(shortcut(for: .toggleResults)) diff --git a/TablePro/Views/Editor/EditorEventRouter.swift b/TablePro/Views/Editor/EditorEventRouter.swift index da81d58f0..36e80f852 100644 --- a/TablePro/Views/Editor/EditorEventRouter.swift +++ b/TablePro/Views/Editor/EditorEventRouter.swift @@ -95,6 +95,26 @@ internal final class EditorEventRouter { coordinator.showFindPanel() } + internal func findNextForKeyWindow() { + guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return } + coordinator.findNextMatch() + } + + internal func findPreviousForKeyWindow() { + guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return } + coordinator.findPreviousMatch() + } + + internal func useSelectionForFindForKeyWindow() { + guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return } + coordinator.useSelectionForFind() + } + + internal func jumpToSelectionForKeyWindow() { + guard let (coordinator, _) = editor(for: NSApp.keyWindow) else { return } + coordinator.jumpToSelection() + } + // MARK: - Lookup private func editor(for window: NSWindow?) -> (SQLEditorCoordinator, TextView)? { diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index 3214a2dd0..a29e7ca96 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -526,6 +526,31 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate { controller?.showFindPanel() } + func findNextMatch() { + controller?.findNextMatch() + } + + func findPreviousMatch() { + controller?.findPreviousMatch() + } + + func useSelectionForFind() { + guard let controller, let textView = controller.textView else { return } + let range = textView.selectedRange() + guard range.length > 0 else { return } + let selected = (textView.string as NSString).substring(with: range) + guard !selected.isEmpty else { return } + controller.setFindText(selected) + controller.showFindPanel() + } + + func jumpToSelection() { + guard let controller, let textView = controller.textView else { return } + let range = textView.selectedRange() + guard range.location != NSNotFound else { return } + textView.scrollToRange(range) + } + // MARK: - CodeEditSourceEditor Workarounds /// Reorder FindViewController's subviews so the find panel is on top for hit testing. diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index f22f2bcff..428af0a6d 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -251,6 +251,8 @@ final class MainContentCommandActions { var currentDatabaseType: DatabaseType { connection.type } + var connectionId: UUID { connection.id } + var supportsDatabaseSwitching: Bool { PluginManager.shared.supportsDatabaseSwitching(for: connection.type) } @@ -292,6 +294,28 @@ final class MainContentCommandActions { coordinator?.toolbarState.hasStructureChanges ?? false } + var isSidebarVisible: Bool { + guard let collapsed = coordinator?.splitViewController?.isSidebarCollapsed else { return false } + return !collapsed + } + + var isInspectorVisible: Bool { + coordinator?.inspectorProxy?.isInspectorVisible ?? false + } + + var isFilterPanelVisible: Bool { + filterStateManager.isVisible + } + + var isHistoryPanelVisible: Bool { + coordinator?.toolbarState.isHistoryPanelVisible ?? false + } + + var isResultsVisible: Bool { + guard let collapsed = coordinator?.toolbarState.isResultsCollapsed else { return false } + return !collapsed + } + // MARK: - Unsaved Changes Check private var hasUnsavedChanges: Bool { diff --git a/TablePro/Views/Toolbar/TableProToolbarView.swift b/TablePro/Views/Toolbar/TableProToolbarView.swift index 1d13af062..8e58c5d55 100644 --- a/TablePro/Views/Toolbar/TableProToolbarView.swift +++ b/TablePro/Views/Toolbar/TableProToolbarView.swift @@ -207,7 +207,7 @@ struct TableProToolbar: ViewModifier { } label: { Label("History", systemImage: "clock") } - .help(String(localized: "Toggle Query History (⌘Y)")) + .help(String(localized: "Toggle Query History (⇧⌘Y)")) Button { actions?.exportTables() diff --git a/docs/features/keyboard-shortcuts.mdx b/docs/features/keyboard-shortcuts.mdx index b4877fc81..58773dda1 100644 --- a/docs/features/keyboard-shortcuts.mdx +++ b/docs/features/keyboard-shortcuts.mdx @@ -15,7 +15,8 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut |--------|----------| | Execute query | `Cmd+Enter` | | New connection | `Cmd+N` | -| Open history | `Cmd+Y` | +| New window | `Cmd+Ctrl+N` | +| Show history | `Cmd+Shift+Y` | | Settings | `Cmd+,` | | Quick Switcher | `Cmd+Shift+O` | | Close window | `Cmd+W` | @@ -71,9 +72,10 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut | Action | Shortcut | |--------|----------| | Find | `Cmd+F` | -| Find and replace | `Cmd+Option+F` | | Find next | `Cmd+G` | | Find previous | `Cmd+Shift+G` | +| Use Selection for Find | `Cmd+E` | +| Jump to Selection | `Cmd+J` | ### Selection @@ -104,7 +106,7 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut | Preview FK reference | `Space` | | Cancel edit | `Escape` | | Add row | `Cmd+Shift+N` | -| Duplicate row | `Cmd+Shift+D` | +| Duplicate row | `Cmd+D` | | Delete row | `Delete` or `Backspace` | | Commit changes | `Cmd+S` | @@ -159,7 +161,9 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut | Action | Shortcut | |--------|----------| | New connection | `Cmd+N` | -| Switch connection | `Cmd+Control+C` | +| New window | `Cmd+Ctrl+N` | +| Manage connections | None (menu only) | +| Switch connection | None (menu only) | | Refresh connection | `Cmd+R` | | Delete selected connections | `Cmd+Delete` | @@ -176,9 +180,9 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut | Action | Shortcut | |--------|----------| -| Query History | `Cmd+Y` | -| Toggle Cell Inspector | `Cmd+Option+I` | -| Toggle Results | `Cmd+Option+R` | +| Show / Hide History | `Cmd+Shift+Y` | +| Show / Hide Inspector | `Cmd+Option+I` | +| Show / Hide Results | `Cmd+Option+R` | | Open Terminal | `Ctrl+Cmd+`` | | Settings | `Cmd+,` | @@ -194,7 +198,7 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut | Action | Shortcut | Description | |--------|----------|-------------| -| Explain with AI | `Cmd+L` | Send current query to AI for explanation | +| Explain with AI | None (menu only) | Send current query to AI for explanation | | Optimize with AI | `Cmd+Option+L` | Send current query to AI for optimization | ## ER Diagram