Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use semantic selected-text color instead of hardcoded white in selected rows
- Use proper CommandGroup for full-screen shortcut instead of event monitor
- Use sheet presentation for all file open/save panels instead of free-floating dialogs
- Replace event monitor with native SwiftUI .onKeyPress() in connection switcher
- Extract reusable SearchFieldView component from 4 custom search field implementations
- Replace custom resize handle with native NSSplitView for inspector panel

### Added

Expand Down
36 changes: 15 additions & 21 deletions TablePro/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,13 @@ struct ContentView: View {
} detail: {
// MARK: - Detail (Main workspace with optional right sidebar)
if let currentSession = currentSession, let rightPanelState, let sessionState {
HStack(spacing: 0) {
HorizontalSplitView(
isTrailingCollapsed: !rightPanelState.isPresented,
trailingWidth: Bindable(rightPanelState).panelWidth,
minTrailingWidth: RightPanelState.minWidth,
maxTrailingWidth: RightPanelState.maxWidth,
autosaveName: "InspectorSplit"
) {
MainContentView(
connection: currentSession.connection,
payload: payload,
Expand All @@ -244,23 +250,15 @@ struct ContentView: View {
toolbarState: sessionState.toolbarState,
coordinator: sessionState.coordinator
)
.frame(maxWidth: .infinity)

if rightPanelState.isPresented {
PanelResizeHandle(panelWidth: Bindable(rightPanelState).panelWidth)
Divider()
UnifiedRightPanelView(
state: rightPanelState,
inspectorContext: inspectorContext,
connection: currentSession.connection,
tables: currentSession.tables
)
.frame(width: rightPanelState.panelWidth)
.background(Color(nsColor: .windowBackgroundColor))
.transition(.move(edge: .trailing))
}
} trailing: {
UnifiedRightPanelView(
state: rightPanelState,
inspectorContext: inspectorContext,
connection: currentSession.connection,
tables: currentSession.tables
)
.background(Color(nsColor: .windowBackgroundColor))
}
.animation(.easeInOut(duration: 0.2), value: rightPanelState.isPresented)
} else {
VStack(spacing: 16) {
ProgressView()
Expand Down Expand Up @@ -431,10 +429,6 @@ struct ContentView: View {
}
}

private func saveCurrentSessionState() {
// State is automatically saved through bindings
}

// MARK: - Persistence

private func deleteConnection(_ connection: DatabaseConnection) {
Expand Down
143 changes: 143 additions & 0 deletions TablePro/Views/Components/HorizontalSplitView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//
// HorizontalSplitView.swift
// TablePro
//

import AppKit
import SwiftUI

struct HorizontalSplitView<Leading: View, Trailing: View>: NSViewRepresentable {
var isTrailingCollapsed: Bool
@Binding var trailingWidth: CGFloat
var minTrailingWidth: CGFloat
var maxTrailingWidth: CGFloat
var autosaveName: String
@ViewBuilder var leading: Leading
@ViewBuilder var trailing: Trailing

func makeCoordinator() -> Coordinator {
Coordinator(trailingWidth: $trailingWidth)
}

func makeNSView(context: Context) -> NSSplitView {
let splitView = NSSplitView()
splitView.isVertical = true
splitView.dividerStyle = .thin
splitView.autosaveName = autosaveName
splitView.delegate = context.coordinator

let leadingHosting = NSHostingView(rootView: leading)
leadingHosting.sizingOptions = [.minSize]

let trailingHosting = NSHostingView(rootView: trailing)
trailingHosting.sizingOptions = [.minSize]

splitView.addArrangedSubview(leadingHosting)
splitView.addArrangedSubview(trailingHosting)

context.coordinator.leadingHosting = leadingHosting
context.coordinator.trailingHosting = trailingHosting
context.coordinator.lastCollapsedState = isTrailingCollapsed
context.coordinator.minWidth = minTrailingWidth
context.coordinator.maxWidth = maxTrailingWidth

if isTrailingCollapsed {
trailingHosting.isHidden = true
}

return splitView
}

func updateNSView(_ splitView: NSSplitView, context: Context) {
context.coordinator.leadingHosting?.rootView = leading
context.coordinator.trailingHosting?.rootView = trailing
context.coordinator.trailingWidth = $trailingWidth
context.coordinator.minWidth = minTrailingWidth
context.coordinator.maxWidth = maxTrailingWidth

guard let trailingView = context.coordinator.trailingHosting else { return }
let wasCollapsed = context.coordinator.lastCollapsedState

if isTrailingCollapsed != wasCollapsed {
context.coordinator.lastCollapsedState = isTrailingCollapsed
if isTrailingCollapsed {
if splitView.subviews.count >= 2 {
context.coordinator.savedDividerPosition = splitView.subviews[1].frame.width
}
splitView.setPosition(splitView.bounds.width, ofDividerAt: 0)
trailingView.isHidden = true
} else {
trailingView.isHidden = false
let targetWidth = context.coordinator.savedDividerPosition ?? trailingWidth
splitView.adjustSubviews()
splitView.setPosition(splitView.bounds.width - targetWidth, ofDividerAt: 0)
}
}
}

final class Coordinator: NSObject, NSSplitViewDelegate {
var leadingHosting: NSHostingView<Leading>?
var trailingHosting: NSHostingView<Trailing>?
var lastCollapsedState = false
var savedDividerPosition: CGFloat?
var minWidth: CGFloat = 0
var maxWidth: CGFloat = 0
var trailingWidth: Binding<CGFloat>

init(trailingWidth: Binding<CGFloat>) {
self.trailingWidth = trailingWidth
}

func splitView(
_ splitView: NSSplitView,
constrainMinCoordinate proposedMinimumPosition: CGFloat,
ofSubviewAt dividerIndex: Int
) -> CGFloat {
splitView.bounds.width - maxWidth
}

func splitView(
_ splitView: NSSplitView,
constrainMaxCoordinate proposedMaximumPosition: CGFloat,
ofSubviewAt dividerIndex: Int
) -> CGFloat {
splitView.bounds.width - minWidth
}

func splitView(
_ splitView: NSSplitView,
canCollapseSubview subview: NSView
) -> Bool {
subview == trailingHosting
}

func splitView(
_ splitView: NSSplitView,
effectiveRect proposedEffectiveRect: NSRect,
forDrawnRect drawnRect: NSRect,
ofDividerAt dividerIndex: Int
) -> NSRect {
if trailingHosting?.isHidden == true {
return .zero
}
return proposedEffectiveRect
}

func splitView(
_ splitView: NSSplitView,
shouldHideDividerAt dividerIndex: Int
) -> Bool {
trailingHosting?.isHidden == true
}

func splitViewDidResizeSubviews(_ notification: Notification) {
guard let splitView = notification.object as? NSSplitView,
splitView.subviews.count >= 2,
trailingHosting?.isHidden != true
else { return }
let width = splitView.subviews[1].frame.width
guard width > 0, abs(width - trailingWidth.wrappedValue) > 0.5 else { return }
trailingWidth.wrappedValue = width
}
}
}
40 changes: 0 additions & 40 deletions TablePro/Views/Components/PanelResizeHandle.swift

This file was deleted.

37 changes: 37 additions & 0 deletions TablePro/Views/Components/SearchFieldView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// SearchFieldView.swift
// TablePro
//

import SwiftUI

struct SearchFieldView: View {
let placeholder: String
@Binding var text: String
var fontSize: CGFloat?

var body: some View {
let resolvedSize = fontSize ?? ThemeEngine.shared.activeTheme.typography.body
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.font(.system(size: resolvedSize))
.foregroundStyle(.tertiary)

TextField(placeholder, text: $text)
.textFieldStyle(.plain)
.font(.system(size: resolvedSize))

if !text.isEmpty {
Button { text = "" } label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(Color(nsColor: .controlBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
28 changes: 5 additions & 23 deletions TablePro/Views/DatabaseSwitcher/DatabaseSwitcherSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,31 +155,13 @@ struct DatabaseSwitcherSheet: View {
private var toolbar: some View {
HStack(spacing: 8) {
// Search
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.font(.system(size: ThemeEngine.shared.activeTheme.typography.body))
.foregroundStyle(.tertiary)

TextField(isSchemaMode
SearchFieldView(
placeholder: isSchemaMode
? String(localized: "Search schemas...")
: String(localized: "Search databases..."),
text: $viewModel.searchText)
.textFieldStyle(.plain)
.font(.system(size: ThemeEngine.shared.activeTheme.typography.body))
.focused($focus, equals: .search)

if !viewModel.searchText.isEmpty {
Button(action: { viewModel.searchText = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(Color(nsColor: .controlBackgroundColor))
.cornerRadius(6)
text: $viewModel.searchText
)
.focused($focus, equals: .search)

// Refresh
Button(action: {
Expand Down
27 changes: 5 additions & 22 deletions TablePro/Views/QuickSwitcher/QuickSwitcherView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,28 +95,11 @@ internal struct QuickSwitcherSheet: View {
// MARK: - Search Toolbar

private var searchToolbar: some View {
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.font(.system(size: ThemeEngine.shared.activeTheme.typography.body))
.foregroundStyle(.tertiary)

TextField("Search tables, views, databases...", text: $viewModel.searchText)
.textFieldStyle(.plain)
.font(.system(size: ThemeEngine.shared.activeTheme.typography.body))
.focused($focus, equals: .search)

if !viewModel.searchText.isEmpty {
Button(action: { viewModel.searchText = "" }) {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(Color(nsColor: .controlBackgroundColor))
.cornerRadius(6)
SearchFieldView(
placeholder: "Search tables, views, databases...",
text: $viewModel.searchText
)
.focused($focus, equals: .search)
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
Expand Down
24 changes: 5 additions & 19 deletions TablePro/Views/RightSidebar/RightSidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,26 +146,12 @@ struct RightSidebarView: View {

return VStack(spacing: 0) {
// Inline search field
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.tertiary)
.font(.system(size: ThemeEngine.shared.activeTheme.typography.small))
TextField("Search for field...", text: $searchText)
.textFieldStyle(.plain)
.font(.system(size: ThemeEngine.shared.activeTheme.typography.small))
if !searchText.isEmpty {
Button {
searchText = ""
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.tertiary)
.font(.system(size: ThemeEngine.shared.activeTheme.typography.small))
}
.buttonStyle(.plain)
}
}
SearchFieldView(
placeholder: "Search for field...",
text: $searchText,
fontSize: ThemeEngine.shared.activeTheme.typography.small
)
.padding(.horizontal, 10)
.padding(.vertical, 6)

Divider()

Expand Down
Loading
Loading