Tuesday, June 9, 2009

XI2 Recipies, Part 2

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.

Update 13.07.09: adjusted to the new cookie event API

A word of warning: XI2 is still in flux and the code documented here may change before the 2.0 release.

In Part 1 I covered how to initialise and select for events. In this part, I will cover how to query and modify the device hierarchy.

What is the device hierarchy?


The device hierarchy is the tree of master and slave devices. A master pointer is represented by a visible cursor, a master keyboard is represented by a keyboard focus. Slave pointers and keyboards are (usually) physical devices attached to one master device.

The distinction may sound odd first but we've been using it for years. A computer has two sets of interfaces: Physical interfaces are what we humans employ to interact with the computer (e.g. mouse, keyboard, touchpad). Virtual interfaces is what applications actually see. Think of it: if you have a laptop with two physical devices (a mouse and a touchpad) you're still only controlling one virtual device (the cursor). So although you have two very different physical interfaces, the application isn't aware of it at all.

This works mostly fine as long as you have only one virtual interface per type but it gets confusing really quickly if you have multiple users on the same screen at the same time. Hence the explicit device hierarchy in XI2.

We call virtual devices master devices, and physical devices slave devices. Note that there are exceptions where a slave device is a emulation of a physical device.

A device may be of one of five device types:

  • Master pointers are devices that represent a cursor on the screen. One master pointer is always available (the "Virtual core pointer"). Master pointers usually send core events, meaning they appear like a normal pointer device to non-XI applications.

  • Master keyboards are devices that represent a keyboard focus. One master keyboard is always available (the "Virtual core keyboard"). Master keyboards usually send core events, meaning they appear like a normal keyboard device to non-XI applications.

  • Slave pointers are pointer devices that are attached to a master pointer. Slave pointers never send core events, they are invisible to non-XI applications and can only interact with a core application if they are attached to a master device (in which case it's actually the master device that interacts)

  • Slave keyboards are keyboard devices that are attached to a master keyboard. Slave keyboards never send core events, they are invisible to non-XI applications and can only interact with a core application if they are attached to a master device (in which case it's actually the master device that interacts)

  • Floating slaves are slave devices that are currently not attached to a master device. They can only be used by XI or XI2 applications and do not have a visible cursor or keyboard focus.



So what does attachment mean? A master device cannot generate events by itself. If a slave device is attached to a master device, then each event that the slave device generates is also passed through the master device. This is how the X server works since 1.4, if you click a mouse button, the server sends a click event from the mouse and from the "virtual core pointer".

A floating device on the other hand does not send events through the master device. They don't control a visible cursor or keyboard focus and any application listening to a floating slave device needs to control focus and cursor manually. One example where floating slaves are useful is the use of graphics tablets in the GIMP (where the area of the tablet is mapped to the canvas).

For most applications, you will only ever care about master devices.

Querying the device hierarchy



At some point, clients may need to know which devices are actually present in the system right now.


int ndevices;
XIDeviceInfo *devices, device;

devices = XIQueryDevice(display, XIAllDevices, &ndevices);

for (i = 0; i < ndevices; i++) {
device = &devices[i];
printf("Device %s (id: %d) is a ", device->name, device->deviceid);

switch(device->use) {
case XIMasterPointer: printf("master pointer\n"); break;
case XIMasterKeyboard: printf("master keyboard\n"); break;
case XISlavePointer: printf("slave pointer\n"); break;
case XISlaveKeyboard: printf("slave keyboard\n"); break;
case XIFloatingSlave: printf("floating slave\n"); break;
}

printf("Device is attached to/paired with %d\n", device->attachement);
}

XIFreeDeviceInfo(devices);



As with event selection, XIAllDevices and XIAllMaster devices are valid as device ID parameter. Alternatively, just supply the device ID of the device you're interested in.

The attachment simply states which device this device is attached to. For master pointers, this is always the paired master keyboard and the other way round. For floating slaves, this value is undefined.

Now we know the layout of the hierarchy, including how many master devices and physical devices are actually present.

XIQueryDevice returns more information such as the capabilities of each device. I'll leave this for another post (mainly because I just found a deficiency in the XI2 protocol that needs to be fixed first. :)


Hierarchy events


Now, knowing the hierarchy is only useful for a short time as another client may change it at any point in time. So your client should listen for hierarchy events. These events are sent to all windows, so it doesn't really matter where you register. The traditional approach is to register on the root window.


XIEventMask evmask;
unsigned char mask[2] = { 0, 0 };

XISetMask(mask, XI_HierarchyChanged);
evmask.deviceid = XIAllDevices;
evmask.mask_len = sizeof(mask);
evmask.mask = mask;

XISelectEvents(dpy, DefaultRootWindow(dpy), &evmask, 1);


Device hierarchy events are a bit special, they can only be selected for XIAllDevices. Trying to set the mask on any other device will result in a BadValue error.

In your event loop, you need something like this:

XEvent ev;
XNextEvent(dpy, &ev);

if (ev.xcookie.type == GenericEvent &&
ev.xcookie.extension == xi_opcode &&
XGetEventData(dpy, &ev.xcookie)) {
switch(ev.xcookie.type) {
case XI_HierarchyChanged:
printf("Hierarchy has changed!\n");
process_hierarchy_event(ev.xcookie.data);
break;
/* process other events */
}
XFreeEventData(dpy, &ev);
}


The XIHierarchyEvent has a couple of interesting fields:

typedef struct {
int type; /* GenericEvent */
unsigned long serial; /* # of last request processed by server */
Bool send_event; /* true if this came from a SendEvent request */
Display *display; /* Display the event was read from */
int extension; /* XI extension offset */
int evtype; /* XI_DeviceHierarchyChangedNotify */
Time time;
int flags;
int num_devices;
XIHierarchyInfo *info;
} XIHierarchyEvent;


The first couple of fields are standard. The flags field lists all changes that occurred, a combination of:

XIMasterAdded

A new master device has been created.

XIMasterRemoved

A master device has been deleted.

XISlaveAdded

A new slave device has been added (e.g. plugged in).

XISlaveRemoved

A slave device has been removed (e.g. unplugged).

XISlaveAttached

A slave device has been attached to a master device.

XISlaveDetached

A slave device has been detached (set floating).

XIDeviceEnabled

A device has been enabled (i.e. it may send events now)).

XIDeviceDisabled

A device has been disabled (i.e. it will not send events until enabled again).



The info field contains the details for all devices currently present and those removed with this change. Each info->flags states what happened to this particular device. For example, one could search for the slave device that just got removed by searching for the XISlaveRemoved flag.


if (!(ev.flags & XISlaveRemoved)) {
printf("No slave device removed.\n");
return;
}

for (i = 0; i < ev.num_devices; i++) {
if (info[i].flags & XISlaveRemoved) {
printf("Device %d has been removed.\n", info[i].deviceid);
}
}


With this information, it is possible to keep track of the device hierarchy at all times and adjust the user interface as necessary.


Modifying the device hierarchy



First of all - there is hardly a reason why you should do this. Only configuration tools should ever touch the device hierarchy.

XIChangeHierarchy allows to move devices around at will. It takes a number of XIAnyHierarchyChangeInfo structs and applies them in order.


XIAddMasterInfo add;

add.type = XIAddMaster;
add.name = "My new master";
add.sendCore = True;
add.enable = True;

XIChangeHierarchy(dpy, &add, 1);


After this change is performed, there will be two new master devices: "My new master pointer" and "My new master keyboard" (remember, master devices always come in pairs). They will send core events (i.e. they are usable in non-XI applications) and they will be enabled immediately. If you registered for hierarchy events, you will get and event with XIMasterAdded and XIDeviceEnabled flags.

The master device can be removed again by executing:


XIRemoveMasterInfo remove;
remove.type = XIRemoveMaster;
remove.deviceid = 10; /* assuming this is one of the master devices */
remove.return_mode = XIAttachToMaster;
remove.return_pointer = 2;
remove.return_keyboard = 3;

XIChangeHierarchy(dpy, &remove, 1);


This has the effect of removing device 10 (provided it is a master device) and also its paired master device. Any slave devices currently attached to device 10 or it's paired master device will be reattached to device 2 (for pointers) or device 3 (for keyboards).

Attaching and detaching slave devices is equally simple and shouldn't require a lot of explanation. If you want to change a slave from one master to the other, a single attach command is sufficient, the slave device does not need to be detached first. Note that attaching a slave device will also enable it if it is currently disabled.

Finally, if you submit multiple commands in one go, they are applied in order until all of them are processed or an error occurs. If an error occurs, the client is notified and processing stops, but already processed commands will take effect. Regardless of an error, a hierarchy event is sent to all clients with the new state.


That's it. You now know how to stay aware of the device hierarchy and how to modify it. In the next part, I'll discuss how to get more information about the devices.

1 comment:

C. said...

The code above contains the following typos:

XIDeviceInfo *devices, device;

--> XIDeviceInfo *devices, *device;

and


printf("Device is attached to/paired with %d\n", device->attachement);

--> printf("Device is attached to/paired with %d\n", device->attachment);


By the way: What is the official way to translate an XI2-keycode to a keysym?

CC