From cb3da45cfa7e77d9e5f8076c4634392eeac41d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Sat, 12 Sep 2026 10:21:16 +0700 Subject: [PATCH 1/3] feat(settings): theme the content panes outside the editor and the data grid (#2781) Claude-Session: https://claude.ai/code/session_01EKDGt17u6TaVBDHm8rzSEj --- CHANGELOG.md | 4 ++ .../Core/Autocomplete/SQLCompletionItem.swift | 17 +++++---- .../MainSplitViewController.swift | 6 +-- TablePro/Models/UI/JSONTreeNode.swift | 11 +++--- TablePro/Models/UI/PhpTreeNode.swift | 11 +++--- .../Resources/Themes/tablepro.dracula.json | 8 ++++ TablePro/Resources/Themes/tablepro.nord.json | 8 ++++ TablePro/Theme/BuiltInThemes.swift | 15 ++++++++ TablePro/Theme/ThemeDefinition.swift | 37 +++++++++++++++++++ TablePro/Theme/ThemedContentSurface.swift | 27 ++++++++++++++ .../Editor/FileModifiedOnDiskBanner.swift | 4 +- .../Editor/History/HistoryDetailPane.swift | 2 +- .../Views/Editor/History/HistoryRowView.swift | 2 +- .../Views/Editor/QueryCompletionAdapter.swift | 5 ++- .../Editor/QueryDiagnosticsController.swift | 12 +++++- TablePro/Views/Editor/QueryEditorView.swift | 1 - .../Views/Editor/SQLEditorCoordinator.swift | 7 ++++ TablePro/Views/Editor/SQLEditorView.swift | 1 + TablePro/Views/Import/SQLCodePreview.swift | 2 +- .../QueryInsightsActivityChart.swift | 2 +- .../QueryInsightsGroupList.swift | 6 +-- .../QueryInsightsSummaryBar.swift | 2 +- .../Views/Results/ArrayValueEditorView.swift | 2 +- .../Results/ColumnValueFilterPopover.swift | 2 +- .../Views/Results/DataGridCoordinator.swift | 1 + .../Views/Results/ForeignKeyPickerView.swift | 2 +- .../Views/Results/ForeignKeyPreviewView.swift | 2 +- .../Views/Results/HexEditorContentView.swift | 16 +++++--- .../Views/Results/InlineErrorBanner.swift | 4 +- TablePro/Views/Results/JSONTreeView.swift | 2 +- TablePro/Views/Results/PhpTreeView.swift | 2 +- .../Views/Results/ResultChartCanvas.swift | 14 +++---- .../Views/Results/ResultSuccessView.swift | 2 +- .../Selection/GridSelectionOverlay.swift | 2 +- .../FieldEditors/BlobHexEditorView.swift | 4 +- .../FieldEditors/FieldEditorContent.swift | 4 +- .../FieldEditors/ImageFieldView.swift | 2 +- .../FieldEditors/JsonEditorView.swift | 2 +- .../FieldEditors/MultiLineEditorView.swift | 2 +- .../FieldEditors/PhpSerializedFieldView.swift | 2 +- .../ResizableEditorContainer.swift | 2 +- .../FieldEditors/SingleLineEditorView.swift | 4 +- .../JSON/JSONRowInspectorView.swift | 2 +- .../ServerDashboard/MetricsBarView.swift | 2 +- .../ServerDashboard/SessionsTableView.swift | 8 ++-- .../ServerDashboard/SlowQueryListView.swift | 4 +- .../Views/Structure/ClickHousePartsView.swift | 4 +- .../Views/Structure/CreateTableView.swift | 2 +- TablePro/Views/Structure/DDLTextView.swift | 2 +- .../Structure/TableStructureView+Schema.swift | 2 +- .../Views/Structure/TableStructureView.swift | 2 +- .../Views/Structure/TriggerDetailView.swift | 4 +- docs/customization/appearance.mdx | 2 + 53 files changed, 212 insertions(+), 85 deletions(-) create mode 100644 TablePro/Theme/ThemedContentSurface.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe136b7c4..a8960a4cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Header, grid line, selection and focus colors in the theme editor. - Themes that name a system color for a slot, so the built-in themes keep the system's own contrast settings. - Reason shown in Settings > Appearance for a theme file that could not be loaded. +- Panel and status colors in the theme editor, covering the results, inspector, structure, compare and query plan panes. - **Refresh Materialized View…** on PostgreSQL, with a concurrent refresh where the view qualifies. (#2726) - **Show DDL** and **Copy DDL** for views and materialized views. (#2726) - **Edit Comment…** for PostgreSQL tables, views, materialized views and foreign tables. (#2726) @@ -75,6 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Query history preview ignoring the theme and the editor font. - Theme editor changing the active theme instead of the theme selected for the slot being edited. - Malformed theme files loading as Default Light under their own name. +- Content panes outside the editor and the data grid ignoring the theme. +- JSON and PHP tree values colored differently from the same values in the row inspector. +- Autocomplete icon colors ignoring the theme. - Color with a typo in it rendering as a different color instead of being reported. - Text past the first 64 KB of a UTF-16 SQL import arriving byte-swapped. - SQL import failing on a file whose encoding is not UTF-8 when a character lands on a 64 KB boundary. diff --git a/TablePro/Core/Autocomplete/SQLCompletionItem.swift b/TablePro/Core/Autocomplete/SQLCompletionItem.swift index 39e7030e3a..378d170948 100644 --- a/TablePro/Core/Autocomplete/SQLCompletionItem.swift +++ b/TablePro/Core/Autocomplete/SQLCompletionItem.swift @@ -36,16 +36,17 @@ enum SQLCompletionKind: String, CaseIterable { } /// Color for the icon + @MainActor var iconColor: NSColor { switch self { - case .keyword: return .systemBlue - case .table: return .systemTeal - case .view: return .systemPurple - case .column: return .systemOrange - case .function: return .systemPink - case .schema: return .systemGreen - case .alias: return .systemGray - case .operator: return .systemIndigo + case .keyword: return ThemeEngine.shared.palette[.syntaxKeyword] + case .table: return ThemeEngine.shared.palette[.syntaxType] + case .view: return ThemeEngine.shared.palette[.syntaxType] + case .column: return ThemeEngine.shared.palette[.syntaxNull] + case .function: return ThemeEngine.shared.palette[.syntaxFunction] + case .schema: return ThemeEngine.shared.palette[.syntaxType] + case .alias: return ThemeEngine.shared.palette[.syntaxNull] + case .operator: return ThemeEngine.shared.palette[.syntaxOperator] case .favorite: return .systemYellow } } diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index f1b3c867f8..f6802b77e0 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -714,8 +714,8 @@ internal final class MainSplitViewController: NSSplitViewController, TrailingPan /// intermediate value instead of letting one run-loop turn settle on the final one. private func refreshPanes(of workspace: ConnectionWorkspace) { workspace.panes.sidebar.rootView = AnyView(buildSidebarView(for: workspace)) - workspace.panes.detail.rootView = AnyView(buildDetailView(for: workspace)) - workspace.panes.inspector.rootView = AnyView(buildInspectorView(for: workspace)) + workspace.panes.detail.rootView = AnyView(buildDetailView(for: workspace).themedContent()) + workspace.panes.inspector.rootView = AnyView(buildInspectorView(for: workspace).themedContent()) workspace.panes.assistant.rootView = AnyView(buildAssistantView(for: workspace)) refreshTabStripPane(of: workspace) workspace.panes.markRendered(workspace.paneRenderKey) @@ -903,7 +903,7 @@ internal final class MainSplitViewController: NSSplitViewController, TrailingPan /// publishes those actions. func rebuildTrailingPanes() { guard let selected = workspaces.selected else { return } - selected.panes.inspector.rootView = AnyView(buildInspectorView(for: selected)) + selected.panes.inspector.rootView = AnyView(buildInspectorView(for: selected).themedContent()) selected.panes.assistant.rootView = AnyView(buildAssistantView(for: selected)) } diff --git a/TablePro/Models/UI/JSONTreeNode.swift b/TablePro/Models/UI/JSONTreeNode.swift index 8ed335fdc7..1b6ccd3b6f 100644 --- a/TablePro/Models/UI/JSONTreeNode.swift +++ b/TablePro/Models/UI/JSONTreeNode.swift @@ -27,13 +27,14 @@ internal enum JSONValueType { } } + @MainActor var color: NSColor { switch self { - case .object, .array: return .systemBlue - case .string: return .systemRed - case .number: return .systemPurple - case .boolean, .null: return .systemOrange - case .truncated: return .secondaryLabelColor + case .object, .array: return ThemeEngine.shared.palette[.syntaxKeyword] + case .string: return ThemeEngine.shared.palette[.syntaxString] + case .number: return ThemeEngine.shared.palette[.syntaxNumber] + case .boolean, .null: return ThemeEngine.shared.palette[.syntaxNull] + case .truncated: return ThemeEngine.shared.palette[.panelSecondaryText] } } } diff --git a/TablePro/Models/UI/PhpTreeNode.swift b/TablePro/Models/UI/PhpTreeNode.swift index 3683a5e417..11eec35a0e 100644 --- a/TablePro/Models/UI/PhpTreeNode.swift +++ b/TablePro/Models/UI/PhpTreeNode.swift @@ -35,13 +35,14 @@ internal enum PhpNodeType { } } + @MainActor var color: NSColor { switch self { - case .array, .object: return .systemBlue - case .string: return .systemRed - case .int, .float: return .systemPurple - case .bool, .null: return .systemOrange - case .serializable: return .systemTeal + case .array, .object: return ThemeEngine.shared.palette[.syntaxKeyword] + case .string: return ThemeEngine.shared.palette[.syntaxString] + case .int, .float: return ThemeEngine.shared.palette[.syntaxNumber] + case .bool, .null: return ThemeEngine.shared.palette[.syntaxNull] + case .serializable: return ThemeEngine.shared.palette[.syntaxType] case .reference: return .systemGray case .unsupported, .truncated: return .secondaryLabelColor } diff --git a/TablePro/Resources/Themes/tablepro.dracula.json b/TablePro/Resources/Themes/tablepro.dracula.json index 70ffe0a603..1148a8f2f9 100644 --- a/TablePro/Resources/Themes/tablepro.dracula.json +++ b/TablePro/Resources/Themes/tablepro.dracula.json @@ -42,6 +42,14 @@ }, "text": "#F8F8F2" }, + "panel": { + "background": "#282A36", + "controlBackground": "#343746", + "secondaryText": "#BFC0C9", + "separator": "#44475A", + "tertiaryText": "#6272A4", + "text": "#F8F8F2" + }, "status": { "error": "#FF5555", "success": "#50FA7B", diff --git a/TablePro/Resources/Themes/tablepro.nord.json b/TablePro/Resources/Themes/tablepro.nord.json index ef58c5fe3c..7448128435 100644 --- a/TablePro/Resources/Themes/tablepro.nord.json +++ b/TablePro/Resources/Themes/tablepro.nord.json @@ -42,6 +42,14 @@ }, "text": "#D8DEE9" }, + "panel": { + "background": "#2E3440", + "controlBackground": "#3B4252", + "secondaryText": "#9DA5B4", + "separator": "#434C5E", + "tertiaryText": "#616E88", + "text": "#D8DEE9" + }, "status": { "error": "#BF616A", "success": "#A3BE8C", diff --git a/TablePro/Theme/BuiltInThemes.swift b/TablePro/Theme/BuiltInThemes.swift index 61481ad8d6..560c37bd84 100644 --- a/TablePro/Theme/BuiltInThemes.swift +++ b/TablePro/Theme/BuiltInThemes.swift @@ -39,6 +39,7 @@ internal enum BuiltInThemes { deleted: "#FF3B304D", deletedText: "#FF3B3080" ), + panel: .systemPanel, status: StatusThemeColors( success: .hex("#248A3D"), warning: .hex("#C55B00"), @@ -77,6 +78,7 @@ internal enum BuiltInThemes { deleted: "#FF453A26", deletedText: "#FF453A80" ), + panel: .systemPanel, status: StatusThemeColors( success: .hex("#32D74B"), warning: .hex("#FF9F0A"), @@ -125,3 +127,16 @@ internal enum BuiltInThemes { ) } } + +internal extension PanelThemeColors { + /// The semantic colours these surfaces already named before the theme owned them, so a default + /// theme is invisible against the unthemed app and keeps the system's contrast handling. + static let systemPanel = PanelThemeColors( + background: .system(.controlBackground), + controlBackground: .system(.textBackground), + text: .system(.label), + secondaryText: .system(.secondaryLabel), + tertiaryText: .system(.tertiaryLabel), + separator: .system(.separator) + ) +} diff --git a/TablePro/Theme/ThemeDefinition.swift b/TablePro/Theme/ThemeDefinition.swift index 4fccb5f3d5..39cddb54c6 100644 --- a/TablePro/Theme/ThemeDefinition.swift +++ b/TablePro/Theme/ThemeDefinition.swift @@ -55,6 +55,15 @@ internal struct StatusThemeColors: Equatable, Sendable { var error: ThemeColorValue } +internal struct PanelThemeColors: Equatable, Sendable { + var background: ThemeColorValue + var controlBackground: ThemeColorValue + var text: ThemeColorValue + var secondaryText: ThemeColorValue + var tertiaryText: ThemeColorValue + var separator: ThemeColorValue +} + internal struct ThemeDefinition: Identifiable, Equatable, Sendable { var id: String var name: String @@ -62,6 +71,7 @@ internal struct ThemeDefinition: Identifiable, Equatable, Sendable { var appearance: ThemeAppearance var editor: EditorThemeColors var dataGrid: DataGridThemeColors + var panel: PanelThemeColors var status: StatusThemeColors internal static let builtInPrefix = "tablepro." @@ -115,10 +125,18 @@ internal enum ThemeSlot: String, CaseIterable, Sendable { case gridDeleted = "content.dataGrid.deleted" case gridDeletedText = "content.dataGrid.deletedText" + case panelBackground = "content.panel.background" + case panelControlBackground = "content.panel.controlBackground" + case panelText = "content.panel.text" + case panelSecondaryText = "content.panel.secondaryText" + case panelTertiaryText = "content.panel.tertiaryText" + case panelSeparator = "content.panel.separator" + case statusSuccess = "content.status.success" case statusWarning = "content.status.warning" case statusError = "content.status.error" + internal var since: Int { 2 } internal var group: ThemeSlotGroup { @@ -134,6 +152,9 @@ internal enum ThemeSlot: String, CaseIterable, Sendable { .gridNullValue, .gridBoolTrue, .gridBoolFalse, .gridRowNumber, .gridModified, .gridInserted, .gridDeleted, .gridDeletedText: return .dataGrid + case .panelBackground, .panelControlBackground, .panelText, + .panelSecondaryText, .panelTertiaryText, .panelSeparator: + return .panel case .statusSuccess, .statusWarning, .statusError: return .status } @@ -178,9 +199,17 @@ internal enum ThemeSlot: String, CaseIterable, Sendable { case .gridDeleted: return \.dataGrid.deleted case .gridDeletedText: return \.dataGrid.deletedText + case .panelBackground: return \.panel.background + case .panelControlBackground: return \.panel.controlBackground + case .panelText: return \.panel.text + case .panelSecondaryText: return \.panel.secondaryText + case .panelTertiaryText: return \.panel.tertiaryText + case .panelSeparator: return \.panel.separator + case .statusSuccess: return \.status.success case .statusWarning: return \.status.warning case .statusError: return \.status.error + } } @@ -220,6 +249,12 @@ internal enum ThemeSlot: String, CaseIterable, Sendable { case .statusSuccess: return String(localized: "Success") case .statusWarning: return String(localized: "Warning") case .statusError: return String(localized: "Error") + case .panelBackground: return String(localized: "Pane Background") + case .panelControlBackground: return String(localized: "Field Background") + case .panelText: return String(localized: "Pane Text") + case .panelSecondaryText: return String(localized: "Secondary Text") + case .panelTertiaryText: return String(localized: "Tertiary Text") + case .panelSeparator: return String(localized: "Separator") } } } @@ -228,6 +263,7 @@ internal enum ThemeSlotGroup: String, CaseIterable, Sendable { case editor case syntax case dataGrid + case panel case status internal var label: String { @@ -235,6 +271,7 @@ internal enum ThemeSlotGroup: String, CaseIterable, Sendable { case .editor: return String(localized: "Editor") case .syntax: return String(localized: "Syntax Colors") case .dataGrid: return String(localized: "Data Grid") + case .panel: return String(localized: "Panels") case .status: return String(localized: "Status") } } diff --git a/TablePro/Theme/ThemedContentSurface.swift b/TablePro/Theme/ThemedContentSurface.swift new file mode 100644 index 0000000000..16998c8479 --- /dev/null +++ b/TablePro/Theme/ThemedContentSurface.swift @@ -0,0 +1,27 @@ +import SwiftUI + +/// Re-resolves `.secondary` and `.tertiary` inside a content pane to the theme's own text levels, +/// which is how 184 hierarchical foreground styles across the content surfaces follow a theme +/// without each one being rewritten. +/// +/// It never branches on the theme. `WorkspacePanes` depends on every pane builder erasing one +/// stable view identity, so an `if` here would tear down the grid, the editor's undo stack and the +/// scroll position on a theme change. It reads the engine inside `body`, so the palette it applies +/// is the current one rather than whatever was captured when the pane was built. +private struct ThemedContentSurface: ViewModifier { + func body(content: Content) -> some View { + let palette = ThemeEngine.shared.palette + + return content.foregroundStyle( + palette.color(.panelText), + palette.color(.panelSecondaryText), + palette.color(.panelTertiaryText) + ) + } +} + +internal extension View { + func themedContent() -> some View { + modifier(ThemedContentSurface()) + } +} diff --git a/TablePro/Views/Editor/FileModifiedOnDiskBanner.swift b/TablePro/Views/Editor/FileModifiedOnDiskBanner.swift index e8cb0ebde2..c11b033544 100644 --- a/TablePro/Views/Editor/FileModifiedOnDiskBanner.swift +++ b/TablePro/Views/Editor/FileModifiedOnDiskBanner.swift @@ -13,7 +13,7 @@ internal struct FileModifiedOnDiskBanner: View { var body: some View { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.yellow) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .accessibilityHidden(true) Text(String(format: String(localized: "\"%@\" was modified on disk."), fileName)) @@ -41,6 +41,6 @@ internal struct FileModifiedOnDiskBanner: View { } .padding(.horizontal, 12) .padding(.vertical, 6) - .background(.yellow.opacity(0.12)) + .background(ThemeEngine.shared.palette.color(.statusWarning).opacity(0.12)) } } diff --git a/TablePro/Views/Editor/History/HistoryDetailPane.swift b/TablePro/Views/Editor/History/HistoryDetailPane.swift index 9cc5aa74c8..30cc5ba1a0 100644 --- a/TablePro/Views/Editor/History/HistoryDetailPane.swift +++ b/TablePro/Views/Editor/History/HistoryDetailPane.swift @@ -66,7 +66,7 @@ struct HistoryDetailPane: View { if let errorMessage = entry.errorMessage { RevealedTextView(errorMessage) .font(.caption) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } diff --git a/TablePro/Views/Editor/History/HistoryRowView.swift b/TablePro/Views/Editor/History/HistoryRowView.swift index 37151159af..7bc2ac4603 100644 --- a/TablePro/Views/Editor/History/HistoryRowView.swift +++ b/TablePro/Views/Editor/History/HistoryRowView.swift @@ -71,7 +71,7 @@ struct HistoryRowView: View { .foregroundStyle(Color.secondary) } else { Image(systemName: "exclamationmark.circle.fill") - .selectionAwareTint(.red) + .selectionAwareTint(ThemeEngine.shared.palette.color(.statusError)) } } diff --git a/TablePro/Views/Editor/QueryCompletionAdapter.swift b/TablePro/Views/Editor/QueryCompletionAdapter.swift index 915c16e2c0..a87138ec7d 100644 --- a/TablePro/Views/Editor/QueryCompletionAdapter.swift +++ b/TablePro/Views/Editor/QueryCompletionAdapter.swift @@ -222,7 +222,10 @@ final class SQLSuggestionEntry: CodeSuggestionEntry { Image(systemName: item.kind.iconName) } + /// The suggestion protocol is nonisolated and the panel only ever reads this on the main + /// thread. The kind is lifted out first so the closure sends a value rather than `self`. var imageColor: Color { - Color(nsColor: item.kind.iconColor) + let kind = item.kind + return MainActor.assumeIsolated { Color(nsColor: kind.iconColor) } } } diff --git a/TablePro/Views/Editor/QueryDiagnosticsController.swift b/TablePro/Views/Editor/QueryDiagnosticsController.swift index 5f4237932f..ce15ccb4ed 100644 --- a/TablePro/Views/Editor/QueryDiagnosticsController.swift +++ b/TablePro/Views/Editor/QueryDiagnosticsController.swift @@ -75,6 +75,14 @@ final class QueryDiagnosticsController { apply(produced, in: controller) } + /// An emphasis bakes its colour into a `CAShapeLayer`, so a theme change inside one appearance + /// leaves an existing underline on the previous colour. `refresh(for:)` cannot repaint it: the + /// diagnostics themselves have not changed, so it returns early. + func reapplyColors(in controller: TextViewController) { + guard !diagnostics.isEmpty else { return } + apply(diagnostics, in: controller) + } + func clear(in controller: TextViewController?) { pendingTask?.cancel() pendingTask = nil @@ -104,8 +112,8 @@ final class QueryDiagnosticsController { private func color(for severity: QueryDiagnostic.Severity) -> NSColor { switch severity { - case .error: return .systemRed - case .warning: return .systemOrange + case .error: return ThemeEngine.shared.palette[.statusError] + case .warning: return ThemeEngine.shared.palette[.statusWarning] } } } diff --git a/TablePro/Views/Editor/QueryEditorView.swift b/TablePro/Views/Editor/QueryEditorView.swift index aa6bfd91b3..6ca5dcc3b0 100644 --- a/TablePro/Views/Editor/QueryEditorView.swift +++ b/TablePro/Views/Editor/QueryEditorView.swift @@ -97,7 +97,6 @@ struct QueryEditorView: View { .frame(minHeight: 100) .clipped() } - .background(Color(nsColor: .textBackgroundColor)) } // MARK: - Toolbar diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index ebbab9e1ed..4a92746fe6 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -27,6 +27,13 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate { private static let languageServiceLengthLimit = EditorHighlighting.maxHighlightableCharacters @ObservationIgnored weak var controller: TextViewController? + + /// The editor configuration carries the new colours, but an emphasis already on screen baked + /// its own into a `CAShapeLayer` that nothing else repaints. + func reapplyThemeColors() { + guard let controller else { return } + diagnosticsController.reapplyColors(in: controller) + } @ObservationIgnored private lazy var diagnosticsController = QueryDiagnosticsController( databaseType: databaseType ) diff --git a/TablePro/Views/Editor/SQLEditorView.swift b/TablePro/Views/Editor/SQLEditorView.swift index ec3db9799b..022862a380 100644 --- a/TablePro/Views/Editor/SQLEditorView.swift +++ b/TablePro/Views/Editor/SQLEditorView.swift @@ -147,6 +147,7 @@ struct SQLEditorView: View { } .onReceive(AppEvents.shared.themeChanged) { _ in editorConfiguration = Self.makeConfiguration() + coordinator.reapplyThemeColors() } .onAppear { initializeEditor() diff --git a/TablePro/Views/Import/SQLCodePreview.swift b/TablePro/Views/Import/SQLCodePreview.swift index 2789b33323..669a10ca0e 100644 --- a/TablePro/Views/Import/SQLCodePreview.swift +++ b/TablePro/Views/Import/SQLCodePreview.swift @@ -19,7 +19,7 @@ struct SQLCodePreview: View { var body: some View { if text.isEmpty { - Color(nsColor: .textBackgroundColor) + ThemeEngine.shared.palette.color(.editorBackground) } else { SourceEditor( $text, diff --git a/TablePro/Views/QueryInsights/QueryInsightsActivityChart.swift b/TablePro/Views/QueryInsights/QueryInsightsActivityChart.swift index 8470242aa3..8252d6689e 100644 --- a/TablePro/Views/QueryInsights/QueryInsightsActivityChart.swift +++ b/TablePro/Views/QueryInsights/QueryInsightsActivityChart.swift @@ -53,7 +53,7 @@ struct QueryInsightsActivityChart: View { } .chartForegroundStyleScale([ Outcome.succeeded: Color.accentColor, - Outcome.failed: Color.orange, + Outcome.failed: ThemeEngine.shared.palette.color(.statusWarning), ]) .chartLegend(position: .top, alignment: .trailing, spacing: 8) .chartYAxis { diff --git a/TablePro/Views/QueryInsights/QueryInsightsGroupList.swift b/TablePro/Views/QueryInsights/QueryInsightsGroupList.swift index 8cdd2d1e60..4f3e392275 100644 --- a/TablePro/Views/QueryInsights/QueryInsightsGroupList.swift +++ b/TablePro/Views/QueryInsights/QueryInsightsGroupList.swift @@ -48,7 +48,7 @@ struct QueryInsightsGroupList: View { if let error = group.latestErrorMessage, case .failures = metric { RevealedTextView(error) .font(.caption) - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .lineLimit(2) } } @@ -86,7 +86,7 @@ struct QueryInsightsGroupList: View { private func headlineTint(_ group: QueryInsightsGroup) -> Color { switch metric { - case .failures: return .orange + case .failures: return ThemeEngine.shared.palette.color(.statusWarning) case .duration: return .primary case .callCount: return .primary } @@ -160,7 +160,7 @@ struct QueryInsightsRegressionList: View { .font(.system(.callout, design: .monospaced)) .fontWeight(.medium) .monospacedDigit() - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .frame(minWidth: 76, alignment: .trailing) VStack(alignment: .leading, spacing: 3) { diff --git a/TablePro/Views/QueryInsights/QueryInsightsSummaryBar.swift b/TablePro/Views/QueryInsights/QueryInsightsSummaryBar.swift index 24f099283c..c7d09b1c09 100644 --- a/TablePro/Views/QueryInsights/QueryInsightsSummaryBar.swift +++ b/TablePro/Views/QueryInsights/QueryInsightsSummaryBar.swift @@ -30,7 +30,7 @@ struct QueryInsightsSummaryBar: View { totals.totalCount.formatted() ), systemImage: "exclamationmark.triangle", - tint: totals.failedCount > 0 ? .orange : .secondary + tint: totals.failedCount > 0 ? ThemeEngine.shared.palette.color(.statusWarning) : .secondary ) metric( label: String(localized: "Average"), diff --git a/TablePro/Views/Results/ArrayValueEditorView.swift b/TablePro/Views/Results/ArrayValueEditorView.swift index c720bcff49..03d3df7a12 100644 --- a/TablePro/Views/Results/ArrayValueEditorView.swift +++ b/TablePro/Views/Results/ArrayValueEditorView.swift @@ -162,7 +162,7 @@ struct ArrayValueEditorView: View { .pickerStyle(.menu) if ArrayValueEditorModel.isDriftedValue(row.element, allowedValues: allowedValues) { Image(systemName: "exclamationmark.triangle") - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .help(Text("This value is not one of the type's current labels")) } } diff --git a/TablePro/Views/Results/ColumnValueFilterPopover.swift b/TablePro/Views/Results/ColumnValueFilterPopover.swift index 007afaec75..f6159949d5 100644 --- a/TablePro/Views/Results/ColumnValueFilterPopover.swift +++ b/TablePro/Views/Results/ColumnValueFilterPopover.swift @@ -102,7 +102,7 @@ struct ColumnValueFilterPopover: View { Text(label(for: value)) .lineLimit(1) .truncationMode(.tail) - .foregroundStyle(value.isNull ? Color.secondary : Color.primary) + .foregroundStyle(value.isNull ? ThemeEngine.shared.palette.color(.gridNullValue) : Color.primary) Spacer(minLength: 8) Text("\(value.count)") .font(.callout.monospacedDigit()) diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index 7b592681e1..08466bc598 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -626,6 +626,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData /// the width was already going stale here. self?.resizeRowNumberColumnForCurrentRange() self?.repaintRowGutter() + self?.selectionController.overlay?.needsDisplay = true } } diff --git a/TablePro/Views/Results/ForeignKeyPickerView.swift b/TablePro/Views/Results/ForeignKeyPickerView.swift index 85b6f64b8c..2d2e5b7211 100644 --- a/TablePro/Views/Results/ForeignKeyPickerView.swift +++ b/TablePro/Views/Results/ForeignKeyPickerView.swift @@ -96,7 +96,7 @@ struct ForeignKeyPickerView: View { private var content: some View { if let errorMessage { RevealedTextView(errorMessage) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) .font(.callout) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) diff --git a/TablePro/Views/Results/ForeignKeyPreviewView.swift b/TablePro/Views/Results/ForeignKeyPreviewView.swift index 4dad919716..e1f52d8de8 100644 --- a/TablePro/Views/Results/ForeignKeyPreviewView.swift +++ b/TablePro/Views/Results/ForeignKeyPreviewView.swift @@ -104,7 +104,7 @@ struct ForeignKeyPreviewView: View { .frame(height: 60) } else if let errorMessage { RevealedTextView(errorMessage) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) .font(.callout) .padding(10) } else if values.isEmpty { diff --git a/TablePro/Views/Results/HexEditorContentView.swift b/TablePro/Views/Results/HexEditorContentView.swift index a66ae62a3b..b24a121953 100644 --- a/TablePro/Views/Results/HexEditorContentView.swift +++ b/TablePro/Views/Results/HexEditorContentView.swift @@ -99,11 +99,11 @@ struct HexEditorBody: View { if sourceIsTruncated || isTruncated { Text(String(localized: "Truncated, read only")) .font(.caption) - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) } else if !isValid, !editableHex.isEmpty { Text(String(localized: "Invalid hex")) .font(.caption) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) } Spacer() @@ -227,8 +227,8 @@ private struct HexDumpDisplayView: NSViewRepresentable { textView.isSelectable = true textView.font = font textView.textContainerInset = NSSize(width: 8, height: 8) - textView.backgroundColor = NSColor.textBackgroundColor - textView.textColor = NSColor.secondaryLabelColor + textView.backgroundColor = ThemeEngine.shared.palette[.panelControlBackground] + textView.textColor = ThemeEngine.shared.palette[.panelSecondaryText] textView.string = text return scrollView @@ -239,6 +239,8 @@ private struct HexDumpDisplayView: NSViewRepresentable { if textView.font != font { textView.font = font } + textView.backgroundColor = ThemeEngine.shared.palette[.panelControlBackground] + textView.textColor = ThemeEngine.shared.palette[.panelSecondaryText] if textView.string != text { textView.string = text } @@ -265,8 +267,8 @@ private struct HexInputTextView: NSViewRepresentable { textView.isSelectable = true textView.font = font textView.textContainerInset = NSSize(width: 8, height: 8) - textView.backgroundColor = NSColor.textBackgroundColor - textView.textColor = NSColor.labelColor + textView.backgroundColor = ThemeEngine.shared.palette[.panelControlBackground] + textView.textColor = ThemeEngine.shared.palette[.panelText] textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticDashSubstitutionEnabled = false textView.isAutomaticTextReplacementEnabled = false @@ -288,6 +290,8 @@ private struct HexInputTextView: NSViewRepresentable { if textView.font != font { textView.font = font } + textView.backgroundColor = ThemeEngine.shared.palette[.panelControlBackground] + textView.textColor = ThemeEngine.shared.palette[.panelText] if textView.string != text, !context.coordinator.isUpdating { textView.string = text } diff --git a/TablePro/Views/Results/InlineErrorBanner.swift b/TablePro/Views/Results/InlineErrorBanner.swift index fd174460de..5c604273ef 100644 --- a/TablePro/Views/Results/InlineErrorBanner.swift +++ b/TablePro/Views/Results/InlineErrorBanner.swift @@ -21,7 +21,7 @@ struct InlineErrorBanner: View { var body: some View { HStack(alignment: .top, spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) ScrollView(.vertical) { RevealedTextView(message) .font(.subheadline) @@ -63,7 +63,7 @@ struct InlineErrorBanner: View { } .padding(.horizontal, 12) .padding(.vertical, 8) - .background(.red.opacity(0.08)) + .background(ThemeEngine.shared.palette.color(.statusError).opacity(0.08)) } } diff --git a/TablePro/Views/Results/JSONTreeView.swift b/TablePro/Views/Results/JSONTreeView.swift index 049e99f901..332e26dc54 100644 --- a/TablePro/Views/Results/JSONTreeView.swift +++ b/TablePro/Views/Results/JSONTreeView.swift @@ -30,7 +30,7 @@ private struct JSONTreeRowView: View { if let key = node.key { Text(key) .font(ThemeEngine.shared.valueFontEmphasizedSwiftUI) - .foregroundStyle(.blue) + .foregroundStyle(ThemeEngine.shared.palette.color(.syntaxKeyword)) .lineLimit(1) Text(":") .foregroundStyle(.secondary) diff --git a/TablePro/Views/Results/PhpTreeView.swift b/TablePro/Views/Results/PhpTreeView.swift index 7722b604a0..efdb111ab4 100644 --- a/TablePro/Views/Results/PhpTreeView.swift +++ b/TablePro/Views/Results/PhpTreeView.swift @@ -30,7 +30,7 @@ private struct PhpTreeRowView: View { if let key = node.key { Text(key) .font(ThemeEngine.shared.valueFontEmphasizedSwiftUI) - .foregroundStyle(.blue) + .foregroundStyle(ThemeEngine.shared.palette.color(.syntaxKeyword)) .lineLimit(1) if let badge = node.visibilityBadge { Text(badge) diff --git a/TablePro/Views/Results/ResultChartCanvas.swift b/TablePro/Views/Results/ResultChartCanvas.swift index 799b3e871f..4632248492 100644 --- a/TablePro/Views/Results/ResultChartCanvas.swift +++ b/TablePro/Views/Results/ResultChartCanvas.swift @@ -41,27 +41,27 @@ struct ResultChartCanvas: View { .chartYAxis { AxisMarks(position: .leading) { AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) - .foregroundStyle(Color(nsColor: .separatorColor).opacity(0.55)) + .foregroundStyle(ThemeEngine.shared.palette.color(.panelSeparator).opacity(0.55)) AxisTick(stroke: StrokeStyle(lineWidth: 0.5)) - .foregroundStyle(Color(nsColor: .separatorColor)) + .foregroundStyle(ThemeEngine.shared.palette.color(.panelSeparator)) AxisValueLabel() - .foregroundStyle(Color(nsColor: .secondaryLabelColor)) + .foregroundStyle(ThemeEngine.shared.palette.color(.panelSecondaryText)) } } .chartXAxis { AxisMarks(values: .automatic(desiredCount: 7)) { AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5)) - .foregroundStyle(Color(nsColor: .separatorColor).opacity(0.55)) + .foregroundStyle(ThemeEngine.shared.palette.color(.panelSeparator).opacity(0.55)) AxisTick(stroke: StrokeStyle(lineWidth: 0.5)) - .foregroundStyle(Color(nsColor: .separatorColor)) + .foregroundStyle(ThemeEngine.shared.palette.color(.panelSeparator)) AxisValueLabel(collisionResolution: .greedy(minimumSpacing: 8)) - .foregroundStyle(Color(nsColor: .secondaryLabelColor)) + .foregroundStyle(ThemeEngine.shared.palette.color(.panelSecondaryText)) } } .chartPlotStyle { plotArea in plotArea .background( - Color(nsColor: .controlBackgroundColor).opacity(0.45), + ThemeEngine.shared.palette.color(.panelBackground).opacity(0.45), in: .rect(cornerRadius: 8) ) } diff --git a/TablePro/Views/Results/ResultSuccessView.swift b/TablePro/Views/Results/ResultSuccessView.swift index 3954698442..1488dcae13 100644 --- a/TablePro/Views/Results/ResultSuccessView.swift +++ b/TablePro/Views/Results/ResultSuccessView.swift @@ -28,7 +28,7 @@ struct ResultSuccessView: View { Spacer() Image(systemName: "checkmark.circle.fill") .font(.largeTitle) - .foregroundStyle(.green) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusSuccess)) Text(primaryMessage) .font(.body) if let time = executionTime { diff --git a/TablePro/Views/Results/Selection/GridSelectionOverlay.swift b/TablePro/Views/Results/Selection/GridSelectionOverlay.swift index bd1cd85a65..f12f6d0709 100644 --- a/TablePro/Views/Results/Selection/GridSelectionOverlay.swift +++ b/TablePro/Views/Results/Selection/GridSelectionOverlay.swift @@ -37,7 +37,7 @@ final class GridSelectionOverlay: NSView { let totalRows = tableView.numberOfRows let editingCell = activeOverlayCell(in: coordinator) - NSColor.selectedContentBackgroundColor.withAlphaComponent(Self.borderAlpha).setStroke() + ThemeEngine.shared.palette[.gridSelection].withAlphaComponent(Self.borderAlpha).setStroke() for rect in selection.rectangles { guard let frame = frame(for: rect, in: tableView, coordinator: coordinator) else { continue } guard frame.intersects(dirtyRect) else { continue } diff --git a/TablePro/Views/RowInspector/FieldEditors/BlobHexEditorView.swift b/TablePro/Views/RowInspector/FieldEditors/BlobHexEditorView.swift index d2f150327d..20ab3e650e 100644 --- a/TablePro/Views/RowInspector/FieldEditors/BlobHexEditorView.swift +++ b/TablePro/Views/RowInspector/FieldEditors/BlobHexEditorView.swift @@ -94,11 +94,11 @@ internal struct BlobHexEditorView: View { if isTruncated { Text("Truncated, read only") .font(.caption2) - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) } else if BlobFormattingService.shared.parseHex(hexEditText) == nil, !hexEditText.isEmpty { Text("Invalid hex") .font(.caption2) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) } } } diff --git a/TablePro/Views/RowInspector/FieldEditors/FieldEditorContent.swift b/TablePro/Views/RowInspector/FieldEditors/FieldEditorContent.swift index affe056c61..3f19742212 100644 --- a/TablePro/Views/RowInspector/FieldEditors/FieldEditorContent.swift +++ b/TablePro/Views/RowInspector/FieldEditors/FieldEditorContent.swift @@ -106,7 +106,7 @@ internal struct PendingStatePill: View { .frame(maxWidth: .infinity, minHeight: minHeight, alignment: .topLeading) .padding(.horizontal, 6) .padding(.vertical, 3) - .background(Color(nsColor: .textBackgroundColor), in: RoundedRectangle(cornerRadius: 5)) - .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color(nsColor: .separatorColor))) + .background(ThemeEngine.shared.palette.color(.panelControlBackground), in: RoundedRectangle(cornerRadius: 5)) + .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(ThemeEngine.shared.palette.color(.panelSeparator))) } } diff --git a/TablePro/Views/RowInspector/FieldEditors/ImageFieldView.swift b/TablePro/Views/RowInspector/FieldEditors/ImageFieldView.swift index 369a9da12c..d0c4bfbe0b 100644 --- a/TablePro/Views/RowInspector/FieldEditors/ImageFieldView.swift +++ b/TablePro/Views/RowInspector/FieldEditors/ImageFieldView.swift @@ -34,7 +34,7 @@ internal struct ImageFieldView: View { ) .frame(height: 220) .clipShape(RoundedRectangle(cornerRadius: 5)) - .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color(nsColor: .separatorColor))) + .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(ThemeEngine.shared.palette.color(.panelSeparator))) .task(id: context.value.wrappedValue) { data = context.value.wrappedValue.storedBytes } diff --git a/TablePro/Views/RowInspector/FieldEditors/JsonEditorView.swift b/TablePro/Views/RowInspector/FieldEditors/JsonEditorView.swift index 65428a310f..c4da8a40f0 100644 --- a/TablePro/Views/RowInspector/FieldEditors/JsonEditorView.swift +++ b/TablePro/Views/RowInspector/FieldEditors/JsonEditorView.swift @@ -29,7 +29,7 @@ internal struct JsonEditorView: View { ) { JSONCodeEditor(text: $displayText, isEditable: !context.isReadOnly) .clipShape(RoundedRectangle(cornerRadius: 5)) - .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color(nsColor: .separatorColor))) + .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(ThemeEngine.shared.palette.color(.panelSeparator))) .overlay(alignment: .bottomTrailing) { actionButtons } } .onChange(of: displayText) { propagateEdit() } diff --git a/TablePro/Views/RowInspector/FieldEditors/MultiLineEditorView.swift b/TablePro/Views/RowInspector/FieldEditors/MultiLineEditorView.swift index e2b38f2a64..f0a875d36f 100644 --- a/TablePro/Views/RowInspector/FieldEditors/MultiLineEditorView.swift +++ b/TablePro/Views/RowInspector/FieldEditors/MultiLineEditorView.swift @@ -26,7 +26,7 @@ internal struct MultiLineEditorView: View { movesFocusOnTab: true ) .clipShape(RoundedRectangle(cornerRadius: 5)) - .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color(nsColor: .separatorColor))) + .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(ThemeEngine.shared.palette.color(.panelSeparator))) .overlay(alignment: .topLeading) { placeholder } .overlay(alignment: .bottomTrailing) { popOutButton } } diff --git a/TablePro/Views/RowInspector/FieldEditors/PhpSerializedFieldView.swift b/TablePro/Views/RowInspector/FieldEditors/PhpSerializedFieldView.swift index cf499ad52e..0d919505b1 100644 --- a/TablePro/Views/RowInspector/FieldEditors/PhpSerializedFieldView.swift +++ b/TablePro/Views/RowInspector/FieldEditors/PhpSerializedFieldView.swift @@ -18,6 +18,6 @@ internal struct PhpSerializedFieldView: View { .frame(height: isExpanded ? ResizableFieldMetrics.expandedHeight : nil) .frame(minHeight: isExpanded ? nil : 80, maxHeight: isExpanded ? nil : 200) .clipShape(RoundedRectangle(cornerRadius: 5)) - .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color(nsColor: .separatorColor))) + .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(ThemeEngine.shared.palette.color(.panelSeparator))) } } diff --git a/TablePro/Views/RowInspector/FieldEditors/ResizableEditorContainer.swift b/TablePro/Views/RowInspector/FieldEditors/ResizableEditorContainer.swift index 6438601394..ad065ee488 100644 --- a/TablePro/Views/RowInspector/FieldEditors/ResizableEditorContainer.swift +++ b/TablePro/Views/RowInspector/FieldEditors/ResizableEditorContainer.swift @@ -39,7 +39,7 @@ internal struct ResizableEditorContainer: View { private var resizeHandle: some View { Capsule() - .fill(Color(nsColor: .tertiaryLabelColor)) + .fill(ThemeEngine.shared.palette.color(.panelTertiaryText)) .frame(width: 26, height: 4) .opacity(isHandleHovered ? 1 : 0.5) .frame(maxWidth: .infinity, minHeight: 11) diff --git a/TablePro/Views/RowInspector/FieldEditors/SingleLineEditorView.swift b/TablePro/Views/RowInspector/FieldEditors/SingleLineEditorView.swift index f960266bb5..d6abbbe425 100644 --- a/TablePro/Views/RowInspector/FieldEditors/SingleLineEditorView.swift +++ b/TablePro/Views/RowInspector/FieldEditors/SingleLineEditorView.swift @@ -29,7 +29,7 @@ internal struct SingleLineEditorView: View { .frame(maxWidth: .infinity, minHeight: 16, alignment: .leading) .padding(.horizontal, 6) .padding(.vertical, 4) - .background(Color(nsColor: .textBackgroundColor), in: RoundedRectangle(cornerRadius: 5)) - .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(Color(nsColor: .separatorColor))) + .background(ThemeEngine.shared.palette.color(.panelControlBackground), in: RoundedRectangle(cornerRadius: 5)) + .overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(ThemeEngine.shared.palette.color(.panelSeparator))) } } diff --git a/TablePro/Views/RowInspector/JSON/JSONRowInspectorView.swift b/TablePro/Views/RowInspector/JSON/JSONRowInspectorView.swift index 94ec4eadd6..d6926bc4da 100644 --- a/TablePro/Views/RowInspector/JSON/JSONRowInspectorView.swift +++ b/TablePro/Views/RowInspector/JSON/JSONRowInspectorView.swift @@ -66,7 +66,7 @@ struct JSONRowInspectorView: View { ) .overlay( RoundedRectangle(cornerRadius: 6) - .strokeBorder(Color.red.opacity(0.6)) + .strokeBorder(ThemeEngine.shared.palette.color(.statusError).opacity(0.6)) .opacity(viewModel.isFilterInvalid ? 1 : 0) ) .help(viewModel.isFilterInvalid diff --git a/TablePro/Views/ServerDashboard/MetricsBarView.swift b/TablePro/Views/ServerDashboard/MetricsBarView.swift index 89f4c503d6..61d9839754 100644 --- a/TablePro/Views/ServerDashboard/MetricsBarView.swift +++ b/TablePro/Views/ServerDashboard/MetricsBarView.swift @@ -17,7 +17,7 @@ struct MetricsBarView: View { Image(systemName: "exclamationmark.triangle") } .font(.caption) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) } } .padding(.horizontal, 12) diff --git a/TablePro/Views/ServerDashboard/SessionsTableView.swift b/TablePro/Views/ServerDashboard/SessionsTableView.swift index 93300d3e49..e211f48cb8 100644 --- a/TablePro/Views/ServerDashboard/SessionsTableView.swift +++ b/TablePro/Views/ServerDashboard/SessionsTableView.swift @@ -19,7 +19,7 @@ struct SessionsTableView: View { Image(systemName: "exclamationmark.triangle") } .font(.caption) - .foregroundStyle(.red) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusError)) } } .padding(.horizontal, 12) @@ -86,10 +86,10 @@ struct SessionsTableView: View { private func stateColor(_ state: String) -> Color { switch state.lowercased() { - case "active", "running": return .green + case "active", "running": return ThemeEngine.shared.palette.color(.statusSuccess) case "idle": return .secondary - case "idle in transaction": return .orange - case "waiting", "locked": return .red + case "idle in transaction": return ThemeEngine.shared.palette.color(.statusWarning) + case "waiting", "locked": return ThemeEngine.shared.palette.color(.statusError) default: return .primary } } diff --git a/TablePro/Views/ServerDashboard/SlowQueryListView.swift b/TablePro/Views/ServerDashboard/SlowQueryListView.swift index 5c2358ef5e..12aa31a35d 100644 --- a/TablePro/Views/ServerDashboard/SlowQueryListView.swift +++ b/TablePro/Views/ServerDashboard/SlowQueryListView.swift @@ -19,7 +19,7 @@ struct SlowQueryListView: View { Image(systemName: "exclamationmark.triangle.fill") } .font(.caption) - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) } } .padding(.horizontal, 12) @@ -49,7 +49,7 @@ struct SlowQueryListView: View { Text(query.duration) .font(.system(.caption, design: .monospaced)) .monospacedDigit() - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .frame(width: 50, alignment: .trailing) VStack(alignment: .leading, spacing: 2) { diff --git a/TablePro/Views/Structure/ClickHousePartsView.swift b/TablePro/Views/Structure/ClickHousePartsView.swift index 18874f9dbb..7b50340c39 100644 --- a/TablePro/Views/Structure/ClickHousePartsView.swift +++ b/TablePro/Views/Structure/ClickHousePartsView.swift @@ -30,7 +30,7 @@ struct ClickHousePartsView: View { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .accessibilityHidden(true) RevealedTextView(error) .foregroundStyle(.secondary) @@ -99,7 +99,7 @@ struct ClickHousePartsView: View { .width(min: 100, ideal: 160) TableColumn("Active") { part in Image(systemName: part.active ? "checkmark.circle.fill" : "xmark.circle") - .foregroundStyle(part.active ? .green : .secondary) + .foregroundStyle(part.active ? ThemeEngine.shared.palette.color(.statusSuccess) : .secondary) } .width(min: 50, ideal: 60) } diff --git a/TablePro/Views/Structure/CreateTableView.swift b/TablePro/Views/Structure/CreateTableView.swift index 1131978c7c..b458e3ed4f 100644 --- a/TablePro/Views/Structure/CreateTableView.swift +++ b/TablePro/Views/Structure/CreateTableView.swift @@ -178,7 +178,7 @@ struct CreateTableView: View { Spacer() } .padding() - .background(Color(nsColor: .controlBackgroundColor)) + .background(ThemeEngine.shared.palette.color(.panelBackground)) .onChange(of: draft.tableOptions.charset) { _, newCharset in if let first = CreateTableOptions.collations[newCharset]?.first { draft.tableOptions.collation = first diff --git a/TablePro/Views/Structure/DDLTextView.swift b/TablePro/Views/Structure/DDLTextView.swift index 3f16dd064c..858f8492e9 100644 --- a/TablePro/Views/Structure/DDLTextView.swift +++ b/TablePro/Views/Structure/DDLTextView.swift @@ -32,7 +32,7 @@ struct DDLTextView: View { var body: some View { if ddl.isEmpty { - Color(nsColor: .textBackgroundColor) + ThemeEngine.shared.palette.color(.editorBackground) } else { SourceEditor( $text, diff --git a/TablePro/Views/Structure/TableStructureView+Schema.swift b/TablePro/Views/Structure/TableStructureView+Schema.swift index ea56cc249c..1a9c4c9fec 100644 --- a/TablePro/Views/Structure/TableStructureView+Schema.swift +++ b/TablePro/Views/Structure/TableStructureView+Schema.swift @@ -110,7 +110,7 @@ extension TableStructureView { if showCopyConfirmation { HStack { Image(systemName: "checkmark.circle.fill") - .foregroundStyle(.green) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusSuccess)) Text("Copied!") } .transition(.opacity) diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index a768c3365e..c81ba75f00 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -527,7 +527,7 @@ struct TableStructureView: View { VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) - .foregroundStyle(.orange) + .foregroundStyle(ThemeEngine.shared.palette.color(.statusWarning)) .accessibilityHidden(true) RevealedTextView(message) .foregroundStyle(.secondary) diff --git a/TablePro/Views/Structure/TriggerDetailView.swift b/TablePro/Views/Structure/TriggerDetailView.swift index 54aae67b7e..ca27c0b2ef 100644 --- a/TablePro/Views/Structure/TriggerDetailView.swift +++ b/TablePro/Views/Structure/TriggerDetailView.swift @@ -252,7 +252,7 @@ private struct TriggerListPane: View { private func enabledIndicator(_ trigger: TriggerInfo) -> some View { if let enabled = trigger.enabled { Image(systemName: enabled ? "checkmark.circle.fill" : "xmark.circle") - .foregroundStyle(enabled ? Color.green : Color.secondary) + .foregroundStyle(enabled ? ThemeEngine.shared.palette.color(.statusSuccess) : Color.secondary) .accessibilityLabel(enabled ? String(localized: "Enabled") : String(localized: "Disabled")) } } @@ -274,7 +274,7 @@ private struct TriggerDetailPane: View { onOpenInEditor: { onOpenInEditor(trigger) } ) } else { - Color(nsColor: .textBackgroundColor) + ThemeEngine.shared.palette.color(.editorBackground) } } diff --git a/docs/customization/appearance.mdx b/docs/customization/appearance.mdx index 97ae4631e6..e85b74eb77 100644 --- a/docs/customization/appearance.mdx +++ b/docs/customization/appearance.mdx @@ -39,6 +39,7 @@ Custom themes get color wells here. A built-in or registry theme shows a lock an | Editor | Background, text, cursor, selection, current line, current statement, line number, invisibles | | Syntax Colors | Keyword, string, number, comment, NULL, operator, function, type | | Data Grid | Background, text, alternate row, header background, header text, grid line, selection, selected text, inactive selection, focus border, NULL value, bool true/false, row number, modified/inserted/deleted rows, deleted text | +| Panels | Pane background, field background, pane text, secondary text, tertiary text, separator | | Status | Success, warning, error | A slot holds either a hex color or the name of a macOS system color. Default Light and Default Dark keep the data grid surrounds on system colors, which is why an untouched theme follows your system accent and its Increase Contrast setting. Right-click a well that a built-in leaves on a system color to put that system color back. @@ -54,6 +55,7 @@ A theme is one JSON file. **Export…** in the gear menu writes every color the | `appearance` | `light` or `dark`. Decides which slot lists the theme | | `content.editor` | Editor colors, with syntax colors nested under `syntax` | | `content.dataGrid` | Grid colors | +| `content.panel` | Colors of the panes around the editor and the grid: results, inspector, structure, compare, query plan | | `content.status` | Success, warning and error colors | ```json From 14fa5022cfc6673003c63821e439608584c22327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Sat, 12 Sep 2026 10:50:43 +0700 Subject: [PATCH 2/3] docs(claude-md): record that NSAppearance is the only native lever a theme has on chrome (#2785) * feat(settings): theme the content panes outside the editor and the data grid Claude-Session: https://claude.ai/code/session_01EKDGt17u6TaVBDHm8rzSEj * docs(claude-md): record that NSAppearance is the only native lever a theme has on chrome Claude-Session: https://claude.ai/code/session_01EKDGt17u6TaVBDHm8rzSEj --- CLAUDE.md | 2 ++ docs/customization/appearance.mdx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index a86f64d6b4..ec0fe83e60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -199,6 +199,8 @@ To ship one: add the record type or field in CloudKit Console (or `xcrun cktool **An installed driver is a handle, not a live connection**: `ConnectionSession.driver` is held from the moment it is built until something replaces it, and a socket the server closed is indistinguishable from a working one until a ping asks. `reconnectDriver` and `reconnectSession` both disconnect it and leave it installed, so `hasDriver` stayed true for the whole of an outage and for good once the monitor gave up: the window went on showing rows, tabs and an enabled toolbar over a dead handle, `ensureConnected` returned early so Reconnect did nothing, and the connections strip painted the failure from `status` while the pane beside it disagreed. `ConnectionSession.liveness` is the single answer to "can this driver be believed", and `ConnectionWindowPhaseMachine.onSessionChanged` reads it before `hasDriver`. `status` cannot serve: it is `.connecting` throughout an ordinary database switch on the four engines that reconnect to perform one, and `.disconnected` is the struct's own default value. Four rules follow. The driver is never nil'd to signal this, because it is the handle every metadata read, every query route and the reconnect itself still goes through, and taking it away makes a database switch look like a dropped connection and trips the `exists && !hasDriver` arm into a permanent fake "Connecting". `recovering` is not a failure, so a blip that repairs itself inside `DatabaseManager.unreachableAfterAttempt` leaves the window untouched; the threshold is the fifth attempt because the 2, 4, 8, 16 backoff announces it a full 30 seconds after the ping that failed, which is one whole ping interval of not answering. Every give-up site and the threshold go through `markSessionUnreachable(_:startedWith:info:)`, whose driver-identity check is the generation guard a reconnect blocked in a C call needs, and every path that installs a working driver calls `markSessionLive` so the mark and its reason go together. And everything that reports connection health reads `ConnectionSession.reportedStatus`, never `status` directly, or the strip and the pane drift apart again. +**A theme reaches content, and `NSAppearance` is the only lever it has on chrome**: the sidebar, the trailing pane, the editor tab strip and the window background cannot be painted from a theme without fighting AppKit, so the app does not try. Measured on macOS 27 against a plain `NSSplitViewController` in a `.fullSizeContentView` window with a toolbar. The sidebar's material is a framework-owned `NSGlassEffectView` two levels above the pane's own root (`NSView > ContentHolderView > NSGlassEffectView > _NSSplitViewItemViewWrapper`), so painting the pane means covering a view AppKit owns, and `NSSplitViewItem.h` documents that translucent material as standard sidebar behaviour with no knob to turn it off. The pane is also full-height (`allowsFullHeightLayout` is `true` by default and its root reports `safeAreaInsets.top` of 66pt), so a fill pinned to that root paints the whole titlebar and toolbar band, which Apple's Liquid Glass guidance tells apps not to do and which is what broke native window tabs in Sublime Text. Sidebar selection is the same story from the other side: under `.sourceList` AppKit inserts its own `NSVisualEffectView` as the row's first subview and calls `drawSelection(in:)` zero times, so the documented override never runs; setting `selectionHighlightStyle = .none` does suppress it but collapses `interiorBackgroundStyle` from emphasized to normal, and that is the channel `NSTableCellView.backgroundStyle` publishes as SwiftUI's `backgroundProminence`, which every sidebar row label answers through `.primary` and `.secondary`. An absolute colour ignores prominence, so a themed label paints the wrong colour on a selected row under the default theme too. The editor tab strip is titlebar-resident (`NSTitlebarAccessoryViewController`), so it falls under the same rule as the toolbar. What remains is the one lever macOS does give: `ThemeEngine` sets `NSApp.appearance` from the appearance mode, a theme declares `light` or `dark`, and `ThemeSlotValidation` keeps a theme in the slot whose appearance it matches, so choosing a dark theme gives dark chrome. Every theme slot is namespaced `content.` for this reason, and `ThemeSlotCoverageTests` fails the build for a slot no call site reads, which is what let the old `ui`, `sidebar` and `toolbar` groups ship with zero readers for two years. + **The app runs the AppKit lifecycle, and AppKit owns the menu bar**: `main.swift` assigns the delegate before `NSApplicationMain`, and `MainMenuBuilder.install` runs in `applicationWillFinishLaunching`. Do not reintroduce a SwiftUI `App`. SwiftUI reconciles `NSApp.mainMenu` once shortly after launch and removes every item it did not build itself, and no hook can undo it: `NSApp.mainMenu` is not KVO-compliant, `didUpdateNotification`, `didBecomeKeyNotification` and the `applicationDidUpdate(_:)` delegate method never fire under `@NSApplicationDelegateAdaptor`, and `applicationDidBecomeActive` fires before the reconciliation. Only a wall-clock delay worked, which is why #2057 shipped a menu bar that vanished half a second after launch and had to be reverted (#2071). Every window is an `NSWindowController`; the Welcome window is one too, so closing it is an ordinary `close()` and the old "closed, never ordered out" rule no longer applies. **An emptied tab manager is not the same as "the user closed every tab"**: a coordinator torn down by a lost session has already emptied `tabManager.tabs`, so any persistence path that reads "no tabs" as "clear the saved tabs" wipes tabs the user never closed. The fix is that the teardown path cannot clear at all: `TabPersistenceCoordinator.saveAggregatedSync()`, which disconnect and window-close call, opens with `guard !tabs.isEmpty else { return }`. Clearing requires explicit consent and happens on the `closeTabsByUser` path instead. Keep those two paths separate; the moment a teardown path can write an empty aggregate, the bug is back. diff --git a/docs/customization/appearance.mdx b/docs/customization/appearance.mdx index e85b74eb77..502011b2d7 100644 --- a/docs/customization/appearance.mdx +++ b/docs/customization/appearance.mdx @@ -44,6 +44,8 @@ Custom themes get color wells here. A built-in or registry theme shows a lock an A slot holds either a hex color or the name of a macOS system color. Default Light and Default Dark keep the data grid surrounds on system colors, which is why an untouched theme follows your system accent and its Increase Contrast setting. Right-click a well that a built-in leaves on a system color to put that system color back. +A theme colors the panes that show database content. The sidebar, the toolbar, the tab strip and the window frame stay on the macOS appearance, which is what the **Appearance** control at the top of the tab sets: pick a dark theme and the window frame goes dark with it. + ## Theme files A theme is one JSON file. **Export…** in the gear menu writes every color the selected theme sets, which is the shortest way to start a new one; **Import…** in the **+** menu reads one back into `~/Library/Application Support/TablePro/Themes/`. Community themes install from the [registry](/features/plugins) under **Settings > Plugins > Browse**, Themes category. From a3295e11cf3c46474f92b5647ca053055f3ccc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 15 Sep 2026 22:58:32 +0700 Subject: [PATCH 3/3] refactor(settings): group the Editor pane, name the theme color wells, and unbreak lint on main (#2787) * style(settings): drop a blank line left before a closing brace Claude-Session: https://claude.ai/code/session_01EKDGt17u6TaVBDHm8rzSEj * refactor(settings): group the Editor pane and name the theme color wells for VoiceOver Claude-Session: https://claude.ai/code/session_01EKDGt17u6TaVBDHm8rzSEj * refactor(datagrid): split the grid coordinator's notification wiring into its own extension Claude-Session: https://claude.ai/code/session_01EKDGt17u6TaVBDHm8rzSEj --- CHANGELOG.md | 2 + TablePro/Theme/ThemeDefinition.swift | 1 - .../Views/Results/DataGridCoordinator.swift | 68 +---------------- .../TableViewCoordinator+Observation.swift | 75 +++++++++++++++++++ .../Appearance/ThemeEditorColorsSection.swift | 1 + .../Views/Settings/EditorSettingsView.swift | 10 ++- .../Settings/Sections/TypographySection.swift | 8 +- .../Views/Settings/ThemePreviewCard.swift | 7 ++ docs/customization/editor-settings.mdx | 13 +++- 9 files changed, 110 insertions(+), 75 deletions(-) create mode 100644 TablePro/Views/Results/Extensions/TableViewCoordinator+Observation.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a8960a4cf9..4546a3fdd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Beancount connections held at Safe Mode Read-Only. (#2030) - MySQL sessions on the server's default `utf8mb4` collation. - Theme file format 2. Themes written for earlier versions are not read and need to be recreated. +- Settings > Editor grouped into Display, Gutter and Editing. - Editor Font and Data Grid Font moved to Settings > Editor and Settings > Data & Results, and kept per Mac. - Editor font size range of 10 to 24 points everywhere, including zoom. - Structure editor options the connected PostgreSQL server does not support left out: generated columns before 12, BRIN before 9.5, and the MySQL-only FULLTEXT and SPATIAL index types. @@ -79,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Content panes outside the editor and the data grid ignoring the theme. - JSON and PHP tree values colored differently from the same values in the row inspector. - Autocomplete icon colors ignoring the theme. +- Theme color wells and the theme preview unreadable to VoiceOver. - Color with a typo in it rendering as a different color instead of being reported. - Text past the first 64 KB of a UTF-16 SQL import arriving byte-swapped. - SQL import failing on a file whose encoding is not UTF-8 when a character lands on a 64 KB boundary. diff --git a/TablePro/Theme/ThemeDefinition.swift b/TablePro/Theme/ThemeDefinition.swift index 39cddb54c6..e0904f6f61 100644 --- a/TablePro/Theme/ThemeDefinition.swift +++ b/TablePro/Theme/ThemeDefinition.swift @@ -209,7 +209,6 @@ internal enum ThemeSlot: String, CaseIterable, Sendable { case .statusSuccess: return \.status.success case .statusWarning: return \.status.warning case .statusError: return \.status.error - } } diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index 08466bc598..dea8c95985 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -507,7 +507,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData var settingsCancellable: AnyCancellable? var themeCancellable: AnyCancellable? var systemTimeZoneCancellable: AnyCancellable? - private var accessibilityActivationObserver: (any NSObjectProtocol)? + internal var accessibilityActivationObserver: (any NSObjectProtocol)? private var accessibilityMountedRows = NSRange(location: 0, length: 0) private var lastDataGridSettings: DataGridSettings @@ -593,72 +593,6 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData } } - func applyDataGridSettingsChange(from previous: DataGridSettings, to settings: DataGridSettings) { - guard let tableView else { return } - let newRowHeight = CGFloat(settings.rowHeight.rawValue) - if tableView.rowHeight != newRowHeight { - tableView.rowHeight = newRowHeight - tableView.tile() - repaintRowGutter() - } - - let dataChanged = previous.dateFormat != settings.dateFormat - || previous.nullDisplay != settings.nullDisplay - || previous.enableSmartValueDetection != settings.enableSmartValueDetection - - if dataChanged { - reformatDisplayedText() - } - } - - func observeThemeChanges() { - themeCancellable = AppEvents.shared.themeChanged - .receive(on: RunLoop.main) - .sink { [weak self] _ in - if let tableView = self?.tableView { - DataGridBodyChrome.applyBackground(to: tableView) - tableView.headerView?.needsDisplay = true - tableView.cornerView?.needsDisplay = true - } - self?.reloadVisibleRowsAndStates() - /// The row-number font is a theme value and it decides the column's width, which - /// the pinned gutter mirrors. Nothing re-measured it on a theme change before, so - /// the width was already going stale here. - self?.resizeRowNumberColumnForCurrentRange() - self?.repaintRowGutter() - self?.selectionController.overlay?.needsDisplay = true - } - } - - /// The grid mounts no view for a data cell, so a client that attaches mid-session finds a table - /// of empty cells until the visible rows are built again. The remount is deferred off the - /// accessibility query that raised the flag, because rebuilding rows inside it would re-enter - /// the tree AppKit is walking. - private func observeAccessibilityActivation() { - accessibilityActivationObserver = NotificationCenter.default.addObserver( - forName: DataGridAccessibility.didActivateNotification, - object: nil, - queue: nil - ) { [weak self] _ in - Task { @MainActor [weak self] in - self?.remountAccessibilityCells() - } - } - } - - /// Registered for the life of the coordinator, so it has to come off when the grid goes. - /// - /// `NotificationCenter` retains a block observer's closure, and a coordinator is built fresh on - /// every mount, so an entry left behind at teardown is never fired again and never reclaimed. - /// The closure captures `self` weakly, so this leaks the registration rather than the grid. - func detachAccessibilityActivationObserver() { - guard let accessibilityActivationObserver else { return } - NotificationCenter.default.removeObserver(accessibilityActivationObserver) - self.accessibilityActivationObserver = nil - } - - var hasAccessibilityActivationObserver: Bool { accessibilityActivationObserver != nil } - /// Whether this row is one an assistive client can be reading right now. /// /// Walking the accessibility tree makes `NSTableView` prepare every row of the page, not the diff --git a/TablePro/Views/Results/Extensions/TableViewCoordinator+Observation.swift b/TablePro/Views/Results/Extensions/TableViewCoordinator+Observation.swift new file mode 100644 index 0000000000..1378099164 --- /dev/null +++ b/TablePro/Views/Results/Extensions/TableViewCoordinator+Observation.swift @@ -0,0 +1,75 @@ +// +// TableViewCoordinator+Observation.swift +// TablePro +// + +import AppKit +import Combine + +internal extension TableViewCoordinator { + func applyDataGridSettingsChange(from previous: DataGridSettings, to settings: DataGridSettings) { + guard let tableView else { return } + let newRowHeight = CGFloat(settings.rowHeight.rawValue) + if tableView.rowHeight != newRowHeight { + tableView.rowHeight = newRowHeight + tableView.tile() + repaintRowGutter() + } + + let dataChanged = previous.dateFormat != settings.dateFormat + || previous.nullDisplay != settings.nullDisplay + || previous.enableSmartValueDetection != settings.enableSmartValueDetection + + if dataChanged { + reformatDisplayedText() + } + } + + func observeThemeChanges() { + themeCancellable = AppEvents.shared.themeChanged + .receive(on: RunLoop.main) + .sink { [weak self] _ in + if let tableView = self?.tableView { + DataGridBodyChrome.applyBackground(to: tableView) + tableView.headerView?.needsDisplay = true + tableView.cornerView?.needsDisplay = true + } + self?.reloadVisibleRowsAndStates() + /// The row-number font is a theme value and it decides the column's width, which + /// the pinned gutter mirrors. Nothing re-measured it on a theme change before, so + /// the width was already going stale here. + self?.resizeRowNumberColumnForCurrentRange() + self?.repaintRowGutter() + self?.selectionController.overlay?.needsDisplay = true + } + } + + /// The grid mounts no view for a data cell, so a client that attaches mid-session finds a table + /// of empty cells until the visible rows are built again. The remount is deferred off the + /// accessibility query that raised the flag, because rebuilding rows inside it would re-enter + /// the tree AppKit is walking. + func observeAccessibilityActivation() { + accessibilityActivationObserver = NotificationCenter.default.addObserver( + forName: DataGridAccessibility.didActivateNotification, + object: nil, + queue: nil + ) { [weak self] _ in + Task { @MainActor [weak self] in + self?.remountAccessibilityCells() + } + } + } + + /// Registered for the life of the coordinator, so it has to come off when the grid goes. + /// + /// `NotificationCenter` retains a block observer's closure, and a coordinator is built fresh on + /// every mount, so an entry left behind at teardown is never fired again and never reclaimed. + /// The closure captures `self` weakly, so this leaks the registration rather than the grid. + func detachAccessibilityActivationObserver() { + guard let accessibilityActivationObserver else { return } + NotificationCenter.default.removeObserver(accessibilityActivationObserver) + self.accessibilityActivationObserver = nil + } + + var hasAccessibilityActivationObserver: Bool { accessibilityActivationObserver != nil } +} diff --git a/TablePro/Views/Settings/Appearance/ThemeEditorColorsSection.swift b/TablePro/Views/Settings/Appearance/ThemeEditorColorsSection.swift index c37f98a5ae..1af7d0b232 100644 --- a/TablePro/Views/Settings/Appearance/ThemeEditorColorsSection.swift +++ b/TablePro/Views/Settings/Appearance/ThemeEditorColorsSection.swift @@ -45,6 +45,7 @@ internal struct ThemeEditorColorsSection: View { ColorPicker("", selection: binding(for: slot), supportsOpacity: true) .labelsHidden() + .accessibilityLabel(Text(slot.label)) } .contextMenu { if case .hex = value, case let .system(name) = BuiltInThemes.default(for: theme.appearance)[keyPath: slot.keyPath] { diff --git a/TablePro/Views/Settings/EditorSettingsView.swift b/TablePro/Views/Settings/EditorSettingsView.swift index 7faf144016..7213093f1b 100644 --- a/TablePro/Views/Settings/EditorSettingsView.swift +++ b/TablePro/Views/Settings/EditorSettingsView.swift @@ -13,16 +13,22 @@ struct EditorSettingsView: View { Form { TypographySection(domain: .editor, settings: $typography) - Section("SQL Editor") { + Section("Display") { Toggle("Show line numbers", isOn: $settings.showLineNumbers) Toggle("Highlight current line", isOn: $settings.highlightCurrentLine) Toggle("Highlight current statement", isOn: $settings.highlightCurrentStatement) + Toggle("Show invisible characters", isOn: $settings.showInvisibleCharacters) Toggle("Word wrap", isOn: $settings.wordWrap) + } + + Section("Gutter") { Toggle("Code folding", isOn: $settings.codeFoldingEnabled) Toggle("Run button beside each statement", isOn: $settings.showStatementRunControls) .disabled(!settings.showLineNumbers) .help(Text("The run button sits in the gutter, which needs line numbers.")) - Toggle("Show invisible characters", isOn: $settings.showInvisibleCharacters) + } + + Section("Editing") { Picker("Tab width:", selection: $settings.tabWidth) { Text("2 spaces").tag(2) Text("4 spaces").tag(4) diff --git a/TablePro/Views/Settings/Sections/TypographySection.swift b/TablePro/Views/Settings/Sections/TypographySection.swift index cb50b3a815..1817451d6e 100644 --- a/TablePro/Views/Settings/Sections/TypographySection.swift +++ b/TablePro/Views/Settings/Sections/TypographySection.swift @@ -29,7 +29,7 @@ internal struct TypographySection: View { @Binding internal var settings: TypographySettings internal var body: some View { - Section(domain.title) { + Section { Picker(String(localized: "Family:"), selection: familyBinding) { ForEach(EditorFontResolver.availableMonospacedFamilies) { family in Text(family.displayName).tag(family.id) @@ -41,10 +41,10 @@ internal struct TypographySection: View { Text(verbatim: "\(size) pt").tag(size) } } - + } header: { + Text(domain.title) + } footer: { Text(domain.caption) - .font(.caption) - .foregroundStyle(.secondary) } } diff --git a/TablePro/Views/Settings/ThemePreviewCard.swift b/TablePro/Views/Settings/ThemePreviewCard.swift index c43c1147d7..e261be638c 100644 --- a/TablePro/Views/Settings/ThemePreviewCard.swift +++ b/TablePro/Views/Settings/ThemePreviewCard.swift @@ -91,7 +91,14 @@ struct ThemePreviewCard: View { size == .compact ? 14 : 28 } + /// A miniature of the theme's own colors. The name and kind beside it carry the meaning, so + /// the shapes are decorative and say nothing. private var thumbnail: some View { + thumbnailContent + .accessibilityHidden(true) + } + + private var thumbnailContent: some View { HStack(spacing: 0) { sidebarStrip .frame(width: sidebarStripWidth) diff --git a/docs/customization/editor-settings.mdx b/docs/customization/editor-settings.mdx index 5b2ea15c38..2da58ec33d 100644 --- a/docs/customization/editor-settings.mdx +++ b/docs/customization/editor-settings.mdx @@ -20,16 +20,27 @@ The font here is set per Mac, not per theme, and is not synced. The picker lists the monospaced families installed on your Mac, at 10 to 24 pt. `Cmd+=` and `Cmd+-` change the size over the same range. -## SQL editor +## Display | Setting | Default | Notes | |---------|---------|-------| | Show line numbers | On | Turning this off also hides the per-statement run button | | Highlight current line | On | | | Highlight current statement | On | A faint band behind the statement the cursor is in. See [Statement markers](/features/sql-editor#statement-markers) | +| Show invisible characters | Off | See [Invisible characters](/features/sql-editor#invisible-characters) | | Word wrap | Off | Off means long lines scroll horizontally | + +## Gutter + +| Setting | Default | Notes | +|---------|---------|-------| | Code folding | On | Shows the fold ribbon in the gutter. See [Code Folding](/features/code-folding) | | Run button beside each statement | On | Gutter run buttons, revealed when the pointer is over the gutter. Needs line numbers on | + +## Editing + +| Setting | Default | Notes | +|---------|---------|-------| | Tab width | 4 spaces | 2, 4, or 8 | | Auto-uppercase keywords | Off | Uppercases SQL keywords on word boundaries. Strings, comments, and quoted identifiers are untouched | | Query parameters (`:name` syntax) | On | Detects `:name` placeholders and shows the parameter panel. See [Query Parameters](/features/query-parameters) |