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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor.
- Pagination buttons no longer fire their page shortcut twice.
- Running a PostgreSQL script with a `DO $$ ... $$` block or a dollar-quoted function body no longer fails with an unterminated dollar-quoted string error. (#1559)
- 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.
- AWS IAM connections no longer ask for a password on connect or reconnect. IAM supplies the credentials, so the prompt was never needed. The same now holds for any auth mode that replaces the password, such as a Postgres password file.
- Oracle connection failures show the listener's actual reason (such as an unknown service name) instead of a generic "server closed the connection" message. (#483)

Expand Down
1 change: 1 addition & 0 deletions Libs/dylibs
162 changes: 101 additions & 61 deletions TablePro/Core/Services/Infrastructure/MainSplitViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi

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

private func materializeInspectorIfNeeded() {
Expand All @@ -236,6 +236,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: NSSize(width: Self.baseContentMinWidth, height: Self.baseContentMinHeight),
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 @@ -257,7 +340,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
}

installObservers()
recomputeWindowMinSize()
recomputeWindowMinimumSize()
window.recalculateKeyViewLoop()
}

Expand Down Expand Up @@ -324,11 +407,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 @@ -356,10 +435,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 @@ -526,15 +604,17 @@ 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)
recomputeWindowMinSize()
}

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

@objc override func toggleInspector(_ sender: Any?) {
Expand All @@ -560,59 +640,19 @@ 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
}
}

// MARK: - Dynamic Window Minimum Size

private static let baseWindowMinWidth: CGFloat = 720
private static let baseWindowMinHeight: CGFloat = 480

private func recomputeWindowMinSize() {
guard let window = view.window else { return }
let sidebarVisible = !(sidebarSplitItem?.isCollapsed ?? true)
let inspectorVisible = !(inspectorSplitItem?.isCollapsed ?? true)

let detailMin: CGFloat = detailSplitItem?.minimumThickness ?? 400
let sidebarMin: CGFloat = sidebarSplitItem?.minimumThickness ?? 280
let inspectorMin: CGFloat = inspectorSplitItem?.minimumThickness ?? 270
let dividerThickness = splitView.dividerThickness

var width: CGFloat = detailMin
if sidebarVisible {
width += sidebarMin + dividerThickness
}
if inspectorVisible {
width += inspectorMin + dividerThickness
}

let resolvedWidth = max(Self.baseWindowMinWidth, width)
let newMinSize = NSSize(width: resolvedWidth, height: Self.baseWindowMinHeight)

guard window.minSize != newMinSize else { return }
window.minSize = newMinSize

var frame = window.frame
var resized = false
if frame.size.width < resolvedWidth {
frame.size.width = resolvedWidth
resized = true
}
if frame.size.height < Self.baseWindowMinHeight {
frame.size.height = Self.baseWindowMinHeight
resized = true
}
if resized {
window.setFrame(frame, display: true, animate: false)
}
}

// MARK: - Constants

internal static let baseContentMinWidth: CGFloat = 720
internal static let baseContentMinHeight: CGFloat = 480
private static let inspectorPresentedKey = "com.TablePro.rightPanel.isPresented"
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
defer: false
)
window.identifier = NSUserInterfaceItemIdentifier("main")
window.minSize = NSSize(width: 720, height: 480)
window.isRestorable = AppSettingsStorage.shared.loadGeneral().startupBehavior == .reopenLast
window.restorationClass = TabWindowRestoration.self
window.toolbarStyle = .unified
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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)
}

@Test("Applies the controller base constants as the runtime content floor")
func usesControllerBaseConstantsAsFloor() {
#expect(MainSplitViewController.baseContentMinWidth == 720)
#expect(MainSplitViewController.baseContentMinHeight == 480)

let base = NSSize(
width: MainSplitViewController.baseContentMinWidth,
height: MainSplitViewController.baseContentMinHeight
)

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

#expect(sidebarAndDetail.width == MainSplitViewController.baseContentMinWidth)
#expect(sidebarAndDetail.height == MainSplitViewController.baseContentMinHeight)

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

#expect(allPanesVisible.width == 954)
#expect(allPanesVisible.height == MainSplitViewController.baseContentMinHeight)
}
}
Loading