From 9dd96d1a09d089d8eb88b74ae317e055afc1f48b Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Mon, 27 Apr 2026 20:28:43 +0700 Subject: [PATCH] refactor: move selectedRowIndices to @Observable GridSelectionState on coordinator --- CHANGELOG.md | 1 + TablePro/Models/Query/QueryTabState.swift | 5 ++ .../Main/Child/DataTabGridDelegate.swift | 7 +-- .../Main/Child/MainEditorContentView.swift | 19 +++++--- ...MainContentCoordinator+RowOperations.swift | 36 ++++++-------- .../MainContentCoordinator+SidebarSave.swift | 6 +-- .../MainContentCoordinator+TabSwitch.swift | 3 +- .../Extensions/MainContentView+Bindings.swift | 9 ++-- .../MainContentView+EventHandlers.swift | 16 +++---- .../Extensions/MainContentView+Helpers.swift | 11 +++-- .../Extensions/MainContentView+Setup.swift | 2 +- .../Main/MainContentCommandActions.swift | 47 +++++++------------ .../Views/Main/MainContentCoordinator.swift | 5 +- TablePro/Views/Main/MainContentView.swift | 30 +++++------- 14 files changed, 88 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c7be50e..c913815e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Row selection state moved from MainContentView @State to GridSelectionState @Observable class, preventing full view tree invalidation on every row click - QueryTab decomposed into focused sub-types: TabExecutionState, TabTableContext, TabQueryContent, TabDisplayState - Inspector: dense field list, PK/FK key icons, Set NULL/DEFAULT in picker dropdowns, toolbar tracking separator - Feedback dialog uses NSWindow instead of NSPanel, shortcut recorder uses CALayer, query split uses NSSplitViewController diff --git a/TablePro/Models/Query/QueryTabState.swift b/TablePro/Models/Query/QueryTabState.swift index 0d83edb76..a61971f63 100644 --- a/TablePro/Models/Query/QueryTabState.swift +++ b/TablePro/Models/Query/QueryTabState.swift @@ -5,6 +5,11 @@ import Foundation +@MainActor @Observable +final class GridSelectionState { + var indices: Set = [] +} + /// Type of tab enum TabType: Equatable, Codable, Hashable { case query // SQL editor tab diff --git a/TablePro/Views/Main/Child/DataTabGridDelegate.swift b/TablePro/Views/Main/Child/DataTabGridDelegate.swift index bba44f74d..02f0105e9 100644 --- a/TablePro/Views/Main/Child/DataTabGridDelegate.swift +++ b/TablePro/Views/Main/Child/DataTabGridDelegate.swift @@ -14,7 +14,7 @@ final class DataTabGridDelegate: DataGridViewDelegate { weak var coordinator: MainContentCoordinator? var columnVisibilityManager: ColumnVisibilityManager? - var selectedRowIndices: Binding>? + var selectionState: GridSelectionState? var editingCell: Binding? var onCellEdit: ((Int, Int, String?) -> Void)? @@ -71,10 +71,7 @@ final class DataTabGridDelegate: DataGridViewDelegate { } func dataGridUndo() { - guard let selectedRowIndices else { return } - var indices = selectedRowIndices.wrappedValue - coordinator?.undoLastChange(selectedRowIndices: &indices) - selectedRowIndices.wrappedValue = indices + coordinator?.undoLastChange() } func dataGridRedo() { diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index d5dadaba6..ba15fd81a 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -39,9 +39,9 @@ struct MainEditorContentView: View { let windowId: UUID let connectionId: UUID - // MARK: - Bindings + // MARK: - Selection State - @Binding var selectedRowIndices: Set + let selectionState: GridSelectionState @Binding var editingCell: CellPosition? // MARK: - Callbacks @@ -50,6 +50,7 @@ struct MainEditorContentView: View { let onSort: (Int, Bool, Bool) -> Void let onAddRow: () -> Void let onUndoInsert: (Int) -> Void + let onSelectionChange: (Set) -> Void let onFilterColumn: (String) -> Void let onApplyFilters: ([TableFilter]) -> Void let onClearFilters: () -> Void @@ -173,6 +174,9 @@ struct MainEditorContentView: View { guard let tab = tabManager.selectedTab else { return } cacheRowProvider(for: tab) } + .onChange(of: selectionState.indices) { _, newIndices in + onSelectionChange(newIndices) + } } // MARK: - Tab Content @@ -407,7 +411,7 @@ struct MainEditorContentView: View { columns: tab.resultColumns, columnTypes: tab.columnTypes, rows: tab.resultRows, - selectedRowIndices: selectedRowIndices + selectedRowIndices: selectionState.indices ) case .data: if let explainText = tab.display.explainText { @@ -526,7 +530,7 @@ struct MainEditorContentView: View { let _ = { // swiftlint:disable:this redundant_discardable_let dataTabDelegate.coordinator = coordinator dataTabDelegate.columnVisibilityManager = columnVisibilityManager - dataTabDelegate.selectedRowIndices = $selectedRowIndices + dataTabDelegate.selectionState = selectionState dataTabDelegate.editingCell = $editingCell dataTabDelegate.onCellEdit = onCellEdit dataTabDelegate.onSort = onSort @@ -553,7 +557,10 @@ struct MainEditorContentView: View { hiddenColumns: columnVisibilityManager.hiddenColumns ), delegate: dataTabDelegate, - selectedRowIndices: $selectedRowIndices, + selectedRowIndices: Binding( + get: { selectionState.indices }, + set: { selectionState.indices = $0 } + ), sortState: sortStateBinding(for: tab), editingCell: $editingCell, columnLayout: columnLayoutBinding(for: tab) @@ -804,7 +811,7 @@ struct MainEditorContentView: View { filterStateManager: filterStateManager, columnVisibilityManager: columnVisibilityManager, allColumns: tab.resultColumns, - selectedRowIndices: selectedRowIndices, + selectedRowIndices: selectionState.indices, viewMode: resultsViewModeBinding(for: tab), onFirstPage: onFirstPage, onPreviousPage: onPreviousPage, diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+RowOperations.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+RowOperations.swift index ca1766cb3..90c9f38b8 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+RowOperations.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+RowOperations.swift @@ -10,7 +10,7 @@ import Foundation extension MainContentCoordinator { // MARK: - Row Operations - func addNewRow(selectedRowIndices: inout Set, editingCell: inout CellPosition?) { + func addNewRow(editingCell: inout CellPosition?) { guard !safeModeLevel.blocksAllWrites, let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count else { return } @@ -24,13 +24,13 @@ extension MainContentCoordinator { resultRows: &tabManager.tabs[tabIndex].resultRows ) else { return } - selectedRowIndices = [result.rowIndex] + selectionState.indices = [result.rowIndex] editingCell = CellPosition(row: result.rowIndex, column: 0) tabManager.tabs[tabIndex].hasUserInteraction = true tabManager.tabs[tabIndex].resultVersion += 1 } - func deleteSelectedRows(indices: Set, selectedRowIndices: inout Set) { + func deleteSelectedRows(indices: Set) { guard !safeModeLevel.blocksAllWrites, let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count, @@ -43,16 +43,16 @@ extension MainContentCoordinator { ) if nextRow >= 0 && nextRow < tabManager.tabs[tabIndex].resultRows.count { - selectedRowIndices = [nextRow] + selectionState.indices = [nextRow] } else { - selectedRowIndices.removeAll() + selectionState.indices.removeAll() } tabManager.tabs[tabIndex].hasUserInteraction = true tabManager.tabs[tabIndex].resultVersion += 1 } - func duplicateSelectedRow(index: Int, selectedRowIndices: inout Set, editingCell: inout CellPosition?) { + func duplicateSelectedRow(index: Int, editingCell: inout CellPosition?) { guard !safeModeLevel.blocksAllWrites, let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count else { return } @@ -67,32 +67,32 @@ extension MainContentCoordinator { resultRows: &tabManager.tabs[tabIndex].resultRows ) else { return } - selectedRowIndices = [result.rowIndex] + selectionState.indices = [result.rowIndex] editingCell = CellPosition(row: result.rowIndex, column: 0) tabManager.tabs[tabIndex].hasUserInteraction = true tabManager.tabs[tabIndex].resultVersion += 1 } - func undoInsertRow(at rowIndex: Int, selectedRowIndices: inout Set) { + func undoInsertRow(at rowIndex: Int) { guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count else { return } - selectedRowIndices = rowOperationsManager.undoInsertRow( + selectionState.indices = rowOperationsManager.undoInsertRow( at: rowIndex, resultRows: &tabManager.tabs[tabIndex].resultRows, - selectedIndices: selectedRowIndices + selectedIndices: selectionState.indices ) tabManager.tabs[tabIndex].resultVersion += 1 } - func undoLastChange(selectedRowIndices: inout Set) { + func undoLastChange() { guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count else { return } if let adjustedSelection = rowOperationsManager.undoLastChange( resultRows: &tabManager.tabs[tabIndex].resultRows ) { - selectedRowIndices = adjustedSelection + selectionState.indices = adjustedSelection } tabManager.tabs[tabIndex].hasUserInteraction = true @@ -153,13 +153,12 @@ extension MainContentCoordinator { ClipboardService.shared.writeText(converter.generateJson(rows: rows)) } - func pasteRows(selectedRowIndices: inout Set, editingCell: inout CellPosition?) { + func pasteRows(editingCell: inout CellPosition?) { guard !safeModeLevel.blocksAllWrites, let index = tabManager.selectedTabIndex else { return } var tab = tabManager.tabs[index] - // Only paste in table tabs (not query tabs) guard tab.tabType == .table else { return } let pastedRows = rowOperationsManager.pasteRowsFromClipboard( @@ -171,19 +170,12 @@ extension MainContentCoordinator { tabManager.tabs[index].resultRows = tab.resultRows tabManager.tabs[index].resultVersion += 1 - // Select pasted rows and scroll to first one if !pastedRows.isEmpty { let newIndices = Set(pastedRows.map { $0.rowIndex }) - selectedRowIndices = newIndices + selectionState.indices = newIndices tabManager.tabs[index].selectedRowIndices = newIndices tabManager.tabs[index].hasUserInteraction = true - - // Scroll to first pasted row - if pastedRows.first?.rowIndex != nil { - // Trigger scroll via notification if needed - // For now, selection change will handle visibility - } } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarSave.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarSave.swift index 673dc2731..35919933e 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarSave.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarSave.swift @@ -11,11 +11,10 @@ extension MainContentCoordinator { // MARK: - Sidebar Save func saveSidebarEdits( - selectedRowIndices: Set, editState: MultiRowEditState ) async throws { guard let tab = tabManager.selectedTab, - !selectedRowIndices.isEmpty, + !selectionState.indices.isEmpty, tab.tableContext.tableName != nil else { return @@ -24,8 +23,7 @@ extension MainContentCoordinator { let editedFields = editState.getEditedFields() guard !editedFields.isEmpty else { return } - // Build RowChange array from sidebar edits - let changes: [RowChange] = selectedRowIndices.sorted().compactMap { rowIndex in + let changes: [RowChange] = selectionState.indices.sorted().compactMap { rowIndex in guard rowIndex < tab.resultRows.count else { return nil } let originalRow = tab.resultRows[rowIndex] return RowChange( diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift index 7c5cb41a1..2eaa7bd77 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift @@ -13,7 +13,6 @@ extension MainContentCoordinator { func handleTabChange( from oldTabId: UUID?, to newTabId: UUID?, - selectedRowIndices: inout Set, tabs: [QueryTab] ) { let start = Date() @@ -65,7 +64,7 @@ extension MainContentCoordinator { // Restore column visibility for new tab columnVisibilityManager.restoreFromColumnLayout(newTab.columnLayout.hiddenColumns) - selectedRowIndices = newTab.selectedRowIndices + selectionState.indices = newTab.selectedRowIndices toolbarState.isTableTab = newTab.tabType == .table toolbarState.isResultsCollapsed = newTab.display.isResultsCollapsed diff --git a/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift b/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift index 34dfb30fd..e06e71962 100644 --- a/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift +++ b/TablePro/Views/Main/Extensions/MainContentView+Bindings.swift @@ -14,8 +14,8 @@ extension MainContentView { /// Compute selected row data for right sidebar display var selectedRowDataForSidebar: [(column: String, value: String?, type: String)]? { guard let tab = coordinator.tabManager.selectedTab, - !selectedRowIndices.isEmpty, - let firstIndex = selectedRowIndices.min(), + !coordinator.selectionState.indices.isEmpty, + let firstIndex = coordinator.selectionState.indices.min(), firstIndex < tab.resultRows.count else { return nil } let row = tab.resultRows[firstIndex] @@ -50,15 +50,14 @@ extension MainContentView { guard !coordinator.safeModeLevel.blocksAllWrites, let tab = coordinator.tabManager.selectedTab, tab.tabType == .table || tab.tableContext.tableName != nil, - !selectedRowIndices.isEmpty else { + !coordinator.selectionState.indices.isEmpty else { return false } return true } - /// Check if selected row is deleted var isSelectedRowDeleted: Bool { - guard let firstIndex = selectedRowIndices.min() else { return false } + guard let firstIndex = coordinator.selectionState.indices.min() else { return false } return coordinator.changeManager.isRowDeleted(firstIndex) } diff --git a/TablePro/Views/Main/Extensions/MainContentView+EventHandlers.swift b/TablePro/Views/Main/Extensions/MainContentView+EventHandlers.swift index cb9d270de..d76a9bba5 100644 --- a/TablePro/Views/Main/Extensions/MainContentView+EventHandlers.swift +++ b/TablePro/Views/Main/Extensions/MainContentView+EventHandlers.swift @@ -21,7 +21,6 @@ extension MainContentView { coordinator.handleTabChange( from: oldTabId, to: newTabId, - selectedRowIndices: &selectedRowIndices, tabs: tabManager.tabs ) let t1 = Date() @@ -136,7 +135,7 @@ extension MainContentView { case .skip: return case .openInPlace: - selectedRowIndices = [] + coordinator.selectionState.indices = [] coordinator.openTableTab(tableName, isView: isView) case .revertAndOpenNewWindow: coordinator.openTableTab(tableName, isView: isView) @@ -171,8 +170,9 @@ extension MainContentView { // MARK: - Sidebar Edit Handling func updateSidebarEditState() { + let selectedIndices = coordinator.selectionState.indices guard let tab = coordinator.tabManager.selectedTab, - !selectedRowIndices.isEmpty + !selectedIndices.isEmpty else { rightPanelState.editState.fields = [] rightPanelState.editState.onFieldChanged = nil @@ -180,7 +180,7 @@ extension MainContentView { } var allRows: [[String?]] = [] - for index in selectedRowIndices.sorted() { + for index in selectedIndices.sorted() { if index < tab.resultRows.count { allRows.append(tab.resultRows[index]) } @@ -206,7 +206,7 @@ extension MainContentView { // Collect columns modified in data grid so sidebar shows green dots var modifiedColumns = Set() - for rowIndex in selectedRowIndices { + for rowIndex in selectedIndices { modifiedColumns.formUnion(changeManager.getModifiedColumnsForRow(rowIndex)) } @@ -221,7 +221,7 @@ extension MainContentView { let fkColumns = Set(tab.columnForeignKeys.keys) rightPanelState.editState.configure( - selectedRowIndices: selectedRowIndices, + selectedRowIndices: selectedIndices, allRows: allRows, columns: tab.resultColumns, columnTypes: columnTypes, @@ -270,10 +270,10 @@ extension MainContentView { // Lazy-load full values for excluded columns when a single row is selected if !excludedNames.isEmpty, - selectedRowIndices.count == 1, + selectedIndices.count == 1, let tableName = tab.tableContext.tableName, let pkColumn = tab.tableContext.primaryKeyColumn, - let rowIndex = selectedRowIndices.first, + let rowIndex = selectedIndices.first, rowIndex < tab.resultRows.count { let row = tab.resultRows[rowIndex] diff --git a/TablePro/Views/Main/Extensions/MainContentView+Helpers.swift b/TablePro/Views/Main/Extensions/MainContentView+Helpers.swift index bfea48e7c..9ac5d923d 100644 --- a/TablePro/Views/Main/Extensions/MainContentView+Helpers.swift +++ b/TablePro/Views/Main/Extensions/MainContentView+Helpers.swift @@ -62,13 +62,14 @@ extension MainContentView { // MARK: - Inspector Context - /// Synchronously updates inspector state. Previously deferred by 100ms to coalesce - /// multiple onChange calls, but the deferred Task caused a double layout pass. func scheduleInspectorUpdate() { - inspectorUpdateTask?.cancel() - inspectorUpdateTask = nil updateSidebarEditState() - updateInspectorContext() + inspectorUpdateTask?.cancel() + inspectorUpdateTask = Task { @MainActor in + try? await Task.sleep(for: .milliseconds(50)) + guard !Task.isCancelled else { return } + updateInspectorContext() + } } func updateInspectorContext() { diff --git a/TablePro/Views/Main/Extensions/MainContentView+Setup.swift b/TablePro/Views/Main/Extensions/MainContentView+Setup.swift index e168d6d1b..842ecdd9d 100644 --- a/TablePro/Views/Main/Extensions/MainContentView+Setup.swift +++ b/TablePro/Views/Main/Extensions/MainContentView+Setup.swift @@ -275,7 +275,7 @@ extension MainContentView { coordinator: coordinator, filterStateManager: filterStateManager, connection: connection, - selectedRowIndices: $selectedRowIndices, + selectionState: coordinator.selectionState, selectedTables: Binding( get: { sidebarState.selectedTables }, set: { sidebarState.selectedTables = $0 } diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index 125f63dca..3a2712eb0 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -28,7 +28,7 @@ final class MainContentCommandActions { // MARK: - Bindings - @ObservationIgnored private let selectedRowIndices: Binding> + @ObservationIgnored private let selectionState: GridSelectionState @ObservationIgnored private let selectedTables: Binding> @ObservationIgnored private let pendingTruncates: Binding> @ObservationIgnored private let pendingDeletes: Binding> @@ -50,7 +50,7 @@ final class MainContentCommandActions { coordinator: MainContentCoordinator, filterStateManager: FilterStateManager, connection: DatabaseConnection, - selectedRowIndices: Binding>, + selectionState: GridSelectionState, selectedTables: Binding>, pendingTruncates: Binding>, pendingDeletes: Binding>, @@ -61,7 +61,7 @@ final class MainContentCommandActions { self.coordinator = coordinator self.filterStateManager = filterStateManager self.connection = connection - self.selectedRowIndices = selectedRowIndices + self.selectionState = selectionState self.selectedTables = selectedTables self.pendingTruncates = pendingTruncates self.pendingDeletes = pendingDeletes @@ -121,7 +121,6 @@ final class MainContentCommandActions { Task { do { try await self.coordinator?.saveSidebarEdits( - selectedRowIndices: self.selectedRowIndices.wrappedValue, editState: self.rightPanelState.editState ) } catch { @@ -166,16 +165,14 @@ final class MainContentCommandActions { // the public methods re-post these notifications for structure view. observeKeyWindowOnly(.copySelectedRows) { [weak self] _ in guard let self else { return } - let indices = self.selectedRowIndices.wrappedValue + let indices = self.selectionState.indices self.coordinator?.copySelectedRowsToClipboard(indices: indices) } observeKeyWindowOnly(.pasteRows) { [weak self] _ in guard let self else { return } - var indices = self.selectedRowIndices.wrappedValue var cell = self.editingCell.wrappedValue - self.coordinator?.pasteRows(selectedRowIndices: &indices, editingCell: &cell) - self.selectedRowIndices.wrappedValue = indices + self.coordinator?.pasteRows(editingCell: &cell) self.editingCell.wrappedValue = cell } } @@ -183,23 +180,17 @@ final class MainContentCommandActions { // MARK: - Row Operations (Group A — Called Directly) func addNewRow() { - var indices = selectedRowIndices.wrappedValue var cell = editingCell.wrappedValue - coordinator?.addNewRow(selectedRowIndices: &indices, editingCell: &cell) - selectedRowIndices.wrappedValue = indices + coordinator?.addNewRow(editingCell: &cell) editingCell.wrappedValue = cell } func deleteSelectedRows(rowIndices: Set? = nil) { - // When rowIndices is provided (from data grid), use them directly - // This avoids relying on SwiftUI binding sync timing let fromDataGrid = rowIndices != nil - let indices = rowIndices ?? selectedRowIndices.wrappedValue + let indices = rowIndices ?? selectionState.indices if !indices.isEmpty { - var mutableIndices = indices - coordinator?.deleteSelectedRows(indices: indices, selectedRowIndices: &mutableIndices) - selectedRowIndices.wrappedValue = mutableIndices + coordinator?.deleteSelectedRows(indices: indices) } else if !fromDataGrid, !selectedTables.wrappedValue.isEmpty { // Only toggle table deletion when the call did NOT originate from // the data grid (e.g., from the app menu Cmd+Delete with no rows selected) @@ -221,13 +212,11 @@ final class MainContentCommandActions { } func duplicateRow() { - let indices = selectedRowIndices.wrappedValue + let indices = selectionState.indices guard let selectedIndex = indices.first, indices.count == 1 else { return } - var mutableIndices = indices var cell = editingCell.wrappedValue - coordinator?.duplicateSelectedRow(index: selectedIndex, selectedRowIndices: &mutableIndices, editingCell: &cell) - selectedRowIndices.wrappedValue = mutableIndices + coordinator?.duplicateSelectedRow(index: selectedIndex, editingCell: &cell) editingCell.wrappedValue = cell } @@ -235,18 +224,18 @@ final class MainContentCommandActions { if coordinator?.tabManager.selectedTab?.display.resultsViewMode == .structure { coordinator?.structureActions?.copyRows?() } else { - let indices = selectedRowIndices.wrappedValue + let indices = selectionState.indices coordinator?.copySelectedRowsToClipboard(indices: indices) } } func copySelectedRowsWithHeaders() { - let indices = selectedRowIndices.wrappedValue + let indices = selectionState.indices coordinator?.copySelectedRowsWithHeaders(indices: indices) } func copySelectedRowsAsJson() { - let indices = selectedRowIndices.wrappedValue + let indices = selectionState.indices coordinator?.copySelectedRowsAsJson(indices: indices) } @@ -254,10 +243,8 @@ final class MainContentCommandActions { if coordinator?.tabManager.selectedTab?.display.resultsViewMode == .structure { coordinator?.structureActions?.pasteRows?() } else { - var indices = selectedRowIndices.wrappedValue var cell = editingCell.wrappedValue - coordinator?.pasteRows(selectedRowIndices: &indices, editingCell: &cell) - selectedRowIndices.wrappedValue = indices + coordinator?.pasteRows(editingCell: &cell) editingCell.wrappedValue = cell } } @@ -290,7 +277,7 @@ final class MainContentCommandActions { } var hasRowSelection: Bool { - !selectedRowIndices.wrappedValue.isEmpty + !selectionState.indices.isEmpty } var hasTableSelection: Bool { @@ -734,9 +721,7 @@ final class MainContentCommandActions { if coordinator?.tabManager.selectedTab?.display.resultsViewMode == .structure { coordinator?.structureActions?.undo?() } else { - var indices = selectedRowIndices.wrappedValue - coordinator?.undoLastChange(selectedRowIndices: &indices) - selectedRowIndices.wrappedValue = indices + coordinator?.undoLastChange() } } diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index d3ec07332..f1951067e 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -80,9 +80,8 @@ final class MainContentCoordinator { let connection: DatabaseConnection var connectionId: UUID { connection.id } - /// Live safe mode level — reads from toolbar state (user-editable), - /// not from the immutable connection snapshot. var safeModeLevel: SafeModeLevel { toolbarState.safeModeLevel } + let selectionState = GridSelectionState() let tabManager: QueryTabManager let changeManager: DataChangeManager let filterStateManager: FilterStateManager @@ -1300,7 +1299,7 @@ final class MainContentCoordinator { // MARK: - Sorting - func handleSort(columnIndex: Int, ascending: Bool, isMultiSort: Bool = false, selectedRowIndices: inout Set) { + func handleSort(columnIndex: Int, ascending: Bool, isMultiSort: Bool = false) { guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count else { return } diff --git a/TablePro/Views/Main/MainContentView.swift b/TablePro/Views/Main/MainContentView.swift index d567bb718..0b414bad0 100644 --- a/TablePro/Views/Main/MainContentView.swift +++ b/TablePro/Views/Main/MainContentView.swift @@ -47,7 +47,6 @@ struct MainContentView: View { // MARK: - Local State - @State var selectedRowIndices: Set = [] @State var editingCell: CellPosition? @State var commandActions: MainContentCommandActions? @State var queryResultsSummaryCache: (tabId: UUID, version: Int, summary: String?)? @@ -386,16 +385,6 @@ struct MainContentView: View { sidebarState.selectedTables = [match] } } - .onChange(of: selectedRowIndices) { _, newIndices in - if !newIndices.isEmpty, - AppSettingsManager.shared.dataGrid.autoShowInspector, - tabManager.selectedTab?.tabType == .table - { - coordinator.inspectorProxy?.showInspector() - } - // Deferred: expensive inspector rebuild coalesced with other triggers - scheduleInspectorUpdate() - } } // MARK: - Main Content @@ -411,7 +400,7 @@ struct MainContentView: View { connection: connection, windowId: windowId, connectionId: connection.id, - selectedRowIndices: $selectedRowIndices, + selectionState: coordinator.selectionState, editingCell: $editingCell, onCellEdit: { rowIndex, colIndex, value in coordinator.updateCellInTab( @@ -421,15 +410,22 @@ struct MainContentView: View { onSort: { columnIndex, ascending, isMultiSort in coordinator.handleSort( columnIndex: columnIndex, ascending: ascending, - isMultiSort: isMultiSort, - selectedRowIndices: &selectedRowIndices) + isMultiSort: isMultiSort) }, onAddRow: { - coordinator.addNewRow( - selectedRowIndices: &selectedRowIndices, editingCell: &editingCell) + coordinator.addNewRow(editingCell: &editingCell) }, onUndoInsert: { rowIndex in - coordinator.undoInsertRow(at: rowIndex, selectedRowIndices: &selectedRowIndices) + coordinator.undoInsertRow(at: rowIndex) + }, + onSelectionChange: { newIndices in + if !newIndices.isEmpty, + AppSettingsManager.shared.dataGrid.autoShowInspector, + tabManager.selectedTab?.tabType == .table + { + coordinator.inspectorProxy?.showInspector() + } + scheduleInspectorUpdate() }, onFilterColumn: { columnName in filterStateManager.addFilterForColumn(columnName)