How to convert ASCII character to CGKeyCode?

Viewed 22301

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 kVK_ANSI_N 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?

6 Answers

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

I know this question is years-old now, but in case anyone's still interested I mocked up version of this that doesn't rely on carbon routines. In fact, it goes a bit farther, because it will map any character that can be typed back to its virtual key code and modifier combination.

- (NSDictionary *)characterToKeyCode:(NSString *)character {
    static NSDictionary * keyMapDict;

    if (keyMapDict == nil) {
        keyMapDict = [self makeKeyMap];
    }

    /*
     The returned dictionary contains entries for the virtual key code and boolen flags
     for modifier keys used for the character.
     */
    return [keyMapDict objectForKey:character];
}

- (NSDictionary *)makeKeyMap {
    CGKeyCode keyCode;
    CGEventRef coreEvent;
    NSEvent * keyEvent;
    NSUInteger modifiers = 0;
    NSMutableDictionary * subDict, * keyMapDict;
    NSString *character;
    BOOL modKeyIsUsed;

    static NSDictionary * modFlagDict;
    static NSArray * modFlags;

    // create dictionary of modifier names and keys. I've used all the modifiers, but I doubt theya re all needed.
    if (modFlagDict == nil) {
        modFlagDict = @{@"option":@(NSEventModifierFlagOption),
                        @"shift": @(NSEventModifierFlagShift),
                        @"function":@(NSEventModifierFlagFunction),
                        @"control": @(NSEventModifierFlagControl),
                        @"command":@(NSEventModifierFlagCommand)};
        modFlags = [modFlagDict allValues];
    }
    keyMapDict = [NSMutableDictionary dictionary];

    // run through 128 base key codes to see what they produce
    for (keyCode = 0; keyCode < 128; ++keyCode) {
        // create dummy NSEvent from a CGEvent for a keypress
        coreEvent = CGEventCreateKeyboardEvent(NULL, keyCode, true);
        keyEvent = [NSEvent eventWithCGEvent:coreEvent];
        CFRelease(coreEvent);

        if (keyEvent.type == NSEventTypeKeyDown) {
            // this do/while loop through every permutation of modifier keys for a given key code
            do {
                subDict = [NSMutableDictionary dictionary];
                // cerate dictionary containing current modifier keys and virtual key code
                for (NSString * key in modFlagDict) {
                    modKeyIsUsed = ([(NSNumber *)[modFlagDict objectForKey:key] unsignedLongValue] & modifiers) ? YES : NO;
                    [subDict setObject:[NSNumber numberWithBool:modKeyIsUsed]
                                forKey:key];
                }
                [subDict setObject:[NSNumber numberWithUnsignedLong:(unsigned long)keyCode]
                            forKey:@"virtKeyCode"];

                // manipulate the NSEvent to get character produce by virtual key code and modifiers
                if (modifiers == 0) {
                    character = [keyEvent characters];
                } else {
                    character = [keyEvent charactersByApplyingModifiers:modifiers];
                }

                // add sub-dictionary to main dictionary using character as key
                if (![keyMapDict objectForKey:character]) {
                    [keyMapDict setObject:[NSDictionary dictionaryWithDictionary:subDict]
                                forKey:character];
                }

                // permutate the modifiers
                modifiers = [self permutatateMods:modFlags];
            } while (modifiers != 0);
        }
    }

    return [NSDictionary dictionaryWithDictionary:keyMapDict];
}

- (NSUInteger)permutatateMods:(NSArray *) modFlags {
    static NSMutableIndexSet * idxSet;
    NSArray * modArray;
    NSEnumerator * e;
    NSNumber * modObj;
    NSUInteger modifiers = 0, idx = 0;

    if (idxSet == nil) {
        idxSet = [NSMutableIndexSet indexSet];
    }

    /*
     Starting at 0, if the index exists, remove it and move up; if the index doesn't exist, add it. Will
     cycle through a standard binary progression. Indexes are then applied to the passed array, and the
     selected elements are 'OR'ed together
     */
    BOOL doneFlag = NO;
    while (!doneFlag) {
        if ([idxSet containsIndex:idx]) {
            [idxSet removeIndex:idx++];
            continue;
        }
        if (idx < [modFlags count]) {
            [idxSet addIndex:idx];
        } else {
            [idxSet removeAllIndexes];
        }
        doneFlag = YES;
    }

    modArray = [modFlags objectsAtIndexes:idxSet];
    e = [modArray objectEnumerator];
    while (modObj = [e nextObject]) {
        modifiers |= [modObj unsignedIntegerValue];
    }

    return modifiers;
}

