All about macOS event observation
Takayama Fumihiko
https://pqrs.org/
Karabiner-Elements author
Jul 7, 2019
Overview
Introduction
Input event observing essentials
Typical Usage
macOS event observation methods
Detail and demonstration
[NSView keyDown]
[NSApplication sendEvent]
[NSView keyDown], [NSApplication sendEvent]
Demonstration: [NSView keyDown]
Code: [NSView keyDown]
ExampleView.swift:
class ExampleView: NSView {
override var acceptsFirstResponder: Bool { return true }
override func keyDown(with event: NSEvent) {
}
override func keyUp(with event: NSEvent) {
}
override func flagsChanged(with event: NSEvent) {
}
}
Demonstration: [NSApplication sendEvent]
Code: [NSApplication sendEvent]
ExampleApplication.swift:
class ExampleApplication: NSApplication {
override func sendEvent(_ event: NSEvent) {
switch event.type {
case .keyDown:
...
case .keyUp:
...
case .flagsChanged:
...
default:
break
}
super.sendEvent(event)
}
}
CGEventTapCreate
[NSEvent addGlobalMonitorForEvents]
CGEventTapCreate
Demonstration: CGEventTapCreate
Code: CGEventTapCreate (1)
CGEventTapExample.m:
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | ...;
CGEventTapCreate(kCGHIDEventTap, kCGTailAppendEventTap, kCGEventTapOptionListenOnly,
mask, callback, ...);
CGEventTapEnable(tap, true);
- (CGEventRef)callback:... {
switch (type) {
case kCGEventKeyDown:
case kCGEventKeyUp:
case kCGEventFlagsChanged:
...
break;
}
return event;
}
Code: CGEventTapCreate (2)
CGEventTapExample.m:
Do not forget to handle `kCGEventTapDisabledByTimeout` in `callback`.
- (CGEventRef)callback:... {
switch (type) {
case kCGEventTapDisabledByTimeout:
CGEventTapEnable(self.eventTap, true);
Break;
case kCGEventKeyDown:
...
Code: CGEventTapCreate (3)
AppDelegate.m:
You should check Accessibility state, and relaunch app when the state is changed.
(The Accessibility feature is not activated until the app is relaunched.)
Note: macOS 10.15 or later, you should also do it for Input Monitoring. (Is API provided ???)
- (void)applicationDidFinishLaunching:(NSNotification*)notification {
NSDictionary* options = @{(__bridge NSString*)(kAXTrustedCheckOptionPrompt) : @YES};
if (!AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options)) {
// Create a timer which calls `AXIsProcessTrusted` and relaunch app if it returns true.
}
}
Demonstration: [NSEvent addGlobalMonitorForEvents]
Code: [NSEvent addGlobalMonitorForEvents] (1)
AppDelegate.swift:
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_: Notification) {
NSEvent.addGlobalMonitorForEvents(
matching: [NSEventMask.keyDown, NSEventMask.keyUp, NSEventMask.flagsChanged],
handler: { (event: NSEvent) in
switch event.type {
case .keyDown:
case .keyUp:
case .flagsChanged:
}})
}
}
Code: [NSEvent addGlobalMonitorForEvents] (2)
AppDelegate.swift:
You should check Accessibility state, and relaunch app when the state is changed.
(The Accessibility feature is not activated until the app is relaunched.)
Note: macOS 10.15 or later, you should also do it for Input Monitoring. (Is API provided ???)
func applicationDidFinishLaunching(_: Notification) {
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
if !AXIsProcessTrustedWithOptions(options) {
timer = Timer.scheduledTimer(
withTimeInterval: 3.0,
repeats: true
) { _ in self.relaunchIfProcessTrusted() }
}
}
IOHIDQueueRegisterValueAvailableCallback
Demonstration: IOHIDQueueRegisterValueAvailableCallback
IOKit IOHIDDevice
Code: IOHIDQueueRegisterValueAvailableCallback
vendor/include/pqrs/*:
You should call `IOHIDQueueStart` before you open the device by `IOHIDDeviceOpen` in order to avoid missing events.
IOHIDQueueCreate(...);
IOHIDQueueAddElement(...);
IOHIDQueueRegisterValueAvailableCallback(...);
IOHIDQueueScheduleWithRunLoop(...);
IOHIDQueueStart(...);
IOHIDDeviceOpen(...);
Others
IOHIDDeviceRegisterInputReportCallback
Input source
IOHIKeyboard function pointer replacement
Code: IOHIKeyboard
/IOHIDSystem/IOKit/hidsystem/IOHIKeyboard.h:
typedef void (*KeyboardEventAction)(OSObject * target, ...);
typedef void (*KeyboardSpecialEventAction)(OSObject * target, ...);
typedef void (*UpdateEventFlagsAction)(OSObject * target, ...);
class IOHIKeyboard : public IOHIDevice
{
protected:
OSObject * _keyboardEventTarget;
KeyboardEventAction _keyboardEventAction;
OSObject * _keyboardSpecialEventTarget;
KeyboardSpecialEventAction _keyboardSpecialEventAction;
OSObject * _updateEventFlagsTarget;
UpdateEventFlagsAction _updateEventFlagsAction;
}
Custom Driver
Method swizzling + [NSApplication sendEvent]
Appendix
Lifetime of the user approval for Accessibility
sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db
sqlite> select * from access;