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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Filtering the data grid keeps you on the keyboard. Applying or clearing a filter returns focus to the grid so you can keep moving through cells, Return applies the filter, and Escape closes the filter panel and returns to the grid. (#1490)
- Moving a connection into or out of a group now syncs across devices, instead of leaving it ungrouped on your other Macs.
- Opening a table on a connection with many tables no longer stalls for several seconds while autocomplete and table metadata load. Background schema introspection now runs on separate connections instead of waiting behind, or blocking, the query that fills the grid. (#1483)

Expand Down
14 changes: 14 additions & 0 deletions TablePro/Views/Filter/FilterPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ struct FilterPanelView: View {
}
}
.background(Color(nsColor: .windowBackgroundColor))
.focusSection()
.onExitCommand {
closePanelAndFocusGrid()
}
.onAppear {
if filterState.filters.isEmpty && !columns.isEmpty {
coordinator.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn)
Expand Down Expand Up @@ -85,6 +89,7 @@ struct FilterPanelView: View {
Button("Unset") {
coordinator.clearFilterState()
onUnset()
coordinator.focusActiveGrid()
}
.buttonStyle(.bordered)
.controlSize(.small)
Expand All @@ -96,6 +101,7 @@ struct FilterPanelView: View {
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
.keyboardShortcut(.defaultAction)
.disabled(validFilterCount == 0)
.help(String(localized: "Apply filters"))
}
Expand Down Expand Up @@ -202,9 +208,11 @@ struct FilterPanelView: View {
coordinator.removeFilterAndReload(filter)
if filterState.filters.isEmpty {
coordinator.closeFilterPanel()
coordinator.focusActiveGrid()
}
},
onSubmit: { applyAllValidFilters() },
onCancel: { closePanelAndFocusGrid() },
focusedFilterId: $focusedFilterId
)
}
Expand Down Expand Up @@ -237,6 +245,12 @@ struct FilterPanelView: View {
private func applyAllValidFilters() {
coordinator.applyAllFilters()
onApply(coordinator.selectedTabFilterState.appliedFilters)
coordinator.focusActiveGrid()
}

private func closePanelAndFocusGrid() {
coordinator.closeFilterPanel()
coordinator.focusActiveGrid()
}

private var isSQLDialect: Bool {
Expand Down
9 changes: 7 additions & 2 deletions TablePro/Views/Filter/FilterRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct FilterRowView: View {
let onDuplicate: () -> Void
let onRemove: () -> Void
let onSubmit: () -> Void
let onCancel: () -> Void
@Binding var focusedFilterId: UUID?

private var pickerEligibleOperators: Set<FilterOperator> {
Expand Down Expand Up @@ -65,6 +66,7 @@ struct FilterRowView: View {
.fixedSize()
.labelsHidden()
.accessibilityLabel(String(localized: "Filter column"))
.accessibilityValue(filter.isRawSQL ? String(localized: "Raw SQL") : filter.columnName)
.help(String(localized: "Select filter column"))
}

Expand All @@ -79,6 +81,7 @@ struct FilterRowView: View {
.fixedSize()
.labelsHidden()
.accessibilityLabel(String(localized: "Filter operator"))
.accessibilityValue(filter.filterOperator.displayName)
.help(String(localized: "Select filter operator"))
}

Expand All @@ -95,7 +98,8 @@ struct FilterRowView: View {
placeholder: "e.g. id = 1",
completionSource: rawSQLCompletionSource,
allowsMultiLine: true,
onSubmit: onSubmit
onSubmit: onSubmit,
onCancel: onCancel
)
.accessibilityLabel(String(localized: "WHERE clause"))
} else if filter.filterOperator.requiresValue {
Expand All @@ -109,7 +113,8 @@ struct FilterRowView: View {
identity: filter.id,
placeholder: String(localized: "Value"),
completionSource: .staticValues(completions),
onSubmit: onSubmit
onSubmit: onSubmit,
onCancel: onCancel
)
.frame(minWidth: 80)
.accessibilityLabel(String(localized: "Filter value"))
Expand Down
75 changes: 60 additions & 15 deletions TablePro/Views/Filter/FilterValueTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct FilterValueTextField: NSViewRepresentable {
var completionSource: FilterCompletionSource = .staticValues([])
var allowsMultiLine: Bool = false
var onSubmit: () -> Void = {}
var onCancel: () -> Void = {}

static func suggestions(for input: String, in completions: [String]) -> [String] {
guard !input.isEmpty else { return [] }
Expand Down Expand Up @@ -75,12 +76,14 @@ struct FilterValueTextField: NSViewRepresentable {
textField.lineBreakMode = .byTruncatingTail
}

textField.owner = context.coordinator
context.coordinator.textField = textField
context.coordinator.text = $text
context.coordinator.focusedId = $focusedId
context.coordinator.identity = identity
context.coordinator.completionSource = completionSource
context.coordinator.onSubmit = onSubmit
context.coordinator.onCancel = onCancel

return textField
}
Expand All @@ -91,6 +94,7 @@ struct FilterValueTextField: NSViewRepresentable {
context.coordinator.identity = identity
context.coordinator.completionSource = completionSource
context.coordinator.onSubmit = onSubmit
context.coordinator.onCancel = onCancel
context.coordinator.textField = textField

textField.placeholderString = placeholder
Expand All @@ -101,18 +105,7 @@ struct FilterValueTextField: NSViewRepresentable {
textField.stringValue = text
}

if focusedId == identity {
let binding = $focusedId
let pendingId = identity
DispatchQueue.main.async {
guard let window = textField.window,
binding.wrappedValue == pendingId else { return }
if window.firstResponder !== textField.currentEditor() {
window.makeFirstResponder(textField)
}
binding.wrappedValue = nil
}
}
context.coordinator.focusIfRequested()
}

func makeCoordinator() -> Coordinator {
Expand All @@ -121,7 +114,8 @@ struct FilterValueTextField: NSViewRepresentable {
focusedId: $focusedId,
identity: identity,
completionSource: completionSource,
onSubmit: onSubmit
onSubmit: onSubmit,
onCancel: onCancel
)
}

Expand All @@ -132,6 +126,7 @@ struct FilterValueTextField: NSViewRepresentable {
var identity: UUID
var completionSource: FilterCompletionSource
var onSubmit: () -> Void
var onCancel: () -> Void
weak var textField: NSTextField?

private let suggestionState = SuggestionState()
Expand All @@ -151,13 +146,36 @@ struct FilterValueTextField: NSViewRepresentable {
focusedId: Binding<UUID?>,
identity: UUID,
completionSource: FilterCompletionSource,
onSubmit: @escaping () -> Void
onSubmit: @escaping () -> Void,
onCancel: @escaping () -> Void
) {
self.text = text
self.focusedId = focusedId
self.identity = identity
self.completionSource = completionSource
self.onSubmit = onSubmit
self.onCancel = onCancel
}

func focusIfRequested() {
guard focusedId.wrappedValue == identity,
let textField,
let window = textField.window else { return }
if window.firstResponder !== textField.currentEditor() {
window.makeFirstResponder(textField)
}
}

func handleBecameFirstResponder() {
if focusedId.wrappedValue != identity {
focusedId.wrappedValue = identity
}
}

func handleResignedFirstResponder() {
if focusedId.wrappedValue == identity {
focusedId.wrappedValue = nil
}
}

deinit {
Expand Down Expand Up @@ -195,7 +213,11 @@ struct FilterValueTextField: NSViewRepresentable {
return true
}
if commandSelector == #selector(NSResponder.cancelOperation(_:)) {
dismissSuggestions()
if suggestionPopover != nil {
dismissSuggestions()
return true
}
onCancel()
return true
}
return false
Expand Down Expand Up @@ -391,6 +413,13 @@ struct FilterValueTextField: NSViewRepresentable {
}

private final class SubstitutionDisabledTextField: NSTextField {
weak var owner: Coordinator?

override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
owner?.focusIfRequested()
}

override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder()
if result, let editor = currentEditor() as? NSTextView {
Expand All @@ -399,6 +428,17 @@ struct FilterValueTextField: NSViewRepresentable {
editor.isAutomaticTextReplacementEnabled = false
editor.isAutomaticSpellingCorrectionEnabled = false
}
if result {
owner?.handleBecameFirstResponder()
}
return result
}

override func resignFirstResponder() -> Bool {
let result = super.resignFirstResponder()
if result {
owner?.handleResignedFirstResponder()
}
return result
}
}
Expand Down Expand Up @@ -438,6 +478,11 @@ struct FilterValueTextField: NSViewRepresentable {
.contentShape(Rectangle())
.onTapGesture { onSelect(item) }
.id(index)
.accessibilityElement(children: .ignore)
.accessibilityLabel(item.label)
.accessibilityAddTraits(
state.selectedIndex == index ? [.isButton, .isSelected] : .isButton
)
}
}
.padding(4)
Expand Down
11 changes: 10 additions & 1 deletion TablePro/Views/Main/Child/MainStatusBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ struct MainStatusBarView: View {
private var isStructureMode: Bool { viewMode == .structure }
private var showsDataChrome: Bool { !isStructureMode }

private var filterToggleHelp: String {
let label = String(localized: "Toggle Filters")
guard let combo = AppSettingsManager.shared.keyboard.shortcut(for: .toggleFilters),
!combo.isCleared else {
return label
}
return "\(label) (\(combo.displayString))"
}

var body: some View {
HStack {
if snapshot.tabId != nil {
Expand Down Expand Up @@ -191,7 +200,7 @@ struct MainStatusBarView: View {
}
.toggleStyle(.button)
.controlSize(.small)
.help(String(localized: "Toggle Filters (⇧⌘F)"))
.help(filterToggleHelp)
}

if snapshot.tabType == .table, snapshot.hasTableName, showsPaginationControls {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// MainContentCoordinator+GridFocus.swift
// TablePro
//

import Foundation

internal extension MainContentCoordinator {
func focusActiveGrid() {
dataTabDelegate?.tableViewCoordinator?.focusGrid()
}
}
5 changes: 5 additions & 0 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,11 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
}
}

internal func focusGrid() {
guard let tableView, let window = tableView.window else { return }
window.makeFirstResponder(tableView)
}

func beginEditing(displayRow: Int, column: Int) {
guard let tableView,
let displayCol = DataGridView.tableColumnIndex(for: column, in: tableView, schema: identitySchema)
Expand Down
Loading