Stand up the window architecture — welcome, board, card
Four scenes (welcome, restore bootstrap, board group, card group) with system restoration disabled in favor of the registry's open-now flags: set when a window actually opens, cleared only on user close, so quit — and crash — leave exactly the restoration set behind. AppModel joins windows to sessions (shared store, registry record, card refs, held security scope); CloseFlushCoordinator pins 02's strict close order as a seam-injected machine (card sessions end, windows drain, store flushes, record stamps, teardown) with named slots where m6/m7 flushes land. HostedWindowController proxies — never replaces — SwiftUI's window delegate to intercept windowShouldClose for the flush, report frames, and place saved frames onto live screens. Card windows are (board path, case-folded card id) values: reopen focuses, and a snapshot-pure fate function dismisses on delete, tombstone, tombstoned lane, or cross-board move. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import AppKit
|
||||
import os
|
||||
|
||||
/// The three window-lifecycle answers SwiftUI has no modifier for (02-architecture.md § Launch and
|
||||
/// window lifecycle, § Windows).
|
||||
///
|
||||
/// It holds the `AppModel` rather than reaching for a singleton: `KanbanApp` creates the model and
|
||||
/// hands it over in its own `init`, so there is exactly one and no global to accidentally build a
|
||||
/// second registry behind.
|
||||
@MainActor
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
/// Set by `KanbanApp.init()`. Optional only because the adaptor constructs this object before the
|
||||
/// model exists; it is non-`nil` from the first run-loop turn onward.
|
||||
var appModel: AppModel?
|
||||
|
||||
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "app-delegate")
|
||||
|
||||
/// **The close is respected.** "Closing the last board window leaves the app windowless (menu bar
|
||||
/// alive)" — a document-shaped app whose windows are boards has no business quitting because the
|
||||
/// user tidied one away, and welcome is one Dock click or one menu item back.
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// A Dock click with nothing on screen shows welcome — the other half of the rule above.
|
||||
///
|
||||
/// `false` means "handled, do nothing further"; `true` lets AppKit run its default (unminiaturize,
|
||||
/// open an untitled document), which is right when windows do exist and wrong when they do not —
|
||||
/// this app has no untitled document to make.
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
||||
guard !hasVisibleWindows, let appModel else { return true }
|
||||
appModel.showWelcome()
|
||||
return false
|
||||
}
|
||||
|
||||
/// Quit runs the close flush for **every** open board before the app goes away.
|
||||
///
|
||||
/// The same `CloseFlushCoordinator` sequence as a user close, once per board, in the same fixed
|
||||
/// order — card windows and their sessions, then pending debounced work, then the registry stamp,
|
||||
/// then teardown (02 § Windows: "closing a board window (**and app quit**) first closes the
|
||||
/// board's card windows …"). The one difference is the cause: quit does not clear the open-now
|
||||
/// flags, which is what makes the next launch reopen exactly this set.
|
||||
///
|
||||
/// `.terminateLater` plus a deferred reply is the only way to await anything here — the delegate
|
||||
/// method is synchronous and the flush is not. With no boards open there is nothing to flush and
|
||||
/// the app exits immediately rather than taking a run-loop turn to discover that.
|
||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
guard let appModel, appModel.hasOpenBoards else { return .terminateNow }
|
||||
|
||||
Task { @MainActor in
|
||||
await appModel.flushAllBoardsForQuit()
|
||||
Self.logger.debug("quit flush complete")
|
||||
sender.reply(toApplicationShouldTerminate: true)
|
||||
}
|
||||
return .terminateLater
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user