From 6448c70f84bf2327bf4d77286e8042c46f6378f9 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 30 May 2026 17:37:20 +0700 Subject: [PATCH] fix(datagrid): put inspector field actions in a context menu for keyboard and VoiceOver (#1490) --- CHANGELOG.md | 1 + .../RightSidebar/EditableFieldView.swift | 16 ++++ .../FieldEditors/FieldMenuView.swift | 84 +++++++++++++------ 3 files changed, 74 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8105d916..194843a66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The cell inspector's Set NULL, Set DEFAULT, copy, and SQL-function actions are now in a right-click context menu on each field, so they're reachable by keyboard, Full Keyboard Access, and VoiceOver, not only on hover. (#1490) - Tab moves keyboard focus between the window panes (sidebar, results, inspector) by rebuilding the key view loop when the window appears. (#1490) - The license activation sheet focuses the key field on open, the SQL review sheet closes with Escape even while the editor has focus, and the integration token sheet focuses its Done button. (#1490) - Copy with Headers now has a default keyboard shortcut (Cmd+Option+C), so it works and is discoverable from the keyboard instead of showing in the Edit menu with no key. (#1490) diff --git a/TablePro/Views/RightSidebar/EditableFieldView.swift b/TablePro/Views/RightSidebar/EditableFieldView.swift index 72e12e174..c5e3c0b1d 100644 --- a/TablePro/Views/RightSidebar/EditableFieldView.swift +++ b/TablePro/Views/RightSidebar/EditableFieldView.swift @@ -73,6 +73,22 @@ internal struct FieldDetailView: View { } .labelsHidden() .onHover { isHovered = $0 } + .contextMenu { + if !context.isReadOnly { + FieldMenuContent( + value: context.value.wrappedValue, + columnType: context.columnType, + sqlFunctions: SQLFunctionProvider.functions(for: databaseType), + isPendingNull: isPendingNull, + isPendingDefault: isPendingDefault, + onSetNull: onSetNull, + onSetDefault: onSetDefault, + onSetEmpty: onSetEmpty, + onSetFunction: onSetFunction, + onClear: { context.value.wrappedValue = context.originalValue ?? "" } + ) + } + } } // MARK: - Header diff --git a/TablePro/Views/RightSidebar/FieldEditors/FieldMenuView.swift b/TablePro/Views/RightSidebar/FieldEditors/FieldMenuView.swift index c673be811..73723f76d 100644 --- a/TablePro/Views/RightSidebar/FieldEditors/FieldMenuView.swift +++ b/TablePro/Views/RightSidebar/FieldEditors/FieldMenuView.swift @@ -5,7 +5,9 @@ import SwiftUI -internal struct FieldMenuView: View { +/// The field actions (Set NULL/DEFAULT/EMPTY, copy, SQL functions). Shared by the +/// hover menu button and the field's context menu so both stay in sync. +internal struct FieldMenuContent: View { let value: String let columnType: ColumnType let sqlFunctions: [SQLFunctionProvider.SQLFunction] @@ -18,45 +20,73 @@ internal struct FieldMenuView: View { let onClear: () -> Void var body: some View { - Menu { - Button("Set NULL") { onSetNull() } - Button("Set DEFAULT") { onSetDefault() } - Button("Set EMPTY") { onSetEmpty() } + Button("Set NULL") { onSetNull() } + Button("Set DEFAULT") { onSetDefault() } + Button("Set EMPTY") { onSetEmpty() } - Divider() + Divider() - if columnType.isJsonType { - Button("Pretty Print") { - if let formatted = value.prettyPrintedAsJson() { - ClipboardService.shared.writeText(formatted) - } + if columnType.isJsonType { + Button("Pretty Print") { + if let formatted = value.prettyPrintedAsJson() { + ClipboardService.shared.writeText(formatted) } } + } - if BlobFormattingService.shared.requiresFormatting(columnType: columnType) { - Button("Copy as Hex") { - if let hex = BlobFormattingService.shared.format(value, for: .detail) { - ClipboardService.shared.writeText(hex) - } + if BlobFormattingService.shared.requiresFormatting(columnType: columnType) { + Button("Copy as Hex") { + if let hex = BlobFormattingService.shared.format(value, for: .detail) { + ClipboardService.shared.writeText(hex) } } + } + + Button("Copy Value") { + ClipboardService.shared.writeText(value) + } - Button("Copy Value") { - ClipboardService.shared.writeText(value) + Divider() + + Menu("SQL Functions") { + ForEach(sqlFunctions, id: \.expression) { function in + Button(function.label) { onSetFunction(function.expression) } } + } + if isPendingNull || isPendingDefault { Divider() + Button("Clear") { onClear() } + } + } +} - Menu("SQL Functions") { - ForEach(sqlFunctions, id: \.expression) { function in - Button(function.label) { onSetFunction(function.expression) } - } - } +internal struct FieldMenuView: View { + let value: String + let columnType: ColumnType + let sqlFunctions: [SQLFunctionProvider.SQLFunction] + let isPendingNull: Bool + let isPendingDefault: Bool + let onSetNull: () -> Void + let onSetDefault: () -> Void + let onSetEmpty: () -> Void + let onSetFunction: (String) -> Void + let onClear: () -> Void - if isPendingNull || isPendingDefault { - Divider() - Button("Clear") { onClear() } - } + var body: some View { + Menu { + FieldMenuContent( + value: value, + columnType: columnType, + sqlFunctions: sqlFunctions, + isPendingNull: isPendingNull, + isPendingDefault: isPendingDefault, + onSetNull: onSetNull, + onSetDefault: onSetDefault, + onSetEmpty: onSetEmpty, + onSetFunction: onSetFunction, + onClear: onClear + ) } label: { Image(systemName: "chevron.down") .font(.caption)