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
16 changes: 11 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Auto-selection of first item fails with fast input in Database Switcher (#714)
- AI settings: fix Ollama model selection and improve error messages (#712)
- Rewrite SQL formatter with token-based architecture for better formatting (#705)
- Fix filter logic: `= NULL` auto-converts to `IS NULL`, BETWEEN works on all drivers, IN/NOT IN handles NULL values (#706)
- SQLite/DuckDB: auto-detect schema changes from external tools (#704)
- Database Switcher: auto-select first item on fast typing (#714)
- AI settings: fix Ollama model selection and error messages (#712)
- SQL formatter: rewrite with token-based architecture (#705)
- Filters: `= NULL` auto-converts to `IS NULL`, BETWEEN and IN/NOT IN NULL handling (#706)
- SQLite: auto-detect schema changes from external tools (#704)
- UI layout stability when toggling menus, panels, and inspectors (#702)

### Changed

- Keyboard shortcuts follow macOS HIG — `⌘F` is Find, `⌘⇧F` for filters, `⌘⌥I` for inspector, `⌘0` for sidebar
- Format Query and Pagination shortcuts now customizable in Settings

## [0.31.2] - 2026-04-13

Expand Down
2 changes: 1 addition & 1 deletion TablePro/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ struct ContentView: View {
toolbarState: sessionState.toolbarState,
coordinator: sessionState.coordinator
)
.transaction { $0.animation = nil }
.frame(maxWidth: .infinity)

if RightPanelVisibility.shared.isPresented {
Expand All @@ -241,7 +242,6 @@ struct ContentView: View {
.transition(.move(edge: .trailing))
}
}
.animation(.easeInOut(duration: 0.2), value: RightPanelVisibility.shared.isPresented)
} else {
VStack(spacing: 16) {
ProgressView()
Expand Down
50 changes: 36 additions & 14 deletions TablePro/Models/UI/KeyboardShortcutModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case closeTab
case refresh
case explainQuery
case formatQuery
case export
case importData
case quickSwitcher

// Navigation
case previousPage
case nextPage

// Edit
case undo
case redo
Expand Down Expand Up @@ -93,7 +98,8 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
switch self {
case .newConnection, .newTab, .openDatabase, .openFile, .switchConnection,
.saveChanges, .saveAs, .previewSQL, .closeTab, .refresh,
.explainQuery, .export, .importData, .quickSwitcher:
.explainQuery, .formatQuery, .export, .importData, .quickSwitcher,
.previousPage, .nextPage:
return .file
case .undo, .redo, .cut, .copy, .copyWithHeaders, .copyAsJson, .paste,
.delete, .selectAll, .clearSelection, .addRow,
Expand Down Expand Up @@ -123,9 +129,12 @@ enum ShortcutAction: String, Codable, CaseIterable, Identifiable {
case .closeTab: return String(localized: "Close Tab")
case .refresh: return String(localized: "Refresh")
case .explainQuery: return String(localized: "Explain Query")
case .formatQuery: return String(localized: "Format Query")
case .export: return String(localized: "Export")
case .importData: return String(localized: "Import")
case .quickSwitcher: return String(localized: "Quick Switcher")
case .previousPage: return String(localized: "Previous Page")
case .nextPage: return String(localized: "Next Page")
case .undo: return String(localized: "Undo")
case .redo: return String(localized: "Redo")
case .cut: return String(localized: "Cut")
Expand Down Expand Up @@ -251,7 +260,9 @@ struct KeyCombo: Codable, Equatable, Hashable {
// NSDeleteFunctionKey (0xF728) is always a valid Unicode scalar
// swiftlint:disable:next force_unwrapping
case "forwardDelete": return KeyEquivalent(Character(UnicodeScalar(NSDeleteFunctionKey)!))
default: return KeyEquivalent(Character(key))
default:
guard key.count == 1 else { return .escape }
return KeyEquivalent(Character(key))
}
}
return KeyEquivalent(Character(key))
Expand Down Expand Up @@ -296,7 +307,7 @@ struct KeyCombo: Codable, Equatable, Hashable {
case "end": return "↘"
case "pageUp": return "⇞"
case "pageDown": return "⇟"
default: return key.uppercased()
default: return key.count == 1 ? key.uppercased() : "?"
}
}
return key.uppercased()
Expand Down Expand Up @@ -345,10 +356,18 @@ struct KeyCombo: Codable, Equatable, Hashable {

/// Shortcuts that are reserved by macOS and should not be overridden
static let systemReserved: [KeyCombo] = [
KeyCombo(key: "q", command: true), // Quit
KeyCombo(key: "h", command: true), // Hide
KeyCombo(key: "m", command: true), // Minimize
KeyCombo(key: ",", command: true), // Settings
KeyCombo(key: "q", command: true), // Quit
KeyCombo(key: "h", command: true), // Hide
KeyCombo(key: "m", command: true), // Minimize
KeyCombo(key: ",", command: true), // Settings
KeyCombo(key: "tab", command: true, isSpecialKey: true), // App switcher
KeyCombo(key: "space", command: true, isSpecialKey: true), // Spotlight
KeyCombo(key: "`", command: true), // Window cycling
KeyCombo(key: "escape", command: true, option: true, isSpecialKey: true), // Force Quit
KeyCombo(key: "q", command: true, shift: true), // Logout
KeyCombo(key: "3", command: true, shift: true), // Screenshot full
KeyCombo(key: "4", command: true, shift: true), // Screenshot area
KeyCombo(key: "5", command: true, shift: true), // Screenshot options
]

/// Check if this combo is reserved by the system
Expand Down Expand Up @@ -430,23 +449,26 @@ struct KeyboardSettings: Codable, Equatable {

// MARK: - Default Shortcuts

/// All default shortcuts matching the hardcoded values in TableProApp.swift
/// Default shortcuts — applied when user has no overrides
static let defaultShortcuts: [ShortcutAction: KeyCombo] = [
// File
.newConnection: KeyCombo(key: "n", command: true),
.newTab: KeyCombo(key: "t", command: true),
.openDatabase: KeyCombo(key: "k", command: true),
.openFile: KeyCombo(key: "o", command: true),
.switchConnection: KeyCombo(key: "c", command: true, option: true),
.switchConnection: KeyCombo(key: "c", command: true, control: true),
.saveChanges: KeyCombo(key: "s", command: true),
.saveAs: KeyCombo(key: "s", command: true, shift: true),
.previewSQL: KeyCombo(key: "p", command: true, shift: true),
.closeTab: KeyCombo(key: "w", command: true),
.refresh: KeyCombo(key: "r", command: true),
.explainQuery: KeyCombo(key: "e", command: true, option: true),
.formatQuery: KeyCombo(key: "f", command: true, option: true),
.export: KeyCombo(key: "e", command: true, shift: true),
.importData: KeyCombo(key: "i", command: true, shift: true),
.quickSwitcher: KeyCombo(key: "p", command: true),
.previousPage: KeyCombo(key: "[", command: true),
.nextPage: KeyCombo(key: "]", command: true),

// Edit
.undo: KeyCombo(key: "z", command: true),
Expand All @@ -459,15 +481,15 @@ struct KeyboardSettings: Codable, Equatable {
.delete: KeyCombo(key: "delete", command: true, isSpecialKey: true),
.selectAll: KeyCombo(key: "a", command: true),
.clearSelection: KeyCombo(key: "escape", isSpecialKey: true),
.addRow: KeyCombo(key: "i", command: true),
.duplicateRow: KeyCombo(key: "d", command: true),
.addRow: KeyCombo(key: "n", command: true, shift: true),
.duplicateRow: KeyCombo(key: "d", command: true, shift: true),
.truncateTable: KeyCombo(key: "delete", option: true, isSpecialKey: true),
.previewFKReference: KeyCombo(key: "space", isSpecialKey: true),

// View
.toggleTableBrowser: KeyCombo(key: "b", command: true),
.toggleInspector: KeyCombo(key: "b", command: true, shift: true),
.toggleFilters: KeyCombo(key: "f", command: true),
.toggleTableBrowser: KeyCombo(key: "0", command: true),
.toggleInspector: KeyCombo(key: "i", command: true, option: true),
.toggleFilters: KeyCombo(key: "f", command: true, shift: true),
.toggleHistory: KeyCombo(key: "y", command: true),
.toggleResults: KeyCombo(key: "r", command: true, option: true),
.previousResultTab: KeyCombo(key: "[", command: true, option: true),
Expand Down
4 changes: 3 additions & 1 deletion TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ struct AppMenuCommands: Commands {
if let actions {
actions.closeTab()
} else {
NSApp.keyWindow?.close()
// No active connection — fall back to standard macOS close behavior.
// This handles Settings, Welcome, and other non-main windows.
NSApp.keyWindow?.performClose(nil)
}
}
.optionalKeyboardShortcut(shortcut(for: .closeTab))
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Views/Components/PaginationControlsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ struct PaginationControlsView: View {
.buttonStyle(.borderless)
.disabled(!pagination.hasPreviousPage || pagination.isLoading)
.help(String(localized: "Previous Page (⌘[)"))
.keyboardShortcut("[", modifiers: .command)
.optionalKeyboardShortcut(AppSettingsManager.shared.keyboard.keyboardShortcut(for: .previousPage))

// Page indicator: "1 of 25"
Text("\(pagination.currentPage) of \(pagination.totalPages)")
Expand All @@ -87,7 +87,7 @@ struct PaginationControlsView: View {
.buttonStyle(.borderless)
.disabled(!pagination.hasNextPage || pagination.isLoading)
.help(String(localized: "Next Page (⌘])"))
.keyboardShortcut("]", modifiers: .command)
.optionalKeyboardShortcut(AppSettingsManager.shared.keyboard.keyboardShortcut(for: .nextPage))
}
}

Expand Down
4 changes: 0 additions & 4 deletions TablePro/Views/ERDiagram/ERDiagramToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ struct ERDiagramToolbar: View {
Image(systemName: "minus.magnifyingglass")
}
.buttonStyle(.borderless)
.keyboardShortcut("-", modifiers: .command)
.accessibilityLabel(String(localized: "Zoom Out"))

Button {
Expand All @@ -25,15 +24,13 @@ struct ERDiagramToolbar: View {
}
.buttonStyle(.plain)
.help(String(localized: "Reset Zoom"))
.keyboardShortcut("0", modifiers: .command)

Button {
viewModel.zoom(to: viewModel.magnification + 0.25)
} label: {
Image(systemName: "plus.magnifyingglass")
}
.buttonStyle(.borderless)
.keyboardShortcut("=", modifiers: .command)
.accessibilityLabel(String(localized: "Zoom In"))

Button {
Expand All @@ -42,7 +39,6 @@ struct ERDiagramToolbar: View {
Image(systemName: "arrow.up.left.and.arrow.down.right")
}
.buttonStyle(.borderless)
.keyboardShortcut("0", modifiers: [.command, .shift])
.accessibilityLabel(String(localized: "Fit to Window"))
.help(String(localized: "Fit to Window"))

Expand Down
11 changes: 5 additions & 6 deletions TablePro/Views/ERDiagram/ERDiagramView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ struct ERDiagramView: View {
diagramContent
}
ERDiagramToolbar(viewModel: viewModel, onExport: exportDiagram)
.background(
Button("") { copyDiagramToClipboard() }
.keyboardShortcut("c", modifiers: .command)
.opacity(0)
.allowsHitTesting(false)
)
.onKeyPress(characters: .init(charactersIn: "c"), phases: .down) { keyPress in
guard keyPress.modifiers.contains(.command) else { return .ignored }
copyDiagramToClipboard()
return .handled
}
}
}
.task { await viewModel.loadDiagram() }
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Editor/QueryEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ struct QueryEditorView: View {
}
.buttonStyle(.borderless)
.help(String(localized: "Format Query (⌥⌘F)"))
.keyboardShortcut("f", modifiers: [.option, .command])
.optionalKeyboardShortcut(AppSettingsManager.shared.keyboard.keyboardShortcut(for: .formatQuery))

Divider()
.frame(height: 16)
Expand Down
39 changes: 22 additions & 17 deletions TablePro/Views/Editor/QuerySplitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,28 @@ struct QuerySplitView<TopContent: View, BottomContent: View>: NSViewRepresentabl

if isBottomCollapsed != wasCollapsed {
context.coordinator.lastCollapsedState = isBottomCollapsed
if isBottomCollapsed {
// Save divider position before collapsing
if splitView.subviews.count >= 2 {
context.coordinator.savedDividerPosition = splitView.subviews[0].frame.height
let collapse = isBottomCollapsed
let coordinator = context.coordinator
DispatchQueue.main.async {
guard splitView.bounds.height > 0 else { return }
if collapse {
// Save divider position before collapsing
if splitView.subviews.count >= 2 {
coordinator.savedDividerPosition = splitView.subviews[0].frame.height
}
// Move divider to bottom edge to collapse
splitView.setPosition(splitView.bounds.height, ofDividerAt: 0)
bottomView.isHidden = true
splitView.display()
} else {
bottomView.isHidden = false
splitView.adjustSubviews()
// Restore divider position
if let saved = coordinator.savedDividerPosition {
splitView.setPosition(saved, ofDividerAt: 0)
}
splitView.display()
}
// Move divider to bottom edge to collapse
splitView.setPosition(splitView.bounds.height, ofDividerAt: 0)
bottomView.isHidden = true
splitView.display()
} else {
bottomView.isHidden = false
splitView.adjustSubviews()
// Restore divider position
if let saved = context.coordinator.savedDividerPosition {
splitView.setPosition(saved, ofDividerAt: 0)
}
splitView.display()
}
}
}
Expand All @@ -96,7 +101,7 @@ struct QuerySplitView<TopContent: View, BottomContent: View>: NSViewRepresentabl
constrainMaxCoordinate proposedMaximumPosition: CGFloat,
ofSubviewAt dividerIndex: Int
) -> CGFloat {
splitView.bounds.height - 150
max(splitView.bounds.height - 150, 100)
}

func splitView(
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Editor/SQLEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ final class SQLEditorCoordinator: TextViewCoordinator, TextViewDelegate {

// Deferred to next run loop because prepareCoordinator runs during
// TextViewController.init, before the view hierarchy is fully loaded.
Task { [weak self] in
Task { @MainActor [weak self] in
try? await Task.sleep(for: .milliseconds(50))
guard let self else { return }
self.fixFindPanelHitTesting(controller: controller)
self.installAIContextMenu(controller: controller)
Expand Down
3 changes: 0 additions & 3 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,9 @@ struct MainEditorContentView: View {
Divider()
HistoryPanelView(connectionId: connectionId)
.frame(height: 300)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.background(.background)
.animation(.easeInOut(duration: 0.2), value: isHistoryVisible)
.sheet(item: $favoriteDialogQuery) { item in
FavoriteEditDialog(
connectionId: connectionId,
Expand Down Expand Up @@ -407,7 +405,6 @@ struct MainEditorContentView: View {
onApply: onApplyFilters,
onUnset: onClearFilters
)
.transition(.move(edge: .top).combined(with: .opacity))
Divider()
}

Expand Down
14 changes: 5 additions & 9 deletions TablePro/Views/Main/Extensions/MainContentView+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,13 @@ extension MainContentView {

// MARK: - Inspector Context

/// Coalesces multiple onChange-triggered updates into a single deferred call.
/// During tab switch, onChange handlers fire 3-4x — this ensures we only rebuild once,
/// and defers the work so SwiftUI can render the tab switch first.
/// Synchronously updates inspector state. Previously deferred by 100ms to coalesce
/// multiple onChange calls, but the deferred Task caused a double layout pass.
func scheduleInspectorUpdate() {
inspectorUpdateTask?.cancel()
inspectorUpdateTask = Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
guard !Task.isCancelled else { return }
updateSidebarEditState()
updateInspectorContext()
}
inspectorUpdateTask = nil
updateSidebarEditState()
updateInspectorContext()
}

func updateInspectorContext() {
Expand Down
4 changes: 3 additions & 1 deletion TablePro/Views/Main/MainContentCommandActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,9 @@ final class MainContentCommandActions {
}

func toggleRightSidebar() {
RightPanelVisibility.shared.isPresented.toggle()
withAnimation(.easeInOut(duration: 0.2)) {
RightPanelVisibility.shared.isPresented.toggle()
}
}

func toggleResults() {
Expand Down
Loading
Loading