From 3362cdbdf978fad688ffb031f5a1876b663f82d1 Mon Sep 17 00:00:00 2001 From: decodism <77468771+decodism@users.noreply.github.com> Date: Tue, 28 Feb 2023 22:11:45 +0100 Subject: [PATCH] Fix keyboard shortcuts when `NSMenu` is open (#122) Co-authored-by: Sindre Sorhus --- .../CarbonKeyboardShortcuts.swift | 157 ++++++++++++++++-- .../KeyboardShortcuts/KeyboardShortcuts.swift | 42 +++++ readme.md | 1 + 3 files changed, 190 insertions(+), 10 deletions(-) diff --git a/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift b/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift index 09fe3d2..98bcd44 100644 --- a/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/CarbonKeyboardShortcuts.swift @@ -8,7 +8,7 @@ enum CarbonKeyboardShortcuts { private final class HotKey { let shortcut: KeyboardShortcuts.Shortcut let carbonHotKeyId: Int - let carbonHotKey: EventHotKeyRef + var carbonHotKey: EventHotKeyRef? let onKeyDown: (KeyboardShortcuts.Shortcut) -> Void let onKeyUp: (KeyboardShortcuts.Shortcut) -> Void @@ -37,6 +37,15 @@ enum CarbonKeyboardShortcuts { private static var hotKeyId = 0 private static var eventHandler: EventHandlerRef? + private static let hotKeyEventTypes = [ + EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), + EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) + ] + private static let rawKeyEventTypes = [ + EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventRawKeyDown)), + EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventRawKeyUp)) + ] + private static func setUpEventHandlerIfNeeded() { guard eventHandler == nil, @@ -45,19 +54,48 @@ enum CarbonKeyboardShortcuts { return } - let eventSpecs = [ - EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)), - EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)) - ] - - InstallEventHandler( + var handler: EventHandlerRef? + let error = InstallEventHandler( dispatcher, carbonKeyboardShortcutsEventHandler, - eventSpecs.count, - eventSpecs, + 0, nil, - &eventHandler + nil, + &handler ) + + guard + error == noErr, + let handler + else { + return + } + + eventHandler = handler + + updateEventHandler() + } + + static func updateEventHandler() { + guard eventHandler != nil else { + return + } + + if KeyboardShortcuts.isEnabled { + if KeyboardShortcuts.isMenuOpen { + softUnregisterAll() + RemoveEventTypesFromHandler(eventHandler, hotKeyEventTypes.count, hotKeyEventTypes) + AddEventTypesToHandler(eventHandler, rawKeyEventTypes.count, rawKeyEventTypes) + } else { + softRegisterAll() + RemoveEventTypesFromHandler(eventHandler, rawKeyEventTypes.count, rawKeyEventTypes) + AddEventTypesToHandler(eventHandler, hotKeyEventTypes.count, hotKeyEventTypes) + } + } else { + softUnregisterAll() + RemoveEventTypesFromHandler(eventHandler, hotKeyEventTypes.count, hotKeyEventTypes) + RemoveEventTypesFromHandler(eventHandler, rawKeyEventTypes.count, rawKeyEventTypes) + } } static func register( @@ -95,6 +133,34 @@ enum CarbonKeyboardShortcuts { setUpEventHandlerIfNeeded() } + private static func softRegisterAll() { + for hotKey in hotKeys.values { + guard hotKey.carbonHotKey == nil else { + continue + } + + var eventHotKey: EventHotKeyRef? + let error = RegisterEventHotKey( + UInt32(hotKey.shortcut.carbonKeyCode), + UInt32(hotKey.shortcut.carbonModifiers), + EventHotKeyID(signature: hotKeySignature, id: UInt32(hotKey.carbonHotKeyId)), + GetEventDispatcherTarget(), + 0, + &eventHotKey + ) + + guard + error == noErr, + let eventHotKey + else { + hotKeys.removeValue(forKey: hotKey.carbonHotKeyId) + continue + } + + hotKey.carbonHotKey = eventHotKey + } + } + private static func unregisterHotKey(_ hotKey: HotKey) { UnregisterEventHotKey(hotKey.carbonHotKey) hotKeys.removeValue(forKey: hotKey.carbonHotKeyId) @@ -112,11 +178,31 @@ enum CarbonKeyboardShortcuts { } } + private static func softUnregisterAll() { + for hotKey in hotKeys.values { + UnregisterEventHotKey(hotKey.carbonHotKey) + hotKey.carbonHotKey = nil + } + } + fileprivate static func handleEvent(_ event: EventRef?) -> OSStatus { guard let event else { return OSStatus(eventNotHandledErr) } + switch Int(GetEventKind(event)) { + case kEventHotKeyPressed, kEventHotKeyReleased: + return handleHotKeyEvent(event) + case kEventRawKeyDown, kEventRawKeyUp: + return handleRawKeyEvent(event) + default: + break + } + + return OSStatus(eventNotHandledErr) + } + + private static func handleHotKeyEvent(_ event: EventRef) -> OSStatus { var eventHotKeyId = EventHotKeyID() let error = GetEventParameter( event, @@ -152,6 +238,57 @@ enum CarbonKeyboardShortcuts { return OSStatus(eventNotHandledErr) } + + private static func handleRawKeyEvent(_ event: EventRef) -> OSStatus { + var eventKeyCode = UInt32() + let keyCodeError = GetEventParameter( + event, + UInt32(kEventParamKeyCode), + typeUInt32, + nil, + MemoryLayout.size, + nil, + &eventKeyCode + ) + + guard keyCodeError == noErr else { + return keyCodeError + } + + var eventKeyModifiers = UInt32() + let keyModifiersError = GetEventParameter( + event, + UInt32(kEventParamKeyModifiers), + typeUInt32, + nil, + MemoryLayout.size, + nil, + &eventKeyModifiers + ) + + guard keyModifiersError == noErr else { + return keyModifiersError + } + + let shortcut = KeyboardShortcuts.Shortcut(carbonKeyCode: Int(eventKeyCode), carbonModifiers: Int(eventKeyModifiers)) + + guard let hotKey = (hotKeys.values.first { $0.shortcut == shortcut }) else { + return OSStatus(eventNotHandledErr) + } + + switch Int(GetEventKind(event)) { + case kEventRawKeyDown: + hotKey.onKeyDown(hotKey.shortcut) + return noErr + case kEventRawKeyUp: + hotKey.onKeyUp(hotKey.shortcut) + return noErr + default: + break + } + + return OSStatus(eventNotHandledErr) + } } extension CarbonKeyboardShortcuts { diff --git a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift index 3b1906a..9e498f8 100644 --- a/Sources/KeyboardShortcuts/KeyboardShortcuts.swift +++ b/Sources/KeyboardShortcuts/KeyboardShortcuts.swift @@ -37,6 +37,48 @@ public enum KeyboardShortcuts { */ static var isPaused = false + /** + Enable/disable monitoring of all keyboard shortcuts. + */ + public static var isEnabled = true { + didSet { + guard isEnabled != oldValue else { + return + } + + CarbonKeyboardShortcuts.updateEventHandler() + } + } + + /** + Set according to the opening state of your NSMenu if you want your keyboard shortcuts to work when it is open. + + ```swift + let menu = NSMenu() + let menuDelegate = MenuDelegate() + menu.delegate = menuDelegate + + class MenuDelegate: NSObject, NSMenuDelegate { + func menuWillOpen(_ menu: NSMenu) { + KeyboardShortcuts.isMenuOpen = true + } + + func menuDidClose(_ menu: NSMenu) { + KeyboardShortcuts.isMenuOpen = false + } + } + ``` + */ + public static var isMenuOpen = false { + didSet { + guard isMenuOpen != oldValue else { + return + } + + CarbonKeyboardShortcuts.updateEventHandler() + } + } + private static func register(_ shortcut: Shortcut) { guard !registeredShortcuts.contains(shortcut) else { return diff --git a/readme.md b/readme.md index 04fb15d..74ac060 100644 --- a/readme.md +++ b/readme.md @@ -193,6 +193,7 @@ This package: - Support for listening to key down, not just key up. - Swift Package Manager support. - Connect a shortcut to an `NSMenuItem`. +- Works when [`NSMenu` is open](https://github.com/sindresorhus/KeyboardShortcuts/issues/1) (e.g. menu bar apps). `MASShortcut`: - More mature.