Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,5 @@ fix-1322-plugin-abi-and-registry-overhaul.diff

# Issue analysis blueprints (local only)
.analysis/
.docs/
Local.xcconfig
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The Generate Token sheet focuses the Token Name field on first open. (#1093)
- Double-clicking a CSV or TSV file when TablePro is closed opens the file directly. (#1443)
- Opening a `.sql` file names the tab after the file instead of showing "SQL Query". (#1220)
- Toggling the right inspector in a narrow editor window now updates the window minimum width from the visible split panes, so the inspector no longer squeezes content or overflows.

## [0.45.0] - 2026-05-26

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ Missing a case produces a wrong "{Language} Query" title on the first frame.
| Tab state | JSON persistence | `TabPersistenceService` / `TabStateStorage` |
| Filter presets | UserDefaults | `FilterSettingsStorage` |
| Per-table filters | UserDefaults | `FilterSettingsStorage` (saves `appliedFilters` only) |
| Favorite tables | UserDefaults | `FavoriteTablesStorage` (global, by table name) |

### Logging & Debugging

Expand Down
120 changes: 107 additions & 13 deletions TablePro/Core/Services/Infrastructure/MainSplitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
private var detailHosting: NSHostingController<AnyView>!
private var inspectorHosting: NSHostingController<AnyView>!
private var hasMaterializedInspector = false
private var originalContentMinSize: CGSize = .zero

// MARK: - Toolbar

Expand Down Expand Up @@ -186,6 +187,89 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
inspectorHosting.rootView = AnyView(buildInspectorView())
}

internal struct PaneMinimum {
internal let minimumThickness: CGFloat
internal let isCollapsed: Bool
}

internal static func resolvedContentMinSize(
base: NSSize,
panes: [PaneMinimum],
dividerThickness: CGFloat
) -> NSSize {
let visiblePanes = panes.filter { !$0.isCollapsed }
let paneWidth = visiblePanes.reduce(CGFloat.zero) { partialResult, pane in
partialResult + max(CGFloat.zero, pane.minimumThickness)
}
let dividerCount = max(visiblePanes.count - 1, 0)
let resolvedWidth = max(base.width, paneWidth + (CGFloat(dividerCount) * dividerThickness))
return NSSize(width: resolvedWidth, height: base.height)
}

private func recomputeWindowMinimumSize(
sidebarCollapsed: Bool? = nil,
inspectorCollapsed: Bool? = nil
) {
guard let window = view.window else { return }

let resolvedMinSize = Self.resolvedContentMinSize(
base: originalContentMinSize,
panes: [
PaneMinimum(
minimumThickness: sidebarSplitItem?.minimumThickness ?? .zero,
isCollapsed: sidebarCollapsed ?? (sidebarSplitItem?.isCollapsed ?? true)
),
PaneMinimum(
minimumThickness: detailSplitItem?.minimumThickness ?? .zero,
isCollapsed: detailSplitItem?.isCollapsed ?? false
),
PaneMinimum(
minimumThickness: inspectorSplitItem?.minimumThickness ?? .zero,
isCollapsed: inspectorCollapsed ?? (inspectorSplitItem?.isCollapsed ?? true)
)
],
dividerThickness: splitView.dividerThickness
)

if window.contentMinSize != resolvedMinSize {
window.contentMinSize = resolvedMinSize
}

let currentContentSize = window.contentRect(forFrameRect: window.frame).size
guard currentContentSize.width < resolvedMinSize.width || currentContentSize.height < resolvedMinSize.height else { return }
window.setContentSize(NSSize(
width: max(currentContentSize.width, resolvedMinSize.width),
height: max(currentContentSize.height, resolvedMinSize.height)
))
}

private func setCollapsed(
_ isCollapsed: Bool,
for splitItem: NSSplitViewItem?,
prepareWindowMinimumSize: (() -> Void)? = nil
) {
guard let splitItem else { return }

if splitItem.isCollapsed == isCollapsed {
recomputeWindowMinimumSize()
return
}

prepareWindowMinimumSize?()

guard view.window?.isVisible == true else {
splitItem.isCollapsed = isCollapsed
recomputeWindowMinimumSize()
return
}

NSAnimationContext.runAnimationGroup { _ in
splitItem.animator().isCollapsed = isCollapsed
} completionHandler: { [weak self] in
self?.recomputeWindowMinimumSize()
}
}

override func viewWillAppear() {
super.viewWillAppear()
guard let window = view.window else { return }
Expand All @@ -201,6 +285,10 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
installToolbar(coordinator: sessionState.coordinator)
}

if originalContentMinSize == .zero {
originalContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size
}

if let currentSession, let coordinator = sessionState?.coordinator {
sidebarContainer.updateSidebarState(
SharedSidebarState.forConnection(currentSession.connection.id),
Expand All @@ -209,13 +297,18 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
}

installObservers()
recomputeWindowMinimumSize()
}

override func viewDidDisappear() {
super.viewDidDisappear()
removeObservers()
}

override func splitViewDidResizeSubviews(_ notification: Notification) {
recomputeWindowMinimumSize()
}

// MARK: - Observers

private func installObservers() {
Expand Down Expand Up @@ -274,11 +367,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
sessionState = nil
currentSession = nil
sidebarContainer.updateSidebarState(nil, windowState: nil)
if view.window?.isVisible == true {
sidebarSplitItem.animator().isCollapsed = true
} else {
sidebarSplitItem.isCollapsed = true
}
setCollapsed(true, for: sidebarSplitItem)
}
return
}
Expand Down Expand Up @@ -306,10 +395,9 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
}

