OS X: Detect system-wide keyDown events?

25,104

Solution 1

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).

Solution 2

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];
        }
    }];

}

Solution 3

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;
}

Solution 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...

Share:
25,104
Chris Ladd
Author by

Chris Ladd

Updated on July 08, 2022

Comments

  • Chris Ladd
    Chris Ladd almost 2 years

    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.

  • Chris Ladd
    Chris Ladd over 13 years
    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?
  • ThE uSeFuL
    ThE uSeFuL almost 13 years
    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 if
  • Max Al Farakh
    Max Al Farakh about 12 years
    Did you found the solution to this? I'm quite interested in it too.
  • vaughan
    vaughan over 10 years
    Thank you for the detailed post! Worked great.
  • vaughan
    vaughan over 10 years
    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];