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
23 changes: 23 additions & 0 deletions apple/TablePro/Core/DataGrid/Binding+CommittingGridEdit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Binding+CommittingGridEdit.swift
// TablePro
//

import SwiftUI

extension Binding {
/// For a tab selection whose panes share one grid model. The open cell edit commits against the pane
/// being left before the new tab is published, because the incoming pane's grid can mount before
/// the outgoing one is dismantled, and an edit committed at that teardown would land on the new
/// pane's rows, which reuse the same row and column ids.
@MainActor
func committingEdit(of model: DataGridModel) -> Binding<Value> {
Binding(
get: { wrappedValue },
set: { newValue in
model.commitEditing()
wrappedValue = newValue
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
/// record on the next launch. A user who customized keeps their arrangement and still receives
/// new items where they belong; a bump would throw that away along with their display mode.
/// The record is six keys and `isHidden` is not among them, since it is runtime state that is
/// never persisted, so a context-sensitive item set is no reason to bump either.
/// never persisted, so a context-sensitive item set is no reason to bump either. A reordered
/// default list needs no bump: a toolbar nobody customized stores no item list and takes the
/// new order, and a customized one keeps the user's (`scripts/probes/check-toolbar-autosave-reorder.sh`).
///
/// Bump only for a change that diff cannot express, and say which. Neither fact was measured on
/// macOS 13 or 14.
Expand Down Expand Up @@ -411,8 +413,14 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {

/// Eight controls, and the zones are the panes. AppKit's own sidebar toggle stands alone ahead of
/// `.sidebarTrackingSeparator`, so it lays out in the sidebar's titlebar strip and follows the
/// divider; the connection and its container are centred; Refresh, the commit verb, the Actions
/// pull-down and Safe Mode are the content run; the trailing-pane toggle closes the row.
/// divider; the connection and its container are centred; Refresh, the Actions pull-down, Safe
/// Mode and the commit verb are the content run; the trailing-pane toggle closes the row.
///
/// The commit verb comes last because the HIG gives a critical action like Save "dedicated,
/// familiar, and visually distinct sections" and puts the primary action on the trailing side.
/// Measured on macOS 27, AppKit already gives it a glass section of its own there: Safe Mode's
/// menu indicator ends the shared capsule before it, so no `.space` is needed, and one drew
/// pixel-identical.
///
/// A tracking separator divides the toolbar into pane-aligned sections; it does not align the
/// items inside one. Everything after `.inspectorTrackingSeparator` therefore lays out from
Expand Down Expand Up @@ -444,9 +452,9 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
database,
.flexibleSpace,
refresh,
saveChanges,
actions,
safeMode,
saveChanges,
]
if #available(macOS 14.0, *) {
items.append(.inspectorTrackingSeparator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

import Foundation

/// The glyphs a window toolbar draws for the actions every toolbar shares.
/// The glyphs a toolbar draws for the actions every toolbar shares, in both apps.
///
/// 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 {
/// From macOS 26 and iOS 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.
nonisolated internal enum ToolbarSymbols {
internal static let commit = "checkmark"

internal static var itemsHaveContainer: Bool {
if #available(macOS 26.0, *) {
if #available(macOS 26.0, iOS 26.0, *) {
return true
}
return false
Expand All @@ -42,6 +42,17 @@ internal enum ToolbarSymbols {
return itemsHaveContainer ? "line.3.horizontal.decrease" : "line.3.horizontal.decrease.circle"
}

/// The filled disc is state, a sort in effect, as it is for the filter.
internal static func sort(
isActive: Bool = false,
itemsHaveContainer: Bool = ToolbarSymbols.itemsHaveContainer
) -> String {
if isActive {
return "arrow.up.arrow.down.circle.fill"
}
return itemsHaveContainer ? "arrow.up.arrow.down" : "arrow.up.arrow.down.circle"
}

internal static func disconnect(itemsHaveContainer: Bool = ToolbarSymbols.itemsHaveContainer) -> String {
itemsHaveContainer ? "xmark" : "xmark.circle"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ struct AIChatWalkthroughBlockView: View {
Spacer()
if walkthrough.hasDiff {
Picker(String(localized: "Diff layout"), selection: diffStyleBinding) {
Text("Unified").tag(SqlWalkthroughDiffStyle.unified)
Text("Split").tag(SqlWalkthroughDiffStyle.split)
Text("Unified").tag(SqlWalkthroughDiffStyle.unified)
}
.pickerStyle(.segmented)
.labelsHidden()
Expand Down
11 changes: 5 additions & 6 deletions apple/TablePro/Views/Compare/CompareDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ internal struct CompareDetailView: View {
}

private var paneSelector: some View {
Picker(String(localized: "Detail"), selection: $session.detailPane) {
PaneTabBar(
title: String(localized: "Detail"),
selection: $session.detailPane,
identifier: "compare-detail-tabs"
) {
ForEach(CompareDetailPane.allCases, id: \.self) { pane in
Text(pane.title).tag(pane)
}
}
.pickerStyle(.segmented)
.labelsHidden()
.fixedSize()
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}

@ViewBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ internal final class CompareSyncWindowController: NSWindowController,
private func installToolbar(on window: NSWindow) {
let toolbar = NSToolbar(identifier: "com.TablePro.CompareSyncToolbar")
toolbar.delegate = self
toolbar.displayMode = .iconAndLabel
/// Icon only, the default every other TablePro window and the system apps ship. The source
/// and target items still read as words, since an item's title draws in this mode, and a
/// user who wants labels sets Icon and Text from Customize Toolbar, which is autosaved and
/// restored over this default.
toolbar.displayMode = .iconOnly
toolbar.allowsUserCustomization = true
toolbar.autosavesConfiguration = true
window.toolbar = toolbar
Expand Down
19 changes: 17 additions & 2 deletions apple/TablePro/Views/Components/DiagramZoomToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,26 @@ struct DiagramZoomToolbar<Extras: View>: View {
.buttonStyle(.borderless)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.themeMaterial(.toolbar, .thinMaterial, in: Capsule())
.overlay(Capsule().strokeBorder(.quaternary, lineWidth: 0.5))
.modifier(DiagramZoomSurface())
}

private var zoomPercentage: String {
"\(Int((viewport.magnification * 100).rounded()))%"
}
}

/// Controls floating over content take Liquid Glass from macOS 26, as the Open Quickly panel and
/// the chat's jump button already do. Glass draws its own edge and adapts to Reduce Transparency
/// and Increase Contrast itself, so the hairline and the solid fallback belong to the material
/// only.
private struct DiagramZoomSurface: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(.regular, in: Capsule())
} else {
content
.themeMaterial(.toolbar, .thinMaterial, in: Capsule())
.overlay(Capsule().strokeBorder(.quaternary, lineWidth: 0.5))
}
}
}
65 changes: 65 additions & 0 deletions apple/TablePro/Views/Components/ViewSwitcherStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// ViewSwitcherStyle.swift
// TablePro
//

import SwiftUI

/// Measured on macOS 27, small `.tabs` draws a flat track with a raised neutral thumb, the look Apple
/// gives tab navigation, where small `.segmented` fills the thumb with the accent and reads as a value.
///
/// To accessibility it is then a tab group of tab buttons rather than a radio group of segments.
private struct ViewSwitcherStyle: ViewModifier {
let controlSize: ControlSize

func body(content: Content) -> some View {
switcher(content)
.controlSize(controlSize)
}

@ViewBuilder
private func switcher(_ content: Content) -> some View {
#if SDK_MACOS_27
if #available(macOS 27.0, *) {
content.pickerStyle(.tabs)
} else {
content.pickerStyle(.segmented)
}
#else
content.pickerStyle(.segmented)
#endif
}
}

internal extension View {
/// For a picker that switches what a pane shows: small in a pane bar, status bar, viewer strip or
/// inspector, regular in a `PaneTabBar`. A picker that sets a value stays `.segmented`.
func viewSwitcherStyle(_ controlSize: ControlSize = .small) -> some View {
modifier(ViewSwitcherStyle(controlSize: controlSize))
}
}

/// The tab row a main-window pane switches its views with, centred over content that runs edge to
/// edge. A bordered tab view boxes and insets the grids these panes hold; the tabs role is the
/// control macOS 27 gives tab navigation, and before it the segmented control is.
internal struct PaneTabBar<Selection: Hashable, Tabs: View>: View {
let title: String
@Binding var selection: Selection
let identifier: String
@ViewBuilder let tabs: () -> Tabs

/// Spacers rather than a fixed size, so a pane narrower than the tabs compresses them instead of
/// pushing the outer ones past its edges.
var body: some View {
HStack(spacing: 0) {
Spacer(minLength: 0)
Picker(title, selection: $selection, content: tabs)
.labelsHidden()
.viewSwitcherStyle(.regular)
.accessibilityIdentifier(identifier)
Spacer(minLength: 0)
}
.padding(.horizontal)
.padding(.vertical, 8)
}
}
6 changes: 4 additions & 2 deletions apple/TablePro/Views/DataFiles/DataFileContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,15 @@ struct DataFileContentView: View {
}

struct DataFileSheetTabs: View {
static let segmentedSheetLimit = 8
/// The HIG's limit for tabs, past which it asks for a pop-up menu instead.
static let segmentedSheetLimit = 6

@ObservedObject var controller: DataFileController

var body: some View {
HStack {
if controller.sheets.count <= Self.segmentedSheetLimit {
picker.pickerStyle(.segmented)
picker.viewSwitcherStyle()
} else {
picker.pickerStyle(.menu)
}
Expand All @@ -130,5 +131,6 @@ struct DataFileSheetTabs: View {
.labelsHidden()
.fixedSize()
.accessibilityLabel(String(localized: "Sheet"))
.accessibilityIdentifier("data-file-sheet-picker")
}
}
24 changes: 21 additions & 3 deletions apple/TablePro/Views/DataFiles/DataFileWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ extension NSToolbarItem.Identifier {
static let dataFileColumns = NSToolbarItem.Identifier("com.TablePro.dataFile.columns")
static let dataFileFilters = NSToolbarItem.Identifier("com.TablePro.dataFile.filters")
static let dataFileSearch = NSToolbarItem.Identifier("com.TablePro.dataFile.search")
static let dataFileInspector = NSToolbarItem.Identifier("com.TablePro.dataFile.inspector")
/// AppKit's own inspector toggle from macOS 14, which draws the system glyph and sends
/// `toggleInspector(_:)` down the responder chain to the split controller. macOS 13 has no such
/// item, so it keeps an app-owned one.
static let dataFileInspector: NSToolbarItem.Identifier = {
if #available(macOS 14.0, *) {
return .toggleInspector
}
return NSToolbarItem.Identifier("com.TablePro.dataFile.inspector")
}()
}

@MainActor
Expand Down Expand Up @@ -110,6 +118,7 @@ final class DataFileWindowController: NSWindowController, NSWindowDelegate, NSTo
return makeItem(itemIdentifier, label: String(localized: "Filters"), symbol: ToolbarSymbols.filter(),
action: #selector(DataFileSplitViewController.toggleFilterBar(_:)))
case .dataFileInspector:
guard #unavailable(macOS 14.0) else { return nil }
return makeItem(itemIdentifier, label: String(localized: "Inspector"), symbol: "sidebar.trailing",
action: #selector(DataFileSplitViewController.toggleInspector(_:)))
case .dataFileColumns:
Expand All @@ -132,9 +141,18 @@ final class DataFileWindowController: NSWindowController, NSWindowDelegate, NSTo
}
}

/// The inspector toggle follows the order the main window uses: after the tracking separator
/// and a flexible space, so it sits over the inspector and stays at the trailing edge whether
/// the pane is open or not.
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[.dataFileAddRow, .dataFileDeleteRows, .dataFileColumns, .flexibleSpace, .dataFileFilters,
.dataFileSearch, .dataFileInspector]
var items: [NSToolbarItem.Identifier] = [
.dataFileAddRow, .dataFileDeleteRows, .dataFileColumns, .flexibleSpace, .dataFileFilters, .dataFileSearch,
]
if #available(macOS 14.0, *) {
items.append(contentsOf: [.inspectorTrackingSeparator, .flexibleSpace])
}
items.append(.dataFileInspector)
return items
}

func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,13 @@ struct IntegrationsActivityLogPane: View {
.disabled(isLoading)
}

/// `sidebar.trailing` is the glyph AppKit's own inspector toggle draws, and unlike
/// `sidebar.right` it mirrors in a right-to-left layout.
private var inspectorToggle: some View {
Button {
showInspector.toggle()
} label: {
Label(String(localized: "Details"), systemImage: "sidebar.right")
Label(String(localized: "Details"), systemImage: "sidebar.trailing")
}
.help(String(localized: "Show details"))
}
Expand Down
19 changes: 7 additions & 12 deletions apple/TablePro/Views/ObjectSource/UserType/UserTypeTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,20 +186,15 @@ struct UserTypeTabView: View {

private func editor(_ projection: UserTypeDraft.Projection) -> some View {
VStack(spacing: 0) {
HStack {
Spacer()
Picker(String(localized: "Section"), selection: $model.section) {
ForEach(sections, id: \.self) { section in
Text(title(for: section)).tag(section)
}
PaneTabBar(
title: String(localized: "Section"),
selection: $model.section,
identifier: "user-type-section-picker"
) {
ForEach(sections, id: \.self) { section in
Text(title(for: section)).tag(section)
}
.pickerStyle(.segmented)
.labelsHidden()
.fixedSize()
.accessibilityIdentifier("user-type-section-picker")
Spacer()
}
.padding(.vertical, 8)
Divider()
Group {
switch model.section {
Expand Down
3 changes: 1 addition & 2 deletions apple/TablePro/Views/QueryPlan/QueryPlanResultView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,7 @@ struct QueryPlanResultView: View {
Text(mode.title).tag(mode)
}
}
.pickerStyle(.segmented)
.controlSize(.small)
.viewSwitcherStyle()
.fixedSize()
.labelsHidden()
.accessibilityIdentifier("query-plan-mode-picker")
Expand Down
3 changes: 2 additions & 1 deletion apple/TablePro/Views/Results/BinaryValueViewerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ internal struct BinaryValueViewerView: View {
Text(String(localized: "Text")).tag(BinaryValueViewMode.text)
Text(String(localized: "Hex")).tag(BinaryValueViewMode.hex)
}
.pickerStyle(.segmented)
.viewSwitcherStyle()
.labelsHidden()
.fixedSize()
.accessibilityIdentifier("binary-viewer-mode-picker")
} else {
Text(format.displayName)
.font(.headline)
Expand Down
3 changes: 2 additions & 1 deletion apple/TablePro/Views/Results/CellImageViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ internal struct CellImageViewer<Source: View>: View {
Text(String(localized: "Image")).tag(CellImageViewMode.image)
Text(sourceKind.label).tag(CellImageViewMode.source)
}
.pickerStyle(.segmented)
.viewSwitcherStyle()
.labelsHidden()
.fixedSize()
.accessibilityIdentifier("image-viewer-mode-picker")

Spacer()

Expand Down
Loading
Loading