-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHotKeyRecorderView.swift
More file actions
99 lines (90 loc) · 4.04 KB
/
Copy pathHotKeyRecorderView.swift
File metadata and controls
99 lines (90 loc) · 4.04 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
import SwiftUI
import AppKit
import Carbon.HIToolbox
/// Captures a single global hotkey by listening for a local key event while focused.
/// While "recording", an `NSEvent` local monitor intercepts the next keyDown with
/// at least one modifier (otherwise we'd swallow normal typing) and returns nil so
/// the host text field never sees it.
struct HotKeyRecorderView: View {
@ObservedObject var prefs: PreferencesStore
@State private var recording = false
@State private var monitor: Any?
// Cleanup when the Preferences window resigns key. macOS SwiftUI TabView keeps
// sibling tabs alive (no onDisappear when switching to a sibling), so we'd otherwise
// leave the monitor installed and swallow ⌘W / ⌘, / etc.
@State private var resignObserver: NSObjectProtocol?
var body: some View {
HStack(spacing: 12) {
Text(currentLabel)
.font(.system(.body, design: .monospaced))
.frame(minWidth: 140, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 6)
.strokeBorder(recording ? Color.accentColor : Color.secondary.opacity(0.4),
lineWidth: recording ? 2 : 1)
)
Button(recording
? NSLocalizedString("hotkey.recordingButton", value: "Press a key…", comment: "")
: NSLocalizedString("hotkey.recordButton", value: "Record", comment: "")) {
if recording { stop() } else { start() }
}
.keyboardShortcut(.defaultAction)
Button(NSLocalizedString("hotkey.reset", value: "Reset", comment: "")) {
prefs.setHotKey(keyCode: UInt32(kVK_ANSI_F),
carbonModifiers: UInt32(optionKey))
}
}
.onAppear { startResignObserver() }
.onDisappear { stop(); stopResignObserver() }
}
private var currentLabel: String {
ModifierTranslator.symbolicDescription(carbonModifiers: prefs.hotKeyCarbonModifiers,
keyCode: prefs.hotKeyKeyCode)
}
private func start() {
recording = true
// .keyDown only. We require at least one non-shift modifier so the recorder
// can't capture plain letters — that would brick the user's typing.
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
// Defensive: if the monitor outlives `recording` (TabView keeps siblings alive),
// fall through and let the keyDown propagate normally instead of swallowing it.
guard recording else { return event }
let cocoa = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
let nonShiftModifiers: NSEvent.ModifierFlags = [.command, .option, .control]
if !cocoa.intersection(nonShiftModifiers).isEmpty {
let carbon = ModifierTranslator.carbonFlags(from: cocoa)
prefs.setHotKey(keyCode: UInt32(event.keyCode), carbonModifiers: carbon)
stop()
return nil
}
// Escape cancels recording without changing the binding.
if event.keyCode == UInt16(kVK_Escape) {
stop()
return nil
}
return event
}
}
private func stop() {
recording = false
if let monitor {
NSEvent.removeMonitor(monitor)
self.monitor = nil
}
}
private func startResignObserver() {
guard resignObserver == nil else { return }
resignObserver = NotificationCenter.default.addObserver(
forName: NSWindow.didResignKeyNotification,
object: nil,
queue: .main) { _ in stop() }
}
private func stopResignObserver() {
if let obs = resignObserver {
NotificationCenter.default.removeObserver(obs)
resignObserver = nil
}
}
}