Sunday, June 7, 2009

XI2 Recipes, Part 1

XI2 is now merged into master. Over the next couple of days, I will post various recipes on how to deal with the new functionality. The examples here are merely snippets, full example programs to summarize each part are available here.

Update 13.07.09: Adjusted adjusted to the new cookie event API

In this first part, I will cover general things, initialisation and event selection.

Why XI2?


One of the major reasons for XI2 was the merge of MPX into the server. The current X Input Extension version 1.5 is quite limiting and extending it to fully support MPX has been tough. Programming against it is mostly annoying, so a replacement was sought that makes it easy to program against multiple devices and is flexible enough to cover future use-cases.

The XI2 protocol is fairly conservative, adding only a few requests and events but it also leaves room for more. XI2 and it's APIs are somewhat closer to the core protocol paradigms. The big differences to XI from a client's perspective are: Calls take a deviceid as parameter, there is no need to open devices and event types and masks are constant (more of this below).

Right now, the only bindings for XI2 are Xlib bindings. Nonetheless, I encourage you to start playing with XI2 and think how applications may use it. By testing it now, you can help identify problems and missing bits with the current requests before version 2.0 is released. And of course, fixing issues before a release benefits everyone.

MPX


MPX allows the use of multiple cusors and keyboard foci simultaneously. This again leads to pretty funky user interfaces - bi-manual, multi-user, you name it. It also throws a number of assumptions about current GUIs out of the window but I'll get to that some other time.

MPX introduces an explicit master/slave device hierarchy. The easiest way to remember which is which is: a physical device is a slave device, and a cursor or keyboard focus is a master device.

Slave devices are attached to one master and each time a slave generates an event, this event is routed through the master device to the client. Master devices always come in twos (one cursor, one keyboard focus) and these two are paired.

So a common setup may be a laptop with four master devices (i.e. two cursors and two keyboard foci), the touchpad controls and built-in keyboard control the first pair of master devices, and a USB wireless combo controls the second pair of master devices. The standard setup is to have one pair of master devices, and all devices are attached to this pair's master pointer or master keyboard. Which, incidentally (or not :), is exactly the same setup as we've had since server 1.4. It also means that MPX only takes effect if you create a new pair of master devices, otherwise it's invisible.

I will cover more of MPX in a follow-up post. From most clients' perspective, master device are the devices that matter, only configuration tools and some other specialised apps need to worry about slave devices.

XI2 Initialisation



The typical XI2 program starts something like this:

/* Connect to the X server */
Display *dpy = XOpenDisplay(NULL);

/* XInput Extension available? */
int opcode, event, error;
if (!XQueryExtension(dpy, "XInputExtension", &opcode, &event, &error)) {
printf("X Input extension not available.\n");
return -1;
}

/* Which version of XI2? We support 2.0 */
int major = 2, minor = 0;
if (XIQueryVersion(dpy, &major, &minor) == BadRequest) {
printf("XI2 not available. Server supports %d.%d\n", major, minor);
return -1;
}


First the client connects to the X server, then asks whether the extension is available. XQueryExtension not only tells us whether the X Input Extension is supported, but it also returns the opcode of the extension. This opcode is needed for event parsing (all XI2 events use this opcode). This opcode is set when the server started, so you cannot rely on it being constant across server sessions.

Finally, we announce that we support XI 2.0 and the server returns the version it supports. Although XIQueryVersion is a pure XI2 call, it is implemented so that it will not result in a BadRequest error if you run it against a server that doesn't support XI2 (this part is implemented in Xlib, so if you use xcb this behaviour is not the same).

XIQueryVersion not only returns the supported version, the server will also store the version your client supports. As XI2 progresses it becomes important that you use this call as the server may treat the client differently depending on the supported version.

Selecting for events


After initialising the GUI, a client usually needs to select for events. This can be achieved with XISelectEvents.


XIEventMask eventmask;
unsigned char mask[1] = { 0 }; /* the actual mask */

eventmask.deviceid = 2;
eventmask.mask_len = sizeof(mask); /* always in bytes */
eventmask.mask = mask;
/* now set the mask */
XISetMask(mask, XI_ButtonPress);
XISetMask(mask, XI_Motion);
XISetMask(mask, XI_KeyPress);

/* select on the window */
XISelectEvents(display, window, &eventmask, 1);


An XIEventMask defines the mask for one device. A mask is defined as (1 << event type) and the matching bits must be set in the eventmask.mask field. The size of eventmask.mask can be arbitrary as long as it has enough bits for the masks you need to set. The XI_LASTEVENT define specifies the highest event type in the current XI2 protocol version, so you can use this to determine the mask size. In this example, we only need 6 bits, so a 1 byte mask is enough.

XISelectEvents takes multiple event masks, so you can submit many of these (e.g. one for each device) in one go.

As shown above, each XIEventMask takes a deviceid. This is either the numeric ID for a device or the special ID of XIAllDevices or XIAllMasterDevices. If you select an event mask for XIAllDevices, all devices will send the selected events. XIAllMasterDevices does the same, but only for master devices.

Event masks for XIAllDevices and XIAllMasterDevices are in addition to device-specific event masks. For example, if you select for button press on XIAllDevices, button release on XIAllMasterDevices and motion events on device 2, device 2 will report all three types of events to the client (the effective mask is a bitwise OR).

Furthermore, XIAllDevices and XIAllMasterDevices always apply, even if the device has been added after the client has selected for events. So you only need to issue the event mask once, regardless of how many devices are currently connected and how many will be connected in the future.

The check for events is quite simple:

XEvent event;
XNextEvent(display, &event);
if (ev.xcookie.type == GenericEvent &&
ev.xcookie.extension == opcode &&
XGetEventData(dpy, &ev.xcookie))
{
switch(ev.xcookie.evtype)
{
case XI_ButtonPress:
case XI_Motion:
case XI_KeyPress:
do_something(ev.xcookie.data);
break;
}
}
XFreeEventData(dpy, &ev.xcookie);


A more detailed analysis of the data in each event will be described in a later post.
Note that you need to check against the opcode of the X Input Extension as returned by XQueryExtension(3) (as discussed before).

That's it. With this knowledge, you can already go and write simple programs that listen for events from any device. In the next part, I will cover how to list the input devices and listen for changes in the master/slave device hierarchy.

5 comments:

Ahmed Kamal said...

Thanks for the details, and for all the great work to advance the free desktop. Do the new capabilities enable multi-seat easily ? i.e. can we expect fully functioning multi-seat in F12 for example

Thanks again

Myxal said...

Am I the only one seeing the code snippets trimmed to the width of the article without word wrapping?
Using Opera 10.0

Denis Dzyubenko said...

Neat! What about blob events and single-user multitouch support, are there any plans to have it?

djkurtz said...

Thanks for your blog, it is very well written and helpful.

You have a slight typo in your example:

XEvent event;
XNextEvent(display, &event);

s/event/ev

shadone said...

actually there is another typo - XFreeEventData should be inside the if statement - to ensure it is only called when XGetEventData call was successful.