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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Drag-selecting many columns in a wide result set scrolls smoothly instead of lagging; the selection overlay and row highlight now look up column positions from a cache that refreshes when columns are added, removed, or reordered.
- The connection Export Options dialog keeps a steady size when you turn on Include Credentials, and saves through the standard macOS save dialog.
- Data grid now serves the row count from its existing cache instead of recomputing it on every layout pass, reducing CPU churn while scrolling large result sets.
- Typing in the sidebar table search stays responsive on databases with thousands of tables; filtering runs after a short pause instead of on every keystroke.
Expand Down
39 changes: 27 additions & 12 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
private(set) var identitySchema: ColumnIdentitySchema = .empty
var currentSortState = SortState()

private var columnIndexByDataIndex: [Int: Int] = [:]
private static let selectionCacheLogger = Logger(subsystem: "com.TablePro", category: "DataGrid.ColumnIndexCache")

func tableColumnIndex(for dataIndex: Int) -> Int? {
if let cached = columnIndexByDataIndex[dataIndex] {
return cached
}
guard let tableView,
let identifier = identitySchema.identifier(for: dataIndex) else { return nil }
let resolved = tableView.column(withIdentifier: identifier)
guard resolved >= 0 else { return nil }
columnIndexByDataIndex[dataIndex] = resolved
return resolved
}

func invalidateColumnIndexCache() {
guard !columnIndexByDataIndex.isEmpty else { return }
Self.selectionCacheLogger.debug("invalidate column index cache (had \(self.columnIndexByDataIndex.count))")
columnIndexByDataIndex.removeAll()
}

