Thursday, March 26, 2009

Global Keyboard Shortcuts with Carbon Events

If your new to Cocoa you've probably not heard of Carbon, on the other hand if you've been developing for the Mac for a while now you've probably at least heard of Carbon and if you're really experienced you've probably used carbon. If you've used carbon at all this article really isn't for you. What I want to do is gently introduce you to carbon events, the 1 scenario where we'd use it for in terms of cocoa apps, why Cocoa Events can't handle this one scenario and why Carbon Events can, and how to implement it in a Cocoa Application. Why Carbon Events There is a specific scenario I had in mind as to why you would use Carbon Events ( actually if you have a Cocoa app it's a you have to use Carbon Events), the scenario is this: you have a Cocoa application and you know your customers will be running it but it will not be the active application running in the foreground, your customers may be looking at Mail, NetNewsWire, Xcode, etc basically some other application than yours and yet you still want to receive a keyboard shortcut key pressed event. How do you do this? Not with Cocoa, in its current form it's literally impossible. Here's why Cocoa Events works as such, according to the Cocoa Event Handling Guide it says "In the main event loop, the application object (NSApp) continuously gets the next (topmost) event in the event queue, converts it to an NSEvent object, and dispatches it toward its final destination. It performs this fetching of events by invoking the nextEventMatchingMask:untilDate:inMode:dequeue: method in a closed loop. When there are no events in the event queue, this method blocks, resuming only when there are more events to process." It then goes on "After fetching and converting an event, NSApp performs the first stage of event dispatching in the sendEvent: method. In most cases NSApp merely forwards the event to the window in which the user action occurred by invoking the sendEvent: method of that NSWindow object. The window object then dispatches most events to the NSView object associated with the user action in an NSResponder message such as mouseDown: or keyDown:. An event message includes as its sole argument an NSEvent object describing the event."
cocoasamurai_nsevent_window_button.png
So in other words the NSApplication has an event queue and receives an event (1) converts it into NSEvent objects (2) which it then forwards to a Window (3) which then dispatches the event to the view associated with the user action calling on it -mouseDown:(NSEvent *)event, -keyDown:(NSEvent *)event, etc (4). This is great, but if our application is running in the background there is no way our Window will receive events. Cocoa Events simply does not allow for a regular cocoa application to be in the background and receive keyboard events. Now I could go on to further explain NSResponder and how it events could then go onto the menu's after exhausting all possible views in a window that could respond to an event, but that's another article onto itself. The point is our app is a regular cocoa app and it has a window and menu's, but we know our application is not going to have focus and thus won't have a window that can forward events or even application menu's to receive events, but we still want to receive an event and invoke some sort of functionality. There are plenty of examples of this: OmniFocus has a quick entry window which you can invoke from anywhere, applications like Quicksilver use Carbon Events so you can invoke them anywhere from in Mac OS X. These are both Cocoa applications, but they depend on Carbon Events to give them notifications of global keyboard shortcut events. Conceptually Carbon events is vastly different from Cocoa Events. When I first finally got a Carbon Events example working it seemed very strange compared to Cocoa Events, it was more like there is a stream of events going by in the system and you install something to indicate a interest in a particular kind of event. If that event matches the specific event you are looking for then your code is called and you have a global keyboard shortcut. So how do we do this? Setting up a project In Xcode, go ahead and create a new Cocoa application. At this point if you build in go all you will have is a small window and an application that does nothing. Now go to File->New File and create a plain Cocoa class and call it AppController. Then click on the disclosure triangle by the Frameworks folder and then right-click on Linked Frameworks and go to Add -> Existing Frameworks and in your SDK add "Carbon.Framework." At this point this should be all you have for AppController.h
#import <Cocoa/Cocoa.h> @interface AppController : NSObject { } @end
Now in AppController.m add #import <Carbon/Carbon.h> and implement -(void)awakeFromNib and you should have
#import "AppController.h" #import <Carbon/Carbon.h> @implementation AppController -(void)awakeFromNib { } @end
Now the next thing we need to do is add the declaration for our method which will handle the global keyboard shortcut. Above the "@implementation AppController" add the following line
OSStatus myHotKeyHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData);
this method will be the rough equivalent of implementing say -(void)mouseDown:(NSEvent *)event in a cocoa class. The Carbon Event handler will pass the EventHandlerCallRef (next event handler reference), EventRef (roughly equivalent to NSEvent) arguments to us. If we were doing really deep Carbon Events work then we might also be interested in all the arguments, but for this simplistic task all we need is the EventRef reference. Into the guts of Carbon Events Now we've set the foundation so now lets go to work in the awake from Nib method. Add the following code to the top of the awakeFromNib Method
    EventHotKeyRef myHotKeyRef;     EventHotKeyID myHotKeyID;     EventTypeSpec eventType;
