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 ? ) {
@@ -49,6 +32,8 @@ private final class EditorWindow: NSWindow {
4932internal final class TabWindowController : NSWindowController , NSWindowDelegate {
5033 private static let lifecycleLogger = Logger ( subsystem: " com.TablePro " , category: " NativeTabLifecycle " )
5134
35+ internal static let frameAutosaveName : NSWindow . FrameAutosaveName = " MainEditorWindow "
36+
5237 private lazy var dataGridFieldEditor : DataGridFieldEditor = {
5338 let editor = DataGridFieldEditor ( )
5439 editor. isFieldEditor = true
@@ -57,18 +42,8 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
5742
5843 internal let payload : EditorTabPayload
5944
60- /// Stable identifier for this controller. Distinct from the
61- /// `MainContentView.@State windowId` used inside WindowLifecycleMonitor —
62- /// that one remains the authoritative per-view UUID in Phase 1. Phase 2
63- /// will unify them on this controller's identifier.
6445 internal let controllerId : UUID
6546
66- /// NSUserActivity published while this window is key, so Handoff and
67- /// other continuity flows can pick up the connection (and table, if
68- /// viewing one). Replaces the SwiftUI `.userActivity(...)` modifier we
69- /// removed in Phase 2 — `.userActivity` requires a Scene context and
70- /// emitted `Cannot use Scene methods for URL, NSUserActivity...` warnings
71- /// when used inside an `NSHostingView`.
7247 private var activity : NSUserActivity ?
7348
7449 internal init ( payload: EditorTabPayload , sessionState: SessionStateFactory . SessionState ? = nil ) {
@@ -84,40 +59,29 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
8459 window. identifier = NSUserInterfaceItemIdentifier ( " main " )
8560 window. minSize = NSSize ( width: 720 , height: 480 )
8661 window. isRestorable = false
87- window. applyAutosaveName ( " MainEditorWindow " )
8862 window. toolbarStyle = . unified
89- // Hide the window title ("Query 1 / TablePro") embedded in the unified
90- // toolbar — otherwise it claims leading space and pushes our navigation
91- // items to the right of it. Tab group's tab bar already shows the same
92- // "Query N" label, so no information is lost. The Principal toolbar item
93- // continues to show connection name + DB version.
9463 window. titleVisibility = . hidden
9564 window. tabbingMode = . preferred
9665 window. tabbingIdentifier = WindowManager . tabbingIdentifier ( for: payload. connectionId)
9766 window. collectionBehavior. insert ( [ . fullScreenPrimary, . managed] )
9867
99- // NSSplitViewController as contentViewController so .toggleSidebar and
100- // .sidebarTrackingSeparator find the split view via the responder chain.
10168 let splitVC = MainSplitViewController ( payload: payload, sessionState: sessionState)
10269 window. contentViewController = splitVC
10370
10471 super. init ( window: window)
10572
106- // Keep the controller alive after the window closes so NSWindowDelegate
107- // hooks have time to run teardown. WindowManager drops its strong
108- // reference on willClose, which triggers dealloc.
10973 window. isReleasedWhenClosed = false
110-
111- // Become the window's delegate so didBecomeKey/didResignKey/willClose
112- // dispatch to methods on this controller — eliminates the global
113- // NotificationCenter fan-out that previously ran every ContentView
114- // instance's observer per focus change.
11574 window. delegate = self
11675
117- // Toolbar is installed by MainSplitViewController.viewWillAppear when
118- // the session state is available. NSSplitViewController does not
119- // overwrite window.toolbar (unlike NavigationSplitView), so no KVO
120- // workaround is needed.
76+ if !window. setFrameUsingName ( Self . frameAutosaveName) {
77+ let visibleSize = ( window. screen ?? NSScreen . main) ? . visibleFrame. size
78+ ?? NSSize ( width: 1_440 , height: 900 )
79+ window. setContentSize ( NSSize (
80+ width: min ( 1_200 , visibleSize. width) ,
81+ height: min ( 800 , visibleSize. height)
82+ ) )
83+ window. center ( )
84+ }
12185
12286 Self . lifecycleLogger. info (
12387 " [open] TabWindowController.init payloadId= \( payload. id, privacy: . public) connId= \( payload. connectionId, privacy: . public) controllerId= \( self . controllerId, privacy: . public) eagerToolbar= \( sessionState != nil ) "
@@ -136,6 +100,22 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
136100 return dataGridFieldEditor
137101 }
138102
103+ internal func windowDidResize( _ notification: Notification ) {
104+ guard let window = notification. object as? NSWindow else { return }
105+ guard !window. inLiveResize else { return }
106+ window. saveFrame ( usingName: Self . frameAutosaveName)
107+ }
108+
109+ internal func windowDidEndLiveResize( _ notification: Notification ) {
110+ guard let window = notification. object as? NSWindow else { return }
111+ window. saveFrame ( usingName: Self . frameAutosaveName)
112+ }
113+
114+ internal func windowDidMove( _ notification: Notification ) {
115+ guard let window = notification. object as? NSWindow else { return }
116+ window. saveFrame ( usingName: Self . frameAutosaveName)
117+ }
118+
139119 internal func windowDidBecomeKey( _ notification: Notification ) {
140120 let seq = MainContentCoordinator . nextSwitchSeq ( )
141121 let t0 = Date ( )
@@ -180,6 +160,8 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
180160 guard let window = notification. object as? NSWindow else { return }
181161 Self . lifecycleLogger. info ( " [close] windowWillClose seq= \( seq) controllerId= \( self . controllerId, privacy: . public) " )
182162
163+ window. saveFrame ( usingName: Self . frameAutosaveName)
164+
183165 if let splitVC = window. contentViewController as? MainSplitViewController {
184166 splitVC. invalidateToolbar ( )
185167 }
@@ -198,10 +180,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
198180
199181 // MARK: - NSUserActivity
200182
201- /// Publish (or refresh) this window's NSUserActivity. Called by
202- /// `windowDidBecomeKey` and by `MainContentView` when the selected tab
203- /// changes — only the second case is a no-op when the window isn't key
204- /// (Handoff only cares about the active activity).
205183 internal func refreshUserActivity( ) {
206184 guard let window, window. isKeyWindow,
207185 let coordinator = MainContentCoordinator . coordinator ( forWindow: window)
@@ -215,8 +193,6 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
215193 let tableName : String ? = ( selectedTab? . tabType == . table) ? selectedTab? . tableContext. tableName : nil
216194 let activityType = tableName != nil ? " com.TablePro.viewTable " : " com.TablePro.viewConnection "
217195
218- // Recreate when the activity type flips between viewConnection and
219- // viewTable — NSUserActivity.activityType is immutable.
220196 if activity? . activityType != activityType {
221197 activity? . invalidate ( )
222198 let newActivity = NSUserActivity ( activityType: activityType)
@@ -232,13 +208,11 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
232208 }
233209 activity. userInfo = info
234210
235- // Always promote to current. Both call sites (`windowDidBecomeKey` and
236- // `refreshUserActivity` which guards on `window.isKeyWindow`) only
237- // invoke this method when the window owns Handoff. The previous
238- // `becomeCurrent: Bool` parameter dropped Continuity mid-session
239- // whenever the user switched between table and query tabs in the
240- // same window — the type-flip branch above invalidated the old
241- // activity but never promoted the replacement.
211+ // becomeCurrent is unconditional. A previous becomeCurrent: Bool gate
212+ // dropped Continuity mid-session whenever the user switched between
213+ // table and query tabs in the same window, because the activity-type
214+ // flip above invalidates the old activity but never promotes its
215+ // replacement.
242216 activity. becomeCurrent ( )
243217 }
244218}
0 commit comments