22// TabWindowController.swift
33// TablePro
44//
5- // NSWindowController for an editor-tab-window. Replaces the SwiftUI
6- // `WindowGroup(id: "main", for: EditorTabPayload.self)` scene.
7- //
8- // Phase 1 scope: window creation, NSHostingView installation, tabbing
9- // configuration. Existing MainContentView lifecycle hooks (.onAppear,
10- // .onDisappear, NSWindow notification observers, .userActivity) continue to
11- // work unchanged — this controller's job in Phase 1 is limited to replacing
12- // SwiftUI scene-driven window construction.
13- //
14- // Phase 2 will migrate lifecycle responsibilities (markActivated, teardown,
15- // userActivity, didBecomeKey/didResignKey) into NSWindowDelegate methods
16- // on this controller.
17- //
185
196import AppKit
207import os
218import SwiftUI
229
23- /// NSWindow subclass that routes the standard tab-related responder selectors
24- /// (`performClose:`, `newWindowForTab:`) through the coordinator. Menus and
25- /// toolbar buttons reach this via `NSApp.sendAction(_:to:nil:from:)`, the same
26- /// pattern AppKit uses for `selectNextTab:` and `selectPreviousTab:`.
2710@MainActor
2811private final class EditorWindow : NSWindow {
2912 override func performClose( _ sender: Any ? ) {
@@ -48,18 +31,11 @@ private final class EditorWindow: NSWindow {
4831@MainActor
4932internal final class TabWindowController : NSWindowController , NSWindowDelegate {
5033 private static let lifecycleLogger = Logger ( subsystem: " com.TablePro " , category: " NativeTabLifecycle " )
51- private static let frameLogger = Logger ( subsystem: " com.TablePro " , category: " WindowFrame " )
5234
5335 internal static let frameAutosaveName : NSWindow . FrameAutosaveName = " MainEditorWindow "
5436
55- private static var frameDefaultsKey : String { " NSWindow Frame \( frameAutosaveName) " }
56-
5737 internal static var hasSavedFrame : Bool {
58- UserDefaults . standard. object ( forKey: frameDefaultsKey) != nil
59- }
60-
61- private static func currentSavedFrameString( ) -> String {
62- UserDefaults . standard. string ( forKey: frameDefaultsKey) ?? " <nil> "
38+ UserDefaults . standard. object ( forKey: " NSWindow Frame \( frameAutosaveName) " ) != nil
6339 }
6440
6541 private lazy var dataGridFieldEditor : DataGridFieldEditor = {
@@ -70,18 +46,8 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
7046
7147 internal let payload : EditorTabPayload
7248
73- /// Stable identifier for this controller. Distinct from the
74- /// `MainContentView.@State windowId` used inside WindowLifecycleMonitor —
75- /// that one remains the authoritative per-view UUID in Phase 1. Phase 2
76- /// will unify them on this controller's identifier.
7749 internal let controllerId : UUID
7850
79- /// NSUserActivity published while this window is key, so Handoff and
80- /// other continuity flows can pick up the connection (and table, if
81- /// viewing one). Replaces the SwiftUI `.userActivity(...)` modifier we
82- /// removed in Phase 2 — `.userActivity` requires a Scene context and
83- /// emitted `Cannot use Scene methods for URL, NSUserActivity...` warnings
84- /// when used inside an `NSHostingView`.
8551 private var activity : NSUserActivity ?
8652
8753 internal init ( payload: EditorTabPayload , sessionState: SessionStateFactory . SessionState ? = nil ) {
@@ -98,49 +64,22 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
9864 window. minSize = NSSize ( width: 720 , height: 480 )
9965 window. isRestorable = false
10066 window. toolbarStyle = . unified
101- // Hide the window title ("Query 1 / TablePro") embedded in the unified
102- // toolbar — otherwise it claims leading space and pushes our navigation
103- // items to the right of it. Tab group's tab bar already shows the same
104- // "Query N" label, so no information is lost. The Principal toolbar item
105- // continues to show connection name + DB version.
10667 window. titleVisibility = . hidden
10768 window. tabbingMode = . preferred
10869 window. tabbingIdentifier = WindowManager . tabbingIdentifier ( for: payload. connectionId)
10970 window. collectionBehavior. insert ( [ . fullScreenPrimary, . managed] )
11071
111- // NSSplitViewController as contentViewController so .toggleSidebar and
112- // .sidebarTrackingSeparator find the split view via the responder chain.
72+ window. setFrameAutosaveName ( Self . frameAutosaveName)
73+ _ = window. setFrameUsingName ( Self . frameAutosaveName)
74+
11375 let splitVC = MainSplitViewController ( payload: payload, sessionState: sessionState)
11476 window. contentViewController = splitVC
11577
11678 super. init ( window: window)
11779
118- Self . frameLogger. notice ( " [init] before-autosave persistedFrame= \( Self . currentSavedFrameString ( ) , privacy: . public) windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) " )
119-
120- self . windowFrameAutosaveName = Self . frameAutosaveName
121- Self . frameLogger. notice ( " [init] after windowFrameAutosaveName setter windowAutosaveName= \( window. frameAutosaveName, privacy: . public) windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) " )
122-
123- let restored = window. setFrameUsingName ( Self . frameAutosaveName)
124- Self . frameLogger. notice ( " [init] explicit setFrameUsingName returned= \( restored, privacy: . public) windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) " )
125-
126- installFrameAutosaveTrace ( on: window)
127-
128- // Keep the controller alive after the window closes so NSWindowDelegate
129- // hooks have time to run teardown. WindowManager drops its strong
130- // reference on willClose, which triggers dealloc.
13180 window. isReleasedWhenClosed = false
132-
133- // Become the window's delegate so didBecomeKey/didResignKey/willClose
134- // dispatch to methods on this controller — eliminates the global
135- // NotificationCenter fan-out that previously ran every ContentView
136- // instance's observer per focus change.
13781 window. delegate = self
13882
139- // Toolbar is installed by MainSplitViewController.viewWillAppear when
140- // the session state is available. NSSplitViewController does not
141- // overwrite window.toolbar (unlike NavigationSplitView), so no KVO
142- // workaround is needed.
143-
14483 Self . lifecycleLogger. info (
14584 " [open] TabWindowController.init payloadId= \( payload. id, privacy: . public) connId= \( payload. connectionId, privacy: . public) controllerId= \( self . controllerId, privacy: . public) eagerToolbar= \( sessionState != nil ) "
14685 )
@@ -151,46 +90,29 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
15190 fatalError ( " TabWindowController does not support NSCoder init " )
15291 }
15392
154- private var frameTraceObservers : [ NSObjectProtocol ] = [ ]
155-
156- private func installFrameAutosaveTrace( on window: NSWindow ) {
157- let center = NotificationCenter . default
158-
159- frameTraceObservers. append ( center. addObserver (
160- forName: NSWindow . didResizeNotification,
161- object: window,
162- queue: . main
163- ) { [ weak window] _ in
164- guard let window else { return }
165- Self . frameLogger. notice ( " [event] didResize windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) inLiveResize= \( window. inLiveResize, privacy: . public) persistedFrame= \( Self . currentSavedFrameString ( ) , privacy: . public) " )
166- } )
167-
168- frameTraceObservers. append ( center. addObserver (
169- forName: NSWindow . didMoveNotification,
170- object: window,
171- queue: . main
172- ) { [ weak window] _ in
173- guard let window else { return }
174- Self . frameLogger. notice ( " [event] didMove windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) persistedFrame= \( Self . currentSavedFrameString ( ) , privacy: . public) " )
175- } )
176-
177- frameTraceObservers. append ( center. addObserver (
178- forName: NSWindow . didEndLiveResizeNotification,
179- object: window,
180- queue: . main
181- ) { [ weak window] _ in
182- guard let window else { return }
183- Self . frameLogger. notice ( " [event] didEndLiveResize windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) persistedFrame= \( Self . currentSavedFrameString ( ) , privacy: . public) " )
184- } )
185- }
186-
18793 // MARK: - NSWindowDelegate
18894
18995 func windowWillReturnFieldEditor( _ sender: NSWindow , to client: Any ? ) -> Any ? {
19096 guard client is CellTextField else { return nil }
19197 return dataGridFieldEditor
19298 }
19399
100+ internal func windowDidResize( _ notification: Notification ) {
101+ guard let window = notification. object as? NSWindow else { return }
102+ guard !window. inLiveResize else { return }
103+ window. saveFrame ( usingName: Self . frameAutosaveName)
104+ }
105+
106+ internal func windowDidEndLiveResize( _ notification: Notification ) {
107+ guard let window = notification. object as? NSWindow else { return }
108+ window. saveFrame ( usingName: Self . frameAutosaveName)
109+ }
110+
111+ internal func windowDidMove( _ notification: Notification ) {
112+ guard let window = notification. object as? NSWindow else { return }
113+ window. saveFrame ( usingName: Self . frameAutosaveName)
114+ }
115+
194116 internal func windowDidBecomeKey( _ notification: Notification ) {
195117 let seq = MainContentCoordinator . nextSwitchSeq ( )
196118 let t0 = Date ( )
@@ -235,14 +157,7 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
235157 guard let window = notification. object as? NSWindow else { return }
236158 Self . lifecycleLogger. info ( " [close] windowWillClose seq= \( seq) controllerId= \( self . controllerId, privacy: . public) " )
237159
238- Self . frameLogger. notice ( " [close] before saveFrame windowFrame= \( NSStringFromRect ( window. frame) , privacy: . public) styleMask.fullScreen= \( window. styleMask. contains ( . fullScreen) , privacy: . public) persistedFrame= \( Self . currentSavedFrameString ( ) , privacy: . public) " )
239160 window. saveFrame ( usingName: Self . frameAutosaveName)
240- Self . frameLogger. notice ( " [close] after saveFrame persistedFrame= \( Self . currentSavedFrameString ( ) , privacy: . public) " )
241-
242- for observer in frameTraceObservers {
243- NotificationCenter . default. removeObserver ( observer)
244- }
245- frameTraceObservers. removeAll ( )
246161
247162 if let splitVC = window. contentViewController as? MainSplitViewController {
248163 splitVC. invalidateToolbar ( )
@@ -262,10 +177,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
262177
263178 // MARK: - NSUserActivity
264179
265- /// Publish (or refresh) this window's NSUserActivity. Called by
266- /// `windowDidBecomeKey` and by `MainContentView` when the selected tab
267- /// changes — only the second case is a no-op when the window isn't key
268- /// (Handoff only cares about the active activity).
269180 internal func refreshUserActivity( ) {
270181 guard let window, window. isKeyWindow,
271182 let coordinator = MainContentCoordinator . coordinator ( forWindow: window)
@@ -279,8 +190,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
279190 let tableName : String ? = ( selectedTab? . tabType == . table) ? selectedTab? . tableContext. tableName : nil
280191 let activityType = tableName != nil ? " com.TablePro.viewTable " : " com.TablePro.viewConnection "
281192
282- // Recreate when the activity type flips between viewConnection and
283- // viewTable — NSUserActivity.activityType is immutable.
284193 if activity? . activityType != activityType {
285194 activity? . invalidate ( )
286195 let newActivity = NSUserActivity ( activityType: activityType)
@@ -296,13 +205,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
296205 }
297206 activity. userInfo = info
298207
299- // Always promote to current. Both call sites (`windowDidBecomeKey` and
300- // `refreshUserActivity` which guards on `window.isKeyWindow`) only
301- // invoke this method when the window owns Handoff. The previous
302- // `becomeCurrent: Bool` parameter dropped Continuity mid-session
303- // whenever the user switched between table and query tabs in the
304- // same window — the type-flip branch above invalidated the old
305- // activity but never promoted the replacement.
306208 activity. becomeCurrent ( )
307209 }
308210}
0 commit comments