oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import AppKit
3import SwiftUI
4
5struct DocsCloseGuard: NSViewRepresentable {
6 let hasUnsaved: @MainActor () -> Bool
7 let save: @MainActor () async -> Bool
8 let discard: @MainActor () async -> Void
9 func makeNSView(context: Context) -> DocsCloseGuardView {
10 let view = DocsCloseGuardView()
11 view.hasUnsaved = hasUnsaved; view.save = save; view.discard = discard
12 return view
13 }
14 func updateNSView(_ view: DocsCloseGuardView, context: Context) {
15 view.hasUnsaved = hasUnsaved; view.save = save; view.discard = discard
16 }
17}
18
19@MainActor final class DocsCloseGuardView: NSView, NSWindowDelegate {
20 var hasUnsaved: @MainActor () -> Bool = { false }
21 var save: @MainActor () async -> Bool = { true }
22 var discard: @MainActor () async -> Void = {}
23 var presenting = false
24 nonisolated(unsafe) weak var previousDelegate: (any NSWindowDelegate)?
25 override func viewDidMoveToWindow() {
26 super.viewDidMoveToWindow()
27 guard let window, window.delegate !== self else { return }
28 DocsCloseRegistry.views.removeAll { $0.value == nil }
29 if !DocsCloseRegistry.views.contains(where: { $0.value === self }) {
30 DocsCloseRegistry.views.append(WeakDocsCloseGuard(value: self))
31 }
32 previousDelegate = window.delegate
33 window.delegate = self
34 }
35 override func responds(to selector: Selector!) -> Bool {
36 super.responds(to: selector) || previousDelegate?.responds(to: selector) == true
37 }
38 override func forwardingTarget(for selector: Selector!) -> Any? { previousDelegate }
39 func windowShouldClose(_ sender: NSWindow) -> Bool {
40 guard hasUnsaved() else { return previousDelegate?.windowShouldClose?(sender) ?? true }
41 guard !presenting else { return false }
42 presenting = true
43 let alert = Self.alert()
44 alert.beginSheetModal(for: sender) { [weak self] response in
45 guard let self else { return }
46 Task { @MainActor in
47 defer { self.presenting = false }
48 if response == .alertFirstButtonReturn, await self.save() { sender.performClose(nil) }
49 if response == .alertThirdButtonReturn { await self.discard(); sender.performClose(nil) }
50 }
51 }
52 return false
53 }
54 static func alert() -> NSAlert {
55 let alert = NSAlert()
56 alert.messageText = String(localized: "native.close.pending.title", bundle: DocsResources.bundle)
57 alert.informativeText = String(localized: "native.close.pending.detail", bundle: DocsResources.bundle)
58 alert.addButton(withTitle: String(localized: "native.close.pending.save", bundle: DocsResources.bundle))
59 alert.addButton(withTitle: String(localized: "native.close.pending.cancel", bundle: DocsResources.bundle))
60 alert.addButton(withTitle: String(localized: "native.discard.action", bundle: DocsResources.bundle))
61 return alert
62 }
63}
64@MainActor private struct WeakDocsCloseGuard { weak var value: DocsCloseGuardView? }
65@MainActor private enum DocsCloseRegistry { static var views: [WeakDocsCloseGuard] = [] }
66
67@MainActor public final class DocsApplicationDelegate: NSObject, NSApplicationDelegate {
68 public func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
69 let views = DocsCloseRegistry.views.compactMap(\.value).filter { $0.window != nil }
70 if views.contains(where: \.presenting) { return .terminateCancel }
71 let pending = views.filter { $0.hasUnsaved() }
72 guard !pending.isEmpty else { return .terminateNow }
73 let response = DocsCloseGuardView.alert().runModal()
74 guard response == .alertFirstButtonReturn || response == .alertThirdButtonReturn else { return .terminateCancel }
75 Task { @MainActor in
76 var saved = true
77 for view in pending {
78 if response == .alertThirdButtonReturn { await view.discard() }
79 else if !(await view.save()) { saved = false }
80 }
81 sender.reply(toApplicationShouldTerminate: saved && !views.contains { $0.hasUnsaved() })
82 }
83 return .terminateLater
84 }
85}
86#endif
87