forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEscapeKeyHandler.swift
More file actions
149 lines (128 loc) · 4.38 KB
/
Copy pathEscapeKeyHandler.swift
File metadata and controls
149 lines (128 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import AppKit
import SwiftUI
/// Maps Esc to a dismiss closure for custom overlay modals. These overlays are
/// ZStack layers, not NSWindow sheets, so AppKit gives them no cancel handling,
/// `onExitCommand` needs focus they never receive, and hidden SwiftUI buttons
/// with a cancel key equivalent get culled from key-equivalent dispatch. A
/// local key-down monitor scoped to the hosting window delivers Esc
/// deterministically. Render it only while its overlay is the topmost modal.
@MainActor
struct OverlayModalEscapeCatcher: View {
let action: () -> Void
var body: some View {
EscapeKeyHandler(priority: .modal) {
action()
return true
}
}
}
/// Declaration order is the priority order: `dispatchEscape` walks `allCases` **backwards**, so the
/// last case declared gets first refusal and the first case declared is the fallback nobody else
/// wanted.
enum EscapeKeyPriority: Int, CaseIterable {
/// The window itself. Lowest by construction, because putting the shell away is what Escape means
/// only when nothing inside it claimed the key — a page navigates Home first (`navigation`), an open
/// editor cancels first (`editing`), a modal closes first (`modal`). Registered by `ShellSummon`
/// against the window, not by a view, so no destination can forget to compose it.
case shell
case navigation
case content
case editing
case modal
}
@MainActor
struct EscapeKeyHandler: NSViewRepresentable {
let priority: EscapeKeyPriority
let action: () -> Bool
func makeNSView(context: Context) -> EscapeKeyHandlerView {
let view = EscapeKeyHandlerView()
view.priority = priority
view.action = action
return view
}
func updateNSView(_ nsView: EscapeKeyHandlerView, context: Context) {
nsView.priority = priority
nsView.action = action
}
}
@MainActor
final class EscapeKeyHandlerView: NSView {
var priority: EscapeKeyPriority = .content
var action: (() -> Bool)?
private var registration: UUID?
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
removeRegistration()
if let window {
registration = WindowEscapeKeyMonitor.shared.register(window: window, priority: priority) { [weak self] in
self?.action?() ?? false
}
}
}
override func hitTest(_ point: NSPoint) -> NSView? { nil }
deinit {
if let registration {
Task { @MainActor in
WindowEscapeKeyMonitor.shared.unregister(registration)
}
}
}
private func removeRegistration() {
if let registration {
WindowEscapeKeyMonitor.shared.unregister(registration)
self.registration = nil
}
}
}
@MainActor
final class WindowEscapeKeyMonitor {
private final class Registration {
weak var window: NSWindow?
let priority: EscapeKeyPriority
let action: () -> Bool
init(window: NSWindow, priority: EscapeKeyPriority, action: @escaping () -> Bool) {
self.window = window
self.priority = priority
self.action = action
}
}
static let shared = WindowEscapeKeyMonitor()
private var registrations: [UUID: Registration] = [:]
private var order: [UUID] = []
private var monitor: Any?
private init() {
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
self?.handle(event) ?? event
}
}
func register(window: NSWindow, priority: EscapeKeyPriority = .content, action: @escaping () -> Bool) -> UUID {
let id = UUID()
registrations[id] = Registration(window: window, priority: priority, action: action)
order.append(id)
return id
}
func unregister(_ id: UUID) {
registrations[id] = nil
order.removeAll { $0 == id }
}
private func handle(_ event: NSEvent) -> NSEvent? {
guard event.keyCode == 53 else { return event }
return dispatchEscape(in: event.window) ? nil : event
}
func dispatchEscape(in window: NSWindow?) -> Bool {
for priority in EscapeKeyPriority.allCases.reversed() {
for id in order.reversed() {
guard let registration = registrations[id] else { continue }
guard registration.priority == priority, registration.window === window else { continue }
if registration.action() { return true }
}
}
return false
}
}
extension View {
@MainActor
func onEscapeKey(priority: EscapeKeyPriority = .content, _ action: @escaping () -> Bool) -> some View {
overlay { EscapeKeyHandler(priority: priority, action: action) }
}
}