From a9df84b1c87bf860a3ce35e8f120fd48e6631cef Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 30 May 2026 03:56:32 +0700 Subject: [PATCH 1/3] fix(datagrid): focus the grid when a table tab opens (#1490) --- CHANGELOG.md | 1 + TablePro/Views/Editor/SQLEditorCoordinator.swift | 2 +- TablePro/Views/Results/KeyHandlingTableView.swift | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4737d8292..2ccdd80ee 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 now focuses the data grid, so you can move through cells with the arrow keys without clicking first. (#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/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/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 3c72af83f..8bb7fa2b8 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 else { return } + let current = window.firstResponder + guard current == nil || current === window else { return } + window.makeFirstResponder(self) + } + override func didAddSubview(_ subview: NSView) { super.didAddSubview(subview) guard !isRaisingOverlay else { return } From 372886e1d99cc98093d8744a0855484d681c8b97 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 30 May 2026 04:22:48 +0700 Subject: [PATCH 2/3] fix(datagrid): move focus to grid on explicit table open, not on appear (#1490) --- CHANGELOG.md | 2 +- .../Infrastructure/MainSplitViewController.swift | 3 ++- .../MainContentCoordinator+GridFocus.swift | 6 ++++++ .../MainContentCoordinator+Navigation.swift | 16 +++++++++++++--- TablePro/Views/Main/MainContentCoordinator.swift | 4 ++++ .../Views/Results/KeyHandlingTableView.swift | 6 +++--- TablePro/Views/Sidebar/DatabaseTreeView.swift | 6 +++--- TablePro/Views/Sidebar/FavoritesTabView.swift | 4 ++-- TablePro/Views/Sidebar/SidebarContextMenu.swift | 2 +- 9 files changed, 35 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ccdd80ee..4907169dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +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 now focuses the data grid, so you can move through cells with the arrow keys without clicking first. (#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..bfdbdc035 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.focusActiveGrid() } else { - coordinator.openTableTab(table, forceNonPreview: true) + coordinator.openTableTab(table, forceNonPreview: true, activateGridFocus: true) } }, pendingTruncates: sessionPendingTruncatesBinding, diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift index 034b724c0..28cf0c968 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift @@ -9,4 +9,10 @@ internal extension MainContentCoordinator { func focusActiveGrid() { dataTabDelegate?.tableViewCoordinator?.focusGrid() } + + func consumePendingGridFocus() -> Bool { + guard pendingGridFocusOnOpen else { return false } + pendingGridFocusOnOpen = false + return true + } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index e7f8d3088..a111cb147 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) { 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/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 8bb7fa2b8..0e57de9e6 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -12,9 +12,9 @@ final class KeyHandlingTableView: NSTableView { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() - guard coordinator?.tabType == .table, let window else { return } - let current = window.firstResponder - guard current == nil || current === window else { return } + guard coordinator?.tabType == .table, let window, + let mainCoordinator = (coordinator?.delegate as? DataTabGridDelegate)?.coordinator, + mainCoordinator.consumePendingGridFocus() else { return } window.makeFirstResponder(self) } 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) } } } From 5d3f7e211350e86179cfffea9ef76c7789d0e13d Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 30 May 2026 04:33:02 +0700 Subject: [PATCH 3/3] refactor(datagrid): harden grid-focus-on-open, cover quick switcher, bound stale intent (#1490) --- .../Services/Infrastructure/MainSplitViewController.swift | 2 +- .../Main/Extensions/MainContentCoordinator+GridFocus.swift | 7 +++++++ .../Extensions/MainContentCoordinator+Navigation.swift | 3 +++ .../Extensions/MainContentCoordinator+QuickSwitcher.swift | 4 ++-- TablePro/Views/Results/DataGridCoordinator.swift | 6 ++++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index bfdbdc035..b00218ca2 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -357,7 +357,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi let activeTab = coordinator.tabManager.selectedTab if activeTab?.tabType == .table, activeTab?.tableContext.tableName == table.name { coordinator.promotePreviewTab() - coordinator.focusActiveGrid() + coordinator.requestGridFocus() } else { coordinator.openTableTab(table, forceNonPreview: true, activateGridFocus: true) } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift index 28cf0c968..ba06eb60a 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+GridFocus.swift @@ -15,4 +15,11 @@ internal extension MainContentCoordinator { 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 a111cb147..040c6c341 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -92,6 +92,8 @@ extension MainContentCoordinator { } catch { navigationLogger.error("openTableTab addTableTab failed: \(error.localizedDescription, privacy: .public)") } + } else { + pendingGridFocusOnOpen = false } return } @@ -112,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/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) {