From 8b1a9ce78c2f35c8a55dcc95897573abd2cc4f6e Mon Sep 17 00:00:00 2001 From: decodism <77468771+decodism@users.noreply.github.com> Date: Sun, 16 Jul 2023 13:57:56 +0200 Subject: [PATCH] Support `NSMenu` on macOS 14 (#136) Co-authored-by: Sindre Sorhus --- .../CarbonKeyboardShortcuts.swift | 3 +- Sources/KeyboardShortcuts/Utilities.swift | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift b/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift index e1d5a35e..3d8dc4b8 100644 --- a/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift @@ -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 diff --git a/Sources/KeyboardShortcuts/Utilities.swift b/Sources/KeyboardShortcuts/Utilities.swift index e3bc0ba7..5ac686e1 100644 --- a/Sources/KeyboardShortcuts/Utilities.swift +++ b/Sources/KeyboardShortcuts/Utilities.swift @@ -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