GSoC part 9: the button stack page

Posted:  • 8 minute read • Last modified:

GSoC logo horizontal

When I discussed this project with my mentor before GSoC, he told me that the button mappings were going to be the most complicated piece. This week I’ve been working on precisely that and, well, let’s just say he wasn’t wrong 😉

If you’ve been following along on GitHub, you’re probably thinking that it was a slow week. Indeed, there hasn’t been that much activity this week as in previous weeks. I’ve mostly just been stuck in my own little branch iterating and debugging away to get these pesky buttons into shape.

The button stack page

As usual I’ll discuss the various obstacles I’ve hit this past week. All of that work has resulted in the following work-in-progress:

The mockup intentionally left the button configuration dialog empty: too much was unclear to me at the time of making the mockups and I wanted the freedom to experiment for a bit to iterate towards the best design.

At first, I tried a dialog with a different stack page for each configuration (for lack of a better word) that you can set on a button. Ratbag has, at the moment, four such configurations:

  1. button mapping, where a physical button is mapped to another, logical button. This way you can for example make the right mouse button emit a left mouse button click when clicked;
  2. key mapping, where a physical button is mapped to a series of key presses on the keyboard. This way you can for example make a thumb button emit :+q to always have a way out of Vim 😉;
  3. special mappings, where a physical button is mapped to a special, mouse-specific function. This way you can for example make the right mouse button switch profiles;
  4. macros, where a physical button may be assigned a series of key events or waiting periods. This way you can for example make a thumb button press two different keys with a certain interval in between.

This first iteration looked like this:

First iteration

Those numbers should be button descriptions such as Left mouse button click, but because my device doesn’t return those (yet) it falls back to just button numbers. However, as you’ve probably already concluded yourself, many of the different button options are quite similar. In fact, I bet that as a user you won’t notice the difference between a button mapping and a special mapping. Similarly, macros are just a superset of key mappings: in addition to only key presses, a macro can also work with key releases and time intervals. There is talk among libratbag developers to merge the key mapping and macro functionality into one for precisely this reason.

Because the options are very much alike I’ve slowly been doing away with the individual stack pages, to what you’ve just seen in the video. I’m not sure if this is the final design; I might switch it around to something like GNOME Control Center’s Wacom panel. In any case, it needs more work, as you’ve probably also concluded already from the visual bug showing!

Technical details

As mentioned, the button page is the most complex part of Piper. This week has had a somewhat larger amount of head → wall than usual; let me explain.

To capture keys as shown in the video, the button dialog needs to first grab the keyboard and then process the key events in its do_key_press_event function. This function gets a Gdk.EventKey that has several fields related to the event. Of use to us are the following:

Before we continue, it’s important to note that at the moment ratbag expects a keymap signature of a list, with the first item the keycode of the regular key followed by zero or more keycodes of modifiers. Now you might think that we can just capture a key press, take the keycode and the state mask and extract from the state mask the keycodes of the modifiers. That’s exactly what my first approach was, and it failed for the following reason: converting actual modifier keypresses into a bit-mask is lossy. If you look at your keyboard, you’ll see for example a left and a right Control key; no matter which one is pressed, there is a single bit in the bit-mask representing Control. There is thus no way to accurately determine from the bit-mask which modifier keycodes were pressed.

The next attempt is then to capture the key events of the modifier keys individually, instead of collectively with the key event of a regular key. To do so, we need to cache individual key presses if they are modifier keys, and apply the whole bunch as soon as a regular key is pressed. Remember the is_modifier field of Gdk.EventKey? Yea, that’s not going to work: this field is not exposed from C through PyGObject because it is a bit of a misnomer. What you want is the state field. Hm, I guess they didn’t think of this use-case. Well, I hear you think, why don’t you just check the state bit-mask then?. Because that is always 0 for individual modifier keypresses: it contains the modifiers that apply before the key press happens, and when we press e.g. Control, it isn’t pressed before we press it.

