Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Toolbar Save is a plain checkmark, and the Safe Mode icon is filled only at the two Full levels. (#3250)
- Actions, filter and Disconnect toolbar icons lose their circle on macOS 26 and later, and Actions its chevron. (#3250)

### Fixed

- Export in the Structure and object source views showing the Import icon. (#3250)
- VoiceOver reading the welcome window and Integrations filter menus as "chevron.pulldown". (#3250)
- Database icon filled in the database switcher and query editor, outline in the toolbar and sidebar. (#3250)
- Status bar buttons a point or two taller or shorter than each other depending on their icon. (#3250)

## [0.77.1] - 2026-10-03

### Added
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Plugins/PluginDriverAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable, DatabaseRepor
sizeBytes: pluginMeta.sizeBytes,
lastAccessed: nil,
isSystemDatabase: isSystem,
icon: isSystem ? "gearshape.fill" : "cylinder.fill"
icon: isSystem ? "gearshape" : "cylinder"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,21 @@ extension MainWindowToolbar {

/// The long tail of what a context can do, in one control whose menu changes with the tab.
///
/// `ellipsis.circle` is the glyph Finder gives its own Action pull-down. The menu is built by
/// `ConnectionActionsMenuDelegate` when it opens. The overflow entry is AppKit's own and is
/// left to it: measured on macOS 27, an `NSMenuToolbarItem` answers `menuFormRepresentation`
/// with a fresh item titled with its label over this same menu, whatever was assigned, so a
/// narrow window's overflow offers exactly what the control would.
/// The glyph and the missing indicator are Finder's own Action pull-down, which is why they
/// come from `ToolbarSymbols` rather than being named here: Finder draws it differently before
/// and after macOS 26. The menu is built by `ConnectionActionsMenuDelegate` when it opens. The
/// overflow entry is AppKit's own and is left to it: measured on macOS 27, an
/// `NSMenuToolbarItem` answers `menuFormRepresentation` with a fresh item titled with its label
/// over this same menu, whatever was assigned, so a narrow window's overflow offers exactly
/// what the control would.
func makeActionsItem() -> NSToolbarItem {
let label = String(localized: "Actions")
let item = StatefulMenuToolbarItem(itemIdentifier: Self.actions)
item.label = label
item.paletteLabel = label
item.isBordered = true
item.image = NSImage(systemSymbolName: "ellipsis.circle", accessibilityDescription: label)
item.image = NSImage(systemSymbolName: ToolbarSymbols.more(), accessibilityDescription: label)
item.showsIndicator = ToolbarSymbols.moreShowsIndicator()
item.toolTip = String(localized: "Commands for the current tab and connection")
item.isEnabledProvider = enablement(of: Self.actions)
item.menu = menu(delegate: actionsMenuDelegate)
Expand Down Expand Up @@ -217,14 +220,20 @@ extension MainWindowToolbar {
/// Labelled with the verb the tab commits with, and re-labelled by `refreshCommitVerb(for:)` when
/// the tab kind moves, so the palette, the overflow entry and the tooltip never offer to save a
/// table definition that is about to be created.
///
/// The overflow entry carries no image. A check drawn in a menu row is the mark the system
/// gives an item that is on, so the glyph that reads as "commit" in the toolbar would read as
/// "already saved" in the overflow list.
func makeSaveChangesItem() -> NSToolbarItem {
menuOnlyItem(
let item = menuOnlyItem(
id: Self.saveChanges,
label: commitVerb,
symbol: "checkmark.circle.fill",
symbol: ToolbarSymbols.commit,
action: #selector(performSaveChanges(_:)),
shortcut: .saveChanges
)
item.menuFormRepresentation?.image = nil
return item
}

/// A row insert is a change to the data, so it belongs with the other data commands rather than
Expand Down
48 changes: 48 additions & 0 deletions TablePro/Core/Services/Infrastructure/Toolbar/ToolbarSymbols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// ToolbarSymbols.swift
// TablePro
//

import Foundation

/// The glyphs a window toolbar draws for the actions every toolbar shares.
///
/// From macOS 26 a toolbar item sits in its own glass container, and the HIG asks for symbols
/// without borders there: the container is the border, so a circled glyph draws two. Earlier
/// releases give an item no container, and the circled form is the one their own toolbars use.
/// Each answer is a function of that one fact, so both forms can be tested on either release.
internal enum ToolbarSymbols {
internal static let commit = "checkmark"

internal static var itemsHaveContainer: Bool {
if #available(macOS 26.0, *) {
return true
}
return false
}

internal static func more(itemsHaveContainer: Bool = ToolbarSymbols.itemsHaveContainer) -> String {
itemsHaveContainer ? "ellipsis" : "ellipsis.circle"
}

/// A More control inside a container draws no menu indicator, measured on Finder and Notes.
/// Every other pull-down keeps the system's.
internal static func moreShowsIndicator(itemsHaveContainer: Bool = ToolbarSymbols.itemsHaveContainer) -> Bool {
!itemsHaveContainer
}

/// The filled disc is state, a filter narrowing the list, so it stays where the idle border goes.
internal static func filter(
isActive: Bool = false,
itemsHaveContainer: Bool = ToolbarSymbols.itemsHaveContainer
) -> String {
if isActive {
return "line.3.horizontal.decrease.circle.fill"
}
return itemsHaveContainer ? "line.3.horizontal.decrease" : "line.3.horizontal.decrease.circle"
}

internal static func disconnect(itemsHaveContainer: Bool = ToolbarSymbols.itemsHaveContainer) -> String {
itemsHaveContainer ? "xmark" : "xmark.circle"
}
}
6 changes: 4 additions & 2 deletions TablePro/Models/Connection/SafeModeLevel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,16 @@ internal extension SafeModeLevel {
}
}

/// Filled exactly when `appliesToAllQueries`, so the fill says one thing: this level gates
/// reads as well as writes. Silent and Read-Only differ by the padlock's shape instead.
var iconName: String {
switch self {
case .silent: return "lock.open.fill"
case .silent: return "lock.open"
case .alert: return "exclamationmark.triangle"
case .alertFull: return "exclamationmark.triangle.fill"
case .safeMode: return "lock.shield"
case .safeModeFull: return "lock.shield.fill"
case .readOnly: return "lock.fill"
case .readOnly: return "lock"
}
}

Expand Down
2 changes: 1 addition & 1 deletion TablePro/Models/Database/DatabaseMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct DatabaseMetadata: Identifiable, Equatable {
sizeBytes: nil,
lastAccessed: nil,
isSystemDatabase: isSystem,
icon: isSystem ? "gearshape.fill" : "cylinder.fill"
icon: isSystem ? "gearshape" : "cylinder"
)
}
}
4 changes: 2 additions & 2 deletions TablePro/Models/Query/ResultStatusPresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import CoreGraphics
/// tier whose ideal width fits, and `narrow` is measured to fit the narrowest pane the window
/// allows, so no tier can over-size the column again.
enum StatusBarTier: CaseIterable {
/// Every control at full width, titles beside icons.
/// Every control at full width, titles beside icons. Highlight Rules alone stays a glyph.
case regular
/// Titles drop to their icons and the page edges go. Every control is still on the bar.
case compact
Expand All @@ -37,7 +37,7 @@ enum StatusBarTier: CaseIterable {
/// which controls a result offers, this one answers how they are drawn. Both are pure so the whole
/// matrix is decidable without mounting a view.
struct ResultStatusPresentation: Equatable {
/// Whether a control carries its title beside its icon.
/// Whether Columns and Filters carry their title beside their icon.
let showsControlTitles: Bool
/// A segmented control while the modes fit, and a pull-down naming the current one once they do
/// not. `View > Result View` offers the same choice either way.
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/DataFiles/DataFileWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ final class DataFileWindowController: NSWindowController, NSWindowDelegate, NSTo
return makeItem(itemIdentifier, label: String(localized: "Delete"), symbol: "minus",
action: #selector(DataFileSplitViewController.dataFileDeleteSelectedRows(_:)))
case .dataFileFilters:
return makeItem(itemIdentifier, label: String(localized: "Filters"), symbol: "line.3.horizontal.decrease.circle",
return makeItem(itemIdentifier, label: String(localized: "Filters"), symbol: ToolbarSymbols.filter(),
action: #selector(DataFileSplitViewController.toggleFilterBar(_:)))
case .dataFileInspector:
return makeItem(itemIdentifier, label: String(localized: "Inspector"), symbol: "sidebar.trailing",
Expand Down
11 changes: 4 additions & 7 deletions TablePro/Views/Integrations/IntegrationsActivityLogPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,14 @@ struct IntegrationsActivityLogPane: View {
}
}
} label: {
Label(String(localized: "Filters"), systemImage: filterIcon)
Label(String(localized: "Filters"), systemImage: ToolbarSymbols.filter(isActive: hasActiveFilters))
}
/// Named here for the reason `WelcomeViewOptionsMenu` gives: a toolbar-hosted `Menu`
/// publishes "chevron.pulldown" from its label alone.
.accessibilityLabel(String(localized: "Filters"))
.help(String(localized: "Filter activity"))
}

private var filterIcon: String {
hasActiveFilters
? "line.3.horizontal.decrease.circle.fill"
: "line.3.horizontal.decrease.circle"
}

private var exportButton: some View {
Button(action: exportCSV) {
Label(String(localized: "Export"), systemImage: "square.and.arrow.up")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct IntegrationsConnectedClientsPane: View {
disconnectCandidate = client
}
} label: {
Label(String(localized: "Disconnect"), systemImage: "xmark.circle")
Label(String(localized: "Disconnect"), systemImage: ToolbarSymbols.disconnect())
}
.help(String(localized: "Disconnect the selected client"))
.disabled(selection == nil)
Expand Down
6 changes: 6 additions & 0 deletions TablePro/Views/Results/ResultStatusBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ struct ResultStatusBar: View {
Text("Columns")
} icon: {
Image(systemName: hasHiddenColumns ? "eye.slash" : "eye")
.statusBarControlIcon(besideTitle: presentation.showsControlTitles)
}
}
.statusBarLabelStyle(showsTitle: presentation.showsControlTitles)
Expand Down Expand Up @@ -351,8 +352,12 @@ struct ResultStatusBar: View {
Text("Highlight Rules")
} icon: {
Image(systemName: "highlighter")
.statusBarControlIcon(besideTitle: false)
}
}
/// A glyph at every tier. Its title would add 88pt, and measured at the window's default
/// 1200pt with the sidebar open the regular tier then no longer fits, so drawing it would
/// take the titles off Columns and Filters at the size most windows are.
.labelStyle(.iconOnly)
.controlSize(.small)
.disabled(highlightState.columns.isEmpty)
Expand Down Expand Up @@ -388,6 +393,7 @@ struct ResultStatusBar: View {
Image(systemName: filterState.hasAppliedFilters
? "line.3.horizontal.decrease.circle.fill"
: "line.3.horizontal.decrease.circle")
.statusBarControlIcon(besideTitle: presentation.showsControlTitles)
}
}
.statusBarLabelStyle(showsTitle: presentation.showsControlTitles)
Expand Down
14 changes: 14 additions & 0 deletions TablePro/Views/Results/StatusBarChrome.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ enum StatusBarChrome {
static let height: CGFloat = 28
static let horizontalPadding: CGFloat = 10
static let clusterSpacing: CGFloat = 8

/// The layout height a control's glyph is given. A small bordered button takes its height from
/// its glyph, measured at 18pt for `eye`, 19pt for the circled filter and 20pt for `eye.slash`
/// when the glyph stands alone, and 20 or 21pt beside a title, so the controls in one bar stood
/// at three heights. One box makes each the height of a text-only control: 12pt beside a title,
/// whose own line sets the height, and 14pt alone.
static func controlIconHeight(besideTitle: Bool) -> CGFloat {
besideTitle ? 12 : 14
}
}

struct StatusBarSeparator: View {
Expand Down Expand Up @@ -60,4 +69,9 @@ extension View {
func statusBarChrome() -> some View {
modifier(StatusBarChromeModifier())
}

/// Height only: the glyph keeps its size and its width, so no control gets wider or narrower.
func statusBarControlIcon(besideTitle: Bool) -> some View {
frame(height: StatusBarChrome.controlIconHeight(besideTitle: besideTitle))
}
}
2 changes: 1 addition & 1 deletion TablePro/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ enum SettingsPane: String, CaseIterable {
case .mcp: "network"
case .plugins: "puzzlepiece.extension"
case .sync: "arrow.triangle.2.circlepath"
case .account: "key.fill"
case .account: "key"
}
}
}
2 changes: 1 addition & 1 deletion TablePro/Views/Shared/ObjectSourceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ struct ObjectSourceView: View {
Button {
Task { await export() }
} label: {
Label("Export…", systemImage: "square.and.arrow.down")
Label("Export…", systemImage: "square.and.arrow.up")
}
.buttonStyle(.bordered)
.disabled(!hasSource)
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Structure/TableStructureView+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ extension TableStructureView {
.buttonStyle(.bordered)

Button(action: exportDDL) {
Label("Export", systemImage: "square.and.arrow.down")
Label("Export", systemImage: "square.and.arrow.up")
}
.buttonStyle(.bordered)
}
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Views/Welcome/WelcomeLibraryPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,12 @@ internal struct WelcomeViewOptionsMenu: View {
}
}
} label: {
Label(String(localized: "View Options"), systemImage: "line.3.horizontal.decrease.circle")
Label(String(localized: "View Options"), systemImage: ToolbarSymbols.filter())
}
/// A toolbar-hosted `Menu` is named by this modifier and not by its label: built against
/// the macOS 26 SDK, the label alone publishes "chevron.pulldown". An in-view `Menu` is the
/// reverse, which is the rule `MenuDisclosureIndicatorTests` holds everywhere else.
.accessibilityLabel(String(localized: "View Options"))
.help(String(localized: "Sort and filter connections"))
.accessibilityIdentifier("welcome-toolbar-view-options")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ struct PluginDriverAdapterSystemDatabaseTests {
let result = try await adapter.fetchAllDatabaseMetadata()

#expect(result.filter(\.isSystemDatabase).map(\.name) == ["master", "tempdb"])
#expect(result.first { $0.name == "master" }?.icon == "gearshape.fill")
#expect(result.first { $0.name == "sales" }?.icon == "cylinder.fill")
#expect(result.first { $0.name == "master" }?.icon == "gearshape")
#expect(result.first { $0.name == "sales" }?.icon == "cylinder")
}

@Test("A database the driver flags stays a system database even when the type's list omits it")
Expand Down
Loading
Loading