Now lets understand these new variables. EventHotKeyRef : The documentation is pretty descriptive about this "Represents a registered global hot key." In essence we have this reference so we can unregister it later if we want to. EventHotKeyID : Again "Represents the ID of a global hot key." This is where you'll set some things to uniquely identify your global keyboard shortcut. EventTypeSpec : "Describes the class and kind of an event." Passing in this reference after setting the class and kind of event tells the event handler what type of event this is. So lets set the EventTypeSpec reference
    eventType.eventClass=kEventClassKeyboard;     eventType.eventKind=kEventHotKeyPressed;
This will tell the event handler we are looking for a keyboard event and that a hot key was pressed. Now add
    InstallApplicationEventHandler(&myHotKeyHandler,1,&eventType,NULL,NULL);
this installs our event handler method we declared earlier, but haven't implemented yet, so when this event happens it will call our code. Now we need to uniquely identify our hotKey...
    myHotKeyID.signature='mhk1';     myHotKeyID.id=1;
If you just intend on having only 1 global keyboard shortcut then this doesn't matter too much, but if you intend on having multiple global keyboard shortcuts then the id will matter greatly, it's what you will use to uniquely identify each specific event in the method we will implement shortly.
    RegisterEventHotKey(49, cmdKey+optionKey, myHotKeyID, GetApplicationEventTarget(), 0, &myHotKeyRef);
This is what actually registers our global shortcut. In Essence it's arguments are (1) the key code to the key you will use in combination with (2) modifier keys to create your global keyboard shortcut and (3) the Application Event Target which the GetApplicationEventTarget() method retrieves for us and finally (4) the EventHotKeyRef we declared earlier which gives us a reference to this keyboard shortcut we are registering. In this case it registers the spacebar (int 49) plus the command key and option key. So when Command+Option+Spacebar is hit our global keyboard shortcut will be triggered. It's important to note that you cannot register the same key combination twice within the same application and have it trigger both methods, however multiple applications can register for the same global keyboard shortcut and Carbon events will trigger events in both applications. I decided to test this by duplicating the keyboard shortcut I use for OmniFocus quick entry which for me is Control+Option+Space Bar
    RegisterEventHotKey(49, controlKey+optionKey, myHotKeyID, GetApplicationEventTarget(), 0, &myHotKeyRef);
