Tuesday, June 16, 2009

XI2 Recipes, Part 3

This post is part of a mini-series of various recipes on how to deal with the new functionality in XI2. The examples here are merely snippets, full example programs to summarize each part are available here.

In Part 1 I covered how to initialise and select for events and in Part 2 how to query for devices and monitor the hierarchy. In this part, I will cover how to get more information about the devices.

Device capabilities for master and slave devices

Once upon a time, mice were simply mice and keyboards were simply keyboards. Nowadays, some mice have 3 buttons, others have 8, some have two scrollwheels, etc. Tablets, touchscreens and touchpads behave like a mouse, but have their own coordinate systems, their own set of buttons etc.

Why is this information important anyway? The valuator (== axis) information is sent in device-dependent coordinates. So for example, a wacom tablet may send coordinates from 0 to 50000 instead of the screen resolution. Likewise, some device may have a particular button that other devices don't have. Since button numbers are sequential, button 8 one one device may have a completely different functionality than button 8 on another device.

In Part 2, I detailed the master/slave device hierarchy and explained that each event from a slave device is routed through the attached master device. In addition to that, whenever a slave device sends an event, the master device changes capabilities to reflect the current slave device. Assuming that two slave devices (one mouse, one graphics tablet) hang off one master device (i.e. cursor), the master device will thus look like either the mouse or the tablet - depending on who sent the last event. This switch happens before the actual event.

The order of things is (if we assume the mouse was the last used device):

  1. Move the mouse

    1. Mouse events are sent to the client

  2. Move the tablet

    1. The master changes capabilities to look like the tablet.

    2. A DeviceChanged event is sent to notify clients about this change.

    3. The move event and all following events originating from the tablet are sent to clients.

  3. Move the mouse

    1. The master changes capabilities to look like the mouse.

    2. A DeviceChanged event is sent to notify clients about this change.

    3. The move event and all following events originating from the mouse are sent to clients.

  4. rinse, wash, repeat

Following the DeviceChanged events, the master device the client will always look like the physical device that is currently sending the events. And since clients should listen to master devices, this means that clients can actually adjust their UI depending on what physical device is in use. For example, a drawing program such as the GIMP could switch automatically between mouse mode and tablet mode. The drawback of this approach though is that depending on when you call XIQueryDevice the information may be different. You can mitigate this by monitoring XIDeviceChangedEvents (see below).

Key and valuator information reflects the currently used slave device. Button information - not quite as simple. A master device always has as many buttons as the slave device with the highest number of buttons. This allows combining multiple devices for button presses, e.g. button 1 on one device and button 3 on another device is a combined 1+3 button state on the master device. The master device will change dynamically to reflect the right number of buttons whenever a slave device is attached or detached. More about that below.

Querying extra device information

So, assuming you just called XIQueryDevice and you got back a set of devices and XIDeviceInfo structs. The device capabilities are in the device classes, much in the same matter as in XI 1.

Right now, three classes are defined: button class, key class, and valuator class. A device may have zero or one button class, zero or one key class and zero or more valuator classes. A typical mouse will have a button class and two valuator classes (one for the x axis and one for the y axis).

static void print_info(XIDeviceInfo *info)
int i;
XIAnyClassInfo **classes = info->classes;

