OS X: Detect system-wide keyDown events?
Asked Answered
M

4

64

I'm working on a typing-tutor application for Mac OS X that needs to have keystrokes forwarded to it, even when the application is not in focus.

Is there a way to have the system forward keystrokes to the app, possibly through NSDistributedNotificationCenter? I've googled myself silly, and haven't been able to find an answer...

EDIT: Sample code below.

Thanks @NSGod for pointing me in the right direction -- I ended up adding a global events monitor using the method addGlobalMonitorForEventsMatchingMask:handler:, which works beautifully. For completeness, my implementation looks like this:

// register for keys throughout the device...
[NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask
                                       handler:^(NSEvent *event){

    NSString *chars = [[event characters] lowercaseString];
    unichar character = [chars characterAtIndex:0];

    NSLog(@"keydown globally! Which key? This key: %c", character);

}];

For me, the tricky part was using blocks, so I'll give a little description in case it helps anyone:

The thing to notice about the above code is that it's all one single method call on NSEvent. The block is supplied as an argument, directly to the function. You could think of it kind of like an inline delegate method. Just because this took a while to sink in for me, I'm going to work through it step by step here:

[NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask

This first bit is no problem. You're calling a class method on NSEvent, and telling it which event you're looking to monitor, in this case NSKeyDownMask. A list of masks for supported event types can be found here.

Now, we come to the tricky part: handler, which expects a block:

handler:^(NSEvent *event){

It took me a few compile errors to get this right, but (thank you Apple) they were very constructive error messages. The first thing to notice is the carat ^. That signals the start of the block. After that, within the parentheses,

NSEvent *event

Which declares the variable that you'll be using within the block to capture the event. You could call it

NSEvent *someCustomNameForAnEvent

doesn't matter, you'll just be using that name within the block. Then, that's just about all there is to it. Make sure to close your curly brace, and bracket to finish the method call:

}];

And you're done! This really is kind of a 'one-liner'. It doesn't matter where you execute this call within your app -- I do it in the AppDelegate's applicationDidFinishLaunching method. Then, within the block, you can call other methods from within your app.

Mannie answered 20/1, 2011 at 20:50 Comment(8)
Hey @Pirripli thanks so much for explaining the block syntax for me. REALLY helps out for those learning! Works out great by the way :)Suppress
No problem. ;) I'm always thankful when others do so for me, so I try to help out when I can.Mannie
When I try this code block, I get the error: Incompatible block pointer types sending 'void (^)(struct NSEvent *)' to parameter of type void (^)(NSEvent *)' in the declaration - @Pirripli , Did I forget to add something? Did you have to include something special to support the block syntax? ScreenshotAplanatic
I was able to solve this by reading the error carefully (duh) and removin "struct" from the statement.Aplanatic
Please go to "System Preferences -> Security & Privacy -> Privacy -> Accessibility" to check if Xcode is enabled or not, in case it doesn't work for anyone.Samarasamarang
Adding onto what Jing Li said, I had to specifically enable my app to "control my computer" from that system preference pane. Not Xcode, but MY app.Reformed
I'm having a problem with this, the code works but my computer makes the "Funk" error sound because the custom key command I've set isn't a valid key command in some apps. Is there a way around this?Dentilabial
@ChrisLadd please add your edited-in "answer" as an actual Answer on this page. Much easier to read and better for the the Q/A format.Labret
Z
27

If you are okay with a minimum requirement of OS X 10.6+, and can suffice with "read-only" access to the stream of events, you can install a global event monitor in Cocoa: Cocoa Event-Handling Guide: Monitoring Events.

If you need to support OS X 10.5 and earlier, and read-only access is okay, and don't mind working with the Carbon Event Manager, you can basically do the Carbon-equivalent using GetEventMonitorTarget(). (You will be hard-pressed to find any (official) documentation on that method though). That API was first available in OS X 10.3, I believe.

If you need read-write access to the event stream, then you will need to look at a slightly lower-level API that is part of ApplicationServices > CoreGraphics:CGEventTapCreate() and friends. This was first available in 10.4.

Note that all 3 methods will require that the user have "Enable access for assistive devices" enabled in the System Preferences > Universal Access preference pane (at least for key events).

Zoba answered 21/1, 2011 at 0:29 Comment(2)
Awesome, this looks great! I'll see if I can get things up and running tomorrow, and then accept! One question: Is there a way I can test if the user has access enabled, e.g. to allow me to prompt them to do so?Mannie
You can do this using an apple script. Try the script below. tell application "System Events" to set isUIScriptingEnabled to UI elements enabled if isUIScriptingEnabled = false then tell application "System Preferences" activate set current pane to pane "com.apple.preference.universalaccess" display dialog "Please select the \"Enable access for assistive devices\" checkbox and restart" return end tell end ifPortwin
W
15

I'm posting the code that worked for my case.

I'm adding the global event handler after the app launches. My shortcut makes ctrl+alt+cmd+T open my app.

- (void) applicationWillFinishLaunching:(NSNotification *)aNotification
{
    // Register global key handler, passing a block as a callback function
    [NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask
                                           handler:^(NSEvent *event){

        // Activate app when pressing cmd+ctrl+alt+T
        if([event modifierFlags] == 1835305 && [[event charactersIgnoringModifiers] compare:@"t"] == 0) {

              [NSApp activateIgnoringOtherApps:YES];
        }
    }];

}
Wolpert answered 3/6, 2012 at 20:1 Comment(0)
C
4

The issue I find with this is that any key registered globally by another app will not be cought... or at least in my case, perhaps I am doing something wrong.

If your program needs to display all keys, like "Command-Shift-3" for example, then it will not see that go by to display it... since it is taken up by the OS.

Or did someone figure that out? I'd love to know...

Caerphilly answered 17/2, 2011 at 19:50 Comment(1)
Did you found the solution to this? I'm quite interested in it too.Jell
V
4

As NSGod already pointed out you can also use CoreGraphics.

In your class (e.g. in -init):

CFRunLoopRef runloop = (CFRunLoopRef)CFRunLoopGetCurrent();

CGEventMask interestedEvents = NSKeyDown;
CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, 
                                        0, interestedEvents, myCGEventCallback, self);
// by passing self as last argument, you can later send events to this class instance

CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, 
                                                           eventTap, 0);
CFRunLoopAddSource((CFRunLoopRef)runloop, source, kCFRunLoopCommonModes);
CFRunLoopRun();

Outside of the class, but in the same .m file:

CGEventRef myCGEventCallback(CGEventTapProxy proxy, 
                             CGEventType type, 
                             CGEventRef event, 
                             void *refcon)
{

    if(type == NX_KEYDOWN)
    {
       // we convert our event into plain unicode
       UniChar myUnichar[2];
       UniCharCount actualLength;
       UniCharCount outputLength = 1;
       CGEventKeyboardGetUnicodeString(event, outputLength, &actualLength, myUnichar);

       // do something with the key

       NSLog(@"Character: %c", *myUnichar);
       NSLog(@"Int Value: %i", *myUnichar);

       // you can now also call your class instance with refcon
       [(id)refcon sendUniChar:*myUnichar];
   }

   // send event to next application
   return event;
}
Volcanism answered 20/2, 2013 at 16:33 Comment(1)
Call [NSEvent eventWithCGEvent:event] to get an NSEvent with a nicer API for working with ObjC code. In ARC you need to bridge. Example: [(__bridge MyClass *) refcon myHandlerInstanceMethod:e];Attachment

© 2022 - 2024 — McMap. All rights reserved.