From a764ef6fcb6a7febb18a0ef08e85a25075f4331f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Mon, 25 May 2026 18:27:33 +0700 Subject: [PATCH 1/6] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2c9051166..9c535254a 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,4 @@ fix-1322-plugin-abi-and-registry-overhaul.diff # Issue analysis blueprints (local only) .analysis/ +.docs/ From bc530fd9b063dab48a44295d79403a48a8d1ddb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Mon, 25 May 2026 19:25:09 +0700 Subject: [PATCH 2/6] Update CLAUDE.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 027755123..f7c9d6caa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 From 97c7f1175275837eb0f0032ce67f47c10818b46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Wed, 27 May 2026 20:44:04 +0700 Subject: [PATCH 3/6] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9c535254a..1dd4bc8e4 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,4 @@ fix-1322-plugin-abi-and-registry-overhaul.diff # Issue analysis blueprints (local only) .analysis/ .docs/ +Local.xcconfig From 0adfd15de6bcc9db1b0317b794bd1ece4c836a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Wed, 27 May 2026 21:59:23 +0700 Subject: [PATCH 4/6] fix(inspector): prevent window overflow on inspector pane toggle --- CHANGELOG.md | 1 + .../MainSplitViewController.swift | 115 ++++++++++++++++-- ...ViewControllerWindowMinimumSizeTests.swift | 56 +++++++++ 3 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index b66a87740..418ca07c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index a3ad26b47..b7e297c88 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -40,6 +40,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi private var detailHosting: NSHostingController! private var inspectorHosting: NSHostingController! private var hasMaterializedInspector = false + private var baseWindowContentMinSize: NSSize? // MARK: - Toolbar @@ -186,6 +187,94 @@ 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 } + + if baseWindowContentMinSize == nil { + baseWindowContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size + } + guard let baseWindowContentMinSize else { return } + + let resolvedMinSize = Self.resolvedContentMinSize( + base: baseWindowContentMinSize, + 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 } @@ -209,6 +298,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi } installObservers() + recomputeWindowMinimumSize() } override func viewDidDisappear() { @@ -274,11 +364,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 } @@ -306,10 +392,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() } @@ -467,12 +552,14 @@ 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) UserDefaults.standard.set(false, forKey: Self.inspectorPresentedKey) } @@ -492,9 +579,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 } diff --git a/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift b/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift new file mode 100644 index 000000000..aaf3b0c8d --- /dev/null +++ b/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift @@ -0,0 +1,56 @@ +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) + } +} From 0ef3d3217e69327c76c448add74bd03bf0fef140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Wed, 27 May 2026 22:13:39 +0700 Subject: [PATCH 5/6] fix(inspector): add resize call site, refresh base size, fix animation race, add test --- .../Infrastructure/MainSplitViewController.swift | 14 ++++++++------ ...litViewControllerWindowMinimumSizeTests.swift | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index b7e297c88..f77bad889 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -40,7 +40,6 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi private var detailHosting: NSHostingController! private var inspectorHosting: NSHostingController! private var hasMaterializedInspector = false - private var baseWindowContentMinSize: NSSize? // MARK: - Toolbar @@ -212,10 +211,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi ) { guard let window = view.window else { return } - if baseWindowContentMinSize == nil { - baseWindowContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size - } - guard let baseWindowContentMinSize else { return } + let baseWindowContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size let resolvedMinSize = Self.resolvedContentMinSize( base: baseWindowContentMinSize, @@ -306,6 +302,10 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi removeObservers() } + override func splitViewDidResizeSubviews(_ notification: Notification) { + recomputeWindowMinimumSize() + } + // MARK: - Observers private func installObservers() { @@ -559,7 +559,9 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi } func hideInspector() { - setCollapsed(true, for: inspectorSplitItem) + setCollapsed(true, for: inspectorSplitItem) { [weak self] in + self?.recomputeWindowMinimumSize(inspectorCollapsed: true) + } UserDefaults.standard.set(false, forKey: Self.inspectorPresentedKey) } diff --git a/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift b/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift index aaf3b0c8d..fb3ab22b3 100644 --- a/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift +++ b/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift @@ -53,4 +53,20 @@ struct MainSplitViewControllerWindowMinimumSizeTests { #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) + } } From 33c1b8e2e3ad1ee26082227b04dda7b9f889e4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Thu, 28 May 2026 20:48:49 +0700 Subject: [PATCH 6/6] fix(inspector): store original content min size to prevent floor drift --- .../MainSplitViewController.swift | 9 ++++-- ...ViewControllerWindowMinimumSizeTests.swift | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift index f77bad889..4dcbf5583 100644 --- a/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift +++ b/TablePro/Core/Services/Infrastructure/MainSplitViewController.swift @@ -40,6 +40,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi private var detailHosting: NSHostingController! private var inspectorHosting: NSHostingController! private var hasMaterializedInspector = false + private var originalContentMinSize: CGSize = .zero // MARK: - Toolbar @@ -211,10 +212,8 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi ) { guard let window = view.window else { return } - let baseWindowContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size - let resolvedMinSize = Self.resolvedContentMinSize( - base: baseWindowContentMinSize, + base: originalContentMinSize, panes: [ PaneMinimum( minimumThickness: sidebarSplitItem?.minimumThickness ?? .zero, @@ -286,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), diff --git a/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift b/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift index fb3ab22b3..a3fe45e47 100644 --- a/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift +++ b/TableProTests/Core/Services/Infrastructure/MainSplitViewControllerWindowMinimumSizeTests.swift @@ -69,4 +69,34 @@ struct MainSplitViewControllerWindowMinimumSizeTests { #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) + } }