func columnIdentifier(for dataIndex: Int) -> NSUserInterfaceItemIdentifier? {
identitySchema.identifier(for: dataIndex)
}
Expand Down Expand Up @@ -224,6 +245,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
columnDisplayFormats = []
cachedRowCount = 0
cachedColumnCount = 0
invalidateColumnIndexCache()
sortedIDs = nil
lastUpdateSnapshot = nil
columnPool.detachFromTableView()
Expand Down Expand Up @@ -477,7 +499,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
switch delta {
case .cellChanged(let row, let column):
guard let tableView,
let tableColumn = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
let tableColumn = tableColumnIndex(for: column)
else { return }
guard row >= 0, row < tableView.numberOfRows else { return }
invalidateDisplayCache(forDisplayRow: row, column: column)
Expand All @@ -494,11 +516,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
if position.row >= 0, position.row < tableView.numberOfRows {
rowSet.insert(position.row)
}
if let tableColumn = DataGridView.tableColumnIndex(
for: position.column,
in: tableView,
schema: identitySchema
) {
if let tableColumn = tableColumnIndex(for: position.column) {
colSet.insert(tableColumn)
}
invalidateDisplayCache(forDisplayRow: position.row, column: position.column)
Expand Down Expand Up @@ -620,7 +638,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData

func beginEditing(displayRow: Int, column: Int) {
guard let tableView,
let displayCol = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
let displayCol = tableColumnIndex(for: column)
else { return }
guard displayRow >= 0, displayRow < tableView.numberOfRows else { return }
tableView.scrollRowToVisible(displayRow)
Expand Down Expand Up @@ -666,6 +684,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
guard schemaChanged else { return false }
identitySchema = nextSchema
displayCache.removeAll()
invalidateColumnIndexCache()
return true
}

Expand Down Expand Up @@ -728,11 +747,7 @@ extension TableViewCoordinator: DataGridCellAccessoryDelegate {

func dataGridCellDidDoubleClick(row: Int, columnIndex: Int) {
guard row >= 0, columnIndex >= 0, let tableView else { return }
guard let tableColumn = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
) else { return }
guard let tableColumn = tableColumnIndex(for: columnIndex) else { return }
handleCellInteraction(row: row, tableColumn: tableColumn, columnIndex: columnIndex, tableView: tableView)
}
}
17 changes: 6 additions & 11 deletions TablePro/Views/Results/DataGridRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ class DataGridRowView: NSTableRowView {
}

private func drawCellSelectionFill(in dirtyRect: NSRect) {
guard let selection = coordinator?.selectionController.selection,
!selection.isEmpty,
let tableView = coordinator?.tableView else { return }
guard let coordinator,
let tableView = coordinator.tableView else { return }
let selection = coordinator.selectionController.selection
guard !selection.isEmpty else { return }
let columns = selection.columns(in: rowIndex)
guard !columns.isEmpty else { return }

Expand All @@ -110,10 +111,8 @@ class DataGridRowView: NSTableRowView {
: NSColor.selectedContentBackgroundColor.withAlphaComponent(0.28)
fillColor.setFill()

let schema = coordinator?.identitySchema
for dataColumn in columns {
guard let schema,
let tableColumnIndex = DataGridView.tableColumnIndex(for: dataColumn, in: tableView, schema: schema) else { continue }
guard let tableColumnIndex = coordinator.tableColumnIndex(for: dataColumn) else { continue }
let columnRect = tableView.rect(ofColumn: tableColumnIndex)
let localRect = NSRect(x: columnRect.minX, y: 0, width: columnRect.width, height: bounds.height)
guard localRect.intersects(dirtyRect) else { continue }
Expand Down Expand Up @@ -531,11 +530,7 @@ class DataGridRowView: NSTableRowView {
@objc private func previewForeignKey(_ sender: NSMenuItem) {
guard let columnIndex = sender.representedObject as? Int,
let coordinator, let tableView = coordinator.tableView,
let column = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: coordinator.identitySchema
) else { return }
let column = coordinator.tableColumnIndex(for: columnIndex) else { return }
coordinator.showForeignKeyPreview(
tableView: tableView, row: rowIndex, column: column, columnIndex: columnIndex
)
Expand Down
11 changes: 1 addition & 10 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ struct DataGridView: NSViewRepresentable {
savedLayout: savedLayout
)
coordinator.isRebuildingColumns = false
coordinator.invalidateColumnIndexCache()

if savedLayout == nil {
coordinator.scheduleLayoutPersist()
Expand Down Expand Up @@ -400,16 +401,6 @@ struct DataGridView: NSViewRepresentable {
tableColumnIndex >= firstDataTableColumnIndex
}

static func tableColumnIndex(
for dataIndex: Int,
in tableView: NSTableView,
schema: ColumnIdentitySchema
) -> Int? {
guard let identifier = schema.identifier(for: dataIndex) else { return nil }
let index = tableView.column(withIdentifier: identifier)
return index >= 0 ? index : nil
}

static func dataColumnIndex(
for tableColumnIndex: Int,
in tableView: NSTableView,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ extension TableViewCoordinator {
invalidateDisplayCache()
visualIndex.updateRow(row, from: changeManager, sortedIDs: sortedIDs)

guard let tableColumnIndex = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
) else { return }
guard let tableColumnIndex = tableColumnIndex(for: columnIndex) else { return }
tableView.reloadData(
forRowIndexes: IndexSet(integer: row),
columnIndexes: IndexSet(integer: tableColumnIndex)
Expand Down
6 changes: 1 addition & 5 deletions TablePro/Views/Results/Extensions/DataGridView+Click.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ extension TableViewCoordinator {
guard row >= 0, columnIndex >= 0 else { return }
guard !changeManager.isRowDeleted(row) else { return }
guard let tableView else { return }
guard let column = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
) else { return }
guard let column = tableColumnIndex(for: columnIndex) else { return }

if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
Expand Down
6 changes: 1 addition & 5 deletions TablePro/Views/Results/Extensions/DataGridView+Columns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,7 @@ extension TableViewCoordinator {
let isFocused: Bool = {
guard let keyTableView = tableView as? KeyHandlingTableView,
keyTableView.focusedRow == row,
let tableColumnIndex = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
),
let tableColumnIndex = tableColumnIndex(for: columnIndex),
keyTableView.focusedColumn == tableColumnIndex else { return false }
return true
}()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,7 @@ extension TableViewCoordinator {
let focusedRow = (tableView as? KeyHandlingTableView)?.focusedRow ?? -1
let newRow = focusedRow >= 0 ? focusedRow : (tableView.selectedRowIndexes.max() ?? -1)
guard newRow >= 0,
let tableColumnIndex = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
) else {
let tableColumnIndex = tableColumnIndex(for: columnIndex) else {
popover.close()
clearFKPreviewState()
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ extension TableViewCoordinator {

func tableViewColumnDidMove(_ notification: Notification) {
guard !isRebuildingColumns else { return }
invalidateColumnIndexCache()
layoutPersistTask?.cancel()
persistColumnLayoutToStorage()
}
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Results/KeyHandlingTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ final class KeyHandlingTableView: NSTableView {
selectRowIndexes(IndexSet(integer: activeCoord.row), byExtendingSelection: false)
}
focusedRow = activeCoord.row
focusedColumn = DataGridView.tableColumnIndex(for: activeCoord.column, in: self, schema: schema) ?? clickedColumn
focusedColumn = coordinator?.tableColumnIndex(for: activeCoord.column) ?? clickedColumn
case .clearFocus:
deselectAll(nil)
focusedRow = -1
Expand Down
9 changes: 4 additions & 5 deletions TablePro/Views/Results/Selection/GridSelectionOverlay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,12 @@ final class GridSelectionOverlay: NSView {

override func draw(_ dirtyRect: NSRect) {
guard let tableView, let coordinator else { return }
let schema = coordinator.identitySchema
let totalRows = tableView.numberOfRows
let editingCell = activeOverlayCell(in: coordinator)

NSColor.selectedContentBackgroundColor.withAlphaComponent(Self.borderAlpha).setStroke()
for rect in selection.rectangles {
guard let frame = frame(for: rect, in: tableView, schema: schema) else { continue }
guard let frame = frame(for: rect, in: tableView, coordinator: coordinator) else { continue }
guard frame.intersects(dirtyRect) else { continue }
if isFullHeight(rect, totalRows: totalRows) { continue }
if let editingCell, rect.contains(editingCell) { continue }
Expand All @@ -53,7 +52,7 @@ final class GridSelectionOverlay: NSView {
if let active = selection.activeCell,
editingCell != active,
selection.rectangles.count > 1 || (selection.rectangles.first?.rows.count ?? 0) > 1 || (selection.rectangles.first?.columns.count ?? 0) > 1,
let frame = frame(for: GridRect(cell: active), in: tableView, schema: schema),
let frame = frame(for: GridRect(cell: active), in: tableView, coordinator: coordinator),
frame.intersects(dirtyRect) {
NSColor.controlAccentColor.setStroke()
let inset = frame.insetBy(dx: Self.activeCellBorderWidth / 2, dy: Self.activeCellBorderWidth / 2)
Expand All @@ -78,7 +77,7 @@ final class GridSelectionOverlay: NSView {
return rect.rows.lowerBound <= 0 && rect.rows.upperBound >= totalRows - 1
}

private func frame(for rect: GridRect, in tableView: NSTableView, schema: ColumnIdentitySchema) -> NSRect? {
private func frame(for rect: GridRect, in tableView: NSTableView, coordinator: TableViewCoordinator) -> NSRect? {
guard tableView.numberOfRows > 0, tableView.numberOfColumns > 0 else { return nil }
let firstRow = max(0, rect.rows.lowerBound)
let lastRow = min(tableView.numberOfRows - 1, rect.rows.upperBound)
Expand All @@ -92,7 +91,7 @@ final class GridSelectionOverlay: NSView {
var leadingX = CGFloat.infinity
var trailingX = -CGFloat.infinity
for dataColumn in rect.columns.lowerBound...rect.columns.upperBound {
guard let tableColumnIndex = DataGridView.tableColumnIndex(for: dataColumn, in: tableView, schema: schema) else { continue }
guard let tableColumnIndex = coordinator.tableColumnIndex(for: dataColumn) else { continue }
let columnRect = tableView.rect(ofColumn: tableColumnIndex)
leadingX = min(leadingX, columnRect.minX)
trailingX = max(trailingX, columnRect.maxX)
Expand Down
Loading
Loading