diff --git a/TablePro/Views/Results/DataGridCellFactory.swift b/TablePro/Views/Results/DataGridCellFactory.swift index befa469b..4bd57189 100644 --- a/TablePro/Views/Results/DataGridCellFactory.swift +++ b/TablePro/Views/Results/DataGridCellFactory.swift @@ -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 { @@ -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 @@ -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) @@ -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 diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index 5a9c175b..e7fa14aa 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -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 diff --git a/TablePro/Views/Results/Extensions/DataGridView+Click.swift b/TablePro/Views/Results/Extensions/DataGridView+Click.swift index 72567508..b2424842 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Click.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Click.swift @@ -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) { @@ -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] { @@ -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 diff --git a/TablePro/Views/Results/Extensions/DataGridView+Columns.swift b/TablePro/Views/Results/Extensions/DataGridView+Columns.swift index 315e8158..28e4f1b4 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Columns.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Columns.swift @@ -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, @@ -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 ) } diff --git a/TablePro/Views/Results/Extensions/DataGridView+Editing.swift b/TablePro/Views/Results/Extensions/DataGridView+Editing.swift index 19dcc8ad..29e3c05f 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Editing.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Editing.swift @@ -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 diff --git a/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift b/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift index 2c1c080a..ac0fa8c6 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Popovers.swift @@ -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"] { @@ -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?) {