Layout-Independent Keyboard Hanling

Member
Posts: 21
Joined: 2009.12
Post: #1
The problem...
Suppose you want your game to say, "press 'w' to move forward." If the user has an American ASCII keyboard, he presses w, and moves forward. Now, suppose a different user has a Dvorak keyboard. He presses w, and nothing happens.

The simple solution to this problem is, instead of basing your game logic on ASCII characters, to instead base your game logic on hardware keycodes. So, when your game tests for input on the "w" key, what it's really doing is testing for input on the third key from the left above the home row. Thus, pressing this key will always move the character forward, regardless of the user's keyboard layout.

The only remaining problem with this approach, is that your game still reads: "Press 'w' to move forward." Assuming the user has a Dvorak keyboard, the message should read, "Press ',' to move forward." To solve this problem, I propose the following method:

Code:
uint16_t GetLayoutEquivalentForKey (GAME_KEY key);

In this method, 'uint16_t' represents a unicode character, and 'key' represents an enumeration which maps each game key (GAME_KEY_A, GAME_KEY_B, etc...) to its associated hardware key. So, when we say GAME_KEY_W, we're really referring to the third key in from the left, above the home row. This method, then, takes a GAME_KEY, and returns it's UNICODE equivalent.

Thus our problem is solved! Instead of displaying the message "Press 'w' to move forward." we do something like this (this might not be syntactically correct):
Code:
cout "Press '" << GetLayoutEquivalentForKey(GAME_KEY_W) << "' to move forward.\n";

So, if the user has an American ASCII keyboard, the text will read:
Code:
Press 'w' to move forward.
If the user has a Dvorak keyboard, the text will read:
Code:
Press ',' to move forward.
If the user has a Chinese keyboard, the text will read:
Code:
Press '我' to move forward.
etc...

Are you still with me? Great! Now, the next step is to implement this function. One approach is to manually create a character mapping for every possible keyboard layout. But that approach is both time consuming and error prone. A more novel idea is to have the OS do the work for us. My idea is that, when the application is launched, create some kind of invisible text view, and send custom NSKeyDown events to that view for every key on the keyboard --then capture the resulting characters and save them to a character map. But this approach seems somewhat excessive, so I was wondering if anyone had other ideas? Also, if anyone can think of how this function might be implemented on Windows and iPhone, that would be useful as well!
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #2
You don't need to do anything that complex, the OS can provide you with keycode to character mappings, and since the "key cap" should be the same as what character the key generates when pressed with no modifiers, modulo case, that should be sufficient.

It looks like "UCKeyTranslate" is the appropriate API. There'll be an equivalent on Windows, but on the iPhone you don't use keys as game controls so I don't see how it's relevant there Rasp
Quote this message in a reply
⌘-R in Chief
Posts: 1,260
Joined: 2002.05
Post: #3
The layout of this code is pretty ugly, but does the trick. The ugliness primarily comes from non-Unicode keyboard support, but I think it's pretty safe to abandon that now. I think the OS started requiring them at some point?

Code:
CFStringRef LiDTS_CopyTranslateHotKey(UInt16 virtualKeyCode)
{
    OSStatus err = noErr;
    const UCKeyboardLayout * uchrData = NULL;
    CFStringRef translatedString = NULL;
    CFMutableStringRef uppercaseString = NULL;
    
    
    #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
        
        // Get a keyboard layout object for the keyboard
        KeyboardLayoutRef theCurrentLayout=NULL;
        err = KLGetCurrentKeyboardLayout(&theCurrentLayout);
        
        // If there's a layout, then get the unicode keyboard layout data
        if (noErr == err) {
            err = KLGetKeyboardLayoutProperty(theCurrentLayout,
                                                kKLuchrData, (const void **)&uchrData);
        }
    
    #else
    
        TISInputSourceRef currentLayoutRef = TISCopyCurrentKeyboardLayoutInputSource();
        CFDataRef uchr = TISGetInputSourceProperty(currentLayoutRef, kTISPropertyUnicodeKeyLayoutData);
        uchrData = (const UCKeyboardLayout *)CFDataGetBytePtr(uchr);
    
    #endif
    
    
    // If we got the uchr / unicode layout data, then...
    if (err == noErr && uchrData != 0) {
        UniChar buf[256];
        UniCharCount actualStringLength=0;
        UInt32 deadKeyState = 0;
        
        UCKeyTranslate(
                                   uchrData,
                                   virtualKeyCode,
                                   kUCKeyActionDisplay,
                                   cmdKey >> 8, // !!!
                                   LMGetKbdType(),
                                   kUCKeyTranslateNoDeadKeysMask,
                                   &deadKeyState,
                                   sizeof(buf)/sizeof(UniChar),
                                   &actualStringLength,
                                   buf
                                   );
        
        /*
        
         UCKeyTranslate(
         const UCKeyboardLayout *  keyLayoutPtr,
         UInt16                    virtualKeyCode,
         UInt16                    keyAction,
         UInt32                    modifierKeyState,
         UInt32                    keyboardType,
         OptionBits                keyTranslateOptions,
         UInt32 *                  deadKeyState,
         UniCharCount              maxStringLength,
         UniCharCount *            actualStringLength,
         UniChar                   unicodeString[])                  AVAILABLE_MAC_OS_X_VERSION_10_0_AND_LATER;

         */
        translatedString = CFStringCreateWithCharacters(kCFAllocatorDefault,
                                                        buf, actualStringLength);
    }
    
    #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
    // Otherwise fallback to non-unicode keyboard data
    else {
        
        UInt32 chars;
        UInt32 deadKeyState = 0;
        TextEncoding keyboardEncoding;
        const void *kchrData;
        
        err = KLGetKeyboardLayoutProperty(theCurrentLayout,
                                                kKLKCHRData, &kchrData);
        
        chars = KeyTranslate(
                             kchrData,
                             (virtualKeyCode & 0x7F) | cmdKey,  // !!!
                             &deadKeyState);
        
        err = UpgradeScriptInfoToTextEncoding(
                                                    (ScriptCode)GetScriptManagerVariable(smKeyScript),
                                                    kTextLanguageDontCare,
                                                    kTextRegionDontCare,
                                                    0, // no font name
                                                    &keyboardEncoding
                                                    );
        
        // There shouldn't be more than one character if dead key state
        // was zero?
        // Accented characters take a single byte in legacy encodings.
        if (!err)
        translatedString = CFStringCreateWithBytes(kCFAllocatorDefault,
                                                   (UInt8*)&chars + 3, 1, keyboardEncoding, FALSE);
    }
    #endif
    
    if (translatedString)
        uppercaseString = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, translatedString);
    
    if (uppercaseString)
        CFStringUppercase(uppercaseString, 0);
    
    if (translatedString)
        CFRelease(translatedString);
    
    return uppercaseString;
}
Quote this message in a reply
Member
Posts: 21
Joined: 2009.12
Post: #4
Yep, UCKeyTranslate looks to be exactly what I need. Thanks! It looks like I need to call this function in conjunction with LMGetKbdType() to get the current layout, and without modifiers (as you mention) and everything should fall into place. Outstanding! One nagging point is that LMGetKbdType() appears to be a carbon function. I'll be looking into this more in depth, tomorrow.

