Part 2: The HotKeys API
About
In a previous article, I wrote about how I'd like to learn about all the APIs that I use to implement shortcuts in MacOS.
The last article detailed the CGEvent API. In this article, we'll be tackling an old deprecated Carbon API.
Learnings
NSApplicationDelegate
Unlike the previous API, this API requires a fully fledged GUI.
I didn't really want to try integrating this deprecated API with SwiftUI, so I build the GUI with NSApplicationDelegate instead.
The last time I wrote an NSApplicationDelegate was with Objective-C. It was actually a pleasant experience to port my demonstration code to Swift.
Many shortcuts, one handler
This API uses a single handler for all shortcuts. This means each time we use
InstallEventHandler, it overwrites the previously installed handler.
That means we must implement one handler for all actions.
// XXX: the action handler must be able to handle all shortcuts
let _ = InstallEventHandler(
GetApplicationEventTarget(), action, 1, [eventTypeSpec], data,
&handlerRef)A signature and some id
The RegisterEventHotKey API accepts a signature and an id as argument when
registering shortcuts.
The signature is just a four byte string to describe a family of shortcuts, and the id is a identifier within that family.
A global shortcut
When the application is running, the registered shortcuts will trigger the handler even when another application is focused.
Program
Finally, here's the program!
import Carbon
import Cocoa
class Hotkeys {
let modifiers = UInt32(optionKey)
func register(id: UInt32, keyCode: Int) {
var hotKeyRef: EventHotKeyRef?
let signature: OSType = 0x6162_6364 // XXX: 'abcd' in bytes
let hotKeyID = EventHotKeyID(
signature: signature, id: id)
let keyCode = UInt32(keyCode)
let _ = RegisterEventHotKey(
keyCode,
modifiers,
hotKeyID,
GetApplicationEventTarget(),
0,
&hotKeyRef)
}
func install(with data: UnsafeMutableRawPointer?) {
var handlerRef: EventHandlerRef?
let eventTypeSpec = EventTypeSpec(
eventClass: UInt32(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))
let _ = InstallEventHandler(
GetApplicationEventTarget(), action, 1, [eventTypeSpec], data,
&handlerRef)
}
let action: EventHandlerUPP = { _, event, data -> OSStatus in
let data = Unmanaged<Data>.fromOpaque(data!).takeUnretainedValue()
var hotkeyID = EventHotKeyID()
let status = GetEventParameter(
event!, UInt32(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil, MemoryLayout<EventHotKeyID>.size, nil, &hotkeyID)
switch hotkeyID.id {
case 1:
data.increment()
data.show()
case 2:
data.decrement()
data.show()
default:
break
}
return 0
}
}
class Data {
var label: NSTextField!
var counter: Int = 0
init() {
label = NSTextField(labelWithString: "0")
label.frame = NSRect(x: 20, y: 130, width: 360, height: 40)
label.alignment = .center
}
func increment() {
counter += 1
}
func decrement() {
counter -= 1
}
func show() {
label.stringValue = "\(counter)"
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var data: Data!
func applicationDidFinishLaunching(_ notification: Notification) {
let frame = NSRect(x: 0, y: 0, width: 400, height: 300)
let style: NSWindow.StyleMask = [.titled, .resizable, .closable]
window = NSWindow(
contentRect: frame, styleMask: style, backing: .buffered,
defer: false)
window.title = "Foobar"
window.center()
data = Data()
let hotkeys = Hotkeys()
hotkeys.register(id: 1, keyCode: kVK_UpArrow)
hotkeys.register(id: 2, keyCode: kVK_DownArrow)
let ptr = Unmanaged.passUnretained(self.data).toOpaque()
hotkeys.install(with: ptr)
window.contentView!.addSubview(data.label)
window.makeKeyAndOrderFront(nil)
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
func main() {
let app = NSApplication.shared
let delegate = AppDelegate()
app.setActivationPolicy(.regular)
app.delegate = delegate
app.run()
}
main()