From 357190397f871242949c0aa89b1751da77f88a2b Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 22 May 2026 21:42:35 +0700 Subject: [PATCH 1/2] refactor(toolbar): use native NSSearchField in the connection and database switchers (#1350) --- .../DatabaseSwitcherPopover.swift | 81 ++----------------- .../Toolbar/ConnectionSwitcherPopover.swift | 64 ++------------- 2 files changed, 16 insertions(+), 129 deletions(-) diff --git a/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherPopover.swift b/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherPopover.swift index f7dd8ac2d..22fb1110a 100644 --- a/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherPopover.swift +++ b/TablePro/Views/DatabaseSwitcher/DatabaseSwitcherPopover.swift @@ -43,13 +43,6 @@ struct DatabaseSwitcherPopover: View { @State private var viewModel: DatabaseSwitcherViewModel @State private var supportsCreateDatabase = false - private enum FocusField { - case search - case list - } - - @FocusState private var focus: FocusField? - private static let popoverWidth: CGFloat = 320 private static let popoverHeight: CGFloat = 360 @@ -99,18 +92,6 @@ struct DatabaseSwitcherPopover: View { .background(refreshShortcut) .task { await viewModel.fetchDatabases() } .task { await refreshCreateSupport() } - .onKeyPress(.return) { - commitSelection() - return .handled - } - .onKeyPress(.upArrow) { - viewModel.moveUp() - return .handled - } - .onKeyPress(.downArrow) { - viewModel.moveDown() - return .handled - } } private var refreshShortcut: some View { @@ -122,61 +103,16 @@ struct DatabaseSwitcherPopover: View { } private var searchField: some View { - HStack(spacing: 5) { - Image(systemName: "magnifyingglass") - .imageScale(.small) - .foregroundStyle(.secondary) - .frame(width: 14) - - TextField( - "", - text: $viewModel.searchText, - prompt: Text(String(localized: "Search databases")) - .foregroundStyle(.tertiary) - ) - .textFieldStyle(.plain) - .font(.body) - .focused($focus, equals: .search) - .onKeyPress(.downArrow) { - viewModel.moveDown() - return .handled - } - .onKeyPress(.upArrow) { - viewModel.moveUp() - return .handled - } - .onKeyPress(.return) { - commitSelection() - return .handled - } - .onKeyPress(.escape) { - if viewModel.searchText.isEmpty { - return .ignored - } - viewModel.searchText = "" - return .handled - } - - if !viewModel.searchText.isEmpty { - Button { - viewModel.searchText = "" - } label: { - Image(systemName: "xmark.circle.fill") - .imageScale(.small) - .foregroundStyle(.tertiary) - } - .buttonStyle(.plain) - } - } - .padding(.horizontal, 6) - .padding(.vertical, 4) - .background( - RoundedRectangle(cornerRadius: 5, style: .continuous) - .fill(Color(nsColor: .quaternaryLabelColor).opacity(0.35)) + NativeSearchField( + text: $viewModel.searchText, + placeholder: String(localized: "Search databases"), + onMoveUp: { viewModel.moveUp() }, + onMoveDown: { viewModel.moveDown() }, + onSubmit: { commitSelection() }, + focusOnAppear: true ) .padding(.horizontal, 8) - .padding(.vertical, 4) - .onAppear { focus = .search } + .padding(.vertical, 6) } @ViewBuilder @@ -203,7 +139,6 @@ struct DatabaseSwitcherPopover: View { } .listStyle(.inset) .scrollContentBackground(.hidden) - .focused($focus, equals: .list) .frame(maxWidth: .infinity, maxHeight: .infinity) .contextMenu(forSelectionType: String.self) { selection in contextMenuItems(for: selection) diff --git a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift index 6c75b2c5f..e99000451 100644 --- a/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift +++ b/TablePro/Views/Toolbar/ConnectionSwitcherPopover.swift @@ -33,7 +33,6 @@ struct ConnectionSwitcherPopover: View { @State private var savedConnections: [DatabaseConnection] = [] @State private var selectedConnectionId: UUID? @State private var searchText = "" - @FocusState private var searchFocused: Bool private static let popoverWidth: CGFloat = 400 private static let popoverHeight: CGFloat = 460 @@ -84,72 +83,25 @@ struct ConnectionSwitcherPopover: View { if selectedConnectionId == nil { selectedConnectionId = currentSessionId ?? orderedIds.first } - searchFocused = true } .onChange(of: searchText) { _, _ in let ids = orderedIds if let id = selectedConnectionId, ids.contains(id) { return } selectedConnectionId = ids.first } - .onKeyPress(.return) { - activateSelected() - return .handled - } } private var searchField: some View { - HStack(spacing: 5) { - Image(systemName: "magnifyingglass") - .imageScale(.small) - .foregroundStyle(.secondary) - .frame(width: 14) - - TextField( - "", - text: $searchText, - prompt: Text(String(localized: "Search connections")) - .foregroundStyle(.tertiary) - ) - .textFieldStyle(.plain) - .font(.body) - .focused($searchFocused) - .onKeyPress(.downArrow) { - moveSelection(by: 1) - return .handled - } - .onKeyPress(.upArrow) { - moveSelection(by: -1) - return .handled - } - .onKeyPress(.return) { - activateSelected() - return .handled - } - .onKeyPress(.escape) { - if searchText.isEmpty { return .ignored } - searchText = "" - return .handled - } - - if !searchText.isEmpty { - Button { - searchText = "" - } label: { - Image(systemName: "xmark.circle.fill") - .imageScale(.small) - .foregroundStyle(.tertiary) - } - .buttonStyle(.plain) - } - } - .padding(.horizontal, 6) - .padding(.vertical, 4) - .background( - RoundedRectangle(cornerRadius: 5, style: .continuous) - .fill(Color(nsColor: .quaternaryLabelColor).opacity(0.35)) + NativeSearchField( + text: $searchText, + placeholder: String(localized: "Search connections"), + onMoveUp: { moveSelection(by: -1) }, + onMoveDown: { moveSelection(by: 1) }, + onSubmit: { activateSelected() }, + focusOnAppear: true ) .padding(.horizontal, 8) - .padding(.vertical, 4) + .padding(.vertical, 6) } @ViewBuilder From 27aa58ed6a35a41286ab3f43b4cedd8b06820ff7 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 22 May 2026 21:42:35 +0700 Subject: [PATCH 2/2] fix(toolbar): give NativeSearchField an intrinsic height so SwiftUI popovers don't stretch it (#1350) --- TablePro/Views/Sidebar/NativeSearchField.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/TablePro/Views/Sidebar/NativeSearchField.swift b/TablePro/Views/Sidebar/NativeSearchField.swift index 41d4ca621..43e862fff 100644 --- a/TablePro/Views/Sidebar/NativeSearchField.swift +++ b/TablePro/Views/Sidebar/NativeSearchField.swift @@ -8,6 +8,13 @@ import AppKit import SwiftUI +private final class IntrinsicHeightSearchField: NSSearchField { + override var intrinsicContentSize: NSSize { + let cellHeight = cell?.cellSize.height ?? super.intrinsicContentSize.height + return NSSize(width: NSView.noIntrinsicMetric, height: cellHeight) + } +} + struct NativeSearchField: NSViewRepresentable { @Binding var text: String var placeholder: String @@ -20,7 +27,7 @@ struct NativeSearchField: NSViewRepresentable { var maxWidth: CGFloat? func makeNSView(context: Context) -> NSSearchField { - let field = NSSearchField() + let field = IntrinsicHeightSearchField() field.placeholderString = placeholder field.delegate = context.coordinator field.controlSize = controlSize @@ -44,6 +51,10 @@ struct NativeSearchField: NSViewRepresentable { if field.stringValue != text { field.stringValue = text } + if field.controlSize != controlSize { + field.controlSize = controlSize + field.invalidateIntrinsicContentSize() + } field.placeholderString = placeholder context.coordinator.onMoveUp = onMoveUp context.coordinator.onMoveDown = onMoveDown