OS X: Detect system-wide keyDown events? - macos

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.

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

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

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

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

Related

IMKit to catch NSKeyup event

As an experiment, I am trying to achieve the following:
Let spacebar work as a modifier key - like the Shift key - where holding the spacebar key down and typing keys print different letters. Releasing the spacebar would set the state back to normal, and just pressing it behaves like a normal space key.
I was thinking of handling the keydown and keyup event, but apparently handleEvent:client: in IMKServerInput Protocol seems to only catch key down and mouse events.
Without much experience with cocoa, I’ve tried some methods with no success:
went through the Technical Note 2128 via internet archive, which gave me the suitable explanations of plist items. Still, nothing about keyup.
tried adding NSKeyUpMask to recognizedEvents: in IMKStateSetting Protocol, but that didn’t seem to catch the event either.
tested a bit with addLocalMonitorForEventsMatchingMask:handler: but nothing happens.
failed to find a way to make NSFlagsChanged event fire with spacebar.
read about Quartz Event Service and CGEventTap which seems to handle user inputs in lower level. Didn’t go further to this route, yet.
IOHIDManager?
I reached to a conclusion that IMKit is only capable of passively receiving events.
Since it is not an application, there is no keyUp: method to override - AFAIK, IMKit does not inherit NSResponder class.
Unfortunately cocoa is way too broad and has much less (or overflowed with non-helping) documentations for a novice like me to dive in.
Can anyone help me to the right direction?
I tried all possible alternatives one by one, and eventually achieved it by creating a global EventTap
with CGEventTap.
The code basically looks like this:
// Create an event tap.
CGEventMask eventMask = ((1 << kCGEventKeyDown) | (1 << kCGEventKeyUp));
CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap,
kCGHeadInsertEventTap,
0,
eventMask,
myCGEventCallback,
NULL);
if (!eventTap) {
NSLog(#"failed to create event tap\n");
return NO;
} else {
// Create a run loop source.
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
// Add to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
// Enable the event tap.
CGEventTapEnable(eventTap, true);
return YES;
}
where myCGEventCallback handles the global states.
Meanwhile here are some of what I've found out:
According to The Key-Input Message Sequence document, the application only passes the keydown event to the Input Method Kit, after trying other bunch of handlers in the chain. You cannot let IMKServerInput 'catch' the NSKeyUp event. Just adding an NSKeyUpMask to recognizedEvents: would not work.
addLocalMonitorForEventsMatchingMask:handler: and CGEventTapCreateForPSN would not catch the event. I suppose this is because though an Input Method may run as a separate process, the event itself is fired from the application, like TextEdit, and handed over to the Input `Method.
IOHIDManager: is for adding new hardware devices and making drivers.
Creating a global EventTap requires either running the process with sudo privilege -- copying the Input Method to /Library/Input Methods does not run with sudo privilege --, or registering the application to the Accessibility control. That's in System Preferences → Security & Privacy → Privacy tab → Accessibility, in Mavericks.
IMKStateSetting protocol's recognizedEvents: method should enable NSKeyUp events:
- (NSUInteger)recognizedEvents:(id)sender {
return NSKeyDownMask | NSKeyUpMask;
}
A client calls this method to check whether an input method supports an event. The default implementation returns NSKeyDownMask.
However, in testing my Input Method still does not catch NSKeyUp events. This seems like a bug.
I've filed the following Radar, and hope others will duplicate it:
rdar://21376535

How check for Command-Period in a tight loop?

I'm implementing a scripting language where the user might be causing an endless loop by accident. I want to give the user the opportunity to cancel such a runaway loop by holding down the command key while typing the period (".") key.
Currently, once for every line, I check for cancellation with this code:
NSEvent * evt = [[NSApplication sharedApplication] nextEventMatchingMask: NSKeyDownMask untilDate: [NSDate date] inMode: WILDScriptExecutionEventLoopMode dequeue: YES];
if( evt )
{
NSString * theKeys = [evt charactersIgnoringModifiers];
if( (evt.modifierFlags & NSCommandKeyMask) && theKeys.length > 0 && [theKeys characterAtIndex: 0] == '.' )
{
// +++ cancel script execution here.
}
}
The problem with this is that it eats any keyboard events that the user might be typing while the script is running, even though scripts should be able to check for keypresses. Also, it doesn't dequeue the corresponding NSKeyUp events. But if I tell it to dequeue key up events as well, it might dequeue the keyUp for a keypress that was being held before my script started and my application might never find out the key was released.
Also, I would like to not dequeue any events until I know it is actually a cancel event, but there is no separate dequeue call, and it feels unreliable to just assume the frontmost event on a second call will be the same one. And even if it is guaranteed to be the first, that would mean that the user typing an 'a' and then Cmd-. would mean I only ever see the 'a' and never the Cmd-. behind it if I don't dequeue events.
Is there a better option than going to the old Carbon stand-by GetKeys()? Fortunately, that seems to be available in 64 bit.
Also, I'm thinking about adding an NSStatusItem that adds a button to cancel the script to the menu bar or so. But how would I process events in a way that doesn't let the user e.g. select a menu while a script expects to be ruler of the main thread?
Any suggestions? Recommendations?
Using -addLocalMonitorForEventsMatchingMask: as Dave suggests is probably the easiest way to go about this, yes.
I just wanted to add that despite your unreliable feeling, the event queue is really a queue, and events don't change order. It is perfectly safe (and standard practice in event loops) to call -nextEventMatchingMask:inMode:dequeue:NO, examine the event, determine that it is one you want to deal with, and then call -nextEventMatchingMask:inMode:dequeue:YES in order to consume it. Just make sure that your mask and mode are identical between the two calls.
I would suggest using an event monitor. Since you're asking NSApp for events, it would seem that you're running the script in the current process, so you only have to monitor events in your own process (and not globally).
There are several ways to do this (subclassing NSApplication and overriding -sendEvent:, putting in an event tap, etc), but the easiest way to do this would be with a local event monitor:
id eventHandler = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDown handler:^(NSEvent *event) {
// check the runloop mode
// check for cmd-.
// abort the script if necessary
return event;
}];
When you're all done monitoring for events, don't forget to unregister your monitor:
[NSEvent removeMonitor:eventHandler];
So there's +[NSEvent modifierFlags] which is intended as a replacement for GetKeys(). It doesn't cover your use case of the period key though, sadly.
The core problem here with the event queue is you want to be able to search it, which isn't something the API exposes. The only workaround to that I can think of is to dequeue all events into an array, checking for a Command-. event, and then re-queue them all using postEvent:atStart:. Not pretty.
Perhaps as an optimisation you could use +[NSEvent modifierFlags] to only check the event queue when the command key is held down, but that sounds open to race conditions to me.
So final suggestion, override -postEvent:atStart: (on either NSApplication or NSWindow) and see if you can fish out the desired info there. I think at worst it could be interesting for debugging.

addGlobalMonitorForEventsMatchingMask doesn't capture global keypresses

I have a status bar app. I'm using this code to capture very user's key press in the system:
_keybordEventMonitor =
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSKeyDownMask) handler:^(NSEvent *incomingEvent)
{
[self inputKeyboardEventHandler: incomingEvent];
}];
It captures everything fine, but doesn't capture global system hotkeys like cmd + space or cmd+shift+3.
Accessibility APIs are enabled. Any ideas?
PS: I tried using CGEventTap and it's kind of works, but had it's own problems and since I'm a cocoa noob, I prefer to keep things simple for now.

What is the trick behind "launch application holding [modifier key]"?

iTunes and Reeder (and I'm sure lots of other) applications has an ability to modify startup behavior whenever they are launched while holding ⌥ (option) key. I tried looking at NSApplicationDelegate methods, but none seem to add any sort of hint to what I'm looking for. How is this functionality achieved?
In your application delegate's applicationDidFinishLaunching: method
NSUInteger flags = ([NSEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask);
BOOL isOptionPressed = (flags == NSAlternateKeyMask);
(Edited to match awesome answer Declaring and checking/comparing (bitmask-)enums in Objective-C
Previously this was
BOOL isOptionPressed = (0 != (flags & NSAlternateKeyMask));
But the zero checking is not necessary to check if a bit mask for equality, unless the bit mask itself represents all zeros in binary.
The provided link gives greater detail.
)
Francis McGrew's answer needs improvement before it can work, but I still see no solution via that route. The answer as stated does not compile. An actual NSEvent pointer is needed for modifierFlags, which is not a class method as the answer would suggest.
One would hope that the needed event might be obtained by the following.
NSEvent *event = [NSApp currentEvent];
However when called from applicationDidFinishLaunching the resulting event is nil. At least this is true in my testing on Snow Leopard.
applicationDidFinishLaunching has an NSNotification argument but I don't know how to make any use of it.
I found a similar question on cocobuilder.com from 2007 and the answer there is basically to call the carbon function GetCurrentKeyModifiers and convert the carbon flags to cocoa form.
See http://www.cocoabuilder.com/archive/cocoa/176882-detecting-modifier-keys-at-launch.html

Catch system event as Cmd-Tab or Spotlight in a Cocoa App

In a Cocoa App, I'm trying to find a way to catch system events like the app switcher usually launched with Cmd-Tab or spotlight, usually launched by Cmd-Space. I'm looking for either a way to catch the key event or any another way that would tell me that one of those event is about to happen, and ideally cancel it.
Apple Screen Sharing remote desktop app does it, so it should be possible. It catches those events and send them to the connected remote computer.
Here is what I already tried :
Catching events with the sendEvent method in NSApplication. I see all events like the Cmd keydown, the Tab keydown, but when both are pressed, I see nothing.
Registering a Carbon Hot key listener. I can register anything like Cmd+Q, but again, when I register Cmd+Tab, it does not respond.
Any other ideas ?
See Event Taps.
Found it!
In my WindowViewController.m file
#import <Carbon/Carbon.h>
void *oldHotKeyMode;
- (void)windowDidBecomeKey:(NSNotification *)notification{
oldHotKeyMode = PushSymbolicHotKeyMode(kHIHotKeyModeAllDisabled);
}
- (void)windowDidResignKey:(NSNotification *)notification{
PopSymbolicHotKeyMode(oldHotKeyMode);
}
This is pretty magic ! and it passes the new Apple sandboxing requirement for the Mac App Store !
I will describe you how to catch cmd+tab. But please note that will work only in fullscreen mode. I beleive there is no way to this in windowed mode. Code is pretty simple. It is a minor fix of SDL mac code - update for handling cmd+tab in fullscreen mode.
NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES ];
if ( event == nil ) {
break;
}
if (([event type] == NSKeyDown) &&
([event modifierFlags] & NSCommandKeyMask)
&&([[event characters] characterAtIndex:0] == '\t')
{
do something here
}

Resources