#if os(macOS) import AppKit import SwiftUI // Native windows must never silently discard a focused or failed edit. struct SheetCloseGuard: NSViewRepresentable { let model: SheetModel func makeNSView(context: Context) -> CloseGuardView { let view = CloseGuardView() view.model = model return view } func updateNSView(_ view: CloseGuardView, context: Context) { view.model = model } } @MainActor final class CloseGuardView: NSView, NSWindowDelegate { weak var model: SheetModel? // AppKit delegate messaging and mutation occur on the window’s main thread. nonisolated(unsafe) weak var previousDelegate: (any NSWindowDelegate)? override func viewDidMoveToWindow() { super.viewDidMoveToWindow() guard let window, window.delegate !== self else { return } SheetCloseRegistry.views.removeAll { $0.value == nil } if !SheetCloseRegistry.views.contains(where: { $0.value === self }) { SheetCloseRegistry.views.append(WeakCloseGuard(value: self)) } previousDelegate = window.delegate window.delegate = self } override func responds(to selector: Selector!) -> Bool { super.responds(to: selector) || previousDelegate?.responds(to: selector) == true } override func forwardingTarget(for selector: Selector!) -> Any? { previousDelegate } func windowShouldClose(_ sender: NSWindow) -> Bool { guard let model else { return previousDelegate?.windowShouldClose?(sender) ?? true } if model.busy { model.error = "Wait for the current save to finish before closing this window." return false } guard model.hasUnconfirmedChanges else { return previousDelegate?.windowShouldClose?(sender) ?? true } let alert = NSAlert() alert.messageText = "Keep your cell edit?" alert.informativeText = "This cell has changes that have not been saved." alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Discard") alert.addButton(withTitle: "Cancel") alert.beginSheetModal(for: sender) { response in Task { @MainActor in if response == .alertFirstButtonReturn { await model.saveAll() if !model.hasUnconfirmedChanges { sender.performClose(nil) } } else if response == .alertSecondButtonReturn { model.discardAll() sender.performClose(nil) } } } return false } } @MainActor struct WeakCloseGuard { weak var value: CloseGuardView? } @MainActor enum SheetCloseRegistry { static var views: [WeakCloseGuard] = [] } @MainActor public final class SheetsApplicationDelegate: NSObject, NSApplicationDelegate { public func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let models = SheetCloseRegistry.views.compactMap { $0.value?.model } if models.contains(where: { $0.busy }) { return .terminateCancel } let pending = models.filter { $0.hasUnconfirmedChanges } if pending.isEmpty { return .terminateNow } let alert = NSAlert() alert.messageText = "Save your spreadsheet edits before quitting?" alert.addButton(withTitle: "Save") alert.addButton(withTitle: "Discard") alert.addButton(withTitle: "Cancel") switch alert.runModal() { case .alertFirstButtonReturn: Task { @MainActor in for model in pending { await model.saveAll() } sender.reply(toApplicationShouldTerminate: !pending.contains { $0.hasUnconfirmedChanges }) } return .terminateLater case .alertSecondButtonReturn: pending.forEach { $0.discardAll() } return .terminateNow default: return .terminateCancel } } } #endif