Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions TablePro/Models/Query/QueryTabState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

import Foundation

@MainActor @Observable
final class GridSelectionState {
var indices: Set<Int> = []
}

/// Type of tab
enum TabType: Equatable, Codable, Hashable {
case query // SQL editor tab
Expand Down
7 changes: 2 additions & 5 deletions TablePro/Views/Main/Child/DataTabGridDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class DataTabGridDelegate: DataGridViewDelegate {
weak var coordinator: MainContentCoordinator?
var columnVisibilityManager: ColumnVisibilityManager?

var selectedRowIndices: Binding<Set<Int>>?
var selectionState: GridSelectionState?
var editingCell: Binding<CellPosition?>?

var onCellEdit: ((Int, Int, String?) -> Void)?
Expand Down Expand Up @@ -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() {
Expand Down
19 changes: 13 additions & 6 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ struct MainEditorContentView: View {
let windowId: UUID
let connectionId: UUID

// MARK: - Bindings
// MARK: - Selection State

@Binding var selectedRowIndices: Set<Int>
let selectionState: GridSelectionState
@Binding var editingCell: CellPosition?

// MARK: - Callbacks
Expand All @@ -50,6 +50,7 @@ struct MainEditorContentView: View {
let onSort: (Int, Bool, Bool) -> Void
let onAddRow: () -> Void
let onUndoInsert: (Int) -> Void
let onSelectionChange: (Set<Int>) -> Void
let onFilterColumn: (String) -> Void
let onApplyFilters: ([TableFilter]) -> Void
let onClearFilters: () -> Void
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
extension MainContentCoordinator {
// MARK: - Row Operations

func addNewRow(selectedRowIndices: inout Set<Int>, editingCell: inout CellPosition?) {
func addNewRow(editingCell: inout CellPosition?) {
guard !safeModeLevel.blocksAllWrites,
let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count else { return }
Expand All @@ -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<Int>, selectedRowIndices: inout Set<Int>) {
func deleteSelectedRows(indices: Set<Int>) {
guard !safeModeLevel.blocksAllWrites,
let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count,
Expand All @@ -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<Int>, editingCell: inout CellPosition?) {
func duplicateSelectedRow(index: Int, editingCell: inout CellPosition?) {
guard !safeModeLevel.blocksAllWrites,
let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count else { return }
Expand All @@ -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<Int>) {
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<Int>) {
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
Expand Down Expand Up @@ -153,13 +153,12 @@ extension MainContentCoordinator {
ClipboardService.shared.writeText(converter.generateJson(rows: rows))
}

func pasteRows(selectedRowIndices: inout Set<Int>, 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(
Expand All @@ -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
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ extension MainContentCoordinator {
// MARK: - Sidebar Save

func saveSidebarEdits(
selectedRowIndices: Set<Int>,
editState: MultiRowEditState
) async throws {
guard let tab = tabManager.selectedTab,
!selectedRowIndices.isEmpty,
!selectionState.indices.isEmpty,
tab.tableContext.tableName != nil
else {
return
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ extension MainContentCoordinator {
func handleTabChange(
from oldTabId: UUID?,
to newTabId: UUID?,
selectedRowIndices: inout Set<Int>,
tabs: [QueryTab]
) {
let start = Date()
Expand Down Expand Up @@ -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

Expand Down
9 changes: 4 additions & 5 deletions TablePro/Views/Main/Extensions/MainContentView+Bindings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ extension MainContentView {
coordinator.handleTabChange(
from: oldTabId,
to: newTabId,
selectedRowIndices: &selectedRowIndices,
tabs: tabManager.tabs
)
let t1 = Date()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -171,16 +170,17 @@ 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
return
}

var allRows: [[String?]] = []
for index in selectedRowIndices.sorted() {
for index in selectedIndices.sorted() {
if index < tab.resultRows.count {
allRows.append(tab.resultRows[index])
}
Expand All @@ -206,7 +206,7 @@ extension MainContentView {

// Collect columns modified in data grid so sidebar shows green dots
var modifiedColumns = Set<Int>()
for rowIndex in selectedRowIndices {
for rowIndex in selectedIndices {
modifiedColumns.formUnion(changeManager.getModifiedColumnsForRow(rowIndex))
}

Expand All @@ -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,
Expand Down Expand Up @@ -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]
Expand Down
11 changes: 6 additions & 5 deletions TablePro/Views/Main/Extensions/MainContentView+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Main/Extensions/MainContentView+Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading
Loading