Skip to content
Closed
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
88 changes: 80 additions & 8 deletions TablePro/Core/Services/Infrastructure/WindowStateController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,55 @@ import AppKit
import Foundation
import os

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

@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] = [:]
fileprivate private(set) var isTerminating = false

private init(defaults: UserDefaults = .standard) {
self.defaults = defaults
observeApplicationLifecycle()
windowStateLogger.notice("[init] WindowStateController initialized")
}

private func observeApplicationLifecycle() {
let center = NotificationCenter.default

center.addObserver(
forName: NSApplication.willTerminateNotification,
object: nil,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated {
self?.handleApplicationWillTerminate()
}
}

center.addObserver(
forName: NSApplication.didFinishLaunchingNotification,
object: nil,
queue: .main
) { _ in
MainActor.assumeIsolated {
let value = UserDefaults.standard.bool(forKey: WindowFramePolicy.editor.fullScreenStateKey)
let frameKey = "NSWindow Frame \(WindowFramePolicy.editor.autosaveName)"
let frame = UserDefaults.standard.object(forKey: frameKey) as? String ?? "<nil>"
windowStateLogger.notice("[launch] persisted editor.isFullScreen=\(value, privacy: .public) frame=\(frame, privacy: .public)")
}
}
}

private func handleApplicationWillTerminate() {
isTerminating = true
windowStateLogger.notice("[terminate] applicationWillTerminate fired, snapshotting \(self.bindings.count) bindings")
for binding in bindings.values {
binding.captureCurrentFullScreenState()
}
}

private func applyFirstRunFrame(to window: NSWindow, policy: WindowFramePolicy) {
Expand All @@ -27,6 +65,7 @@ internal final class WindowStateController {
window.setContentSize(size)
}
window.center()
windowStateLogger.notice("[install] applied first-run frame for \(policy.autosaveName, privacy: .public) size=\(NSStringFromRect(window.frame), privacy: .public)")
}

fileprivate func releaseBinding(forWindowKey key: ObjectIdentifier) {
Expand All @@ -36,16 +75,20 @@ internal final class WindowStateController {

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

if !window.setFrameUsingName(policy.autosaveName) {
let didSetAutosave = window.setFrameAutosaveName(policy.autosaveName)
let restored = window.setFrameUsingName(policy.autosaveName)
if !restored {
applyFirstRunFrame(to: window, policy: policy)
}

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

let restorePending = defaults.bool(forKey: policy.fullScreenStateKey)
windowStateLogger.notice(
"[install] \(policy.autosaveName, privacy: .public) didSetAutosave=\(didSetAutosave, privacy: .public) frameRestored=\(restored, privacy: .public) restoreFullScreen=\(restorePending, privacy: .public) frame=\(NSStringFromRect(window.frame), privacy: .public) styleMask.fullScreen=\(window.styleMask.contains(.fullScreen), privacy: .public)"
)

bindings[key] = WindowStateBinding(
windowKey: key,
window: window,
Expand Down Expand Up @@ -107,6 +150,16 @@ private final class WindowStateBinding {
}
}

func captureCurrentFullScreenState() {
guard let window else {
windowStateLogger.notice("[snapshot] \(self.policy.autosaveName, privacy: .public) skipped, window is nil")
return
}
let isFullScreen = window.styleMask.contains(.fullScreen)
defaults.set(isFullScreen, forKey: policy.fullScreenStateKey)
windowStateLogger.notice("[snapshot] \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=\(isFullScreen, privacy: .public) frame=\(NSStringFromRect(window.frame), privacy: .public)")
}

private func attachLiveObservers() {
guard let window else { return }
let center = NotificationCenter.default
Expand All @@ -117,14 +170,22 @@ private final class WindowStateBinding {
queue: .main
) { [defaults, policy] _ in
defaults.set(true, forKey: policy.fullScreenStateKey)
windowStateLogger.notice("[event] willEnterFullScreen \(policy.autosaveName, privacy: .public) wrote isFullScreen=true")
})

liveObservers.append(center.addObserver(
forName: NSWindow.didExitFullScreenNotification,
object: window,
queue: .main
) { [defaults, policy] _ in
defaults.set(false, forKey: policy.fullScreenStateKey)
) { [weak self] _ in
MainActor.assumeIsolated {
guard let self else { return }
let isTerm = self.owner?.isTerminating == true
windowStateLogger.notice("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) isTerminating=\(isTerm, privacy: .public)")
guard !isTerm else { return }
self.defaults.set(false, forKey: self.policy.fullScreenStateKey)
windowStateLogger.notice("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=false")
}
})

liveObservers.append(center.addObserver(
Expand All @@ -134,6 +195,13 @@ private final class WindowStateBinding {
) { [weak self] _ in
MainActor.assumeIsolated {
guard let self else { return }
let isTerm = self.owner?.isTerminating == true
let isFullScreen = self.window?.styleMask.contains(.fullScreen) ?? false
windowStateLogger.notice("[event] willClose \(self.policy.autosaveName, privacy: .public) isTerminating=\(isTerm, privacy: .public) styleMask.fullScreen=\(isFullScreen, privacy: .public)")
if isTerm && isFullScreen {
self.defaults.set(true, forKey: self.policy.fullScreenStateKey)
windowStateLogger.notice("[event] willClose during terminate while fullscreen, locked isFullScreen=true")
}
self.invalidate()
self.owner?.releaseBinding(forWindowKey: self.windowKey)
}
Expand All @@ -143,6 +211,7 @@ private final class WindowStateBinding {
private func attachFullScreenRestoreObserver() {
guard let window else { return }
let center = NotificationCenter.default
windowStateLogger.notice("[install] \(self.policy.autosaveName, privacy: .public) registered didBecomeKey observer for fullscreen restore")

fullScreenRestoreObserver = center.addObserver(
forName: NSWindow.didBecomeKeyNotification,
Expand All @@ -161,7 +230,10 @@ private final class WindowStateBinding {
NotificationCenter.default.removeObserver(observer)
fullScreenRestoreObserver = nil
}
guard !window.styleMask.contains(.fullScreen) else { return }
let alreadyFullScreen = window.styleMask.contains(.fullScreen)
windowStateLogger.notice("[restore] \(self.policy.autosaveName, privacy: .public) didBecomeKey, alreadyFullScreen=\(alreadyFullScreen, privacy: .public) collectionBehaviorContainsFullScreenPrimary=\(window.collectionBehavior.contains(.fullScreenPrimary), privacy: .public)")
guard !alreadyFullScreen else { return }
window.toggleFullScreen(nil)
windowStateLogger.notice("[restore] \(self.policy.autosaveName, privacy: .public) toggleFullScreen called")
}
}
Loading