How to Listen to Global Hotkeys With Swift in a Macos App

How to listen to global hotkeys with Swift in a macOS app?

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)

How to detect keystrokes globally in Swift on macOS?

Looks like the "duplicate" mark got removed, but so has the answer that I kludged into the comments section. So, for posterity:

The reason this doesn't work is because global monitors for .keyDown events require more permissions than some of the other event handlers, including the one that somebody thought this was a duplicate of. This is mainly because global .keyDown monitors can be used for nefarious purposes, such as keyloggers. So there are additional security measures in place to make sure we're legit:

1) Your app needs to be code-signed.

2) Your app needs to not have the App Sandbox enabled, and:

3) Your app needs to be registered in the Security and Privacy preference pane, under Accessibility.

The third one of these things has to be enabled by the user, but you can nudge them in that direction with this code:

let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true]
let accessEnabled = AXIsProcessTrustedWithOptions(options)

if !accessEnabled {
print("Access Not Enabled")
}

This will prompt the user, giving him/her the option to automatically open the appropriate preference pane where the user can allow your app to control the computer via the Accessibility API, which, assuming your app is signed and not sandboxed, will allow your global .keyDown monitor to work.

macOS App: handling key combinations bound to global keyboard shortcuts

I solved this ages ago but I only just noticed I never posted it here. The answer ended up involving CGSSetGlobalHotKeyOperatingMode(). This is not a public API, but there are a number of Mac App Store apps which use it by obfuscating the function name and looking it up dynamically. Apple doesn't seem to mind. The API is pretty straightforward to use, and there's plenty of open example source code floating about.

Cocoa Listen to Keyboard command+up Event

I've found the solution:

self.keyMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.keyDown, handler: { (event) -> NSEvent? in

if (event.modifierFlags.contains(.command)){
if (Int(event.keyCode) == kVK_UpArrow){
print("command up");
return nil;
}
}

return event;

});

The key point is to interrupt the keydown event and prevent it from being dispatched by returning nil

SwiftUI Invoke NSPopover with Keyboard Shortcut

Swift 5 solution was presented in https://stackoverflow.com/a/58225397/3984522. However, there's a nice package, which does the job https://github.com/soffes/HotKey in a couple of lines of code:


import SwiftUI
import HotKey

@main
struct MenuBarPopoverApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings{
EmptyView()
}
.commands {
MenuBarPopoverCommands(appDelegate: appDelegate)
}
}
}

class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
var popover = NSPopover.init()
var statusBarItem: NSStatusItem?
var contentView: ContentView!
let hotKey = HotKey(key: .x, modifiers: [.control, .shift]) // Global hotkey

override class func awakeFromNib() {}

func applicationDidFinishLaunching(_ notification: Notification) {
print("Application launched")
NSApplication.shared.activate(ignoringOtherApps: true)

contentView = ContentView()
popover.animates = false
popover.behavior = .transient

let contentVc = NSViewController()
contentVc.view = NSHostingView(rootView: contentView.environmentObject(self))
popover.contentViewController = contentVc

statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let itemImage = NSImage(systemSymbolName: "eye", accessibilityDescription: "eye")
itemImage?.isTemplate = true
statusBarItem?.button?.image = itemImage
statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:))

hotKey.keyUpHandler = { // Global hotkey handler
self.togglePopover()
}
}

@objc func showPopover(_ sender: AnyObject? = nil) {
if let button = statusBarItem?.button {
NSApplication.shared.activate(ignoringOtherApps: true)
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}

@objc func closePopover(_ sender: AnyObject? = nil) {
popover.performClose(sender)
}

@objc func togglePopover(_ sender: AnyObject? = nil) {
if popover.isShown {
closePopover(sender)
} else {
showPopover(sender)
}
}
}


Related Topics



Leave a reply



Submit