Skip to content

Commit 8569671

Browse files
authored
refactor(datagrid): cleanup (#954)
* perf(datagrid): cache header font, single-pass cell sanitize, MainActor on theme observer * perf(datagrid): O(1) column index lookup during reorder * refactor(datagrid): route data-column boundary checks through helpers * refactor(datagrid): use representedObject for dropdown menu context * perf(datagrid): O(1) removeChangeAt via swap-and-pop * refactor(datagrid): extract invalidateAllDisplayCaches for full-replace paths * fix(datagrid): translate column indices via schema lookup, persist reorder immediately * fix(datagrid): use already-unwrapped coordinator in row menu
1 parent a972675 commit 8569671

13 files changed

Lines changed: 202 additions & 103 deletions

TablePro/Core/ChangeTracking/PendingChanges.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,11 +364,14 @@ struct PendingChanges: Equatable {
364364
private mutating func removeChangeAt(_ arrayIndex: Int) {
365365
let removed = changes[arrayIndex]
366366
changeIndex.removeValue(forKey: RowChangeKey(rowIndex: removed.rowIndex, type: removed.type))
367-
changes.remove(at: arrayIndex)
368367

369-
for (key, idx) in changeIndex where idx > arrayIndex {
370-
changeIndex[key] = idx - 1
368+
let lastIndex = changes.count - 1
369+
if arrayIndex != lastIndex {
370+
let moved = changes[lastIndex]
371+
changes.swapAt(arrayIndex, lastIndex)
372+
changeIndex[RowChangeKey(rowIndex: moved.rowIndex, type: moved.type)] = arrayIndex
371373
}
374+
changes.removeLast()
372375
}
373376

374377
private mutating func rebuildChangeIndex() {

TablePro/Views/Results/DataGridCellFactory.swift

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ final class DataGridCellFactory {
1313
private static let sampleRowCount = 30
1414
private static let maxMeasureChars = 50
1515

16-
private var headerFont: NSFont {
17-
NSFont.systemFont(ofSize: 13, weight: .semibold)
18-
}
16+
private static let headerFont: NSFont = NSFont.systemFont(ofSize: 13, weight: .semibold)
1917

2018
func calculateColumnWidth(for columnName: String) -> CGFloat {
21-
let attributes: [NSAttributedString.Key: Any] = [.font: headerFont]
19+
let attributes: [NSAttributedString.Key: Any] = [.font: Self.headerFont]
2220
let size = (columnName as NSString).size(withAttributes: attributes)
2321
let width = size.width + 48
2422
return min(max(width, Self.minColumnWidth), Self.maxColumnWidth)
@@ -105,18 +103,28 @@ internal extension String {
105103
let nsString = self as NSString
106104
let length = nsString.length
107105
guard length > 0 else { return self }
108-
guard containsLineBreak else { return self }
109106

110-
let mutable = NSMutableString(capacity: length)
107+
var mutable: NSMutableString?
108+
var copiedUpTo = 0
111109
for i in 0..<length {
112110
let ch = nsString.character(at: i)
113-
if ch == 0x0A || ch == 0x0D || ch == 0x0B || ch == 0x0C ||
114-
ch == 0x85 || ch == 0x2028 || ch == 0x2029 {
115-
mutable.append(" ")
116-
} else {
117-
mutable.append(String(utf16CodeUnits: [ch], count: 1))
111+
guard ch == 0x0A || ch == 0x0D || ch == 0x0B || ch == 0x0C ||
112+
ch == 0x85 || ch == 0x2028 || ch == 0x2029 else { continue }
113+
114+
if mutable == nil {
115+
mutable = NSMutableString(capacity: length)
118116
}
117+
if i > copiedUpTo {
118+
mutable?.append(nsString.substring(with: NSRange(location: copiedUpTo, length: i - copiedUpTo)))
119+
}
120+
mutable?.append(" ")
121+
copiedUpTo = i + 1
122+
}
123+
124+
guard let result = mutable else { return self }
125+
if copiedUpTo < length {
126+
result.append(nsString.substring(with: NSRange(location: copiedUpTo, length: length - copiedUpTo)))
119127
}
120-
return mutable as String
128+
return result as String
121129
}
122130
}

TablePro/Views/Results/DataGridColumnPool.swift

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,38 @@ final class DataGridColumnPool {
137137
NSAnimationContext.current.allowsImplicitAnimation = false
138138
defer { NSAnimationContext.endGrouping() }
139139

140+
var indexByIdentifier: [NSUserInterfaceItemIdentifier: Int] = [:]
141+
indexByIdentifier.reserveCapacity(tableView.tableColumns.count)
142+
for (index, column) in tableView.tableColumns.enumerated() {
143+
indexByIdentifier[column.identifier] = index
144+
}
145+
140146
for (targetPosition, slot) in targetOrder.enumerated() {
141147
let identifier = ColumnIdentitySchema.slotIdentifier(slot)
142-
guard let currentIndex = tableView.tableColumns.firstIndex(where: { $0.identifier == identifier }) else {
143-
continue
144-
}
148+
guard let currentIndex = indexByIdentifier[identifier] else { continue }
145149
let desiredIndex = baseOffset + targetPosition
146150
guard desiredIndex < tableView.tableColumns.count else { continue }
147-
if currentIndex != desiredIndex {
148-
tableView.moveColumn(currentIndex, toColumn: desiredIndex)
151+
if currentIndex == desiredIndex { continue }
152+
153+
tableView.moveColumn(currentIndex, toColumn: desiredIndex)
154+
updateIndexMap(&indexByIdentifier, movedFrom: currentIndex, to: desiredIndex)
155+
}
156+
}
157+
158+
private func updateIndexMap(
159+
_ map: inout [NSUserInterfaceItemIdentifier: Int],
160+
movedFrom source: Int,
161+
to destination: Int
162+
) {
163+
guard source != destination else { return }
164+
let lower = min(source, destination)
165+
let upper = max(source, destination)
166+
let delta = source < destination ? -1 : 1
167+
for (key, value) in map where value >= lower && value <= upper {
168+
if value == source {
169+
map[key] = destination
170+
} else {
171+
map[key] = value + delta
149172
}
150173
}
151174
}

TablePro/Views/Results/DataGridCoordinator.swift

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
108108
var layoutPersistTask: Task<Void, Never>?
109109

110110
static let rowViewIdentifier = NSUserInterfaceItemIdentifier("TableRowView")
111-
internal var pendingDropdownRow: Int = 0
112-
internal var pendingDropdownColumn: Int = 0
113-
internal weak var pendingDropdownTableView: NSTableView?
114111
private var rowVisualStateCache: [Int: RowVisualState] = [:]
115112
private var lastVisualStateCacheVersion: Int = 0
116113
private let largeDatasetThreshold = 5_000
@@ -187,7 +184,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
187184
object: nil,
188185
queue: .main
189186
) { [weak self] _ in
190-
Task {
187+
Task { @MainActor [weak self] in
191188
guard let self, let tableView = self.tableView else { return }
192189
Self.updateVisibleCellFonts(tableView: tableView)
193190
}
@@ -263,8 +260,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
263260

264261
func applyFullReplace() {
265262
guard let tableView else { return }
266-
displayCache.removeAll()
267-
rebuildVisualStateCache()
263+
invalidateAllDisplayCaches()
268264
updateCache()
269265
tableView.reloadData()
270266
}
@@ -312,6 +308,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
312308
displayCache.removeAll()
313309
}
314310

311+
func invalidateAllDisplayCaches() {
312+
displayCache.removeAll()
313+
rebuildVisualStateCache()
314+
}
315+
315316
func updateDisplayFormats(_ formats: [ValueDisplayFormat?]) {
316317
columnDisplayFormats = formats
317318
displayCache.removeAll()
@@ -368,10 +369,10 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
368369
func applyDelta(_ delta: Delta) {
369370
switch delta {
370371
case .cellChanged(let row, let column):
371-
guard let tableView else { return }
372-
let tableColumn = DataGridView.tableColumnIndex(for: column)
372+
guard let tableView,
373+
let tableColumn = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
374+
else { return }
373375
guard row >= 0, row < tableView.numberOfRows else { return }
374-
guard tableColumn >= 0, tableColumn < tableView.numberOfColumns else { return }
375376
invalidateDisplayCache(forDisplayRow: row, column: column)
376377
rebuildVisualStateCache()
377378
tableView.reloadData(
@@ -386,8 +387,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
386387
if position.row >= 0, position.row < tableView.numberOfRows {
387388
rowSet.insert(position.row)
388389
}
389-
let tableColumn = DataGridView.tableColumnIndex(for: position.column)
390-
if tableColumn >= 0, tableColumn < tableView.numberOfColumns {
390+
if let tableColumn = DataGridView.tableColumnIndex(
391+
for: position.column,
392+
in: tableView,
393+
schema: identitySchema
394+
) {
391395
colSet.insert(tableColumn)
392396
}
393397
invalidateDisplayCache(forDisplayRow: position.row, column: position.column)
@@ -406,7 +410,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
406410
applyRemovedRows(indices)
407411
case .columnsReplaced, .fullReplace:
408412
sortedIDs = nil
409-
displayCache.removeAll()
410413
applyFullReplace()
411414
}
412415
}
@@ -431,8 +434,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
431434
}
432435

433436
func invalidateCachesForUndoRedo() {
434-
displayCache.removeAll()
435-
rebuildVisualStateCache()
437+
invalidateAllDisplayCaches()
436438
updateCache()
437439
guard let tableView else { return }
438440
let visibleRange = tableView.rows(in: tableView.visibleRect)
@@ -456,10 +458,10 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
456458
}
457459

458460
func beginEditing(displayRow: Int, column: Int) {
459-
guard let tableView else { return }
460-
let displayCol = DataGridView.tableColumnIndex(for: column)
461-
guard displayRow >= 0, displayRow < tableView.numberOfRows,
462-
displayCol >= 0, displayCol < tableView.numberOfColumns else { return }
461+
guard let tableView,
462+
let displayCol = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
463+
else { return }
464+
guard displayRow >= 0, displayRow < tableView.numberOfRows else { return }
463465
tableView.scrollRowToVisible(displayRow)
464466
tableView.selectRowIndexes(IndexSet(integer: displayRow), byExtendingSelection: false)
465467
tableView.editColumn(displayCol, row: displayRow, with: nil, select: true)

TablePro/Views/Results/DataGridView.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,30 @@ struct DataGridView: NSViewRepresentable {
314314

315315
// MARK: - Column Layout Helpers
316316

317-
static func tableColumnIndex(for dataIndex: Int) -> Int {
318-
dataIndex + 1
317+
static let firstDataTableColumnIndex: Int = 1
318+
319+
static func isDataTableColumn(_ tableColumnIndex: Int) -> Bool {
320+
tableColumnIndex >= firstDataTableColumnIndex
321+
}
322+
323+
static func tableColumnIndex(
324+
for dataIndex: Int,
325+
in tableView: NSTableView,
326+
schema: ColumnIdentitySchema
327+
) -> Int? {
328+
guard let identifier = schema.identifier(for: dataIndex) else { return nil }
329+
let index = tableView.column(withIdentifier: identifier)
330+
return index >= 0 ? index : nil
319331
}
320332

321-
static func dataColumnIndex(for tableColumnIndex: Int) -> Int {
322-
tableColumnIndex - 1
333+
static func dataColumnIndex(
334+
for tableColumnIndex: Int,
335+
in tableView: NSTableView,
336+
schema: ColumnIdentitySchema
337+
) -> Int? {
338+
guard tableColumnIndex >= 0, tableColumnIndex < tableView.tableColumns.count else { return nil }
339+
let identifier = tableView.tableColumns[tableColumnIndex].identifier
340+
return schema.dataIndex(from: identifier)
323341
}
324342

325343
static func dismantleNSView(_ nsView: NSScrollView, coordinator: TableViewCoordinator) {

TablePro/Views/Results/Extensions/DataGridView+CellCommit.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ extension TableViewCoordinator {
4141
invalidateDisplayCache()
4242
rebuildVisualStateCache()
4343

44-
let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex)
44+
guard let tableColumnIndex = DataGridView.tableColumnIndex(
45+
for: columnIndex,
46+
in: tableView,
47+
schema: identitySchema
48+
) else { return }
4549
if storageRow != nil, case .cellChanged = delta {
4650
tableRowsController.apply(.cellChanged(row: row, column: tableColumnIndex))
4751
} else {

TablePro/Views/Results/Extensions/DataGridView+Click.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@ extension TableViewCoordinator {
1515
let row = sender.clickedRow
1616
let column = sender.clickedColumn
1717
guard row >= 0, column > 0 else { return }
18-
19-
let columnIndex = DataGridView.dataColumnIndex(for: column)
18+
guard DataGridView.dataColumnIndex(for: column, in: sender, schema: identitySchema) != nil else { return }
2019
guard !changeManager.isRowDeleted(row) else { return }
21-
22-
// Single click only selects the row. Chevron buttons handle dropdown/picker actions.
2320
}
2421

2522
@objc func handleDoubleClick(_ sender: NSTableView) {
@@ -29,7 +26,7 @@ extension TableViewCoordinator {
2926
let column = sender.clickedColumn
3027
guard row >= 0, column > 0 else { return }
3128

32-
let columnIndex = DataGridView.dataColumnIndex(for: column)
29+
guard let columnIndex = DataGridView.dataColumnIndex(for: column, in: sender, schema: identitySchema) else { return }
3330
guard !changeManager.isRowDeleted(row) else { return }
3431

3532
let tableRows = tableRowsProvider()
@@ -75,8 +72,11 @@ extension TableViewCoordinator {
7572
guard row >= 0, columnIndex >= 0 else { return }
7673
guard !changeManager.isRowDeleted(row) else { return }
7774
guard let tableView else { return }
78-
79-
let column = DataGridView.tableColumnIndex(for: columnIndex)
75+
guard let column = DataGridView.tableColumnIndex(
76+
for: columnIndex,
77+
in: tableView,
78+
schema: identitySchema
79+
) else { return }
8080

8181
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
8282
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)

TablePro/Views/Results/Extensions/DataGridView+Columns.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ extension TableViewCoordinator {
4747
)
4848
let state = visualState(for: row)
4949

50-
let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex)
5150
let isFocused: Bool = {
5251
guard let keyTableView = tableView as? KeyHandlingTableView,
5352
keyTableView.focusedRow == row,
53+
let tableColumnIndex = DataGridView.tableColumnIndex(
54+
for: columnIndex,
55+
in: tableView,
56+
schema: identitySchema
57+
),
5458
keyTableView.focusedColumn == tableColumnIndex else { return false }
5559
return true
5660
}()

TablePro/Views/Results/Extensions/DataGridView+Editing.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,28 +102,32 @@ extension TableViewCoordinator {
102102

103103
if forward {
104104
if nextColumn >= tableView.numberOfColumns {
105-
nextColumn = 1
105+
nextColumn = DataGridView.firstDataTableColumnIndex
106106
nextRow += 1
107107
}
108108
if nextRow >= tableView.numberOfRows {
109109
nextRow = tableView.numberOfRows - 1
110110
nextColumn = tableView.numberOfColumns - 1
111111
}
112112
} else {
113-
if nextColumn < 1 {
113+
if !DataGridView.isDataTableColumn(nextColumn) {
114114
nextColumn = tableView.numberOfColumns - 1
115115
nextRow -= 1
116116
}
117117
if nextRow < 0 {
118118
nextRow = 0
119-
nextColumn = 1
119+
nextColumn = DataGridView.firstDataTableColumnIndex
120120
}
121121
}
122122

123123
tableView.selectRowIndexes(IndexSet(integer: nextRow), byExtendingSelection: false)
124124

125-
let nextColumnIndex = nextColumn - 1
126-
if nextColumnIndex >= 0,
125+
if let nextColumnIndex = DataGridView.dataColumnIndex(
126+
for: nextColumn,
127+
in: tableView,
128+
schema: identitySchema
129+
),
130+
nextColumnIndex >= 0,
127131
let nextDisplayRow = displayRow(at: nextRow),
128132
nextColumnIndex < nextDisplayRow.values.count,
129133
let value = nextDisplayRow.values[nextColumnIndex],
@@ -140,9 +144,12 @@ extension TableViewCoordinator {
140144
let row = tableView.row(for: textField)
141145
let column = tableView.column(for: textField)
142146

143-
guard row >= 0, column > 0 else { return true }
144-
145-
let columnIndex = DataGridView.dataColumnIndex(for: column)
147+
guard row >= 0, column > 0,
148+
let columnIndex = DataGridView.dataColumnIndex(
149+
for: column,
150+
in: tableView,
151+
schema: identitySchema
152+
) else { return true }
146153

147154
if isEscapeCancelling {
148155
isEscapeCancelling = false

0 commit comments

Comments
 (0)