printf("\tReporting %d classes:\n", info->num_classes);
for (i = 0; i < info->num_classes; i++) {
switch(classes[i]->type) {
case XIButtonClass:
case XIKeyClass:
case XIValuatorClass:

All classes share the same two fields, type and sourceid. The sourceid denotes the device this particular class came from. For slave devices, this is the device id. For master devices, this is the device id of the slave that has sent the event through the server.
The current X server implementation always copies all classes from the slave device into the master device, so all sourceids of one device are the same. In the future, we may add partial class copying and you are not guaranteed that the button class is from the same physical device as the valuator class, etc.

Let's look at the button class struct:

typedef struct
int type;
int sourceid;
int num_buttons;
Atom *labels;
XIButtonState state;
} XIButtonClassInfo;

The first field is always XIButtonClass, and num_buttons tells us how many buttons the device actually has. This is complemented by the array of button labels. Each atom defines a button label (note that this label might be None, an Atom of zero, if the device cannot label its buttons). So for a standard mouse you should get num_buttons of at least 3 and the labels are (usually) the atoms for "Button Left", "Button Middle", "Button Right". Finally, the state field has a bit set for each button currently down on this device.

You can now write user interfaces that interpret the buttons as what they are. Previously, you had to apply magic to guess which button would do what, now the devices actually tell you what each button does (provided the kernel drivers do). The order of the buttons array is always the physical order, regardless of any button mapping.

It is a bit trickier on the master device. Since the master device buttons are the union of all attached slave devices, the number of buttons is always the highest number of buttons on any slave device. For example, if three devices with 3, 9 and 20 buttons, the master device will have 20 buttons. If you press button 20 on device 3 and button 3 on device 1, the combined state is 20+3 on the master device. There is one downside to this union: if the devices have different button labels, the labels may change between press and release. For example, pressing button 1 on device 1 ("Button Left") will result in the DeviceChangedEvent with "Button Left" as label for 1. If you then press button 1 on device 2 ("Button 0"), no event is sent from the MD (because the button is already down). Now release device 1 (no release event, the button is still down), then release device 2. At the time of the release event, the label for button 1 was "Button 0", even though it was "Button Left" at the time of the press.

Let's look at the key class struct:

typedef struct
int type;
int sourceid;
int num_keycodes;
int *keycodes;
} XIKeyClassInfo;

Essentially the same as the button class info, except that it specifies numerical keycodes instead of button labels. This is somewhat different to the core approach that gives you a min_keycode and a max_keycode (which incidentally must always be the same anyway). In XI2, a keyboard with three keys will only give you those three keycodes it actually sends.

Finally, let's look at the valuator struct:

typedef struct
int type;
int sourceid;
int number;
Atom label;
double min;
double max;
double value;
int resolution;
int mode;
} XIValuatorClassInfo;

Number is the physical number of the axis (you're not guaranteed to get them in order from the server). The label is - like button labels - an Atom specifying the axis type (e.g. "Rel X"). Min, max and resolution give you the minimum and maximum axis value and the resolution in units/m, mode is either absolute or relative depending on the device.
Value is the current value of the valuator - which of course only makes sense if a valuator is an absolute axis.

That's it. With this information, you can now get all necessary info about a device.

Monitoring device changes

As mentioned above, the XIDeviceChangedEvent notifies you when a device has changed. Its information is mostly the same as what XIQueryDevice provides, with two extra fields: reason and sourceid. The sourceid simply specifies the device that triggered this device. In some cases, this is the slave device, in some cases sourceid and deviceid are the same. The reason field specifies why this event was sent. Right now, there's two reasons: XISlaveSwitch and XIDeviceChange. The former is sent whenever you change between physical devices attached to the same master. The latter is sent whenever a device changes capabilities on its own accord. One example for this is the master device changing the number of available buttons when a slave device is attached or detached.


Unknown said...

the handling of buttons on the 'mouse' devices seems like a problem

while I can see the case where you would want to treat button 1 on all mice the same, I can also see cases where you would want to treat them differently.

At the very least it would seem to me that when a slave is added to a master there should be a flag tha tells the system if the buttons should overlap or be appended

Peter Hutterer said...

that's exactly what the DeviceChanged event is for. it provides you with the list of all buttons on that device including labels.

if you want buttons to not overlap, you need to map them, the labels only tell you the physical meaning of the buttons.

Florian said...

So maybe I'm a little late for this question, but I guess it's worth a try.

Is there a way to determine what type of hardware a certain slave device is? I.e. mouse or touchpad? You mention that gimp could adapt its interface according to the most recently used input device, but I can't figure out how to tell whether it's a mouse or a touchpad.

xinput1 has a "type" property in XDeviceInfo, which is exactly what I'm looking for. Gnome control center hides mouse configuration settings based on the absence of a MOUSE type device, and it seems like, for some mice, that value is incorrectly given as KEYBOARD only so I figured it might work with xinput2. Though now I am unable to find something like "type" in xinput2.

See this bug:

Peter Hutterer said...

XI2 doesn't expose that type for exactly that reason, we don't really know either what type of a device something is and some devices are more than one type.

Florian said...

Thanks Peter! So this is a gnome bug I guess.