EDIT: Regarding Windows, I believe that scancodes are analogous to Apple's keycodes. They only seem to differ in cases where the physical layout differs. For example, on the european keyboard, the key above the enter key changes positions, so naturally the scancode must change positions, as well. There's also an additional key next to the left shift key, so there's a new scancode for that key. These subtle differences must also exist with Apple keycodes as well, so the two types of codes are likely comparable, after all. link

Finally, you're absolutely right about the iPhone. This really wouldn't apply, since you wouldn't be controlling the game with a keyboard. And it's easy enough to use the keyboard for text input (I'm still kinda curious, tho).


@FreakSoftware, I started writing my post, before you made yours (I was busy playing around with code / reading documents, etc...). Your code looks great, and I love supporting old stuff (ugly or not)!
Quote this message in a reply
⌘-R in Chief
Posts: 1,260
Joined: 2002.05
Post: #5
Mister T Wrote:One nagging point is that LMGetKbdType() appears to be a carbon function. I'll be looking into this more in depth, tomorrow.

LMGetKbdType() is supported in 64-bit Carbon.
Quote this message in a reply
Member
Posts: 21
Joined: 2009.12
Post: #6
Okay, freak, your code seems to work beautifully for latin-script layouts. For example, if I send the function the keycode for w, with the American QWERTY layout, I get back 'w'. If I change the layout to Dvorak, I get ',' just as I should. If I change it to AZERTY, I get 'z' just as I should. However, if I change the layout to anything other than latin-script languages, I just get 'w' back, and I'm not really sure why. Am I doing something wrong? I have foreign fonts installed on my system, and they appear correctly in the keyboard Viewer.

Code:
    // Print with CFShow
    printf(">>>>>>>>Print with CFShow()<<<<<<<< \nCharacter: ");
    CFStringRef myRef = LiDTS_CopyTranslateHotKey(13);
    CFShow(myRef);
    
    // Print with wchar
    printf("\n>>>>>>>>Print with wprintf()<<<<<<<< \n");
    setlocale( LC_ALL, "" );
    UniChar uChar = CFStringGetCharacterAtIndex (myRef,0);
    wprintf( L"Character: %lc\nHex value: 0x%3.3X \n\n", uChar, uChar );
    
    // Debug Info
    printf(">>>>>>>>DEBUG INFO<<<<<<<<");
    CFShowStr(myRef);

Here's what I get when I set the layout to Wubi Xing, and send 13 to the function (the keycode for w):
Code:
>>>>>>>>Print with CFShow()<<<<<<<<
Character: W

>>>>>>>>Print with wprintf()<<<<<<<<
Character: W
Hex value: 0x057

>>>>>>>>DEBUG INFO<<<<<<<<
Length 1
IsEightBit 1
HasLengthByte 1
HasNullByte 1
InlineContents 0
Allocator SystemDefault
Mutable 1
CurrentCapacity 32
DesiredCapacity 32
Contents 0x103403b60

Thanks for your help.
Quote this message in a reply
Luminary
Posts: 5,143
Joined: 2002.04
Post: #7
Even JIS keyboards have roman key caps marked: http://www.flickr.com/photos/hetima/1159059244/sizes/l/
Quote this message in a reply
Member
Posts: 21
Joined: 2009.12
Post: #8
I know that's true of some layouts --Japanese in particular-- but I don't think it's true of all non-roman layouts. Just looking at the wikipedia article, there seem to be many layouts without any roman markings.

Meh, I'm probably obsessing over this more than I should...
Quote this message in a reply
Post Reply 

Possibly Related Threads...
Thread: Author Replies: Views: Last Post
  Keyboard Handling skyhawk 10 6,626 Oct 26, 2010 08:47 AM
Last Post: skyhawk
  keyboard limitations daveh84 8 4,519 Feb 13, 2009 02:48 PM
Last Post: ferum
  Cocoa Keyboard help Iceman 5 4,107 Aug 1, 2005 06:06 PM
Last Post: nabobnick
  Keyboard Question rogue 6 3,733 Jul 10, 2003 06:36 AM
Last Post: rogue