Skip to content
9 changes: 6 additions & 3 deletions TablePro/Core/ChangeTracking/PendingChanges.swift
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,14 @@ struct PendingChanges: Equatable {
private mutating func removeChangeAt(_ arrayIndex: Int) {
let removed = changes[arrayIndex]
changeIndex.removeValue(forKey: RowChangeKey(rowIndex: removed.rowIndex, type: removed.type))
changes.remove(at: arrayIndex)

for (key, idx) in changeIndex where idx > arrayIndex {
changeIndex[key] = idx - 1
let lastIndex = changes.count - 1
if arrayIndex != lastIndex {
let moved = changes[lastIndex]
changes.swapAt(arrayIndex, lastIndex)
changeIndex[RowChangeKey(rowIndex: moved.rowIndex, type: moved.type)] = arrayIndex
}
changes.removeLast()
}

private mutating func rebuildChangeIndex() {
Expand Down
32 changes: 20 additions & 12 deletions TablePro/Views/Results/DataGridCellFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ final class DataGridCellFactory {
private static let sampleRowCount = 30
private static let maxMeasureChars = 50

private var headerFont: NSFont {
NSFont.systemFont(ofSize: 13, weight: .semibold)
}
private static let headerFont: NSFont = NSFont.systemFont(ofSize: 13, weight: .semibold)

func calculateColumnWidth(for columnName: String) -> CGFloat {
let attributes: [NSAttributedString.Key: Any] = [.font: headerFont]
let attributes: [NSAttributedString.Key: Any] = [.font: Self.headerFont]
let size = (columnName as NSString).size(withAttributes: attributes)
let width = size.width + 48
return min(max(width, Self.minColumnWidth), Self.maxColumnWidth)
Expand Down Expand Up @@ -105,18 +103,28 @@ internal extension String {
let nsString = self as NSString
let length = nsString.length
guard length > 0 else { return self }
guard containsLineBreak else { return self }

let mutable = NSMutableString(capacity: length)
var mutable: NSMutableString?
var copiedUpTo = 0
for i in 0..<length {
let ch = nsString.character(at: i)
if ch == 0x0A || ch == 0x0D || ch == 0x0B || ch == 0x0C ||
ch == 0x85 || ch == 0x2028 || ch == 0x2029 {
mutable.append(" ")
} else {
mutable.append(String(utf16CodeUnits: [ch], count: 1))
guard ch == 0x0A || ch == 0x0D || ch == 0x0B || ch == 0x0C ||
ch == 0x85 || ch == 0x2028 || ch == 0x2029 else { continue }

if mutable == nil {
mutable = NSMutableString(capacity: length)
}
if i > copiedUpTo {
mutable?.append(nsString.substring(with: NSRange(location: copiedUpTo, length: i - copiedUpTo)))
}
mutable?.append(" ")
copiedUpTo = i + 1
}

guard let result = mutable else { return self }
if copiedUpTo < length {
result.append(nsString.substring(with: NSRange(location: copiedUpTo, length: length - copiedUpTo)))
}
return mutable as String
return result as String
}
}
33 changes: 28 additions & 5 deletions TablePro/Views/Results/DataGridColumnPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,38 @@ final class DataGridColumnPool {
NSAnimationContext.current.allowsImplicitAnimation = false
defer { NSAnimationContext.endGrouping() }

var indexByIdentifier: [NSUserInterfaceItemIdentifier: Int] = [:]
indexByIdentifier.reserveCapacity(tableView.tableColumns.count)
for (index, column) in tableView.tableColumns.enumerated() {
indexByIdentifier[column.identifier] = index
}

for (targetPosition, slot) in targetOrder.enumerated() {
let identifier = ColumnIdentitySchema.slotIdentifier(slot)
guard let currentIndex = tableView.tableColumns.firstIndex(where: { $0.identifier == identifier }) else {
continue
}
guard let currentIndex = indexByIdentifier[identifier] else { continue }
let desiredIndex = baseOffset + targetPosition
guard desiredIndex < tableView.tableColumns.count else { continue }
if currentIndex != desiredIndex {
tableView.moveColumn(currentIndex, toColumn: desiredIndex)
if currentIndex == desiredIndex { continue }

tableView.moveColumn(currentIndex, toColumn: desiredIndex)
updateIndexMap(&indexByIdentifier, movedFrom: currentIndex, to: desiredIndex)
}
}

private func updateIndexMap(
_ map: inout [NSUserInterfaceItemIdentifier: Int],
movedFrom source: Int,
to destination: Int
) {
guard source != destination else { return }
let lower = min(source, destination)
let upper = max(source, destination)
let delta = source < destination ? -1 : 1
for (key, value) in map where value >= lower && value <= upper {
if value == source {
map[key] = destination
} else {
map[key] = value + delta
}
}
}
Expand Down
38 changes: 20 additions & 18 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
var layoutPersistTask: Task<Void, Never>?

static let rowViewIdentifier = NSUserInterfaceItemIdentifier("TableRowView")
internal var pendingDropdownRow: Int = 0
internal var pendingDropdownColumn: Int = 0
internal weak var pendingDropdownTableView: NSTableView?
private var rowVisualStateCache: [Int: RowVisualState] = [:]
private var lastVisualStateCacheVersion: Int = 0
private let largeDatasetThreshold = 5_000
Expand Down Expand Up @@ -187,7 +184,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
object: nil,
queue: .main
) { [weak self] _ in
Task {
Task { @MainActor [weak self] in
guard let self, let tableView = self.tableView else { return }
Self.updateVisibleCellFonts(tableView: tableView)
}
Expand Down Expand Up @@ -263,8 +260,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData

func applyFullReplace() {
guard let tableView else { return }
displayCache.removeAll()
rebuildVisualStateCache()
invalidateAllDisplayCaches()
updateCache()
tableView.reloadData()
}
Expand Down Expand Up @@ -312,6 +308,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
displayCache.removeAll()
}

func invalidateAllDisplayCaches() {
displayCache.removeAll()
rebuildVisualStateCache()
}

func updateDisplayFormats(_ formats: [ValueDisplayFormat?]) {
columnDisplayFormats = formats
displayCache.removeAll()
Expand Down Expand Up @@ -368,10 +369,10 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
func applyDelta(_ delta: Delta) {
switch delta {
case .cellChanged(let row, let column):
guard let tableView else { return }
let tableColumn = DataGridView.tableColumnIndex(for: column)
guard let tableView,
let tableColumn = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
else { return }
guard row >= 0, row < tableView.numberOfRows else { return }
guard tableColumn >= 0, tableColumn < tableView.numberOfColumns else { return }
invalidateDisplayCache(forDisplayRow: row, column: column)
rebuildVisualStateCache()
tableView.reloadData(
Expand All @@ -386,8 +387,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
if position.row >= 0, position.row < tableView.numberOfRows {
rowSet.insert(position.row)
}
let tableColumn = DataGridView.tableColumnIndex(for: position.column)
if tableColumn >= 0, tableColumn < tableView.numberOfColumns {
if let tableColumn = DataGridView.tableColumnIndex(
for: position.column,
in: tableView,
schema: identitySchema
) {
colSet.insert(tableColumn)
}
invalidateDisplayCache(forDisplayRow: position.row, column: position.column)
Expand All @@ -406,7 +410,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
applyRemovedRows(indices)
case .columnsReplaced, .fullReplace:
sortedIDs = nil
displayCache.removeAll()
applyFullReplace()
}
}
Expand All @@ -431,8 +434,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
}

func invalidateCachesForUndoRedo() {
displayCache.removeAll()
rebuildVisualStateCache()
invalidateAllDisplayCaches()
updateCache()
guard let tableView else { return }
let visibleRange = tableView.rows(in: tableView.visibleRect)
Expand All @@ -456,10 +458,10 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
}

func beginEditing(displayRow: Int, column: Int) {
guard let tableView else { return }
let displayCol = DataGridView.tableColumnIndex(for: column)
guard displayRow >= 0, displayRow < tableView.numberOfRows,
displayCol >= 0, displayCol < tableView.numberOfColumns else { return }
guard let tableView,
let displayCol = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
else { return }
guard displayRow >= 0, displayRow < tableView.numberOfRows else { return }
tableView.scrollRowToVisible(displayRow)
tableView.selectRowIndexes(IndexSet(integer: displayRow), byExtendingSelection: false)
tableView.editColumn(displayCol, row: displayRow, with: nil, select: true)
Expand Down
26 changes: 22 additions & 4 deletions TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,30 @@ struct DataGridView: NSViewRepresentable {

// MARK: - Column Layout Helpers

static func tableColumnIndex(for dataIndex: Int) -> Int {
dataIndex + 1
static let firstDataTableColumnIndex: Int = 1

static func isDataTableColumn(_ tableColumnIndex: Int) -> Bool {
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) -> Int {
tableColumnIndex - 1
static func dataColumnIndex(
for tableColumnIndex: Int,
in tableView: NSTableView,
schema: ColumnIdentitySchema
) -> Int? {
guard tableColumnIndex >= 0, tableColumnIndex < tableView.tableColumns.count else { return nil }
let identifier = tableView.tableColumns[tableColumnIndex].identifier
return schema.dataIndex(from: identifier)
}

static func dismantleNSView(_ nsView: NSScrollView, coordinator: TableViewCoordinator) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ extension TableViewCoordinator {
invalidateDisplayCache()
rebuildVisualStateCache()

let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex)
guard let tableColumnIndex = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
) else { return }
if storageRow != nil, case .cellChanged = delta {
tableRowsController.apply(.cellChanged(row: row, column: tableColumnIndex))
} else {
Expand Down
14 changes: 7 additions & 7 deletions TablePro/Views/Results/Extensions/DataGridView+Click.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ extension TableViewCoordinator {
let row = sender.clickedRow
let column = sender.clickedColumn
guard row >= 0, column > 0 else { return }

let columnIndex = DataGridView.dataColumnIndex(for: column)
guard DataGridView.dataColumnIndex(for: column, in: sender, schema: identitySchema) != nil else { return }
guard !changeManager.isRowDeleted(row) else { return }

// Single click only selects the row. Chevron buttons handle dropdown/picker actions.
}

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

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

let tableRows = tableRowsProvider()
Expand Down Expand Up @@ -75,8 +72,11 @@ extension TableViewCoordinator {
guard row >= 0, columnIndex >= 0 else { return }
guard !changeManager.isRowDeleted(row) else { return }
guard let tableView else { return }

let column = DataGridView.tableColumnIndex(for: columnIndex)
guard let column = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
) else { return }

if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Views/Results/Extensions/DataGridView+Columns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ extension TableViewCoordinator {
)
let state = visualState(for: row)

let tableColumnIndex = DataGridView.tableColumnIndex(for: columnIndex)
let isFocused: Bool = {
guard let keyTableView = tableView as? KeyHandlingTableView,
keyTableView.focusedRow == row,
let tableColumnIndex = DataGridView.tableColumnIndex(
for: columnIndex,
in: tableView,
schema: identitySchema
),
keyTableView.focusedColumn == tableColumnIndex else { return false }
return true
}()
Expand Down
23 changes: 15 additions & 8 deletions TablePro/Views/Results/Extensions/DataGridView+Editing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,28 +102,32 @@ extension TableViewCoordinator {

if forward {
if nextColumn >= tableView.numberOfColumns {
nextColumn = 1
nextColumn = DataGridView.firstDataTableColumnIndex
nextRow += 1
}
if nextRow >= tableView.numberOfRows {
nextRow = tableView.numberOfRows - 1
nextColumn = tableView.numberOfColumns - 1
}
} else {
if nextColumn < 1 {
if !DataGridView.isDataTableColumn(nextColumn) {
nextColumn = tableView.numberOfColumns - 1
nextRow -= 1
}
if nextRow < 0 {
nextRow = 0
nextColumn = 1
nextColumn = DataGridView.firstDataTableColumnIndex
}
}

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

let nextColumnIndex = nextColumn - 1
if nextColumnIndex >= 0,
if let nextColumnIndex = DataGridView.dataColumnIndex(
for: nextColumn,
in: tableView,
schema: identitySchema
),
nextColumnIndex >= 0,
let nextDisplayRow = displayRow(at: nextRow),
nextColumnIndex < nextDisplayRow.values.count,
let value = nextDisplayRow.values[nextColumnIndex],
Expand All @@ -140,9 +144,12 @@ extension TableViewCoordinator {
let row = tableView.row(for: textField)
let column = tableView.column(for: textField)

guard row >= 0, column > 0 else { return true }

let columnIndex = DataGridView.dataColumnIndex(for: column)
guard row >= 0, column > 0,
let columnIndex = DataGridView.dataColumnIndex(
for: column,
in: tableView,
schema: identitySchema
) else { return true }

if isEscapeCancelling {
isEscapeCancelling = false
Expand Down
Loading
Loading