Skip to content

Commit

Permalink
Support NSMenu on macOS 14 (#136)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
decodism and sindresorhus committed Jul 16, 2023
1 parent 9b8aa73 commit 8b1a9ce
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 2 deletions.
3 changes: 1 addition & 2 deletions Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift
Expand Up @@ -46,8 +46,7 @@ enum CarbonKeyboardShortcuts {
EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventRawKeyUp))
]

// `keyDown` not working
private static let keyEventMonitor = LocalEventMonitor(events: [.keyDown, .keyUp]) { event in
private static let keyEventMonitor = RunLoopLocalEventMonitor(events: [.keyDown, .keyUp], runLoopMode: .eventTracking) { event in
guard
let eventRef = OpaquePointer(event.eventRef),
handleRawKeyEvent(eventRef) == noErr
Expand Down
59 changes: 59 additions & 0 deletions Sources/KeyboardShortcuts/Utilities.swift
Expand Up @@ -87,6 +87,65 @@ final class LocalEventMonitor {
}


final class RunLoopLocalEventMonitor {
private let runLoopMode: RunLoop.Mode
private let callback: (NSEvent) -> NSEvent?
private let observer: CFRunLoopObserver

init(
events: NSEvent.EventTypeMask,
runLoopMode: RunLoop.Mode,
callback: @escaping (NSEvent) -> NSEvent?
) {
self.runLoopMode = runLoopMode
self.callback = callback

self.observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.beforeSources.rawValue, true, 0) { _, _ in
// Pull all events from the queue and handle the ones matching the given types.
// Non-matching events are left untouched, maintaining their order in the queue.

var eventsToHandle = [NSEvent]()

// Retrieve all events from the event queue to preserve their order (instead of using the `matching` parameter).
while let eventToHandle = NSApp.nextEvent(matching: .any, until: nil, inMode: .default, dequeue: true) {
eventsToHandle.append(eventToHandle)
}

// Iterate over the gathered events, instead of doing it directly in the `while` loop, to avoid potential infinite loops caused by re-retrieving undiscarded events.
for eventToHandle in eventsToHandle {
var handledEvent: NSEvent?

if !events.contains(NSEvent.EventTypeMask(rawValue: 1 << eventToHandle.type.rawValue)) {
handledEvent = eventToHandle
} else if let callbackEvent = callback(eventToHandle) {
handledEvent = callbackEvent
}

guard let handledEvent else {
continue
}

NSApp.postEvent(handledEvent, atStart: false)
}
}
}

deinit {
stop()
}

@discardableResult
func start() -> Self {
CFRunLoopAddObserver(RunLoop.current.getCFRunLoop(), observer, CFRunLoopMode(runLoopMode.rawValue as CFString))
return self
}

func stop() {
CFRunLoopRemoveObserver(RunLoop.current.getCFRunLoop(), observer, CFRunLoopMode(runLoopMode.rawValue as CFString))
}
}


extension NSEvent {
static var modifiers: ModifierFlags {
modifierFlags
Expand Down

0 comments on commit 8b1a9ce

Please sign in to comment.