From c41c25d1d0136926ef3985585eb8411c21b2b5c3 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 10 Apr 2026 20:10:01 +0700 Subject: [PATCH 1/2] feat: auto-uppercase SQL keywords as you type (#660) --- CHANGELOG.md | 4 + TablePro/Core/Autocomplete/SQLKeywords.swift | 2 + TablePro/Models/Settings/EditorSettings.swift | 9 +- TablePro/Resources/Localizable.xcstrings | 3 + .../Views/Editor/SQLEditorCoordinator.swift | 124 +++++++++++++++++- .../Views/Settings/EditorSettingsView.swift | 1 + docs/customization/editor-settings.mdx | 9 ++ 7 files changed, 143 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 803533861..a562a8386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Auto-uppercase SQL keywords setting (#660) + ## [0.30.0] - 2026-04-10 ### Added diff --git a/TablePro/Core/Autocomplete/SQLKeywords.swift b/TablePro/Core/Autocomplete/SQLKeywords.swift index 4c9d21a21..aef7b2119 100644 --- a/TablePro/Core/Autocomplete/SQLKeywords.swift +++ b/TablePro/Core/Autocomplete/SQLKeywords.swift @@ -11,6 +11,8 @@ import Foundation enum SQLKeywords { // MARK: - Keywords + static let keywordSet: Set = Set(keywords.filter { !$0.contains(" ") }.map { $0.lowercased() }) + /// Primary SQL keywords static let keywords: [String] = [ // DQL diff --git a/TablePro/Models/Settings/EditorSettings.swift b/TablePro/Models/Settings/EditorSettings.swift index 8e3d48459..7890794fd 100644 --- a/TablePro/Models/Settings/EditorSettings.swift +++ b/TablePro/Models/Settings/EditorSettings.swift @@ -69,6 +69,7 @@ struct EditorSettings: Codable, Equatable { var autoIndent: Bool var wordWrap: Bool var vimModeEnabled: Bool + var uppercaseKeywords: Bool static let `default` = EditorSettings( showLineNumbers: true, @@ -76,7 +77,8 @@ struct EditorSettings: Codable, Equatable { tabWidth: 4, autoIndent: true, wordWrap: false, - vimModeEnabled: false + vimModeEnabled: false, + uppercaseKeywords: false ) init( @@ -85,7 +87,8 @@ struct EditorSettings: Codable, Equatable { tabWidth: Int = 4, autoIndent: Bool = true, wordWrap: Bool = false, - vimModeEnabled: Bool = false + vimModeEnabled: Bool = false, + uppercaseKeywords: Bool = false ) { self.showLineNumbers = showLineNumbers self.highlightCurrentLine = highlightCurrentLine @@ -93,6 +96,7 @@ struct EditorSettings: Codable, Equatable { self.autoIndent = autoIndent self.wordWrap = wordWrap self.vimModeEnabled = vimModeEnabled + self.uppercaseKeywords = uppercaseKeywords } init(from decoder: Decoder) throws { @@ -104,6 +108,7 @@ struct EditorSettings: Codable, Equatable { autoIndent = try container.decodeIfPresent(Bool.self, forKey: .autoIndent) ?? true wordWrap = try container.decodeIfPresent(Bool.self, forKey: .wordWrap) ?? false vimModeEnabled = try container.decodeIfPresent(Bool.self, forKey: .vimModeEnabled) ?? false + uppercaseKeywords = try container.decodeIfPresent(Bool.self, forKey: .uppercaseKeywords) ?? false } /// Clamped tab width (1-16) diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 89482c58d..bb2a9d579 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -5282,6 +5282,9 @@ } } } + }, + "Auto-uppercase keywords" : { + }, "Automatically check for updates" : { "localizations" : { diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index 568fd4bcc..daa9477c7 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -15,7 +15,7 @@ import os /// Coordinator for the SQL editor — manages find panel, horizontal scrolling, and scroll-to-match @Observable @MainActor -final class SQLEditorCoordinator: TextViewCoordinator { +final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate { // MARK: - Properties private static let logger = Logger(subsystem: "com.TablePro", category: "SQLEditorCoordinator") @@ -32,6 +32,7 @@ final class SQLEditorCoordinator: TextViewCoordinator { /// Debounce work item for frame-change notification to avoid /// triggering syntax highlight viewport recalculation on every keystroke. @ObservationIgnored private var frameChangeTask: Task? + @ObservationIgnored private var isUppercasing = false @ObservationIgnored private var wasEditorFocused = false @ObservationIgnored private var didDestroy = false @@ -121,25 +122,22 @@ final class SQLEditorCoordinator: TextViewCoordinator { } } - func textViewDidChangeText(controller: TextViewController) { - // Invalidate Vim buffer's cached line count after text changes + func textView(_ textView: TextView, didReplaceContentsIn range: NSRange, with string: String) { vimEngine?.invalidateLineCache() - // Notify inline suggestion manager immediately (lightweight) Task { [weak self] in self?.inlineSuggestionManager?.handleTextChange() self?.vimCursorManager?.updatePosition() } - // Throttle frame-change notification — during rapid typing, only the - // last notification matters. The highlighter recalculates the visible - // range on each notification, so coalescing saves redundant layout work. frameChangeTask?.cancel() frameChangeTask = Task { [weak controller] in try? await Task.sleep(for: .milliseconds(50)) guard !Task.isCancelled, let controller, let textView = controller.textView else { return } NotificationCenter.default.post(name: NSView.frameDidChangeNotification, object: textView) } + + uppercaseKeywordIfNeeded(textView: textView, range: range, string: string) } func textViewDidChangeSelection(controller: TextViewController, newPositions: [CursorPosition]) { @@ -350,6 +348,118 @@ final class SQLEditorCoordinator: TextViewCoordinator { } } + // MARK: - Keyword Auto-Uppercase + + private func uppercaseKeywordIfNeeded(textView: TextView, range: NSRange, string: String) { + guard !isUppercasing, + AppSettingsManager.shared.editor.uppercaseKeywords, + isWordBoundary(string) else { return } + + let nsText = textView.textStorage.string as NSString + let wordEnd = range.location + + var wordStart = wordEnd + while wordStart > 0 { + let ch = nsText.character(at: wordStart - 1) + guard isWordCharacter(ch) else { break } + wordStart -= 1 + } + + let wordLength = wordEnd - wordStart + guard wordLength > 0 else { return } + + let word = nsText.substring(with: NSRange(location: wordStart, length: wordLength)) + guard SQLKeywords.keywordSet.contains(word.lowercased()) else { return } + guard !isInsideProtectedContext(nsText, at: wordStart) else { return } + + let uppercased = word.uppercased() + guard uppercased != word else { return } + + // Mutate textStorage directly — we're inside beginEditing/endEditing + // so NSTextStorage consolidates this with the delimiter insertion. + // Cannot use textView.replaceCharacters here because CEUndoManager's + // registerMutation calls inverseMutation which asserts mid-edit. + let wordRange = NSRange(location: wordStart, length: wordLength) + isUppercasing = true + textView.textStorage.replaceCharacters(in: wordRange, with: uppercased) + textView.selectionManager.didReplaceCharacters(in: wordRange, replacementLength: wordLength) + isUppercasing = false + } + + private func isWordBoundary(_ string: String) -> Bool { + guard (string as NSString).length == 1, let ch = string.unicodeScalars.first else { return false } + switch ch { + case " ", "\t", "\n", "\r", "(", ")", ",", ";": + return true + default: + return false + } + } + + private func isWordCharacter(_ ch: unichar) -> Bool { + (ch >= 0x41 && ch <= 0x5A) || (ch >= 0x61 && ch <= 0x7A) || + (ch >= 0x30 && ch <= 0x39) || ch == 0x5F + } + + private func isInsideProtectedContext(_ text: NSString, at position: Int) -> Bool { + let scanStart = max(0, position - 2000) + var inSingleQuote = false + var inDoubleQuote = false + var inBacktick = false + var inLineComment = false + var inBlockComment = false + var i = scanStart + + while i < position { + let ch = text.character(at: i) + + if inBlockComment { + if ch == 0x2A && i + 1 < position && text.character(at: i + 1) == 0x2F { + inBlockComment = false + i += 2 + continue + } + i += 1 + continue + } + if inLineComment { + if ch == 0x0A { inLineComment = false } + i += 1 + continue + } + + // Skip backslash-escaped characters (e.g. \' inside strings) + if ch == 0x5C && (inSingleQuote || inDoubleQuote) { + i += 2 + continue + } + + switch ch { + case 0x27: if !inDoubleQuote && !inBacktick { inSingleQuote.toggle() } + case 0x22: if !inSingleQuote && !inBacktick { inDoubleQuote.toggle() } + case 0x60: if !inSingleQuote && !inDoubleQuote { inBacktick.toggle() } + case 0x2D: + if !inSingleQuote && !inDoubleQuote && !inBacktick && + i + 1 < position && text.character(at: i + 1) == 0x2D { + inLineComment = true + i += 2 + continue + } + case 0x2F: + if !inSingleQuote && !inDoubleQuote && !inBacktick && + i + 1 < position && text.character(at: i + 1) == 0x2A { + inBlockComment = true + i += 2 + continue + } + default: break + } + i += 1 + } + + return inSingleQuote || inDoubleQuote || inBacktick || inLineComment || inBlockComment + } + // MARK: - CodeEditSourceEditor Workarounds /// Reorder FindViewController's subviews so the find panel is on top for hit testing. diff --git a/TablePro/Views/Settings/EditorSettingsView.swift b/TablePro/Views/Settings/EditorSettingsView.swift index fd11fb0b1..bdccf0cd0 100644 --- a/TablePro/Views/Settings/EditorSettingsView.swift +++ b/TablePro/Views/Settings/EditorSettingsView.swift @@ -26,6 +26,7 @@ struct EditorSettingsView: View { Text("8 spaces").tag(8) } Toggle("Vim mode", isOn: $settings.vimModeEnabled) + Toggle("Auto-uppercase keywords", isOn: $settings.uppercaseKeywords) } } .formStyle(.grouped) diff --git a/docs/customization/editor-settings.mdx b/docs/customization/editor-settings.mdx index 63f024262..b54f9b807 100644 --- a/docs/customization/editor-settings.mdx +++ b/docs/customization/editor-settings.mdx @@ -209,6 +209,15 @@ SELECT ## Editor Behavior +### Auto-Uppercase Keywords + +| Option | Description | +|--------|-------------| +| **Off** | Keywords stay as typed (default) | +| **On** | SQL keywords auto-uppercase when you type a space or delimiter | + +When enabled, recognized SQL keywords (`select`, `from`, `where`, `join`, etc.) are converted to uppercase as soon as you type a word boundary character (space, tab, newline, parenthesis, comma, or semicolon). Keywords inside strings, comments, and backtick-quoted identifiers are left unchanged. + ### Autocomplete Autocomplete is always on. Dismiss with `Escape`. See [Autocomplete](/features/autocomplete) for details. From e2da8048c1c9fa58190ecfc6f07b84014e87c000 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 10 Apr 2026 22:05:06 +0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20unify=20DataGrid=20cell=20editors=20?= =?UTF-8?q?=E2=80=94=20chevron=20button=20for=20all=20special=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../Views/Results/DataGridCellFactory.swift | 22 ++- .../Views/Results/DataGridCoordinator.swift | 1 + .../Extensions/DataGridView+Click.swift | 126 +++++++++--------- .../Extensions/DataGridView+Columns.swift | 10 +- .../Extensions/DataGridView+Editing.swift | 2 +- .../Extensions/DataGridView+Popovers.swift | 13 +- 6 files changed, 106 insertions(+), 68 deletions(-) diff --git a/TablePro/Views/Results/DataGridCellFactory.swift b/TablePro/Views/Results/DataGridCellFactory.swift index befa469b8..4bd571894 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 5a9c175b6..e7fa14aa8 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 72567508b..b24248421 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 315e81587..28e4f1b4e 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 19dcc8adc..29e3c05fa 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 2c1c080a9..ac0fa8c6a 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?) {