Tuesday 12 July 2016

Keyboard Layouts

An important step in the process of making a successful keyboard controller is making sure it will work out of the box for most of the users, regardless of where they come from and which keyboard layout is used. The current approach in Mixxx is pretty straight forward. We have different mappings for different keyboard mappings; 12 *.kbd.cfg files, to be specific. Mixxx chooses between one of these based on the current locale or defaults to the American English keyboard layout.

One preset file, for all layouts

Having multiple files is ok for the current implementation, but it doesn't entirely fit in the controller preset architecture. It should, because KeyboardController is a controller, and it doesn't fit the controller preset architecture, because all those 12 files are the same preset, in essence. They all represent the Mixxx default mapping. Therefore, it would be weird to get, for example, the Greek preset listed as an option, being an American. And what would the naming be like? Default Mapping_en_US, Default Mapping_ru_RU, Default Mapping_el_GR? It would be a bit messy, especially considering that users will also be making custom presets. However, if we just ship one keyboard preset: "Default Mapping" it would be much cleaner. This file would just hold mapping information for one keyboard layout. If we had some kind of layouts lookup table, Mixxx could then translate the shortcuts to the current locale, if found in the layouts lookup table. This could be done when loading the preset.

Layouts XML file

In order for Mixxx to translate keyboard shortcuts from one keyboard layout to another, it needs to know about both layouts. It needs to have some kind of unique key ID number per key, which characters will be bound to, for different layouts. For example, we could give the key that is right next to the 'Tab' key, ID: 17. This key will hold the character 'Q' for American English keyboard layouts (QWERTY), and  'A' for French keyboard layouts (AZERTY). The layouts XML file holds this information.

Let's do a breakdown on the following XML snippet:

