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.
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:
This first iteration looked like this:
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!
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:
hardware_keycode
field that gives the raw keycode of the pressed or
released key. This keycode is the number of the physical key on the keyboard.keyval
field as a name of the logical symbol that was pressed or
released. This keyval is decided by the level, which indicates in a vertical
direction which symbol is used (for example, pressing the key with number 1 on
it has keyval 1 for level 0 and keyval ! for level 1), and the group, which,
on a keyboard with more than 1 group (US keyboards typically have a single
group) indicates in a horizontal direction which symbol is used. For example,
on a typical Scandinavian keyboard there are several symbols on
the same key in a horizontal direction.is_modifier
field that tells whether the pressed or released key is a
modifier key. Modifier keys are for example Control or
Alt.state
field that gives a bit-mask representing the state of the
modifier keys.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!
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.