oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

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