-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHotKey.swift
More file actions
166 lines (147 loc) · 5.82 KB
/
Copy pathHotKey.swift
File metadata and controls
166 lines (147 loc) · 5.82 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import AppKit
import Carbon
/// Registers one system-wide Carbon hotkey without requiring Accessibility access.
final class GlobalHotKey {
private static let signature: OSType = 0x436D4C74 // "CmLt"
private let action: () -> Void
private var hotKeyRef: EventHotKeyRef?
private var eventHandlerRef: EventHandlerRef?
init?(shortcut: HotKeySettings, action: @escaping () -> Void) {
self.action = action
var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
let installStatus = InstallEventHandler(
GetApplicationEventTarget(),
{ _, event, userData in
guard let event, let userData else { return OSStatus(eventNotHandledErr) }
let hotKey = Unmanaged<GlobalHotKey>.fromOpaque(userData).takeUnretainedValue()
var identifier = EventHotKeyID()
let status = GetEventParameter(
event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&identifier)
guard status == noErr, identifier.signature == GlobalHotKey.signature else {
return OSStatus(eventNotHandledErr)
}
DispatchQueue.main.async { hotKey.action() }
return noErr
},
1,
&eventType,
Unmanaged.passUnretained(self).toOpaque(),
&eventHandlerRef)
guard installStatus == noErr else {
log("could not install hotkey event handler (\(installStatus))")
return nil
}
let identifier = EventHotKeyID(signature: Self.signature, id: 1)
let registrationStatus = RegisterEventHotKey(
shortcut.keyCode,
shortcut.carbonModifiers,
identifier,
GetApplicationEventTarget(),
0,
&hotKeyRef)
guard registrationStatus == noErr else {
if let eventHandlerRef { RemoveEventHandler(eventHandlerRef) }
self.eventHandlerRef = nil
log("could not register hotkey \(shortcut.displayName) (\(registrationStatus))")
return nil
}
log("hotkey \(shortcut.displayName) registered")
}
deinit {
if let hotKeyRef { UnregisterEventHotKey(hotKeyRef) }
if let eventHandlerRef { RemoveEventHandler(eventHandlerRef) }
}
}
private extension HotKeySettings {
var carbonModifiers: UInt32 {
var result: UInt32 = 0
if command { result |= UInt32(cmdKey) }
if option { result |= UInt32(optionKey) }
if control { result |= UInt32(controlKey) }
if shift { result |= UInt32(shiftKey) }
return result
}
}
/// A small shortcut recorder used by the settings window.
final class HotKeyRecorderButton: NSButton {
var onChange: ((HotKeySettings?) -> Void)?
private var shortcut: HotKeySettings?
private var eventMonitor: Any?
private var isRecording = false
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
bezelStyle = .rounded
target = self
action = #selector(beginRecording(_:))
toolTip = "Click, then type a shortcut. Press Delete to turn it off."
setShortcut(nil)
}
required init?(coder: NSCoder) { fatalError("not used") }
func setShortcut(_ shortcut: HotKeySettings?) {
self.shortcut = shortcut
guard !isRecording else { return }
title = shortcut?.displayName ?? "Set Hotkey…"
}
@objc private func beginRecording(_ sender: Any?) {
stopRecording()
isRecording = true
title = "Type shortcut…"
window?.makeFirstResponder(self)
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
self?.record(event)
return nil
}
}
private func record(_ event: NSEvent) {
if event.keyCode == 53 { // Escape cancels recording.
stopRecording()
setShortcut(shortcut)
return
}
if event.keyCode == 51 || event.keyCode == 117 { // Delete disables it.
stopRecording()
setShortcut(nil)
onChange?(nil)
return
}
let flags = event.modifierFlags.intersection([.command, .option, .control, .shift])
guard !flags.isEmpty else {
NSSound.beep()
return
}
let recorded = HotKeySettings(
keyCode: UInt32(event.keyCode),
key: Self.keyName(for: event),
command: flags.contains(.command),
option: flags.contains(.option),
control: flags.contains(.control),
shift: flags.contains(.shift))
stopRecording()
setShortcut(recorded)
onChange?(recorded)
}
private func stopRecording() {
if let eventMonitor { NSEvent.removeMonitor(eventMonitor) }
eventMonitor = nil
isRecording = false
}
private static func keyName(for event: NSEvent) -> String {
let specialKeys: [UInt16: String] = [
36: "↩", 48: "⇥", 49: "Space", 51: "⌫", 53: "⎋",
115: "Home", 116: "Page Up", 117: "⌦", 119: "End", 121: "Page Down",
123: "←", 124: "→", 125: "↓", 126: "↑",
122: "F1", 120: "F2", 99: "F3", 118: "F4", 96: "F5", 97: "F6",
98: "F7", 100: "F8", 101: "F9", 109: "F10", 103: "F11", 111: "F12",
]
if let special = specialKeys[event.keyCode] { return special }
return event.charactersIgnoringModifiers?.uppercased() ?? "Key \(event.keyCode)"
}
deinit { stopRecording() }
}