and woola the OmniFocus quick entry Window and my event both triggered
multiple_apps_1_carbon_event.png
Now the hard part is behind us all we need to do is implement the method
OSStatus myHotKeyHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData) {     NSLog(@"YEAY WE DID A GLOBAL HOTKEY");          return noErr; }
What if we want to handle multiple global keyboard shortcuts?
OSStatus myHotKeyHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData) {     EventHotKeyID hkRef;     GetEventParameter(anEvent,kEventParamDirectObject,typeEventHotKeyID,NULL,sizeof(hkRef),NULL,&hkRef);          switch (hkRef.id) {         case 1:             NSLog(@"Event 1 was triggered!");             break;         case 2:             NSLog(@"Event 2 was triggered!");             break;     }          return noErr; }
Well then you have to use GetEventParameter() as such to get the EventHotKeyID which you can then get it's ID code and handle the event appropriately. This would also involve creating a 2nd hot key and setting it's id to 2, 3,etc.. Key Code The only thing left is the integer key code I mentioned earlier. I talked to Eric Rocasecca and Jim Turner from Startly (who make QuicKeys which I think would use Carbon Events, but evidently they use their own system) about this and they had a magazine with a reference of all the key codes in it, I didn't have this and didn't want to bother hunting down an old rare magazine. Several Google searches later I found an old app called AsyncKeys! which tells you the integer code of any key you press. I downloaded it a long time ago, but I did turn up a couple locations that actually had a download as the original site appears to be long gone (really sorry but I can only find .sit files): http://asynckeys.mac.findmysoft.com/ and if you Google around you can find other download locations. Here are some random key codes Spacebar 49 a = 0 1=18 F1 = 122 Left arrow = 123 Down arrow = 125 Right arrow = 124 Up Arrow = 126 Enter = 36 Backspace = 51 ` = 50 and I could go on and on... Conclusion Essentially we saw a brief intro into how Cocoa Events work, saw how carbon events work, installed an event handler in our application, registered a global hot key and saw how to handle multiple global hot keys. This is a narrow use scenario, not many apps will need to use this, but if you want to activate a particular function and your application is Cocoa and running in the background, this is the best way you achieve this functionality. I really wouldn't recommend digging into Carbon more as Apple is moving to deprecate it and as a Cocoa developer you should be finding that you'll need carbon less and less as time goes on.

14 comments:

  1. Anonymous10:12 AM

    Here is an ancient page on Apple's legacy dev site that shows key codes. You have to squint to see the codes and this is not quite complete, but it will get ya pretty far. Note the values are in hex.

    --Eric

    ReplyDelete
  2. I went through the same process trying to track down key codes when writing my own Hot Key app awhile ago. Google searches are useless when *everybody* gets it wrong.

    They're in Events.h, in the HIToolbox framework. Just use Spotlight to search for kVK_.

    ReplyDelete
  3. Anonymous11:53 AM

    Dude, where's the credit? http://dbachrach.com/blog/2005/11/28/program-global-hotkeys-in-cocoa-easily/

    ReplyDelete
  4. Eimantas: I would credit him, but I have only seen that recently in the past couple weeks. I first learned about all of this actually by looking at Quicksilver's source code where I first saw InstallEventHandler() used and then started researching from there.

    ReplyDelete
  5. Anonymous8:08 AM

    Could you post the full source as well as the chopped up pieces? Thanks!

    ReplyDelete
  6. Here's a super handy open-source project:

    http://wafflesoftware.net/shortcut/

    We use this in Evernote on the Mac.

    ReplyDelete
  7. Anonymous9:57 PM

    "If your new to Cocoa" should be "If you're new to Cocoa".

    ReplyDelete
  8. Anonymous8:14 AM

    Perfect, very useful and just what I needed. You could maybe add a paragraph to show how to call objective-c methods in your class (self) inside the handler method.

    Most of the time you will need to call a method in your class as the result of pressing a hot key so this would be very useful to include.

    ReplyDelete
  9. this looks like it might do what i need.
    would it be possible for you to share the complete .m and .h file.

    i am new to cocoa and it would allow me to understand where each piece of code goes.

    thank you.

    ReplyDelete
  10. stephan I probably wouldn't use this now, Carbon is on the path to deprecation and there is a equivalent Cocoa API for this in 10.6 Snow Leopard.

    ReplyDelete
  11. Cool, but is it possible to do it on the iPhone as well?

    ReplyDelete
  12. Does this still work, for snow leopard?
    I tried it, it built successfully, but my computer made the system noised when I tried it, instead of doing something.

    ReplyDelete
  13. Tomer: no this wouldn't work on the iPhone

    Connor: it should still work, but i'd be wary about it. Apple seems to want to get rid of these API's and replace them with native Cocoa replacements

    ReplyDelete
  14. I want to use hotkey with a command line service code using mac. i try to use this code in my command line service but it is not working there. can u help how we use this with command line service code?

    ReplyDelete