Skip to content

Commit a972675

Browse files
authored
refactor(datagrid): Phase 3 keyboard interaction cleanup (#952)
* refactor(datagrid): full Phase 3 keyboard interaction cleanup * docs(datagrid): remove Phase 3 audit doc * fix(datagrid): route unhandled keys to super.keyDown so NSTableView handles arrow nav natively * fix(datagrid): draw focus ring even when cell is on emphasized row * fix(datagrid): draw contrasting border for cell focus on emphasized row * refactor(datagrid): extract cell focus indicator into layer-backed CellFocusOverlay * fix(datagrid): route non-navigation keys through interpretKeyEvents so Delete/Return reach overrides * fix(datagrid): defer focus-reload to avoid reentrancy + track active end of multi-row selection * fix(datagrid): review P0/P1 + header truncation + multi-sort weight + arrow nav skips hidden columns + Backspace-only row delete * fix(datagrid): bold all sorted column headers to match native macOS pattern * fix(datagrid): render bold sorted header manually to avoid state-on decoration
1 parent 0631570 commit a972675

12 files changed

Lines changed: 319 additions & 438 deletions

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -369,22 +369,6 @@ final class DataChangeManager: ChangeManaging {
369369
}
370370
}
371371

372-
// MARK: - Undo/Redo Public API
373-
374-
func undoLastChange() -> UndoResult? {
375-
guard let um = undoManagerProvider?(), um.canUndo else { return nil }
376-
lastUndoResult = nil
377-
um.undo()
378-
return lastUndoResult
379-
}
380-
381-
func redoLastChange() -> UndoResult? {
382-
guard let um = undoManagerProvider?(), um.canRedo else { return nil }
383-
lastUndoResult = nil
384-
um.redo()
385-
return lastUndoResult
386-
}
387-
388372
// MARK: - SQL Generation
389373

390374
func generateSQL() throws -> [ParameterizedStatement] {

TablePro/Core/Services/Query/RowOperationsManager.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,6 @@ final class RowOperationsManager {
150150
)
151151
}
152152

