Wednesday, June 11, 2008

So you want to post a keyboard event in OS X

I recently found the need to synthesize keyboard -- actually Command-Key modified -- events from a plug-in I'm working on to it's host browser on OS X. Apparently this isn't the most common task now days, and I didn't find many details (that actually worked for me) on how to accomplish this. Needless to say, this presented me with a few fun issues. Oh and did I mention I'm new to mac dev?

The Task
Given a WindowRef, the key code, and ascii key value post a Command-key keyboard event to the browser that actually gets recognized.

First Attempt: Cocoa Events
This actually worked out alright for Safari 3 and Firefox 3. No such luck for Firefox 2.


bool SynthesizeCommandKey(WindowRef window, unsigned int value, char key)
{
// create a point in the middle of the window
Rect globalBounds;
GetWindowBounds(window, kWindowContentRgn, &globalBounds);

SetWindowBounds(window, region);
NSPoint point;
point.y = globalBounds.top + ((globalBounds.bottom - globalBounds.top) / 2);
point.x = globalBounds.left + ((globalBounds.right - globalBounds.left) / 2);

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// convert the character into a string
NSString* str = [NSString stringWithFormat:@"%c", key];

// create the key-down event
// window number and context were educated guesses
NSEvent* key_down_event = [NSEvent keyEventWithType:NSKeyDown
location:point
modifierFlags:NSCommandKeyMask
timestamp:[NSDate timeIntervalSinceReferenceDate]
windowNumber:0
context:[NSGraphicsContext currentContext]
characters:str
charactersIgnoringModifiers:str
isARepeat:NO
keyCode:value];

// create the corresponding key up event
NSEvent* key_up_event = [NSEvent keyEventWithType:NSKeyUp
location:point
modifierFlags:NSCommandKeyMask
timestamp:[NSDate timeIntervalSinceReferenceDate]
windowNumber:0
context:[NSGraphicsContext currentContext]
characters:str
charactersIgnoringModifiers:str
isARepeat:NO
keyCode:value];

// post to the application
[NSApp postEvent:key_down_event atStart:YES];
[NSApp postEvent:key_up_event atStart:YES];

[pool release];

return true;
}
References

Second Attempt: Quartz Events
It seemed like using Quartz Event Services would work out well. It did not, I'm not going to go into details because I already wasted too much time on it.

Third Attempt: Carbon Event Loop
Third times a charm (after one seriously lucky discovery). It turns out that trying to post the Command-key down event separately, does not in fact work -- contrary to various bits of Apple documentation and examples I've seen floating around elsewhere. The trick is to apply a modifier mask to the event that indicates the Command-key is being pressed in conjunction with the regular key.


bool SynthesizeCommandKey(WindowRef window, unsigned int value, char key)
{
// NB: In this version the window is not necessary

// Create the key down event
// - Set the key code to value passed in
// - Set the character code to the character passed in
// - Set the unicode characters as well by converting the provided character
// - This may not be 100% necessary, but it works
// - Supply a command-key modifier mask to indicate that the command-key is being
// pressed. This was an undocumented feature of kEventParamKeyModifiers, but
// turned out to be the key to getting everything working.
// NOTE: Sending a command-key down event separately does not work (contrary to
// various examples).

// setup: create a unicode version of the character code
// It's quite possible there's a better way
char base_str[2] = {key, '\0' };

CFStringRef cf_str = CFStringCreateWithCString(NULL, base_str, CFStringGetSystemEncoding());
UniChar uni_str[32]; // yes, this excessive
CFRange range;
range.location = 0;
range.length = CFStringGetLength(cf_str);
CFStringGetCharacters(cf_str, range, uni_str);

// create the key modifier bit-mask indicating the Command key is being pressed
UInt32 key_mod = cmdKey;

// create the key down event
EventRef key_down;
CreateEvent(NULL, kEventClassKeyboard, kEventRawKeyDown, 0, 0, &key_down);
SetEventParameter(key_down, kEventParamKeyCode, typeUInt32, sizeof(value), &value);
SetEventParameter(key_down, kEventParamKeyMacCharCodes, typeChar, sizeof(key), &key);
SetEventParameter(key_down, kEventParamKeyModifiers, typeUInt32, sizeof(key_mod), &key_mod);
SetEventParameter(key_down, kEventParamKeyUnicodes, typeUnicodeText, range.length*sizeof(UniChar), uni_str);

// create the key up event using key_down as a reference
EventRef key_up = CopyEventAs(NULL, key_down, kEventClassKeyboard, kEventRawKeyUp);

// Now send the key-down and key-up events
bool ret_val = true;
OSStatus rslt = PostEventToQueue(GetMainEventQueue(), key_down, kEventPriorityStandard);
if (rslt != noErr) {
NSLog(@"Failed to post command-key down event: %d", rslt);
ret_val = false;
}
else {
rslt = PostEventToQueue(GetMainEventQueue(), key_up, kEventPriorityStandard);
if (rslt != noErr) {
NSLog(@"Failed to post command-key up event: %d", rslt);
ret_val = false;
}
}

// cleanup
ReleaseEvent(key_down);
ReleaseEvent(key_up);
CFRelease(cf_str);

return ret_val;
}