diff --git a/CHANGELOG.md b/CHANGELOG.md index 4737d8292..4907169dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Running `EXPLAIN` or `EXPLAIN ANALYZE` typed in the editor now opens the plan viewer instead of squashing the plan into one truncated grid cell. (#1480) - Filtering the data grid keeps you on the keyboard. Applying or clearing a filter returns focus to the grid so you can keep moving through cells, Return applies the filter, and Escape closes the filter panel and returns to the grid. (#1490) +- Opening a table (Return or double-click in the sidebar) moves keyboard focus into the data grid so you can navigate cells with the arrow keys. Arrowing the sidebar still previews tables without taking focus. (#1490) - Moving a connection into or out of a group now syncs across devices, instead of leaving it ungrouped on your other Macs. - Opening a table on a connection with many tables no longer stalls for several seconds while autocomplete and table metadata load. Background schema introspection now runs on separate connections instead of waiting behind, or blocking, the query that fills the grid. (#1483) diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index 37c71314a..b00218ca2 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -357,8 +357,9 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi let activeTab = coordinator.tabManager.selectedTab if activeTab?.tabType == .table, activeTab?.tableContext.tableName == table.name { coordinator.promotePreviewTab() + coordinator.requestGridFocus() } else { - coordinator.openTableTab(table, forceNonPreview: true) + coordinator.openTableTab(table, forceNonPreview: true, activateGridFocus: true) } }, pendingTruncates: sessionPendingTruncatesBinding, diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index aaac701be..a1ab80011 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -110,7 +110,7 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate { // Auto-focus: make the editor first responder, then ensure a // cursor exists. Order matters — setCursorPositions calls // updateSelectionViews which guards on isFirstResponder. - if let window = textView.window { + if !self.isDestroyed, let window = textView.window { window.makeFirstResponder(textView) } if controller.cursorPositions.isEmpty { diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift index 034b724c0..ba06eb60a 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift @@ -9,4 +9,17 @@ internal extension MainContentCoordinator { func focusActiveGrid() { dataTabDelegate?.tableViewCoordinator?.focusGrid() } + + func consumePendingGridFocus() -> Bool { + guard pendingGridFocusOnOpen else { return false } + pendingGridFocusOnOpen = false + return true + } + + /// Focus the active grid now if it is already attached, otherwise defer to the grid + /// as it appears. Use for explicit-open gestures where the target grid may or may not + /// be rebuilt (e.g. promoting a preview tab). + func requestGridFocus() { + pendingGridFocusOnOpen = !(dataTabDelegate?.tableViewCoordinator?.focusGrid() ?? false) + } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index e7f8d3088..040c6c341 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -19,7 +19,8 @@ extension MainContentCoordinator { _ table: TableInfo, showStructure: Bool = false, redirectToSibling: Bool = false, - forceNonPreview: Bool = false + forceNonPreview: Bool = false, + activateGridFocus: Bool = false ) { openTableTab( table.name, @@ -27,7 +28,8 @@ extension MainContentCoordinator { showStructure: showStructure, isView: table.type == .view, redirectToSibling: redirectToSibling, - forceNonPreview: forceNonPreview + forceNonPreview: forceNonPreview, + activateGridFocus: activateGridFocus ) } @@ -37,7 +39,8 @@ extension MainContentCoordinator { showStructure: Bool = false, isView: Bool = false, redirectToSibling: Bool = false, - forceNonPreview: Bool = false + forceNonPreview: Bool = false, + activateGridFocus: Bool = false ) { let navigationModel = PluginMetadataRegistry.shared.snapshot( forTypeId: connection.type.pluginTypeId @@ -65,9 +68,16 @@ extension MainContentCoordinator { if showStructure, let (_, tabIndex) = tabManager.selectedTabAndIndex { tabManager.mutate(at: tabIndex) { $0.display.resultsViewMode = .structure } } + if activateGridFocus { + focusActiveGrid() + } return } + if activateGridFocus { + pendingGridFocusOnOpen = true + } + // During database switch, update the existing tab in-place instead of // opening a new native window tab. if case .loading = SchemaService.shared.state(for: connectionId) { @@ -82,6 +92,8 @@ extension MainContentCoordinator { } catch { navigationLogger.error("openTableTab addTableTab failed: \(error.localizedDescription, privacy: .public)") } + } else { + pendingGridFocusOnOpen = false } return } @@ -102,6 +114,7 @@ extension MainContentCoordinator { guard hasMatch, let windowId = sibling.windowId, let window = WindowLifecycleMonitor.shared.window(for: windowId) else { continue } + pendingGridFocusOnOpen = false window.makeKeyAndOrderFront(nil) return } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift index c3c7f1cca..998fa874b 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift @@ -15,10 +15,10 @@ extension MainContentCoordinator { func handleQuickSwitcherSelection(_ item: QuickSwitcherItem) { switch item.kind { case .table, .systemTable: - openTableTab(item.name, redirectToSibling: true) + openTableTab(item.name, redirectToSibling: true, activateGridFocus: true) case .view: - openTableTab(item.name, isView: true, redirectToSibling: true) + openTableTab(item.name, isView: true, redirectToSibling: true, activateGridFocus: true) case .database: Task { diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index cde7a4156..5b3ac7a7d 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -139,6 +139,10 @@ final class MainContentCoordinator { /// dispatch insertRows/removeRows directly to the NSTableView via DataGridViewDelegate. @ObservationIgnored weak var dataTabDelegate: DataTabGridDelegate? + /// One-shot intent set when the user explicitly opens a table (Return/double-click), + /// consumed by the grid as it appears to move focus into it. Never set on mere selection. + @ObservationIgnored var pendingGridFocusOnOpen = false + /// Proxy for toggling the inspector NSSplitViewItem from coordinator code @ObservationIgnored weak var inspectorProxy: InspectorVisibilityProxy? diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index ef781872b..6d731a81b 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -604,9 +604,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData } } - internal func focusGrid() { - guard let tableView, let window = tableView.window else { return } + @discardableResult + internal func focusGrid() -> Bool { + guard let tableView, let window = tableView.window else { return false } window.makeFirstResponder(tableView) + return true } func beginEditing(displayRow: Int, column: Int) { diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 3c72af83f..0e57de9e6 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -10,6 +10,14 @@ final class KeyHandlingTableView: NSTableView { true } + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + guard coordinator?.tabType == .table, let window, + let mainCoordinator = (coordinator?.delegate as? DataTabGridDelegate)?.coordinator, + mainCoordinator.consumePendingGridFocus() else { return } + window.makeFirstResponder(self) + } + override func didAddSubview(_ subview: NSView) { super.didAddSubview(subview) guard !isRaisingOverlay else { return } diff --git a/TablePro/Views/Sidebar/DatabaseTreeView.swift b/TablePro/Views/Sidebar/DatabaseTreeView.swift index 289174901..8d4ffdd4b 100644 --- a/TablePro/Views/Sidebar/DatabaseTreeView.swift +++ b/TablePro/Views/Sidebar/DatabaseTreeView.swift @@ -156,7 +156,7 @@ struct DatabaseTreeView: View { ) } primaryAction: { selection in guard let ref = selection.first else { return } - openTable(ref.table, in: ref.database, schema: ref.schema) + openTable(ref.table, in: ref.database, schema: ref.schema, activateGridFocus: true) } .onExitCommand { localSelection.removeAll() @@ -367,7 +367,7 @@ struct DatabaseTreeView: View { } } - private func openTable(_ table: TableInfo, in database: String, schema: String?) { + private func openTable(_ table: TableInfo, in database: String, schema: String?, activateGridFocus: Bool = false) { Task { @MainActor in if database != activeDatabase { await coordinator?.switchDatabase(to: database) @@ -377,7 +377,7 @@ struct DatabaseTreeView: View { PluginManager.shared.supportsSchemaSwitching(for: databaseType) { await coordinator?.switchSchema(to: schema) } - coordinator?.openTableTab(table) + coordinator?.openTableTab(table, activateGridFocus: activateGridFocus) } } diff --git a/TablePro/Views/Sidebar/FavoritesTabView.swift b/TablePro/Views/Sidebar/FavoritesTabView.swift index d8a2dbe8b..ec6b7c44a 100644 --- a/TablePro/Views/Sidebar/FavoritesTabView.swift +++ b/TablePro/Views/Sidebar/FavoritesTabView.swift @@ -215,7 +215,7 @@ internal struct FavoritesTabView: View { @ViewBuilder private func favoriteTableContextMenu(_ table: TableInfo) -> some View { Button(String(localized: "Open Table")) { - coordinator?.openTableTab(table) + coordinator?.openTableTab(table, activateGridFocus: true) } Button(String(localized: "Show ER Diagram")) { @@ -265,7 +265,7 @@ internal struct FavoritesTabView: View { switch selection { case .table(let database, let schema, let name): if let table = favoriteTable(database: database, schema: schema, name: name) { - coordinator?.openTableTab(table) + coordinator?.openTableTab(table, activateGridFocus: true) } case .node(let id): guard let node = viewModel.node(forId: id) else { return } diff --git a/TablePro/Views/Sidebar/SidebarContextMenu.swift b/TablePro/Views/Sidebar/SidebarContextMenu.swift index bae5a89bb..997f4c619 100644 --- a/TablePro/Views/Sidebar/SidebarContextMenu.swift +++ b/TablePro/Views/Sidebar/SidebarContextMenu.swift @@ -112,7 +112,7 @@ struct SidebarContextMenu: View { Button("Show Structure") { perform { if let clickedTable { - coordinator?.openTableTab(clickedTable, showStructure: true) + coordinator?.openTableTab(clickedTable, showStructure: true, activateGridFocus: true) } } }