Skip to content

Commit e366ea8

Browse files
authored
fix: unify DataGrid cell editors (#665)
* feat: auto-uppercase SQL keywords as you type (#660) * fix: unify DataGrid cell editors — chevron button for all special columns - Replace passive NSImageView chevron with interactive CellChevronButton - Single click on chevron opens the appropriate editor (boolean dropdown, enum/set popover, date picker, JSON editor, hex editor) - Double click on cell text enters inline editing only - Add chevron to boolean, date, JSON, blob columns (previously had none) - Fix dropdown change tracking: use commitPopoverEdit with changeManager - Fix shouldEdit missing boolean guard (inline edit opened on boolean cells) - Fix cell reuse: always update chevron target/action (prevents dangling pointer) - Fix handleDoubleClick JSON detection running on chevron columns - Fix typePicker priority in handleChevronClick (Structure view compatibility)
1 parent 266db46 commit e366ea8

File tree

6 files changed

+106
-68
lines changed

6 files changed

+106
-68
lines changed

TablePro/Views/Results/DataGridCellFactory.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ final class FKArrowButton: NSButton {
1616
var fkColumnIndex: Int = 0
1717
}
1818

19+
/// Custom button that stores cell row/column context for the chevron click handler
20+
@MainActor
21+
final class CellChevronButton: NSButton {
22+
var cellRow: Int = -1
23+
var cellColumnIndex: Int = -1
24+
}
25+
1926
/// Factory for creating data grid cell views
2027
@MainActor
2128
final class DataGridCellFactory {
@@ -138,6 +145,8 @@ final class DataGridCellFactory {
138145
isFKColumn: Bool = false,
139146
fkArrowTarget: AnyObject? = nil,
140147
fkArrowAction: Selector? = nil,
148+
chevronTarget: AnyObject? = nil,
149+
chevronAction: Selector? = nil,
141150
delegate: NSTextFieldDelegate
142151
) -> NSView {
143152
let cellViewId: NSUserInterfaceItemIdentifier
@@ -177,9 +186,11 @@ final class DataGridCellFactory {
177186
cellView.addSubview(cell)
178187

179188
if isDropdown {
180-
let chevron = NSImageView()
189+
let chevron = CellChevronButton()
181190
chevron.tag = Self.chevronTag
182-
chevron.image = NSImage(systemSymbolName: "chevron.up.chevron.down", accessibilityDescription: nil)
191+
chevron.bezelStyle = .inline
192+
chevron.isBordered = false
193+
chevron.image = NSImage(systemSymbolName: "chevron.up.chevron.down", accessibilityDescription: String(localized: "Open editor"))
183194
chevron.contentTintColor = .tertiaryLabelColor
184195
chevron.translatesAutoresizingMaskIntoConstraints = false
185196
chevron.setContentHuggingPriority(.required, for: .horizontal)
@@ -248,6 +259,13 @@ final class DataGridCellFactory {
248259
button.isHidden = (rawValue == nil || rawValue?.isEmpty == true)
249260
}
250261

262+
if isDropdown, let chevronButton = cellView.viewWithTag(Self.chevronTag) as? CellChevronButton {
263+
chevronButton.cellRow = row
264+
chevronButton.cellColumnIndex = columnIndex
265+
chevronButton.target = chevronTarget
266+
chevronButton.action = chevronAction
267+
}
268+
251269
cell.isEditable = isEditable
252270
cell.delegate = delegate
253271
cell.identifier = cellIdentifier

TablePro/Views/Results/DataGridCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
115115
static let rowViewIdentifier = NSUserInterfaceItemIdentifier("TableRowView")
116116
internal var pendingDropdownRow: Int = 0
117117
internal var pendingDropdownColumn: Int = 0
118+
internal weak var pendingDropdownTableView: NSTableView?
118119
private var rowVisualStateCache: [Int: RowVisualState] = [:]
119120
private var lastVisualStateCacheVersion: Int = 0
120121
private let largeDatasetThreshold = 5_000

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

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,14 @@ extension TableViewCoordinator {
1919
let columnIndex = column - 1
2020
guard !changeManager.isRowDeleted(row) else { return }
2121

22-
// Dropdown columns open on single click
22+
// Structure view dropdown columns (no chevron button — use full-cell click)
2323
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
2424
showDropdownMenu(tableView: sender, row: row, column: column, columnIndex: columnIndex)
2525
return
2626
}
2727

28-
// ENUM/SET columns open on single click
29-
if columnIndex < rowProvider.columnTypes.count,
30-
columnIndex < rowProvider.columns.count {
31-
let ct = rowProvider.columnTypes[columnIndex]
32-
let columnName = rowProvider.columns[columnIndex]
33-
if ct.isEnumType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
34-
showEnumPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
35-
return
36-
}
37-
if ct.isSetType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
38-
showSetPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
39-
return
40-
}
41-
}
28+
// All other special editor columns are handled by their chevron button action.
29+
// Single click on cell text area does nothing.
4230
}
4331

4432
@objc func handleDoubleClick(_ sender: NSTableView) {
@@ -58,30 +46,7 @@ extension TableViewCoordinator {
5846
return
5947
}
6048

61-
// Dropdown columns already handled by single click
62-
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
63-
return
64-
}
65-
66-
// Type picker columns use database-specific type popover
67-
if let typePickerCols = typePickerColumns, typePickerCols.contains(columnIndex) {
68-
showTypePickerPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
69-
return
70-
}
71-
72-
// ENUM/SET columns already handled by single click
73-
if columnIndex < rowProvider.columnTypes.count,
74-
columnIndex < rowProvider.columns.count {
75-
let ct = rowProvider.columnTypes[columnIndex]
76-
if ct.isEnumType || ct.isSetType {
77-
let columnName = rowProvider.columns[columnIndex]
78-
if let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
79-
return
80-
}
81-
}
82-
}
83-
84-
// FK columns use searchable dropdown popover
49+
// FK columns use searchable dropdown popover on double click
8550
if columnIndex < rowProvider.columns.count {
8651
let columnName = rowProvider.columns[columnIndex]
8752
if let fkInfo = rowProvider.columnForeignKeys[columnName] {
@@ -90,42 +55,83 @@ extension TableViewCoordinator {
9055
}
9156
}
9257

93-
// Date columns use date picker popover
94-
if columnIndex < rowProvider.columnTypes.count,
95-
rowProvider.columnTypes[columnIndex].isDateType {
96-
showDatePickerPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
58+
// Multiline values use the overlay editor instead of inline field editor
59+
if let value = rowProvider.value(atRow: row, column: columnIndex),
60+
value.containsLineBreak {
61+
showOverlayEditor(tableView: sender, row: row, column: column, columnIndex: columnIndex, value: value)
9762
return
9863
}
9964

100-
// JSON columns (or text columns containing JSON) use JSON editor popover
101-
if columnIndex < rowProvider.columnTypes.count,
102-
rowProvider.columnTypes[columnIndex].isJsonType {
103-
showJSONEditorPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
104-
return
65+
// JSON-like text values in non-JSON/non-chevron columns
66+
if columnIndex < rowProvider.columnTypes.count {
67+
let ct = rowProvider.columnTypes[columnIndex]
68+
if ct.isBooleanType || ct.isDateType || ct.isBlobType || ct.isEnumType || ct.isSetType {
69+
return
70+
}
10571
}
106-
10772
if let cellValue = rowProvider.value(atRow: row, column: columnIndex),
10873
cellValue.looksLikeJson {
10974
showJSONEditorPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
11075
return
11176
}
11277

113-
// BLOB columns use hex editor popover
114-
if columnIndex < rowProvider.columnTypes.count,
115-
rowProvider.columnTypes[columnIndex].isBlobType {
116-
showBlobEditorPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
117-
return
78+
// Regular columns — start inline editing
79+
sender.editColumn(column, row: row, with: nil, select: true)
80+
}
81+
82+
// MARK: - Chevron Click
83+
84+
@objc func handleChevronClick(_ sender: NSButton) {
85+
guard let button = sender as? CellChevronButton,
86+
isEditable else { return }
87+
88+
let row = button.cellRow
89+
let columnIndex = button.cellColumnIndex
90+
guard row >= 0, columnIndex >= 0 else { return }
91+
guard !changeManager.isRowDeleted(row) else { return }
92+
93+
// Walk up the view hierarchy to find the NSTableView
94+
var current: NSView? = button.superview
95+
var tableView: NSTableView?
96+
while let view = current {
97+
if let tv = view as? NSTableView {
98+
tableView = tv
99+
break
100+
}
101+
current = view.superview
118102
}
103+
guard let tableView else { return }
104+
let column = columnIndex + 1
119105

120-
// Multiline values use the overlay editor instead of inline field editor
121-
if let value = rowProvider.value(atRow: row, column: columnIndex),
122-
value.containsLineBreak {
123-
showOverlayEditor(tableView: sender, row: row, column: column, columnIndex: columnIndex, value: value)
106+
// Structure view: dropdown and type picker columns take priority
107+
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
108+
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
109+
return
110+
}
111+
if let typePickerCols = typePickerColumns, typePickerCols.contains(columnIndex) {
112+
showTypePickerPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
124113
return
125114
}
126115

127-
// Regular columns — start inline editing
128-
sender.editColumn(column, row: row, with: nil, select: true)
116+
guard columnIndex < rowProvider.columnTypes.count,
117+
columnIndex < rowProvider.columns.count else { return }
118+
119+
let ct = rowProvider.columnTypes[columnIndex]
120+
let columnName = rowProvider.columns[columnIndex]
121+
122+
if ct.isBooleanType {
123+
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
124+
} else if ct.isEnumType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
125+
showEnumPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
126+
} else if ct.isSetType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
127+
showSetPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
128+
} else if ct.isDateType {
129+
showDatePickerPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
130+
} else if ct.isJsonType {
131+
showJSONEditorPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
132+
} else if ct.isBlobType {
133+
showBlobEditorPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
134+
}
129135
}
130136

131137
// MARK: - FK Navigation

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ extension TableViewCoordinator {
4646
let isEnumOrSet = enumOrSetColumns.contains(columnIndex)
4747
let isFKColumn = fkColumns.contains(columnIndex)
4848

49+
let hasSpecialEditor: Bool = {
50+
guard columnIndex < rowProvider.columnTypes.count else { return false }
51+
let ct = rowProvider.columnTypes[columnIndex]
52+
return ct.isBooleanType || ct.isDateType || ct.isJsonType || ct.isBlobType
53+
}()
54+
4955
return cellFactory.makeDataCell(
5056
tableView: tableView,
5157
row: row,
@@ -56,10 +62,12 @@ extension TableViewCoordinator {
5662
isEditable: isEditable && !state.isDeleted,
5763
isLargeDataset: isLargeDataset,
5864
isFocused: isFocused,
59-
isDropdown: isEditable && (isDropdown || isTypePicker || isEnumOrSet),
65+
isDropdown: isEditable && (isDropdown || isTypePicker || isEnumOrSet || hasSpecialEditor),
6066
isFKColumn: isFKColumn && !isDropdown && !(typePickerColumns?.contains(columnIndex) == true),
6167
fkArrowTarget: self,
6268
fkArrowAction: #selector(handleFKArrowClick(_:)),
69+
chevronTarget: self,
70+
chevronAction: #selector(handleChevronClick(_:)),
6371
delegate: self
6472
)
6573
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ extension TableViewCoordinator {
3434
}
3535
if columnIndex < rowProvider.columnTypes.count {
3636
let ct = rowProvider.columnTypes[columnIndex]
37-
if ct.isDateType || ct.isJsonType || ct.isEnumType || ct.isSetType || ct.isBlobType { return false }
37+
if ct.isDateType || ct.isJsonType || ct.isEnumType || ct.isSetType || ct.isBlobType || ct.isBooleanType { return false }
3838
}
3939
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
4040
return false

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ extension TableViewCoordinator {
238238
let currentValue = rowProvider.value(atRow: row, column: columnIndex)
239239
pendingDropdownRow = row
240240
pendingDropdownColumn = columnIndex
241+
pendingDropdownTableView = tableView
241242

242243
let menu = NSMenu()
243244
for option in ["YES", "NO"] {
@@ -254,10 +255,14 @@ extension TableViewCoordinator {
254255
}
255256

256257
@objc func dropdownMenuItemSelected(_ sender: NSMenuItem) {
257-
let newValue = sender.title
258-
let oldValue = rowProvider.value(atRow: pendingDropdownRow, column: pendingDropdownColumn)
259-
guard oldValue != newValue else { return }
260-
onCellEdit?(pendingDropdownRow, pendingDropdownColumn, newValue)
258+
guard let tableView = pendingDropdownTableView else { return }
259+
commitPopoverEdit(
260+
tableView: tableView,
261+
row: pendingDropdownRow,
262+
column: pendingDropdownColumn + 1,
263+
columnIndex: pendingDropdownColumn,
264+
newValue: sender.title
265+
)
261266
}
262267

263268
func commitPopoverEdit(tableView: NSTableView, row: Int, column: Int, columnIndex: Int, newValue: String?) {

0 commit comments

Comments
 (0)