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
22 changes: 20 additions & 2 deletions TablePro/Views/Results/DataGridCellFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ final class FKArrowButton: NSButton {
var fkColumnIndex: Int = 0
}

/// Custom button that stores cell row/column context for the chevron click handler
@MainActor
final class CellChevronButton: NSButton {
var cellRow: Int = -1
var cellColumnIndex: Int = -1
}

/// Factory for creating data grid cell views
@MainActor
final class DataGridCellFactory {
Expand Down Expand Up @@ -138,6 +145,8 @@ final class DataGridCellFactory {
isFKColumn: Bool = false,
fkArrowTarget: AnyObject? = nil,
fkArrowAction: Selector? = nil,
chevronTarget: AnyObject? = nil,
chevronAction: Selector? = nil,
delegate: NSTextFieldDelegate
) -> NSView {
let cellViewId: NSUserInterfaceItemIdentifier
Expand Down Expand Up @@ -177,9 +186,11 @@ final class DataGridCellFactory {
cellView.addSubview(cell)

if isDropdown {
let chevron = NSImageView()
let chevron = CellChevronButton()
chevron.tag = Self.chevronTag
chevron.image = NSImage(systemSymbolName: "chevron.up.chevron.down", accessibilityDescription: nil)
chevron.bezelStyle = .inline
chevron.isBordered = false
chevron.image = NSImage(systemSymbolName: "chevron.up.chevron.down", accessibilityDescription: String(localized: "Open editor"))
chevron.contentTintColor = .tertiaryLabelColor
chevron.translatesAutoresizingMaskIntoConstraints = false
chevron.setContentHuggingPriority(.required, for: .horizontal)
Expand Down Expand Up @@ -248,6 +259,13 @@ final class DataGridCellFactory {
button.isHidden = (rawValue == nil || rawValue?.isEmpty == true)
}

if isDropdown, let chevronButton = cellView.viewWithTag(Self.chevronTag) as? CellChevronButton {
chevronButton.cellRow = row
chevronButton.cellColumnIndex = columnIndex
chevronButton.target = chevronTarget
chevronButton.action = chevronAction
}

cell.isEditable = isEditable
cell.delegate = delegate
cell.identifier = cellIdentifier
Expand Down
1 change: 1 addition & 0 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
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
126 changes: 66 additions & 60 deletions TablePro/Views/Results/Extensions/DataGridView+Click.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,14 @@ extension TableViewCoordinator {
let columnIndex = column - 1
guard !changeManager.isRowDeleted(row) else { return }

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

// ENUM/SET columns open on single click
if columnIndex < rowProvider.columnTypes.count,
columnIndex < rowProvider.columns.count {
let ct = rowProvider.columnTypes[columnIndex]
let columnName = rowProvider.columns[columnIndex]
if ct.isEnumType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
showEnumPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
return
}
if ct.isSetType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
showSetPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
return
}
}
// All other special editor columns are handled by their chevron button action.
// Single click on cell text area does nothing.
}

@objc func handleDoubleClick(_ sender: NSTableView) {
Expand All @@ -58,30 +46,7 @@ extension TableViewCoordinator {
return
}

// Dropdown columns already handled by single click
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
return
}

// Type picker columns use database-specific type popover
if let typePickerCols = typePickerColumns, typePickerCols.contains(columnIndex) {
showTypePickerPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
return
}

// ENUM/SET columns already handled by single click
if columnIndex < rowProvider.columnTypes.count,
columnIndex < rowProvider.columns.count {
let ct = rowProvider.columnTypes[columnIndex]
if ct.isEnumType || ct.isSetType {
let columnName = rowProvider.columns[columnIndex]
if let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
return
}
}
}

// FK columns use searchable dropdown popover
// FK columns use searchable dropdown popover on double click
if columnIndex < rowProvider.columns.count {
let columnName = rowProvider.columns[columnIndex]
if let fkInfo = rowProvider.columnForeignKeys[columnName] {
Expand All @@ -90,42 +55,83 @@ extension TableViewCoordinator {
}
}

// Date columns use date picker popover
if columnIndex < rowProvider.columnTypes.count,
rowProvider.columnTypes[columnIndex].isDateType {
showDatePickerPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
// Multiline values use the overlay editor instead of inline field editor
if let value = rowProvider.value(atRow: row, column: columnIndex),
value.containsLineBreak {
showOverlayEditor(tableView: sender, row: row, column: column, columnIndex: columnIndex, value: value)
return
}

// JSON columns (or text columns containing JSON) use JSON editor popover
if columnIndex < rowProvider.columnTypes.count,
rowProvider.columnTypes[columnIndex].isJsonType {
showJSONEditorPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
return
// JSON-like text values in non-JSON/non-chevron columns
if columnIndex < rowProvider.columnTypes.count {
let ct = rowProvider.columnTypes[columnIndex]
if ct.isBooleanType || ct.isDateType || ct.isBlobType || ct.isEnumType || ct.isSetType {
return
}
}

if let cellValue = rowProvider.value(atRow: row, column: columnIndex),
cellValue.looksLikeJson {
showJSONEditorPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
return
}

// BLOB columns use hex editor popover
if columnIndex < rowProvider.columnTypes.count,
rowProvider.columnTypes[columnIndex].isBlobType {
showBlobEditorPopover(tableView: sender, row: row, column: column, columnIndex: columnIndex)
return
// Regular columns — start inline editing
sender.editColumn(column, row: row, with: nil, select: true)
}

// MARK: - Chevron Click

@objc func handleChevronClick(_ sender: NSButton) {
guard let button = sender as? CellChevronButton,
isEditable else { return }

let row = button.cellRow
let columnIndex = button.cellColumnIndex
guard row >= 0, columnIndex >= 0 else { return }
guard !changeManager.isRowDeleted(row) else { return }

// Walk up the view hierarchy to find the NSTableView
var current: NSView? = button.superview
var tableView: NSTableView?
while let view = current {
if let tv = view as? NSTableView {
tableView = tv
break
}
current = view.superview
}
guard let tableView else { return }
let column = columnIndex + 1

// Multiline values use the overlay editor instead of inline field editor
if let value = rowProvider.value(atRow: row, column: columnIndex),
value.containsLineBreak {
showOverlayEditor(tableView: sender, row: row, column: column, columnIndex: columnIndex, value: value)
// Structure view: dropdown and type picker columns take priority
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
return
}
if let typePickerCols = typePickerColumns, typePickerCols.contains(columnIndex) {
showTypePickerPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
return
}

// Regular columns — start inline editing
sender.editColumn(column, row: row, with: nil, select: true)
guard columnIndex < rowProvider.columnTypes.count,
columnIndex < rowProvider.columns.count else { return }

let ct = rowProvider.columnTypes[columnIndex]
let columnName = rowProvider.columns[columnIndex]

if ct.isBooleanType {
showDropdownMenu(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
} else if ct.isEnumType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
showEnumPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
} else if ct.isSetType, let values = rowProvider.columnEnumValues[columnName], !values.isEmpty {
showSetPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
} else if ct.isDateType {
showDatePickerPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
} else if ct.isJsonType {
showJSONEditorPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
} else if ct.isBlobType {
showBlobEditorPopover(tableView: tableView, row: row, column: column, columnIndex: columnIndex)
}
}

// MARK: - FK Navigation
Expand Down
10 changes: 9 additions & 1 deletion TablePro/Views/Results/Extensions/DataGridView+Columns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ extension TableViewCoordinator {
let isEnumOrSet = enumOrSetColumns.contains(columnIndex)
let isFKColumn = fkColumns.contains(columnIndex)

let hasSpecialEditor: Bool = {
guard columnIndex < rowProvider.columnTypes.count else { return false }
let ct = rowProvider.columnTypes[columnIndex]
return ct.isBooleanType || ct.isDateType || ct.isJsonType || ct.isBlobType
}()

return cellFactory.makeDataCell(
tableView: tableView,
row: row,
Expand All @@ -56,10 +62,12 @@ extension TableViewCoordinator {
isEditable: isEditable && !state.isDeleted,
isLargeDataset: isLargeDataset,
isFocused: isFocused,
isDropdown: isEditable && (isDropdown || isTypePicker || isEnumOrSet),
isDropdown: isEditable && (isDropdown || isTypePicker || isEnumOrSet || hasSpecialEditor),
isFKColumn: isFKColumn && !isDropdown && !(typePickerColumns?.contains(columnIndex) == true),
fkArrowTarget: self,
fkArrowAction: #selector(handleFKArrowClick(_:)),
chevronTarget: self,
chevronAction: #selector(handleChevronClick(_:)),
delegate: self
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ extension TableViewCoordinator {
}
if columnIndex < rowProvider.columnTypes.count {
let ct = rowProvider.columnTypes[columnIndex]
if ct.isDateType || ct.isJsonType || ct.isEnumType || ct.isSetType || ct.isBlobType { return false }
if ct.isDateType || ct.isJsonType || ct.isEnumType || ct.isSetType || ct.isBlobType || ct.isBooleanType { return false }
}
if let dropdownCols = dropdownColumns, dropdownCols.contains(columnIndex) {
return false
Expand Down
13 changes: 9 additions & 4 deletions TablePro/Views/Results/Extensions/DataGridView+Popovers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ extension TableViewCoordinator {
let currentValue = rowProvider.value(atRow: row, column: columnIndex)
pendingDropdownRow = row
pendingDropdownColumn = columnIndex
pendingDropdownTableView = tableView

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

@objc func dropdownMenuItemSelected(_ sender: NSMenuItem) {
let newValue = sender.title
let oldValue = rowProvider.value(atRow: pendingDropdownRow, column: pendingDropdownColumn)
guard oldValue != newValue else { return }
onCellEdit?(pendingDropdownRow, pendingDropdownColumn, newValue)
guard let tableView = pendingDropdownTableView else { return }
commitPopoverEdit(
tableView: tableView,
row: pendingDropdownRow,
column: pendingDropdownColumn + 1,
columnIndex: pendingDropdownColumn,
newValue: sender.title
)
}

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