<KeyboardLayoutsResourceFile>

  <layouts>
    <lang>en_US</lang>
    <lang>fr_FR</lang>
    <lang>ru_RU</lang>
  </layouts>

  <key key_id="1">
    <char dead_key="0" lang="en_US" modifier="NONE">`</char>
    <char dead_key="0" lang="en_US" modifier="SHIFT">~</char>
    <char dead_key="0" lang="fr_FR" modifier="NONE">²</char>
    <char dead_key="0" lang="fr_FR" modifier="SHIFT" />
    <char dead_key="0" lang="ru_RU" modifier="NONE">ё</char>
    <char dead_key="0" lang="ru_RU" modifier="SHIFT">Ё</char>
  </key>

  <key key_id="2">
    <char dead_key="0" lang="en_US" modifier="NONE">1</char>
    <char dead_key="0" lang="en_US" modifier="SHIFT">!</char>
    <char dead_key="0" lang="fr_FR" modifier="NONE">&amp;</char>
    <char dead_key="0" lang="fr_FR" modifier="SHIFT">1</char>
    <char dead_key="0" lang="ru_RU" modifier="NONE">1</char>
    <char dead_key="0" lang="ru_RU" modifier="SHIFT">!</char>
  </key>

  <key key_id="3">
    <char dead_key="0" lang="en_US" modifier="NONE">2</char>
    <char dead_key="0" lang="en_US" modifier="SHIFT">@</char>
    <char dead_key="0" lang="fr_FR" modifier="NONE">é</char>
    <char dead_key="0" lang="fr_FR" modifier="SHIFT">2</char>
    <char dead_key="0" lang="ru_RU" modifier="NONE">2</char>
    <char dead_key="0" lang="ru_RU" modifier="SHIFT">&quot;</char>
  </key>

  <!-- ... Etc ... -->

  <key key_id="51">
    <char dead_key="0" lang="en_US" modifier="NONE">n</char>
    <char dead_key="0" lang="en_US" modifier="SHIFT">N</char>
    <char dead_key="0" lang="fr_FR" modifier="NONE">n</char>
    <char dead_key="0" lang="fr_FR" modifier="SHIFT">N</char>
    <char dead_key="0" lang="ru_RU" modifier="NONE">т</char>
    <char dead_key="0" lang="ru_RU" modifier="SHIFT">Т</char>
  </key>

  <key key_id="52">
    <char dead_key="0" lang="en_US" modifier="NONE">m</char>
    <char dead_key="0" lang="en_US" modifier="SHIFT">M</char>
    <char dead_key="0" lang="fr_FR" modifier="NONE">,</char>
    <char dead_key="0" lang="fr_FR" modifier="SHIFT">?</char>
    <char dead_key="0" lang="ru_RU" modifier="NONE">ь</char>
    <char dead_key="0" lang="ru_RU" modifier="SHIFT">Ь</char>
  </key>

  <key key_id="53">
    <char dead_key="0" lang="en_US" modifier="NONE">,</char>
    <char dead_key="0" lang="en_US" modifier="SHIFT">&lt;</char>
    <char dead_key="0" lang="fr_FR" modifier="NONE">;</char>
    <char dead_key="0" lang="fr_FR" modifier="SHIFT">.</char>
    <char dead_key="0" lang="ru_RU" modifier="NONE">б</char>
    <char dead_key="0" lang="ru_RU" modifier="SHIFT">Б</char>
  </key>
</KeyboardLayoutsResourceFile>

KeyboardLayoutsResourceFile
The root element, which is parent node to all other elements.

Layouts
A little manifest node, telling which keyboard layouts this layouts XML file holds information of.

Lang
Parented to layouts, the content of this element is a lowercase ISO 631 language code and an uppercase ISO 3166 country code, separated by an underscore. For example, en_US, for American English, or en_GB, for Brittish, es_ES, for Spanish, or el_GR, for Greek. This follows the QLocale name naming format.

Key
Element representing one physical keyboard key. Each key element is identified by a unique number, based on the key's scancode. This key ID is stored in the key's key_ID attribute.

Char
Tells about which character is bound to the key element, which it is parented to. It specifies the keyboard layout in the lang attribute, which should be the same as one of the keyboard layouts specified in the layouts manifest element.

When targeting the key with ID 2, on an American English layout, the key sequence that will be read by Qt, is just 1. But when targeting the same key, with the shift modifier, the key sequence is Shift+!, not Shift+1. That's why we also need to know which character is bound to the key ID with the shift modifier. The modifier is specified in the modifier attribute.

On some keyboard layouts, some keys that don't do anything and are stored in the OS, until another key is pressed: dead keys. Sometimes a key is not a dead key with modifiers, but it is without, or vice-versa. This is why we need to store whether this char is a dead key or not, in the dead_key attribute. It is a dead key when it is set to 1, and it is not when not set to 1 (everything other than 1 is considered as a not-dead key.

Finally, some keys don't exist on some keyboard layouts. Take the key that is just above the tab key, with ID 1. On French keyboard layouts, this key, with the shift modifier, doesn't exist. In this case, the char element is self-closing.

Key IDs


Layouts editor

It can be a pain to have to create this layout resource files by hand. Fortunately, the layouts editor does the tedious work for you. It is a little tool that makes it easy setting up new layouts and adding them to a layouts resource file.

Here is a little demo, when opening layouts.xml, containing all keyboard layouts supported by Mixxx, as of now.


Some keys are yellow, those are dead keys. Keys can be marked as dead keys by right clicking a key and clicking on Dead key. Keys can be setup by clicking on a key and typing the key that corresponds to the selected key on the GUI. When a key is typed, the focus moves automatically to the following key, so that you can just begin at key 1, and start pressing all keys from left to right till you arrive at key 55.

When pressing shift, the keyboard reveals which characters are bound to the keys with the shift modifier. To setup keys with the shift modifier, do the exact same thing, holding shift.

If a key doesn't exist on your keyboard layout, just let the key open. You can reset a key by pressing Del on your keyboard or by right clicking a key and selecting Reset.

Keyboard presets, new format

Now that we can potentially translate shortcuts from one keyboard layout to another, there is no need anymore to store information for all keyboard layouts. In the new format, each key sequence is given a keyboard layout which it belongs to and a key ID. Then, when a user with keyboard layout ru_RU goes and loads a preset, Mixxx will first look for a key sequence for ru_RU. If it can't find it, it will translate the key sequence from whatever language is present, to Russian. Why add the possibility of overloading key sequences for different layouts? We need to; sometimes a key sequence can not be translated because the key to translate is a dead key in the targeted layout.

Let's breakdown the following XML snippet:

<MixxxKeyboardPreset mixxxVersion="2.0.1+" schemaVersion="1">

  <info>
    <name>Example</name>
    <author>Jordi</author>
    <description>Example preset based on legacy mapping files en_US and fr_FR</description>
  </info>

  <controller>
    <group name="[Microphone]">
      <control action="talkover">
        <keyseq key_id="1" lang="en_US">`</keyseq>
        <keyseq key_id="45" lang="fr_FR">&lt;</keyseq>
      </control>
    </group>

    <group name="[Master]">
      <control action="crossfader_down">
        <keyseq key_id="35" lang="en_US">g</keyseq>
      </control>

      <control action="crossfader_up_small">
        <keyseq key_id="36" lang="en_US">Shift+h</keyseq>
      </control>
    </group>

    <group name="[Channel1]">
      <control action="back">
        <keyseq key_id="31" lang="en_US">a</keyseq>
      </control>

      <control action="reverse">
        <keyseq key_id="31" lang="en_US">Shift+a</keyseq>
      </control>

      <control action="fwd">
        <keyseq key_id="32" lang="en_US">s</keyseq>
      </control>

      <control action="beatsync">
        <keyseq key_id="2" lang="en_US">1</keyseq>
      </control>
    </group>

    <group name="[AutoDJ]">
      <control action="shuffle_playlist">
        <keyseq key_id="universal_key" lang="en_US">Shift+F9</keyseq>
      </control>
      
      <control action="enabled">
        <keyseq key_id="universal_key" lang="en_US">Shift+F12</keyseq>
      </control>
    </group>

    <group name="[KeyboardShortcuts]">
      <control action="FileMenu_LoadDeck1">
        <keyseq>Ctrl+o</keyseq>
      </control>
      
      <control action="FileMenu_LoadDeck2">
        <keyseq>Ctrl+Shift+O</keyseq>
      </control>
    </group>
  </controller>
