| 1 | #if os(macOS) |
| 2 | import AppKit |
| 3 | import SwiftUI |
| 4 | // Native windows must never silently discard a focused or failed edit. |
| 5 | struct SheetCloseGuard: NSViewRepresentable { |
| 6 | let model: SheetModel |
| 7 | func makeNSView(context: Context) -> CloseGuardView { |
| 8 | let view = CloseGuardView() |
| 9 | view.model = model |
| 10 | return view |
| 11 | } |
| 12 | func updateNSView(_ view: CloseGuardView, context: Context) { view.model = model } |
| 13 | } |
| 14 | @MainActor final class CloseGuardView: NSView, NSWindowDelegate { |
| 15 | weak var model: SheetModel? |
| 16 | // AppKit delegate messaging and mutation occur on the window’s main thread. |
| 17 | nonisolated(unsafe) weak var previousDelegate: (any NSWindowDelegate)? |
| 18 | override func viewDidMoveToWindow() { |
| 19 | super.viewDidMoveToWindow() |
| 20 | guard let window, window.delegate !== self else { return } |
| 21 | SheetCloseRegistry.views.removeAll { $0.value == nil } |
| 22 | if !SheetCloseRegistry.views.contains(where: { $0.value === self }) { |
| 23 | SheetCloseRegistry.views.append(WeakCloseGuard(value: self)) |
| 24 | } |
| 25 | previousDelegate = window.delegate |
| 26 | window.delegate = self |
| 27 | } |
| 28 | override func responds(to selector: Selector!) -> Bool { |
| 29 | super.responds(to: selector) || previousDelegate?.responds(to: selector) == true |
| 30 | } |
| 31 | override func forwardingTarget(for selector: Selector!) -> Any? { previousDelegate } |
| 32 | func windowShouldClose(_ sender: NSWindow) -> Bool { |
| 33 | guard let model else { return previousDelegate?.windowShouldClose?(sender) ?? true } |
| 34 | if model.busy { |
| 35 | model.error = "Wait for the current save to finish before closing this window." |
| 36 | return false |
| 37 | } |
| 38 | guard model.hasUnconfirmedChanges else { |
| 39 | return previousDelegate?.windowShouldClose?(sender) ?? true |
| 40 | } |
| 41 | let alert = NSAlert() |
| 42 | alert.messageText = "Keep your cell edit?" |
| 43 | alert.informativeText = "This cell has changes that have not been saved." |
| 44 | alert.addButton(withTitle: "Save") |
| 45 | alert.addButton(withTitle: "Discard") |
| 46 | alert.addButton(withTitle: "Cancel") |
| 47 | alert.beginSheetModal(for: sender) { response in |
| 48 | Task { @MainActor in |
| 49 | if response == .alertFirstButtonReturn { |
| 50 | await model.saveAll() |
| 51 | if !model.hasUnconfirmedChanges { sender.performClose(nil) } |
| 52 | } else if response == .alertSecondButtonReturn { |
| 53 | model.discardAll() |
| 54 | sender.performClose(nil) |
| 55 | } |
| 56 | } |
| 57 | } |
| 58 | return false |
| 59 | } |
| 60 | } |
| 61 | @MainActor struct WeakCloseGuard { weak var value: CloseGuardView? } |
| 62 | @MainActor enum SheetCloseRegistry { static var views: [WeakCloseGuard] = [] } |
| 63 | @MainActor public final class SheetsApplicationDelegate: NSObject, NSApplicationDelegate { |
| 64 | public func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply |
| 65 | { |
| 66 | let models = SheetCloseRegistry.views.compactMap { $0.value?.model } |
| 67 | if models.contains(where: { $0.busy }) { return .terminateCancel } |
| 68 | let pending = models.filter { $0.hasUnconfirmedChanges } |
| 69 | if pending.isEmpty { return .terminateNow } |
| 70 | let alert = NSAlert() |
| 71 | alert.messageText = "Save your spreadsheet edits before quitting?" |
| 72 | alert.addButton(withTitle: "Save") |
| 73 | alert.addButton(withTitle: "Discard") |
| 74 | alert.addButton(withTitle: "Cancel") |
| 75 | switch alert.runModal() { |
| 76 | case .alertFirstButtonReturn: |
| 77 | Task { @MainActor in |
| 78 | for model in pending { await model.saveAll() } |
| 79 | sender.reply(toApplicationShouldTerminate: !pending.contains { $0.hasUnconfirmedChanges }) |
| 80 | } |
| 81 | return .terminateLater |
| 82 | case .alertSecondButtonReturn: |
| 83 | pending.forEach { $0.discardAll() } |
| 84 | return .terminateNow |
| 85 | default: return .terminateCancel |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | #endif |
| 90 | |