My current solution is to “fix” the is_modifier field in Gdk.EventKey by checking the Gdk.EventKey’s keyval field against a static list of known modifier keyvals. I can do this because I also “fix” the state bit-mask by masking out all modifiers keys except for the defaults used by Gtk. These default modifiers keys depend on the Gdk backend in use, but will typically include those in the list.

_MODIFIERS = [
    Gdk.KEY_Shift_L,
    Gdk.KEY_Shift_R,
    Gdk.KEY_Shift_Lock,
    Gdk.KEY_Hyper_L,
    Gdk.KEY_Hyper_R,
    Gdk.KEY_Meta_L,
    Gdk.KEY_Meta_R,
    Gdk.KEY_Control_L,
    Gdk.KEY_Control_R,
    Gdk.KEY_Super_L,
    Gdk.KEY_Super_R,
    Gdk.KEY_Alt_L,
    Gdk.KEY_Alt_R,
]

event.is_modifier = event.keyval in self._MODIFIERS
event.state &= Gtk.accelerator_get_default_mod_mask()

There is another such workaround to display the correct modifiers in the Gtk.ShortcutLabel: we need to reconstruct a state bit-mask from a list of keyvals. Again, we only check for those same default modifiers and simply flag the correct bit:

mask = Gdk.ModifierType(0)
for (_, keyval) in self._modifiers:
    if keyval == Gdk.KEY_Shift_L or keyval == Gdk.KEY_Shift_R or keyval == Gdk.KEY_Shift_Lock:
        mask |= Gdk.ModifierType.SHIFT_MASK
    elif keyval == Gdk.KEY_Hyper_L or keyval == Gdk.KEY_Hyper_R:
        mask |= Gdk.ModifierType.HYPER_MASK
    elif keyval == Gdk.KEY_Meta_L or keyval == Gdk.KEY_Meta_R:
        mask |= Gdk.ModifierType.META_MASK
    elif keyval == Gdk.KEY_Control_L or keyval == Gdk.KEY_Control_R:
        mask |= Gdk.ModifierType.CONTROL_MASK
    elif keyval == Gdk.KEY_Super_L or keyval == Gdk.KEY_Super_R:
        mask |= Gdk.ModifierType.SUPER_MASK
    elif keyval == Gdk.KEY_Alt_L or keyval == Gdk.KEY_Alt_R:
        mask |= Gdk.ModifierType.MOD1_MASK

Masking the modifier bit-mask means that at least for now, these workarounds should be fine. The solution that we want to work towards is to make ratbag not care about the signature of its keymap, so that Piper doesn’t have to either. In this case we can just capture every key press in the same manner, without having to differ modifiers from regular keys. Another possible solution is to merge key mappings and macros, as macros already do not have a specific signature unlike the key mappings. Until then, workarounds it is.

The other big problem I encountered this week is the reverse of the above, basically. When the button dialog opens, we want to display the current key mapping, if any. Keycodes are stored on the device as defined in linux/input.h and as used by evdev. To go from Gdk keycodes to evdev keycodes is rather straightforward: just subtract the magical number of 8 from the keycode. This number stems from the fact that X originally reserved keycodes 0 through 8. Going the other way then is as simple as adding 8 to the evdev keycode. However, to be able to display the shortcut in a Gtk.ShortcutLabel we also need the keyval. If you remember the explanation from above, to get a keyval from a keycode we also need to know the level and the group. This information is simply not available from the device, and hence we cannot accurately map a keycode to a keyval. My mentor said it’s oke to inform the user we are capturing key codes and not key symbols, which means I can use Gdk.Keymap.get_entries_for_keycode with a level and group of 0. This will at the very least give the correct physical key; only the symbol might be off. At least you’ll only notice this in the initial state of the dialog, and only if you 1) have a keyboard with groups and 2) have actually set such a special key.

All of this work can be followed in the pull request, where you will also find the discussion that I gave an overview of here. If you’re interested and have a better idea to fix these problems, please do chime in and tell us!

Smaller changes

In other news, support for enabling and disabling profiles is just around the corner. This is required for the next step, in which I’ll be adding support for profiles to Piper.

This blog post is part of a series. You can read the next part about finishing the button page here or the previous part about the LED stack page here.