views:

871

answers:

4

I need a function that, given a character, returns the CGKeyCode associated with the position of that character on the current keyboard layout. E.g., given "b", it should return kVK_ANSI_B if using U.S. QWERTY, or 45 if using Dvorak. The Win32 API has the function VkKeyScan() for this purpose; X11 has the function XStringToKeySym(). Is there such a function in the CG API?

I need this in order to pass a parameter to CGEventCreateKeyboardEvent(). I've tried using CGEventKeyboardSetUnicodeString() instead, but that apparently does not support modifier flags (which I need).

I have searched extensively for this but cannot find a decent answer. Currently I am using the following code (found online), which works, but is not exactly elegant (and rather difficult to decipher how to simplify) and I would prefer not to use it in production code:

#include <stdint.h>
#include <stdio.h>
#include <ApplicationServices/ApplicationServices.h>

CGKeyCode keyCodeForCharWithLayout(const char c,
                                   const UCKeyboardLayout *uchrHeader);

CGKeyCode keyCodeForChar(const char c)
{
    CFDataRef currentLayoutData;
    TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();

    if (currentKeyboard == NULL) {
     fputs("Could not find keyboard layout\n", stderr);
     return UINT16_MAX;
    }

    currentLayoutData = TISGetInputSourceProperty(currentKeyboard,
                                                kTISPropertyUnicodeKeyLayoutData);
    CFRelease(currentKeyboard);
    if (currentLayoutData == NULL) {
     fputs("Could not find layout data\n", stderr);
     return UINT16_MAX;
    }

    return keyCodeForCharWithLayout(c,
           (const UCKeyboardLayout *)CFDataGetBytePtr(currentLayoutData));
}

/* Beware! Messy, incomprehensible code ahead!
 * TODO: XXX: FIXME! Please! */
CGKeyCode keyCodeForCharWithLayout(const char c,
                                   const UCKeyboardLayout *uchrHeader)
{
    uint8_t *uchrData = (uint8_t *)uchrHeader;
    UCKeyboardTypeHeader *uchrKeyboardList = uchrHeader->keyboardTypeList;

    /* Loop through the keyboard type list. */
    ItemCount i, j;
    for (i = 0; i < uchrHeader->keyboardTypeCount; ++i) {
     /* Get a pointer to the keyToCharTable structure. */
     UCKeyToCharTableIndex *uchrKeyIX = (UCKeyToCharTableIndex *)
     (uchrData + (uchrKeyboardList[i].keyToCharTableIndexOffset));

     /* Not sure what this is for but it appears to be a safeguard... */
     UCKeyStateRecordsIndex *stateRecordsIndex;
     if (uchrKeyboardList[i].keyStateRecordsIndexOffset != 0) {
      stateRecordsIndex = (UCKeyStateRecordsIndex *)
       (uchrData + (uchrKeyboardList[i].keyStateRecordsIndexOffset));

      if ((stateRecordsIndex->keyStateRecordsIndexFormat) !=
          kUCKeyStateRecordsIndexFormat) {
       stateRecordsIndex = NULL;
      }
     } else {
      stateRecordsIndex = NULL;
     }

     /* Make sure structure is a table that can be searched. */
     if ((uchrKeyIX->keyToCharTableIndexFormat) != kUCKeyToCharTableIndexFormat) {
      continue;
     }

     /* Check the table of each keyboard for character */
     for (j = 0; j < uchrKeyIX->keyToCharTableCount; ++j) {
      UCKeyOutput *keyToCharData =
       (UCKeyOutput *)(uchrData + (uchrKeyIX->keyToCharTableOffsets[j]));

      /* Check THIS table of the keyboard for the character. */
      UInt16 k;
      for (k = 0; k < uchrKeyIX->keyToCharTableSize; ++k) {
       /* Here's the strange safeguard again... */
       if ((keyToCharData[k] & kUCKeyOutputTestForIndexMask) ==
           kUCKeyOutputStateIndexMask) {
        long keyIndex = (keyToCharData[k] & kUCKeyOutputGetIndexMask);
        if (stateRecordsIndex != NULL &&
         keyIndex <= (stateRecordsIndex->keyStateRecordCount)) {
         UCKeyStateRecord *stateRecord = (UCKeyStateRecord *)
                                         (uchrData +
         (stateRecordsIndex->keyStateRecordOffsets[keyIndex]));

         if ((stateRecord->stateZeroCharData) == c) {
          return (CGKeyCode)k;
         }
        } else if (keyToCharData[k] == c) {
         return (CGKeyCode)k;
        }
       } else if (((keyToCharData[k] & kUCKeyOutputTestForIndexMask)
          != kUCKeyOutputSequenceIndexMask) &&
            keyToCharData[k] != 0xFFFE &&
                  keyToCharData[k] != 0xFFFF &&
            keyToCharData[k] == c) {
        return (CGKeyCode)k;
       }
      }
     }
    }

    return UINT16_MAX;
}

