macoscocoalocalizationkeyboardmacos-carbon

Convert virtual keycode to Unicode string


I have some code I've been using to get the current keyboard layout and convert a virtual key (VK) code into a string. This works great in most situations, but I'm having trouble with some specific cases. The one that brought this to light is the accent key next to the backspace key on a German QWERTZ keyboards. See this keyboard image.

That key generates the VK code I'd expect kVK_ANSI_Equal, but when using a QWERTZ keyboard layout, I don’t get any description back. It’s ending up as a dead key, because it’s supposed to be composed with another key. Is there a way to catch these cases and do the proper conversion?

My current code is below.

TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);

if(keyboardLayout)
{
    UInt32 deadKeyState = 0;
    UniCharCount maxStringLength = 255;
    UniCharCount actualStringLength = 0;
    UniChar unicodeString[maxStringLength];

    OSStatus status = UCKeyTranslate(keyboardLayout,
                                     keyCode, kUCKeyActionDown, 0,
                                     LMGetKbdType(), kUCKeyTranslateNoDeadKeysBit,
                                     &deadKeyState,
                                     maxStringLength,
                                     &actualStringLength, unicodeString);

    if(actualStringLength > 0 && status == noErr)
        return [[NSString stringWithCharacters:unicodeString length:(NSInteger)actualStringLength] uppercaseString];
}

Solution

  • That key is a dead key, as you can see if you try it yourself or look at the Keyboard Viewer with the German layout active.

    On the Mac, the way to enter a dead key's actual character, without composing it with another character, is to press a space after it. So try that: Turn off kUCKeyTranslateNoDeadKeysBit, and if UCKeyTranslate sets the dead-key state, translate a space after it.

    Here is the fixed code with the right solution.

    TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource();
    CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
    const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr);
    
    if(keyboardLayout)
    {
        UInt32 deadKeyState = 0;
        UniCharCount maxStringLength = 255;
        UniCharCount actualStringLength = 0;
        UniChar unicodeString[maxStringLength];
    
        OSStatus status = UCKeyTranslate(keyboardLayout,
                                         keyCode, kUCKeyActionDown, 0,
                                         LMGetKbdType(), 0,
                                         &deadKeyState,
                                         maxStringLength,
                                         &actualStringLength, unicodeString);
    
        if (actualStringLength == 0 && deadKeyState)
        {
            status = UCKeyTranslate(keyboardLayout,
                                             kVK_Space, kUCKeyActionDown, 0,
                                             LMGetKbdType(), 0,
                                             &deadKeyState,
                                             maxStringLength,
                                             &actualStringLength, unicodeString);
        }
        if(actualStringLength > 0 && status == noErr)
            return [[NSString stringWithCharacters:unicodeString length:(NSUInteger)actualStringLength] uppercaseString];
    }