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

2 comments:

Daddy Cool said...

Can you provide an example of how to call this function? I don't understand why you need the value parameter, and how you would find the correct value. Also, does this work for posting events to other apps or only the app that is calling it?
Thanks!

Unknown said...

great working thanks for sharing

Leawo Video Converter Pro 6.2 Keygen