</MixxxKeyboardPreset>

MixxxKeyboardPreset
The root element, which is parent node to all other elements. This is not unique to keyboard controller presets. Each controller preset XML has this root element.

Info
Contains information that will be visible in preferences -> controllers, when opening a preset. This is also, not unique to keyboard controller presets. Each controller preset should include an info tag, otherwise, it won't load.

Controller
The actual mapping information will be stored inside of this controller element.

Keyseq
Contains a key sequence. The lang attribute stores the keyboard layout this key sequence is valid for. Mixxx will use both this and the key ID stored in the key_id attribute if it needs to translate this key sequence to another layout. Note that some key_id's value is universal_key, which tells Mixxx that it doesn't need to worry about translating the key sequence. Namely, these key sequences are the same for all keyboard layouts. This is true for keys as Space, Enter, Backspace, or the arrow keys.

Control
This element is a parent to keyseq elements. It tells Mixxx which action to execute when the key sequence is pressed (or a translated one). Mixxx puts together a ConfigKey where the group is the group name, and the item is the action attribute. The group name is retrieved from the name attribute of the group element to which this element is parented to.

Keyboard shortcuts
Key sequences of controls belonging to the [KeyboardShortcuts] group are translated in Mixxx using Qt::tr(). Therefore, these keyseq elements won't include nor a key_id attribute, nor a lang attribute.


Migration from *.cfg to *.xml

Converting the 12 *.kbd.cfg files located in mixxx/res/keyboard by hand is really tedious and would take me ages. That's why I made a little conversion tool that translates one or more legacy files to one XML file. This utility is also given a layouts resource file (made with the layouts editor). This way it can take into account which <keyseq> elements to add and which ones to leave out.

Here is a little demo that shows how to add legacy files, choose a layouts resource file, set a preset name and save the preset.



No comments:

Post a Comment