From e410f56cae6235feedf9aac2f38916471ea71413 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 7 May 2026 20:27:20 +0700 Subject: [PATCH 1/3] fix: preserve full screen state when quitting from a full screen window --- .../WindowStateController.swift | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/WindowStateController.swift b/TablePro/Core/Services/Infrastructure/WindowStateController.swift index 45ae35652..2beea554a 100644 --- a/TablePro/Core/Services/Infrastructure/WindowStateController.swift +++ b/TablePro/Core/Services/Infrastructure/WindowStateController.swift @@ -15,9 +15,30 @@ internal final class WindowStateController { private let defaults: UserDefaults private var bindings: [ObjectIdentifier: WindowStateBinding] = [:] + fileprivate private(set) var isTerminating = false private init(defaults: UserDefaults = .standard) { self.defaults = defaults + observeApplicationTermination() + } + + private func observeApplicationTermination() { + NotificationCenter.default.addObserver( + forName: NSApplication.willTerminateNotification, + object: nil, + queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { + self?.handleApplicationWillTerminate() + } + } + } + + private func handleApplicationWillTerminate() { + isTerminating = true + for binding in bindings.values { + binding.captureCurrentFullScreenState() + } } private func applyFirstRunFrame(to window: NSWindow, policy: WindowFramePolicy) { @@ -107,6 +128,11 @@ private final class WindowStateBinding { } } + func captureCurrentFullScreenState() { + guard let window else { return } + defaults.set(window.styleMask.contains(.fullScreen), forKey: policy.fullScreenStateKey) + } + private func attachLiveObservers() { guard let window else { return } let center = NotificationCenter.default @@ -123,8 +149,11 @@ private final class WindowStateBinding { forName: NSWindow.didExitFullScreenNotification, object: window, queue: .main - ) { [defaults, policy] _ in - defaults.set(false, forKey: policy.fullScreenStateKey) + ) { [weak self] _ in + MainActor.assumeIsolated { + guard let self, self.owner?.isTerminating != true else { return } + self.defaults.set(false, forKey: self.policy.fullScreenStateKey) + } }) liveObservers.append(center.addObserver( From 1869fab4cf6e62364f9f118ec1d1ef3e86b76894 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 7 May 2026 20:30:04 +0700 Subject: [PATCH 2/3] chore: add OSLog tracing to WindowStateController for full screen debugging --- .../WindowStateController.swift | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/WindowStateController.swift b/TablePro/Core/Services/Infrastructure/WindowStateController.swift index 2beea554a..55e83989b 100644 --- a/TablePro/Core/Services/Infrastructure/WindowStateController.swift +++ b/TablePro/Core/Services/Infrastructure/WindowStateController.swift @@ -7,23 +7,26 @@ 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 - observeApplicationTermination() + observeApplicationLifecycle() + windowStateLogger.info("[init] WindowStateController initialized") } - private func observeApplicationTermination() { - NotificationCenter.default.addObserver( + private func observeApplicationLifecycle() { + let center = NotificationCenter.default + + center.addObserver( forName: NSApplication.willTerminateNotification, object: nil, queue: .main @@ -32,10 +35,24 @@ internal final class WindowStateController { 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 ?? "" + windowStateLogger.info("[launch] persisted editor.isFullScreen=\(value, privacy: .public) frame=\(frame, privacy: .public)") + } + } } private func handleApplicationWillTerminate() { isTerminating = true + windowStateLogger.info("[terminate] applicationWillTerminate fired, snapshotting \(self.bindings.count) bindings") for binding in bindings.values { binding.captureCurrentFullScreenState() } @@ -48,6 +65,7 @@ internal final class WindowStateController { window.setContentSize(size) } window.center() + windowStateLogger.info("[install] applied first-run frame for \(policy.autosaveName, privacy: .public) size=\(NSStringFromRect(window.frame), privacy: .public)") } fileprivate func releaseBinding(forWindowKey key: ObjectIdentifier) { @@ -57,9 +75,9 @@ 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) } @@ -67,6 +85,10 @@ internal extension WindowStateController { bindings[key]?.invalidate() let restorePending = defaults.bool(forKey: policy.fullScreenStateKey) + windowStateLogger.info( + "[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, @@ -129,8 +151,13 @@ private final class WindowStateBinding { } func captureCurrentFullScreenState() { - guard let window else { return } - defaults.set(window.styleMask.contains(.fullScreen), forKey: policy.fullScreenStateKey) + guard let window else { + windowStateLogger.info("[snapshot] \(self.policy.autosaveName, privacy: .public) skipped, window is nil") + return + } + let isFullScreen = window.styleMask.contains(.fullScreen) + defaults.set(isFullScreen, forKey: policy.fullScreenStateKey) + windowStateLogger.info("[snapshot] \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=\(isFullScreen, privacy: .public) frame=\(NSStringFromRect(window.frame), privacy: .public)") } private func attachLiveObservers() { @@ -143,6 +170,7 @@ private final class WindowStateBinding { queue: .main ) { [defaults, policy] _ in defaults.set(true, forKey: policy.fullScreenStateKey) + windowStateLogger.info("[event] willEnterFullScreen \(policy.autosaveName, privacy: .public) wrote isFullScreen=true") }) liveObservers.append(center.addObserver( @@ -151,8 +179,12 @@ private final class WindowStateBinding { queue: .main ) { [weak self] _ in MainActor.assumeIsolated { - guard let self, self.owner?.isTerminating != true else { return } + guard let self else { return } + let isTerm = self.owner?.isTerminating == true + windowStateLogger.info("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) isTerminating=\(isTerm, privacy: .public)") + guard !isTerm else { return } self.defaults.set(false, forKey: self.policy.fullScreenStateKey) + windowStateLogger.info("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=false") } }) @@ -163,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.info("[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.info("[event] willClose during terminate while fullscreen, locked isFullScreen=true") + } self.invalidate() self.owner?.releaseBinding(forWindowKey: self.windowKey) } @@ -172,6 +211,7 @@ private final class WindowStateBinding { private func attachFullScreenRestoreObserver() { guard let window else { return } let center = NotificationCenter.default + windowStateLogger.info("[install] \(self.policy.autosaveName, privacy: .public) registered didBecomeKey observer for fullscreen restore") fullScreenRestoreObserver = center.addObserver( forName: NSWindow.didBecomeKeyNotification, @@ -190,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.info("[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.info("[restore] \(self.policy.autosaveName, privacy: .public) toggleFullScreen called") } } From 099ad504298d6191e83bf0cc81eb5d074b5610cf Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 7 May 2026 20:34:15 +0700 Subject: [PATCH 3/3] chore: upgrade WindowState trace logs from .info to .notice --- .../WindowStateController.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/TablePro/Core/Services/Infrastructure/WindowStateController.swift b/TablePro/Core/Services/Infrastructure/WindowStateController.swift index 55e83989b..d79159896 100644 --- a/TablePro/Core/Services/Infrastructure/WindowStateController.swift +++ b/TablePro/Core/Services/Infrastructure/WindowStateController.swift @@ -20,7 +20,7 @@ internal final class WindowStateController { private init(defaults: UserDefaults = .standard) { self.defaults = defaults observeApplicationLifecycle() - windowStateLogger.info("[init] WindowStateController initialized") + windowStateLogger.notice("[init] WindowStateController initialized") } private func observeApplicationLifecycle() { @@ -45,14 +45,14 @@ internal final class WindowStateController { 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 ?? "" - windowStateLogger.info("[launch] persisted editor.isFullScreen=\(value, privacy: .public) frame=\(frame, privacy: .public)") + windowStateLogger.notice("[launch] persisted editor.isFullScreen=\(value, privacy: .public) frame=\(frame, privacy: .public)") } } } private func handleApplicationWillTerminate() { isTerminating = true - windowStateLogger.info("[terminate] applicationWillTerminate fired, snapshotting \(self.bindings.count) bindings") + windowStateLogger.notice("[terminate] applicationWillTerminate fired, snapshotting \(self.bindings.count) bindings") for binding in bindings.values { binding.captureCurrentFullScreenState() } @@ -65,7 +65,7 @@ internal final class WindowStateController { window.setContentSize(size) } window.center() - windowStateLogger.info("[install] applied first-run frame for \(policy.autosaveName, privacy: .public) size=\(NSStringFromRect(window.frame), privacy: .public)") + windowStateLogger.notice("[install] applied first-run frame for \(policy.autosaveName, privacy: .public) size=\(NSStringFromRect(window.frame), privacy: .public)") } fileprivate func releaseBinding(forWindowKey key: ObjectIdentifier) { @@ -85,7 +85,7 @@ internal extension WindowStateController { bindings[key]?.invalidate() let restorePending = defaults.bool(forKey: policy.fullScreenStateKey) - windowStateLogger.info( + 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)" ) @@ -152,12 +152,12 @@ private final class WindowStateBinding { func captureCurrentFullScreenState() { guard let window else { - windowStateLogger.info("[snapshot] \(self.policy.autosaveName, privacy: .public) skipped, window is nil") + 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.info("[snapshot] \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=\(isFullScreen, privacy: .public) frame=\(NSStringFromRect(window.frame), privacy: .public)") + windowStateLogger.notice("[snapshot] \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=\(isFullScreen, privacy: .public) frame=\(NSStringFromRect(window.frame), privacy: .public)") } private func attachLiveObservers() { @@ -170,7 +170,7 @@ private final class WindowStateBinding { queue: .main ) { [defaults, policy] _ in defaults.set(true, forKey: policy.fullScreenStateKey) - windowStateLogger.info("[event] willEnterFullScreen \(policy.autosaveName, privacy: .public) wrote isFullScreen=true") + windowStateLogger.notice("[event] willEnterFullScreen \(policy.autosaveName, privacy: .public) wrote isFullScreen=true") }) liveObservers.append(center.addObserver( @@ -181,10 +181,10 @@ private final class WindowStateBinding { MainActor.assumeIsolated { guard let self else { return } let isTerm = self.owner?.isTerminating == true - windowStateLogger.info("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) isTerminating=\(isTerm, privacy: .public)") + 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.info("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=false") + windowStateLogger.notice("[event] didExitFullScreen \(self.policy.autosaveName, privacy: .public) wrote isFullScreen=false") } }) @@ -197,10 +197,10 @@ private final class WindowStateBinding { guard let self else { return } let isTerm = self.owner?.isTerminating == true let isFullScreen = self.window?.styleMask.contains(.fullScreen) ?? false - windowStateLogger.info("[event] willClose \(self.policy.autosaveName, privacy: .public) isTerminating=\(isTerm, privacy: .public) styleMask.fullScreen=\(isFullScreen, privacy: .public)") + 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.info("[event] willClose during terminate while fullscreen, locked isFullScreen=true") + windowStateLogger.notice("[event] willClose during terminate while fullscreen, locked isFullScreen=true") } self.invalidate() self.owner?.releaseBinding(forWindowKey: self.windowKey) @@ -211,7 +211,7 @@ private final class WindowStateBinding { private func attachFullScreenRestoreObserver() { guard let window else { return } let center = NotificationCenter.default - windowStateLogger.info("[install] \(self.policy.autosaveName, privacy: .public) registered didBecomeKey observer for fullscreen restore") + windowStateLogger.notice("[install] \(self.policy.autosaveName, privacy: .public) registered didBecomeKey observer for fullscreen restore") fullScreenRestoreObserver = center.addObserver( forName: NSWindow.didBecomeKeyNotification, @@ -231,9 +231,9 @@ private final class WindowStateBinding { fullScreenRestoreObserver = nil } let alreadyFullScreen = window.styleMask.contains(.fullScreen) - windowStateLogger.info("[restore] \(self.policy.autosaveName, privacy: .public) didBecomeKey, alreadyFullScreen=\(alreadyFullScreen, privacy: .public) collectionBehaviorContainsFullScreenPrimary=\(window.collectionBehavior.contains(.fullScreenPrimary), privacy: .public)") + 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.info("[restore] \(self.policy.autosaveName, privacy: .public) toggleFullScreen called") + windowStateLogger.notice("[restore] \(self.policy.autosaveName, privacy: .public) toggleFullScreen called") } }