Is there a.) (preferably) a standard function I am overlooking, or b.) (almost certainly) a more elegant way write my own?

A: 

Use a table. Better to use a pointer to a table, which allows the changing of keyboard & character mappings for different keyboards. Also, when making changes, the changes are related to data and don't effect code, so they are much safer.

Thomas Matthews
Sorry, what do you mean by a table? Could you elaborate?
meeselet
+1  A: 

This is what I ended up using. Much cleaner.

#include <CoreFoundation/CoreFoundation.h>
#include <Carbon/Carbon.h> /* For kVK_ constants, and TIS functions. */

/* Returns string representation of key, if it is printable.
 * Ownership follows the Create Rule; that is, it is the caller's
 * responsibility to release the returned object. */
CFStringRef createStringForKey(CGKeyCode keyCode)
{
    TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
    CFDataRef layoutData =
        TISGetInputSourceProperty(currentKeyboard,
                                  kTISPropertyUnicodeKeyLayoutData);
    const UCKeyboardLayout *keyboardLayout =
        (const UCKeyboardLayout *)CFDataGetBytePtr(layoutData);

    UInt32 keysDown = 0;
    UniChar chars[4];
    UniCharCount realLength;

    UCKeyTranslate(keyboardLayout,
                   keyCode,
                   kUCKeyActionDisplay,
                   0,
                   LMGetKbdType(),
                   kUCKeyTranslateNoDeadKeysBit,
                   &keysDown,
                   sizeof(chars) / sizeof(chars[0]),
                   &realLength,
                   chars);
    CFRelease(currentKeyboard);    

    return CFStringCreateWithCharacters(kCFAllocatorDefault, chars, 1);
}

/* Returns key code for given character via the above function, or UINT16_MAX
 * on error. */
CGKeyCode keyCodeForChar(const char c)
{
    static CFMutableDictionaryRef charToCodeDict = NULL;
    CGKeyCode code;
    UniChar character = c;
    CFStringRef charStr = NULL;

    /* Generate table of keycodes and characters. */
    if (charToCodeDict == NULL) {
        size_t i;
        charToCodeDict = CFDictionaryCreateMutable(kCFAllocatorDefault,
                                                   128,
                                                   &kCFCopyStringDictionaryKeyCallBacks,
                                                   NULL);
        if (charToCodeDict == NULL) return UINT16_MAX;

        /* Loop through every keycode (0 - 127) to find its current mapping. */
        for (i = 0; i < 128; ++i) {
            CFStringRef string = createStringForKey((CGKeyCode)i);
            if (string != NULL) {
                CFDictionaryAddValue(charToCodeDict, string, (const void *)i);
                CFRelease(string);
            }
        }
    }

    charStr = CFStringCreateWithCharacters(kCFAllocatorDefault, &character, 1);

    /* Our values may be NULL (0), so we need to use this function. */
    if (!CFDictionaryGetValueIfPresent(charToCodeDict, charStr,
                                       (const void **)&code)) {
        code = UINT16_MAX;
    }

    CFRelease(charStr);
    return code;
}
meeselet
This does not work for non-printable keys though, does it? I have exactly the same problem - I need to simulate key presses, given the characters and modifier keys pressed. Any ideas?
Thomi
For non-printable keys, I used the keycode constants defined under <HIToolbox/Events.h> (in Carbon). E.g., kVK_Delete for the delete key, kVK_F# for function keys, etc.). This is the only method I could find that would work.
meeselet
A: 

Hey Michael,

Thanks for taking the time to post this. I have been searching high and low for such a solution. I am trying to compile this in XCode and getting a few errors though.

At the moment I am down to two.

error: 'UINT16_MAX' was not declared in this scope

and....

error: invalid conversion from 'void*' to 'const __CFData*'

for this line

TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);

Not sure what the problem is. Do you have an XCode example, I would love to see it.

Anyway I will try and figure it out.

Thanks again for posting

Cheers

Simon

simon blackmore
Are you sure you're linking Carbon correctly?
meeselet
A: 

Hi

I keep getting 65535 returned for any char i pass to the keyCodeForChar function... i assume thats because the code dictionary is NULL.

What would cause this, do you think?

Thanks for posting, i've been looking for solutions to key codes for ages!

Joe

joec