153-
func undoLastChange(tableRows: inout TableRows) -> UndoApplicationResult? {
154-
guard let result = changeManager.undoLastChange() else { return nil }
155-
return applyUndoResult(result, tableRows: &tableRows)
156-
}
157-
158-
func redoLastChange(tableRows: inout TableRows) -> UndoApplicationResult? {
159-
guard let result = changeManager.redoLastChange() else { return nil }
160-
return applyUndoResult(result, tableRows: &tableRows)
161-
}
162-
163153
func applyUndoResult(_ result: UndoResult, tableRows: inout TableRows) -> UndoApplicationResult {
164154
switch result.action {
165155
case .cellEdit(let rowIndex, let columnIndex, _, let previousValue, _, _):

TablePro/Resources/Localizable.xcstrings

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7740,6 +7740,9 @@
77407740
},
77417741
"Choose a certificate or key file" : {
77427742

7743+
},
7744+
"Choose a fetched model" : {
7745+
77437746
},
77447747
"Choose a folder to watch for .tablepro connection files" : {
77457748
"localizations" : {
@@ -24879,6 +24882,9 @@
2487924882
}
2488024883
}
2488124884
}
24885+
},
24886+
"Model name" : {
24887+
2488224888
},
2488324889
"Model not found: %@" : {
2488424890
"localizations" : {
@@ -31137,6 +31143,9 @@
3113731143
}
3113831144
}
3113931145
}
31146+
},
31147+
"Priority %d" : {
31148+
3114031149
},
3114131150
"Privacy" : {
3114231151
"localizations" : {
@@ -35645,9 +35654,6 @@
3564535654
}
3564635655
}
3564735656
}
35648-
},
35649-
"Select a model" : {
35650-
3565135657
},
3565235658
"Select a Plugin" : {
3565335659
"localizations" : {
@@ -37490,6 +37496,12 @@
3749037496
}
3749137497
}
3749237498
}
37499+
},
37500+
"Sorted ascending" : {
37501+
37502+
},
37503+
"Sorted descending" : {
37504+
3749337505
},
3749437506
"Sorting will reload data and discard all unsaved changes." : {
3749537507
"localizations" : {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// CellFocusOverlay.swift
3+
// TablePro
4+
//
5+
6+
import AppKit
7+
8+
final class CellFocusOverlay: NSView {
9+
enum Style {
10+
case hidden
11+
case contrastingBorder
12+
}
13+
14+
var style: Style = .hidden {
15+
didSet {
16+
guard oldValue != style else { return }
17+
applyStyle()
18+
}
19+
}
20+
21+
override init(frame frameRect: NSRect) {
22+
super.init(frame: frameRect)
23+
wantsLayer = true
24+
translatesAutoresizingMaskIntoConstraints = false
25+
isHidden = true
26+
}
27+
28+
required init?(coder: NSCoder) {
29+
fatalError("init(coder:) has not been implemented")
30+
}
31+
32+
override func hitTest(_ point: NSPoint) -> NSView? { nil }
33+
34+
override func viewDidChangeEffectiveAppearance() {
35+
super.viewDidChangeEffectiveAppearance()
36+
if !isHidden { applyStyle() }
37+
}
38+
39+
private func applyStyle() {
40+
switch style {
41+
case .hidden:
42+
isHidden = true
43+
layer?.borderWidth = 0
44+
case .contrastingBorder:
45+
isHidden = false
46+
layer?.borderWidth = 2
47+
layer?.borderColor = NSColor.alternateSelectedControlTextColor.cgColor
48+
}
49+
}
50+
}

TablePro/Views/Results/Cells/DataGridBaseCellView.swift

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,22 @@ class DataGridBaseCellView: NSTableCellView {
3434
var isFocusedCell: Bool = false {
3535
didSet {
3636
guard oldValue != isFocusedCell else { return }
37-
updateFocusRing()
37+
updateFocusPresentation()
3838
}
3939
}
4040

41+
private lazy var focusOverlay: CellFocusOverlay = {
42+
let overlay = CellFocusOverlay()
43+
addSubview(overlay)
44+
NSLayoutConstraint.activate([
45+
overlay.leadingAnchor.constraint(equalTo: leadingAnchor),
46+
overlay.trailingAnchor.constraint(equalTo: trailingAnchor),
47+
overlay.topAnchor.constraint(equalTo: topAnchor),
48+
overlay.bottomAnchor.constraint(equalTo: bottomAnchor),
49+
])
50+
return overlay
51+
}()
52+
4153
private(set) lazy var backgroundView: NSView = {
4254
let view = NSView()
4355
view.wantsLayer = true
@@ -189,35 +201,23 @@ class DataGridBaseCellView: NSTableCellView {
189201
override var backgroundStyle: NSView.BackgroundStyle {
190202
didSet {
191203
backgroundView.isHidden = (backgroundStyle == .emphasized) || (changeBackgroundColor == nil)
192-
if isFocusedCell { updateFocusRing() }
204+
updateFocusPresentation()
193205
}
194206
}
195207

196-
override var focusRingMaskBounds: NSRect { bounds }
208+
override var focusRingMaskBounds: NSRect {
209+
backgroundStyle == .emphasized ? .zero : bounds
210+
}
197211

198212
override func drawFocusRingMask() {
213+
guard backgroundStyle != .emphasized else { return }
199214
NSBezierPath(rect: bounds).fill()
200215
}
201216

202-
override func draw(_ dirtyRect: NSRect) {
203-
super.draw(dirtyRect)
204-
guard isFocusedCell, backgroundStyle != .emphasized else { return }
205-
NSGraphicsContext.saveGraphicsState()
206-
NSFocusRingPlacement.only.set()
207-
drawFocusRingMask()
208-
NSGraphicsContext.restoreGraphicsState()
209-
}
210-
211-
override func viewDidChangeEffectiveAppearance() {
212-
super.viewDidChangeEffectiveAppearance()
213-
if isFocusedCell {
214-
needsDisplay = true
215-
}
216-
}
217-
218-
private func updateFocusRing() {
219-
focusRingType = isFocusedCell ? .exterior : .none
217+
private func updateFocusPresentation() {
218+
let onEmphasized = backgroundStyle == .emphasized
219+
focusOverlay.style = (isFocusedCell && onEmphasized) ? .contrastingBorder : .hidden
220+
focusRingType = (isFocusedCell && !onEmphasized) ? .exterior : .none
220221
noteFocusRingMaskChanged()
221-
needsDisplay = true
222222
}
223223
}

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,60 @@ extension TableViewCoordinator {
3030
guard !isSyncingSelection else { return }
3131
guard let tableView = notification.object as? NSTableView else { return }
3232

33+
let previousSelection = selectedRowIndices
3334
let newSelection = Set(tableView.selectedRowIndexes.map { $0 })
34-
if newSelection != selectedRowIndices {
35+
if newSelection != previousSelection {
3536
selectedRowIndices = newSelection
3637
}
3738

38-
if let keyTableView = tableView as? KeyHandlingTableView {
39-
if newSelection.isEmpty {
40-
keyTableView.focusedRow = -1
41-
keyTableView.focusedColumn = -1
42-
} else if keyTableView.focusedRow < 0, let firstRow = newSelection.min() {
43-
keyTableView.focusedRow = firstRow
44-
keyTableView.focusedColumn = 1
45-
}
39+
guard let keyTableView = tableView as? KeyHandlingTableView else { return }
40+
41+
let newFocus = resolvedFocus(
42+
previous: previousSelection,
43+
current: newSelection,
44+
existingFocusedRow: keyTableView.focusedRow,
45+
existingFocusedColumn: keyTableView.focusedColumn,
46+
tableView: tableView
47+
)
48+
49+
if keyTableView.focusedRow != newFocus.row {
50+
keyTableView.focusedRow = newFocus.row
51+
}
52+
if keyTableView.focusedColumn != newFocus.column {
53+
keyTableView.focusedColumn = newFocus.column
54+
}
55+
}
56+
57+
private func resolvedFocus(
58+
previous: Set<Int>,
59+
current: Set<Int>,
60+
existingFocusedRow: Int,
61+
existingFocusedColumn: Int,
62+
tableView: NSTableView
63+
) -> (row: Int, column: Int) {
64+
if current.isEmpty {
65+
return (-1, -1)
66+
}
67+
68+
let column = existingFocusedColumn >= 1 ? existingFocusedColumn : 1
69+
let added = current.subtracting(previous)
70+
71+
if let tip = added.max() {
72+
return (tip, column)
73+
}
74+
75+
let removed = previous.subtracting(current)
76+
if let lostTip = removed.max(),
77+
let currentMax = current.max(),
78+
let currentMin = current.min() {
79+
let row = lostTip > currentMax ? currentMax : currentMin
80+
return (row, column)
4681
}
82+
83+
if existingFocusedRow >= 0, current.contains(existingFocusedRow) {
84+
return (existingFocusedRow, column)
85+
}
86+
87+
return (current.min() ?? -1, column)
4788
}
4889
}

0 commit comments

Comments
 (0)