let collapseSidebar = newSession.driver == nil
if view.window?.isVisible == true {
sidebarSplitItem.animator().isCollapsed = collapseSidebar
} else {
sidebarSplitItem.isCollapsed = collapseSidebar
setCollapsed(collapseSidebar, for: sidebarSplitItem) { [weak self] in
guard !collapseSidebar else { return }
self?.recomputeWindowMinimumSize(sidebarCollapsed: false)
}
rebuildPanes()
}
Expand Down Expand Up @@ -467,12 +555,16 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

func showInspector() {
materializeInspectorIfNeeded()
inspectorSplitItem?.animator().isCollapsed = false
setCollapsed(false, for: inspectorSplitItem) { [weak self] in
self?.recomputeWindowMinimumSize(inspectorCollapsed: false)
}
UserDefaults.standard.set(true, forKey: Self.inspectorPresentedKey)
}

func hideInspector() {
inspectorSplitItem?.animator().isCollapsed = true
setCollapsed(true, for: inspectorSplitItem) { [weak self] in
self?.recomputeWindowMinimumSize(inspectorCollapsed: true)
}
UserDefaults.standard.set(false, forKey: Self.inspectorPresentedKey)
}

Expand All @@ -492,9 +584,11 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

if sidebarSplitItem?.isCollapsed == true {
sidebarState.selectedSidebarTab = tab
sidebarSplitItem?.animator().isCollapsed = false
setCollapsed(false, for: sidebarSplitItem) { [weak self] in
self?.recomputeWindowMinimumSize(sidebarCollapsed: false)
}
} else if sidebarState.selectedSidebarTab == tab {
sidebarSplitItem?.animator().isCollapsed = true
setCollapsed(true, for: sidebarSplitItem)
} else {
sidebarState.selectedSidebarTab = tab
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import AppKit
import Testing

@testable import TablePro

@Suite("MainSplitViewController window minimum size")
@MainActor
struct MainSplitViewControllerWindowMinimumSizeTests {
@Test("Uses all visible pane minimums when the inspector is shown")
func includesVisibleInspectorPane() {
let size = MainSplitViewController.resolvedContentMinSize(
base: NSSize(width: 720, height: 448),
panes: [
.init(minimumThickness: 280, isCollapsed: false),
.init(minimumThickness: 400, isCollapsed: false),
.init(minimumThickness: 270, isCollapsed: false)
],
dividerThickness: 2
)

#expect(size.width == 954)
#expect(size.height == 448)
}

@Test("Keeps the base width floor when the inspector is hidden")
func keepsBaseWidthWhenInspectorHidden() {
let size = MainSplitViewController.resolvedContentMinSize(
base: NSSize(width: 720, height: 448),
panes: [
.init(minimumThickness: 280, isCollapsed: false),
.init(minimumThickness: 400, isCollapsed: false),
.init(minimumThickness: 270, isCollapsed: true)
],
dividerThickness: 2
)

#expect(size.width == 720)
#expect(size.height == 448)
}

@Test("Relaxes to the base width when only detail and inspector remain")
func keepsBaseWidthWithSidebarCollapsed() {
let size = MainSplitViewController.resolvedContentMinSize(
base: NSSize(width: 720, height: 448),
panes: [
.init(minimumThickness: 280, isCollapsed: true),
.init(minimumThickness: 400, isCollapsed: false),
.init(minimumThickness: 270, isCollapsed: false)
],
dividerThickness: 2
)

#expect(size.width == 720)
#expect(size.height == 448)
}

@Test("Uses the pane sum when detail and inspector exceed the base floor")
func usesPaneSumWhenItExceedsBaseWithSidebarCollapsed() {
let size = MainSplitViewController.resolvedContentMinSize(
base: NSSize(width: 720, height: 448),
panes: [
.init(minimumThickness: 280, isCollapsed: true),
.init(minimumThickness: 400, isCollapsed: false),
.init(minimumThickness: 400, isCollapsed: false)
],
dividerThickness: 2
)

#expect(size.width == 802)
#expect(size.height == 448)
}

@Test("Returns to the original base width after showing then hiding the inspector")
func relaxesBackToOriginalBaseAfterInspectorCycle() {
let originalBase = NSSize(width: 720, height: 448)

let shownSize = MainSplitViewController.resolvedContentMinSize(
base: originalBase,
panes: [
.init(minimumThickness: 280, isCollapsed: false),
.init(minimumThickness: 400, isCollapsed: false),
.init(minimumThickness: 270, isCollapsed: false)
],
dividerThickness: 2
)

#expect(shownSize.width == 954)

let hiddenSize = MainSplitViewController.resolvedContentMinSize(
base: originalBase,
panes: [
.init(minimumThickness: 280, isCollapsed: false),
.init(minimumThickness: 400, isCollapsed: false),
.init(minimumThickness: 270, isCollapsed: true)
],
dividerThickness: 2
)

#expect(hiddenSize.width == 720)
#expect(hiddenSize.height == 448)
}
}
Loading