diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift index 175898a..22084e7 100644 --- a/Example/Example/AppDelegate.swift +++ b/Example/Example/AppDelegate.swift @@ -24,7 +24,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { keyCombo: keyCombo, target: self, action: #selector(AppDelegate.tappedHotKey), - autoReRegisterOnKeyboardKeyCodesChange: true) + autoReRegisterOnKeyboardKeyCodesChange: true, + pausesWhenMenuIsTracking: true) hotKey.register() // Shift + Control + A @@ -68,6 +69,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func tappedHotKey() { print("hotKey!!!!") + let menu = NSMenu() + menu.addItem(NSMenuItem(title: "Menu", action: nil, keyEquivalent: "")) + menu.addItem(.separator()) + menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil) } @objc func tappedHotKey2() { diff --git a/Lib/Magnet/HotKey.swift b/Lib/Magnet/HotKey.swift index 8cc95b0..59ba033 100644 --- a/Lib/Magnet/HotKey.swift +++ b/Lib/Magnet/HotKey.swift @@ -30,6 +30,16 @@ open class HotKey: NSObject { /// change later, the registered hot key may no longer point to the intended /// physical key unless it is registered again. public let autoReRegisterOnKeyboardKeyCodesChange: Bool + /// When enabled, Magnet unregisters this hot key while an NSMenu is tracking + /// events and restores it after menu tracking ends. + /// + /// This prevents Carbon hot key events from being delivered after the menu + /// closes when they were pressed while AppKit's menu tracking run loop was + /// active. + /// + /// Hot keys whose KeyCombo uses doubled modifiers always behave as if this + /// option is enabled. + public let pausesWhenMenuIsTracking: Bool var hotKeyId: UInt32? var hotKeyRef: EventHotKeyRef? @@ -59,7 +69,8 @@ open class HotKey: NSObject { target: AnyObject, action: Selector, actionQueue: ActionQueue = .main, - autoReRegisterOnKeyboardKeyCodesChange: Bool = false + autoReRegisterOnKeyboardKeyCodesChange: Bool = false, + pausesWhenMenuIsTracking: Bool = false ) { self.identifier = identifier self.keyCombo = keyCombo @@ -68,6 +79,7 @@ open class HotKey: NSObject { self.action = action self.actionQueue = actionQueue self.autoReRegisterOnKeyboardKeyCodesChange = autoReRegisterOnKeyboardKeyCodesChange + self.pausesWhenMenuIsTracking = pausesWhenMenuIsTracking super.init() } @@ -76,6 +88,7 @@ open class HotKey: NSObject { keyCombo: KeyCombo, actionQueue: ActionQueue = .main, autoReRegisterOnKeyboardKeyCodesChange: Bool = false, + pausesWhenMenuIsTracking: Bool = false, handler: @escaping ((HotKey) -> Void) ) { self.identifier = identifier @@ -85,6 +98,7 @@ open class HotKey: NSObject { self.action = nil self.actionQueue = actionQueue self.autoReRegisterOnKeyboardKeyCodesChange = autoReRegisterOnKeyboardKeyCodesChange + self.pausesWhenMenuIsTracking = pausesWhenMenuIsTracking super.init() } } @@ -128,6 +142,7 @@ extension HotKey { return self.identifier == hotKey.identifier && self.keyCombo == hotKey.keyCombo && self.autoReRegisterOnKeyboardKeyCodesChange == hotKey.autoReRegisterOnKeyboardKeyCodesChange && + self.pausesWhenMenuIsTracking == hotKey.pausesWhenMenuIsTracking && self.hotKeyId == hotKey.hotKeyId && self.hotKeyRef == hotKey.hotKeyRef } diff --git a/Lib/Magnet/HotKeyCenter.swift b/Lib/Magnet/HotKeyCenter.swift index 44f5975..fe3d98d 100644 --- a/Lib/Magnet/HotKeyCenter.swift +++ b/Lib/Magnet/HotKeyCenter.swift @@ -18,6 +18,7 @@ open class HotKeyCenter { private var hotKeys = [String: HotKey]() private var hotKeyCount: UInt32 = 0 + private var isMenuTracking = false private let modifierEventHandler: ModifierEventHandler private let notificationCenter: NotificationCenter @@ -29,6 +30,7 @@ open class HotKeyCenter { installModifiersChangedEventHandlerIfNeeded() observeKeyboardKeyCodesChanges() observeApplicationTerminate() + observeMenuTracking() } deinit { @@ -45,6 +47,7 @@ extension HotKeyCenter { hotKeys[hotKey.identifier] = hotKey guard !hotKey.keyCombo.doubledModifiers else { return true } + guard !(isMenuTracking && hotKey.pausesWhenMenuIsTracking) else { return true } guard registerEvent(with: hotKey) else { unregister(with: hotKey) return false @@ -57,7 +60,7 @@ extension HotKeyCenter { private func registerEvent(with hotKey: HotKey) -> Bool { let hotKeyId = EventHotKeyID(signature: UTGetOSTypeFromString("Magnet" as CFString), id: hotKeyCount) var carbonHotKey: EventHotKeyRef? - let currentKeyCode = hotKey.keyCombo.currentKeyCode + let currentKeyCode = hotKey.registeredKeyCode ?? hotKey.keyCombo.currentKeyCode let error = RegisterEventHotKey(UInt32(currentKeyCode), UInt32(hotKey.keyCombo.modifiers), hotKeyId, @@ -92,13 +95,15 @@ extension HotKeyCenter { hotKeys.forEach { unregister(with: $1) } } - private func unregisterEvent(with hotKey: HotKey) { + private func unregisterEvent(with hotKey: HotKey, clearKeyCode: Bool = true) { if let carbonHotKey = hotKey.hotKeyRef { UnregisterEventHotKey(carbonHotKey) } hotKey.hotKeyId = nil hotKey.hotKeyRef = nil - hotKey.registeredKeyCode = nil + if clearKeyCode { + hotKey.registeredKeyCode = nil + } } } @@ -118,6 +123,17 @@ extension HotKeyCenter { object: nil) } + private func observeMenuTracking() { + notificationCenter.addObserver(self, + selector: #selector(HotKeyCenter.menuDidBeginTracking), + name: NSMenu.didBeginTrackingNotification, + object: nil) + notificationCenter.addObserver(self, + selector: #selector(HotKeyCenter.menuDidEndTracking), + name: NSMenu.didEndTrackingNotification, + object: nil) + } + @objc func applicationWillTerminate() { unregisterAll() } @@ -130,9 +146,38 @@ extension HotKeyCenter { let currentKeyCode = hotKey.keyCombo.currentKeyCode guard hotKey.registeredKeyCode != currentKeyCode else { return } unregisterEvent(with: hotKey) + guard !(isMenuTracking && hotKey.pausesWhenMenuIsTracking) else { return } registerEvent(with: hotKey) } } + + @objc func menuDidBeginTracking() { + isMenuTracking = true + pauseHotKeysForMenuTracking() + } + + @objc func menuDidEndTracking() { + isMenuTracking = false + resumeHotKeysPausedForMenuTracking() + } + + private func pauseHotKeysForMenuTracking() { + hotKeys.values + .filter { $0.pausesWhenMenuIsTracking } + .filter { !$0.keyCombo.doubledModifiers } + .filter { $0.hotKeyRef != nil } + .forEach { hotKey in + unregisterEvent(with: hotKey, clearKeyCode: false) + } + } + + private func resumeHotKeysPausedForMenuTracking() { + hotKeys.values + .filter { $0.pausesWhenMenuIsTracking } + .filter { !$0.keyCombo.doubledModifiers } + .filter { $0.hotKeyRef == nil } + .forEach { registerEvent(with: $0) } + } } // MARK: - HotKey Events