How to listen to global hotkeys with Swift in an OS X app?

前端 未结 6 1750
北荒
北荒 2020-12-12 17:08

I\'m trying to have a handler in my Mac OS X app written in Swift for a global (system-wide) hotkey combo but I just cannot find proper documentation for it. I\'ve read that

相关标签:
6条回答
  • 2020-12-12 17:41

    Since Swift 2.0, you can now pass a function pointer to C APIs.

    var gMyHotKeyID = EventHotKeyID()
    gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
    gMyHotKeyID.id = UInt32(keyCode)
    
    var eventType = EventTypeSpec()
    eventType.eventClass = OSType(kEventClassKeyboard)
    eventType.eventKind = OSType(kEventHotKeyPressed)
    
    // Install handler.
    InstallEventHandler(GetApplicationEventTarget(), {(nextHanlder, theEvent, userData) -> OSStatus in
        var hkCom = EventHotKeyID()
        GetEventParameter(theEvent, EventParamName(kEventParamDirectObject), EventParamType(typeEventHotKeyID), nil, sizeof(EventHotKeyID), nil, &hkCom)
    
        // Check that hkCom in indeed your hotkey ID and handle it.
    }, 1, &eventType, nil, nil)
    
    // Register hotkey.
    let status = RegisterEventHotKey(UInt32(keyCode), UInt32(modifierKeys), gMyHotKeyID, GetApplicationEventTarget(), 0, &hotKeyRef)
    
    0 讨论(0)
  • 2020-12-12 17:46

    Take a look at the HotKey Library. You can simply use Carthage to implement it into your own app. HotKey Library

    0 讨论(0)
  • 2020-12-12 17:53

    A quick Swift 3 update for the setup:

    let opts = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionary
    
    guard AXIsProcessTrustedWithOptions(opts) == true else { return }
    
    NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: self.handler)
    
    0 讨论(0)
  • 2020-12-12 17:55

    The following code works for me for Swift 5.0.1. This solution is the combination of the solution from the accepted answer by Charlie Monroe and the recommendation by Rob Napier to use DDHotKey.

    DDHotKey seems to work out of the box but it had one limitation that I had to change: the eventKind is hardcoded to kEventHotKeyReleased while I needed both kEventHotKeyPressed and kEventHotKeyReleased event types.

    eventSpec.eventKind = kEventHotKeyReleased;
    

    If you want to handle both Pressed and Released events, just add a second InstallEventHandler call which registers the other event kind.

    This the complete example of the code that registers the "Command + R" key for the kEventHotKeyReleased type.

    import Carbon
    
    extension String {
      /// This converts string to UInt as a fourCharCode
      public var fourCharCodeValue: Int {
        var result: Int = 0
        if let data = self.data(using: String.Encoding.macOSRoman) {
          data.withUnsafeBytes({ (rawBytes) in
            let bytes = rawBytes.bindMemory(to: UInt8.self)
            for i in 0 ..< data.count {
              result = result << 8 + Int(bytes[i])
            }
          })
        }
        return result
      }
    }
    
    class HotkeySolution {
      static
      func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 {
        let flags = cocoaFlags.rawValue
        var newFlags: Int = 0
    
        if ((flags & NSEvent.ModifierFlags.control.rawValue) > 0) {
          newFlags |= controlKey
        }
    
        if ((flags & NSEvent.ModifierFlags.command.rawValue) > 0) {
          newFlags |= cmdKey
        }
    
        if ((flags & NSEvent.ModifierFlags.shift.rawValue) > 0) {
          newFlags |= shiftKey;
        }
    
        if ((flags & NSEvent.ModifierFlags.option.rawValue) > 0) {
          newFlags |= optionKey
        }
    
        if ((flags & NSEvent.ModifierFlags.capsLock.rawValue) > 0) {
          newFlags |= alphaLock
        }
    
        return UInt32(newFlags);
      }
    
      static func register() {
        var hotKeyRef: EventHotKeyRef?
        let modifierFlags: UInt32 =
          getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags.command)
    
        let keyCode = kVK_ANSI_R
        var gMyHotKeyID = EventHotKeyID()
    
        gMyHotKeyID.id = UInt32(keyCode)
    
        // Not sure what "swat" vs "htk1" do.
        gMyHotKeyID.signature = OSType("swat".fourCharCodeValue)
        // gMyHotKeyID.signature = OSType("htk1".fourCharCodeValue)
    
        var eventType = EventTypeSpec()
        eventType.eventClass = OSType(kEventClassKeyboard)
        eventType.eventKind = OSType(kEventHotKeyReleased)
    
        // Install handler.
        InstallEventHandler(GetApplicationEventTarget(), {
          (nextHanlder, theEvent, userData) -> OSStatus in
          // var hkCom = EventHotKeyID()
    
          // GetEventParameter(theEvent,
          //                   EventParamName(kEventParamDirectObject),
          //                   EventParamType(typeEventHotKeyID),
          //                   nil,
          //                   MemoryLayout<EventHotKeyID>.size,
          //                   nil,
          //                   &hkCom)
    
          NSLog("Command + R Released!")
    
          return noErr
          /// Check that hkCom in indeed your hotkey ID and handle it.
        }, 1, &eventType, nil, nil)
    
        // Register hotkey.
        let status = RegisterEventHotKey(UInt32(keyCode),
                                         modifierFlags,
                                         gMyHotKeyID,
                                         GetApplicationEventTarget(),
                                         0,
                                         &hotKeyRef)
        assert(status == noErr)
      }
    }
    
    0 讨论(0)
  • 2020-12-12 18:00

    there is a pretty hacky, but also pretty simple workaround if your app has a Menu:

    • add a new MenuItem (maybe call it something like "Dummy for Hotkey")
    • in the attributes inspector, conveniently enter your hotkey in the Key Equivalent field
    • set Allowed when Hidden, Enabled and Hidden to true
    • link it with an IBAction to do whatever your hotkey is supposed to do

    done!

    0 讨论(0)
  • 2020-12-12 18:03

    I don't believe you can do this in 100% Swift today. You'll need to call InstallEventHandler() or CGEventTapCreate(), and both of those require a CFunctionPointer, which can't be created in Swift. Your best plan is to use established ObjC solutions such as DDHotKey and bridge to Swift.

    You can try using NSEvent.addGlobalMonitorForEventsMatchingMask(handler:), but that only makes copies of events. You can't consume them. That means the hotkey will also be passed along to the currently active app, which can cause problems. Here's an example, but I recommend the ObjC approach; it's almost certainly going to work better.

    let keycode = UInt16(kVK_ANSI_X)
    let keymask: NSEventModifierFlags = .CommandKeyMask | .AlternateKeyMask | .ControlKeyMask
    
    func handler(event: NSEvent!) {
        if event.keyCode == self.keycode &&
            event.modifierFlags & self.keymask == self.keymask {
                println("PRESSED")
        }
    }
    
    // ... to set it up ...
        let options = NSDictionary(object: kCFBooleanTrue, forKey: kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString) as CFDictionaryRef
        let trusted = AXIsProcessTrustedWithOptions(options)
        if (trusted) {
            NSEvent.addGlobalMonitorForEventsMatchingMask(.KeyDownMask, handler: self.handler)
        }
    

    This also requires that accessibility services be approved for this app. It also doesn't capture events that are sent to your own application, so you have to either capture them with your responder chain, our use addLocalMointorForEventsMatchingMask(handler:) to add a local handler.

    0 讨论(0)
提交回复
热议问题