For example, if you call it with the following

NSDictionary * d = [self characterToKeyCode:@"π"];

it will return a dictionary that reads:

Dictionary: {
    command = 0;
    control = 0;
    function = 0;
    option = 1;
    shift = 0;
    virtKeyCode = 35;
}

Which says that the 'pi' symbol is produced by virtual key code 35 with the option key down (on a standard US keyboard: ⌥P).

Your own solution works fine under Qt too after a small patch (casting to CFDataRef):

Replacing

CFDataRef layoutData = TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);

with

CFDataRef layoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);

avoids the error:

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

For the purposes of this question there appear to be 3 types of keys that produce a CGKeyCode:

  1. plain Unicode character, e.g. A
  2. a modifier key, e.g. Shift
  3. a special key, e.g. F1.

I riffed on Ted Wrigley's answer to create proper Swift initialisers for all 3 types. The example from CGEvent(keyboardEventSource:virtualKey:keyDown:) can now be written:

guard let shiftKeyCode = CGKeyCode(modifierFlag: .shift) else { fatalError() }
guard let zKeyCode = CGKeyCode(character: "z") else { fatalError() }

let event1 = CGEvent(keyboardEventSource: nil, virtualKey: shiftKeyCode, keyDown: true)
let event2 = CGEvent(keyboardEventSource: nil, virtualKey: zKeyCode, keyDown: true)
let event3 = CGEvent(keyboardEventSource: nil, virtualKey: zKeyCode, keyDown: false)
let event4 = CGEvent(keyboardEventSource: nil, virtualKey: shiftKeyCode, keyDown: false)
//
//  CGKeyCodeInitializers.swift
//
//  Created by John Scott on 09/02/2022.
//

import Foundation
import AppKit

extension CGKeyCode {
    public init?(character: String) {
        if let keyCode = Initializers.shared.characterKeys[character] {
            self = keyCode
        } else {
            return nil
        }
    }

    public init?(modifierFlag: NSEvent.ModifierFlags) {
        if let keyCode = Initializers.shared.modifierFlagKeys[modifierFlag] {
            self = keyCode
        } else {
            return nil
        }
    }
    
    public init?(specialKey: NSEvent.SpecialKey) {
        if let keyCode = Initializers.shared.specialKeys[specialKey] {
            self = keyCode
        } else {
            return nil
        }
    }
    
    private struct Initializers {
        let specialKeys: [NSEvent.SpecialKey:CGKeyCode]
        let characterKeys: [String:CGKeyCode]
        let modifierFlagKeys: [NSEvent.ModifierFlags:CGKeyCode]
        
        static let shared = Initializers()
        
        init() {
            var specialKeys = [NSEvent.SpecialKey:CGKeyCode]()
            var characterKeys = [String:CGKeyCode]()
            var modifierFlagKeys = [NSEvent.ModifierFlags:CGKeyCode]()

            for keyCode in (0..<128).map({ CGKeyCode($0) }) {
                guard let cgevent = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(keyCode), keyDown: true) else { continue }
                guard let nsevent = NSEvent(cgEvent: cgevent) else { continue }

                var hasHandledKeyCode = false
                if nsevent.type == .keyDown {
                    if let specialKey = nsevent.specialKey {
                        hasHandledKeyCode = true
                        specialKeys[specialKey] = keyCode
                    } else if let characters = nsevent.charactersIgnoringModifiers, !characters.isEmpty && characters != "\u{0010}" {
                        hasHandledKeyCode = true
                        characterKeys[characters] = keyCode
                    }
                } else if nsevent.type == .flagsChanged, let modifierFlag = nsevent.modifierFlags.first(.capsLock, .shift, .control, .option, .command, .help, .function) {
                    hasHandledKeyCode = true
                    modifierFlagKeys[modifierFlag] = keyCode
                }
                if !hasHandledKeyCode {
                    #if DEBUG
                    print("Unhandled keycode \(keyCode): \(nsevent)")
                    #endif
                }
            }
            self.specialKeys = specialKeys
            self.characterKeys = characterKeys
            self.modifierFlagKeys = modifierFlagKeys
        }
    }

}

extension NSEvent.ModifierFlags: Hashable { }

extension OptionSet {
    public func first(_ options: Self.Element ...) -> Self.Element? {
        for option in options {
            if contains(option) {
                return option
            }
        }
        return nil
    }
}
Related