Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New tab via Cmd+T no longer flashes focus back to the previous tab in the same window group
- Cmd+X with no selection cuts the current line, matching VS Code, Sublime, and Xcode (#1075)
- Cmd+A on a query ending with a newline now highlights every line, not just the first (#1075)
- Editor windows now restore their last size, position, and full screen state on launch instead of opening at a default 1200x800 every time
- First launch picks an initial editor size based on the connected display (about 85% of the visible area) instead of a fixed 1200x800

### Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,6 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
super.viewWillAppear()
guard let window = view.window else { return }

let defaultSize = NSSize(width: 1_200, height: 800)
if window.frame.width < defaultSize.width || window.frame.height < defaultSize.height {
window.setContentSize(NSSize(
width: max(window.frame.width, defaultSize.width),
height: max(window.frame.height, defaultSize.height)
))
window.center()
}

window.title = windowTitle
if let session = currentSession {
window.subtitle = session.connection.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
window.identifier = NSUserInterfaceItemIdentifier("main")
window.minSize = NSSize(width: 720, height: 480)
window.isRestorable = false
window.applyAutosaveName("MainEditorWindow")
WindowStateController.shared.install(on: window, policy: .editor)
window.toolbarStyle = .unified
// Hide the window title ("Query 1 / TablePro") embedded in the unified
// toolbar — otherwise it claims leading space and pushes our navigation
Expand Down
76 changes: 76 additions & 0 deletions TablePro/Core/Services/Infrastructure/WindowFramePolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// WindowFramePolicy.swift
// TablePro
//

import AppKit
import Foundation

internal struct WindowFramePolicy: Sendable {
let autosaveName: NSWindow.FrameAutosaveName
let fullScreenStateKey: String
let firstRunSizing: FirstRunSizing
}

internal extension WindowFramePolicy {
enum FirstRunSizing: Sendable {
case preserveContentSize
case fractionOfMainScreen(fraction: CGSize, minimum: NSSize, maximum: NSSize?)

func contentSize(for screenFrame: NSRect) -> NSSize? {
switch self {
case .preserveContentSize:
return nil
case let .fractionOfMainScreen(fraction, minimum, maximum):
let proposedWidth = screenFrame.width * fraction.width
let proposedHeight = screenFrame.height * fraction.height

let minClampedWidth = max(proposedWidth, minimum.width)
let minClampedHeight = max(proposedHeight, minimum.height)

let maxWidth = min(maximum?.width ?? .greatestFiniteMagnitude, screenFrame.width)
let maxHeight = min(maximum?.height ?? .greatestFiniteMagnitude, screenFrame.height)

let width = min(minClampedWidth, maxWidth)
let height = min(minClampedHeight, maxHeight)
return NSSize(width: width.rounded(), height: height.rounded())
}
}
}

static let editor = WindowFramePolicy(
autosaveName: "MainEditorWindow",
fullScreenStateKey: "com.TablePro.windowState.editor.isFullScreen",
firstRunSizing: .fractionOfMainScreen(
fraction: CGSize(width: 0.85, height: 0.85),
minimum: NSSize(width: 1_200, height: 800),
maximum: nil
)
)

static let jsonViewer = WindowFramePolicy(
autosaveName: "JSONViewerWindow",
fullScreenStateKey: "com.TablePro.windowState.jsonViewer.isFullScreen",
firstRunSizing: .fractionOfMainScreen(
fraction: CGSize(width: 0.45, height: 0.55),
minimum: NSSize(width: 640, height: 500),
maximum: NSSize(width: 1_100, height: 900)
)
)

static let integrationsActivity = WindowFramePolicy(
autosaveName: "IntegrationsActivityWindow",
fullScreenStateKey: "com.TablePro.windowState.integrationsActivity.isFullScreen",
firstRunSizing: .fractionOfMainScreen(
fraction: CGSize(width: 0.55, height: 0.65),
minimum: NSSize(width: 960, height: 600),
maximum: NSSize(width: 1_400, height: 1_000)
)
)

static let feedback = WindowFramePolicy(
autosaveName: "FeedbackWindow",
fullScreenStateKey: "com.TablePro.windowState.feedback.isFullScreen",
firstRunSizing: .preserveContentSize
)
}
4 changes: 3 additions & 1 deletion TablePro/Core/Services/Infrastructure/WindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ internal final class WindowManager {
"[open] WindowManager joined existing tab group payloadId=\(payload.id, privacy: .public) tabbingId=\(tabbingId, privacy: .public)"
)
} else {
window.center()
if !WindowStateController.shared.hasPriorState(for: .editor) {
window.center()
}
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
Self.lifecycleLogger.info(
Expand Down
167 changes: 167 additions & 0 deletions TablePro/Core/Services/Infrastructure/WindowStateController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//
// WindowStateController.swift
// TablePro
//

import AppKit
import Foundation
import os

@MainActor
internal final class WindowStateController {
static let shared = WindowStateController()

private static let logger = Logger(subsystem: "com.TablePro", category: "WindowState")

private let defaults: UserDefaults
private var bindings: [ObjectIdentifier: WindowStateBinding] = [:]

private init(defaults: UserDefaults = .standard) {
self.defaults = defaults
}

private func applyFirstRunFrame(to window: NSWindow, policy: WindowFramePolicy) {
let screenFrame = (window.screen ?? NSScreen.main)?.visibleFrame
?? NSRect(x: 0, y: 0, width: 1_440, height: 900)
if let size = policy.firstRunSizing.contentSize(for: screenFrame) {
window.setContentSize(size)
}
window.center()
}

fileprivate func releaseBinding(forWindowKey key: ObjectIdentifier) {
bindings.removeValue(forKey: key)
}
}

internal extension WindowStateController {
func install(on window: NSWindow, policy: WindowFramePolicy) {
window.setFrameAutosaveName(policy.autosaveName)

if !window.setFrameUsingName(policy.autosaveName) {
applyFirstRunFrame(to: window, policy: policy)
}

let key = ObjectIdentifier(window)
bindings[key]?.invalidate()

let restorePending = defaults.bool(forKey: policy.fullScreenStateKey)
bindings[key] = WindowStateBinding(
windowKey: key,
window: window,
policy: policy,
defaults: defaults,
restoreFullScreenOnFirstKey: restorePending,
owner: self
)
}

func hasPriorState(for policy: WindowFramePolicy) -> Bool {
let frameKey = "NSWindow Frame \(policy.autosaveName)"
let hasSavedFrame = defaults.object(forKey: frameKey) != nil
let wasInFullScreen = defaults.bool(forKey: policy.fullScreenStateKey)
return hasSavedFrame || wasInFullScreen
}
}

@MainActor
private final class WindowStateBinding {
private let windowKey: ObjectIdentifier
private weak var window: NSWindow?
private let policy: WindowFramePolicy
private let defaults: UserDefaults
private weak var owner: WindowStateController?

private var liveObservers: [NSObjectProtocol] = []
private var fullScreenRestoreObserver: NSObjectProtocol?

init(
windowKey: ObjectIdentifier,
window: NSWindow,
policy: WindowFramePolicy,
defaults: UserDefaults,
restoreFullScreenOnFirstKey: Bool,
owner: WindowStateController
) {
self.windowKey = windowKey
self.window = window
self.policy = policy
self.defaults = defaults
self.owner = owner

attachLiveObservers()
if restoreFullScreenOnFirstKey {
attachFullScreenRestoreObserver()
}
}

func invalidate() {
let center = NotificationCenter.default
for observer in liveObservers {
center.removeObserver(observer)
}
liveObservers.removeAll()
if let fullScreenRestoreObserver {
center.removeObserver(fullScreenRestoreObserver)
self.fullScreenRestoreObserver = nil
}
}

private func attachLiveObservers() {
guard let window else { return }
let center = NotificationCenter.default

liveObservers.append(center.addObserver(
forName: NSWindow.willEnterFullScreenNotification,
object: window,
queue: .main
) { [defaults, policy] _ in
defaults.set(true, forKey: policy.fullScreenStateKey)
})

liveObservers.append(center.addObserver(
forName: NSWindow.didExitFullScreenNotification,
object: window,
queue: .main
) { [defaults, policy] _ in
defaults.set(false, forKey: policy.fullScreenStateKey)
})

liveObservers.append(center.addObserver(
forName: NSWindow.willCloseNotification,
object: window,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated {
guard let self else { return }
self.invalidate()
self.owner?.releaseBinding(forWindowKey: self.windowKey)
}
})
}

private func attachFullScreenRestoreObserver() {
guard let window else { return }
let center = NotificationCenter.default

fullScreenRestoreObserver = center.addObserver(
forName: NSWindow.didBecomeKeyNotification,
object: window,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated {
self?.performFullScreenRestore()
}
}
}

private func performFullScreenRestore() {
guard let window else { return }
if let observer = fullScreenRestoreObserver {
NotificationCenter.default.removeObserver(observer)
fullScreenRestoreObserver = nil
}
guard !window.styleMask.contains(.fullScreen) else { return }
window.toggleFullScreen(nil)
}
}
15 changes: 0 additions & 15 deletions TablePro/Extensions/NSWindow+FrameAutosave.swift

This file was deleted.

1 change: 1 addition & 0 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ struct TableProApp: App {

Window("Integrations Activity", id: SceneId.integrationsActivity) {
IntegrationsActivityView()
.background(WindowChromeConfigurator(statePolicy: .integrationsActivity))
}
.windowResizability(.contentMinSize)
.defaultSize(width: 960, height: 600)
Expand Down
3 changes: 1 addition & 2 deletions TablePro/Views/Feedback/FeedbackWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import SwiftUI
@MainActor
final class FeedbackWindowController {
static let shared = FeedbackWindowController()
private static let autosaveName: NSWindow.FrameAutosaveName = "FeedbackWindow"
private var panel: NSPanel?
private var closeObserver: NSObjectProtocol?
private let viewModel = FeedbackViewModel()
Expand Down Expand Up @@ -43,7 +42,7 @@ final class FeedbackWindowController {
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
panel.standardWindowButton(.zoomButton)?.isHidden = true
panel.contentView = hostingView
panel.applyAutosaveName(Self.autosaveName)
WindowStateController.shared.install(on: panel, policy: .feedback)
panel.makeKeyAndOrderFront(nil)
self.panel = panel

Expand Down
5 changes: 5 additions & 0 deletions TablePro/Views/Infrastructure/WindowChromeConfigurator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal struct WindowChromeConfigurator: NSViewRepresentable {
var fullScreenable: Bool = true
var hideMiniaturizeButton: Bool = false
var hideZoomButton: Bool = false
var statePolicy: WindowFramePolicy?

func makeNSView(context: Context) -> NSView {
let view = ChromeHostView()
Expand Down Expand Up @@ -50,5 +51,9 @@ private final class ChromeHostView: NSView {

window.standardWindowButton(.miniaturizeButton)?.isHidden = config.hideMiniaturizeButton
window.standardWindowButton(.zoomButton)?.isHidden = config.hideZoomButton

if let policy = config.statePolicy {
WindowStateController.shared.install(on: window, policy: policy)
}
}
}
6 changes: 3 additions & 3 deletions TablePro/Views/Results/JSONViewerWindowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ final class JSONViewerWindowController {
private static var activeWindows: [ObjectIdentifier: JSONViewerWindowController] = [:]
private static let defaultSize = NSSize(width: 640, height: 500)
private static let minSize = NSSize(width: 400, height: 300)
private static let autosaveName: NSWindow.FrameAutosaveName = "JSONViewerWindow"

private var window: NSWindow?
private var closeObserver: NSObjectProtocol?
Expand Down Expand Up @@ -39,7 +38,8 @@ final class JSONViewerWindowController {
defer: false
)
window.identifier = NSUserInterfaceItemIdentifier("json-viewer")
window.title = columnName.map { "JSON — \($0)" } ?? String(localized: "JSON Viewer")
window.title = columnName.map { String(format: String(localized: "JSON: %@"), $0) }
?? String(localized: "JSON Viewer")
window.isReleasedWhenClosed = false
window.minSize = Self.minSize
window.collectionBehavior = [.fullScreenPrimary]
Expand Down Expand Up @@ -71,7 +71,7 @@ final class JSONViewerWindowController {
}
}

window.applyAutosaveName(Self.autosaveName)
WindowStateController.shared.install(on: window, policy: .jsonViewer)
window.makeKeyAndOrderFront(nil)
}
}
Expand Down
Loading
Loading