23.4. Adding XInput support

It is now possible to buy quite inexpensive input devices such as drawing tablets, which allow drawing with a much greater ease of artistic expression than does a mouse. The simplest way to use such devices is simply as a replacement for the mouse, but that misses out many of the advantages of these devices, such as: For information about the XInput extension, see the XInput HOWTO.

If we examine the full definition of, for example, the EventMotion attributes, we see that it has fields to support extended device information.
 
type
window 
time 
x 
y 
pressure 
xtilt 
ytilt 
state 
is_hint 
source 
deviceid

pressure gives the pressure as a floating point number between 0 and 1. xtilt and ytilt can take on values between -1 and 1, corresponding to the degree of tilt in each direction. source and deviceid specify the device for which the event occurred in two different ways. source gives some simple information about the type of device. It can take the enumeration values:
 
SOURCE_MOUSE
SOURCE_PEN
SOURCE_ERASER
SOURCE_CURSOR

deviceid specifies a unique numeric ID for the device.

23.4.1. Enabling extended device information

To let PyGTK know about our interest in the extended device information, we merely have to add a single line to our program scribblexinpput.py:
 
drawing_area.set_extension_events(EXTENSION_EVENTS_CURSOR)

By giving the value EXTENSION_EVENTS_CURSOR (defined in GDK.py) we say that we are interested in extension events, but only if we don't have to draw our own cursor. See the section Further Sophistications below for more information about drawing the cursor. We could also give the values EXTENSION_EVENTS_ALL if we were willing to draw our own cursor, or EXTENSION_EVENTS_NONE to revert back to the default condition.

This is not completely the end of the story however. By default, no extension devices are enabled. We need a mechanism to allow users to enable and configure their extension devices. GTK provides the InputDialog widget to automate this process. The following procedure manages an InputDialog widget. It creates the dialog if it isn't present, and raises it to the top otherwise.
 
   92   def input_dialog_destroy(w):
   93       global inputd
   94       inputd = None
   95   
   96   def create_input_dialog(widget):
   97       global inputd
   98   
   99       if not inputd:
  100           inputd = gtk.GtkInputDialog()
  101           inputd.connect("destroy", input_dialog_destroy)
  102           inputd.close_button.connect_object("clicked", inputd.hide, inputd)
  103           inputd.save_button.hide()
  104           inputd.show()
  105       else:
  106           if not (inputd.flags() & gtk.MAPPED):
  107               inputd.show()
  108           else:
  109               inputd.get_window()._raise()

(You might want to take note of the way we handle this dialog. By connecting to the "destroy" signal, we make sure that we don't keep a reference to inputd around after it is destroyed - that could lead to a segfault.)

The InputDialog has two buttons "Close" and "Save", which by default have no actions assigned to them. In the above function we make "Close" hide the dialog, and we hide the "Save" button, since we don't implement saving of XInput options in this program.

23.4.2. Using extended device information

Once we've enabled the device, we can just use the extended device information in the extra fields of the event structures. In fact, it is always safe to use this information since these fields will have reasonable default values even when extended events are not enabled.

Once change we do have to make is to call input_get_pointer() instead of using the pointer. and pointer_state attributes. This is necessary because they don'treturn the extended device information.
 
x, y, pressure, xtilt, ytilt, mask = window.input_get_pointer(deviceid)

When calling this function, we need to specify the device ID as well as the window. Usually, we'll get the device ID from the deviceid field of an event structure. Again, this function will return reasonable values when extension events are not enabled. (In this case, event.deviceid will have the value CORE_POINTER).

So the basic structure of our button-press and motion event handlers doesn't change much - we just need to add code to deal with the extended information.
 
   71   def button_press_event(widget, event):
   72       print_button_press(event.deviceid)
   73     
   74       if event.button == 1 and pixmap != None:
   75           draw_brush (widget, event.source, event.x, event.y, event.pressure)
   76       return gtk.TRUE
   77   
   78   def motion_notify_event(widget, event):
   79       if event.is_hint:
   80           x,y,pressure,d,d,state = event.window.input_get_pointer(event.deviceid)
   81       else:
   82           x = event.x
   83           y = event.y
   84           pressure = event.pressure
   85           state = event.state
   86       
   87       if state & GDK.BUTTON1_MASK and pixmap != None:
   88           draw_brush(widget, event.source, x, y, pressure)
   89     
   90       return gtk.TRUE

We also need to do something with the new information. Our new draw_brush() function draws with a different color for each event.source and changes the brush size depending on the pressure.
 
   50   # Draw a rectangle on the screen, size depending on pressure,
   51   # and color on the type of device
   52   def draw_brush(widget, source, x, y, pressure):
   53       if source == GDK.SOURCE_MOUSE:
   54           gc = widget.get_style().dark_gc[gtk.STATE_NORMAL]
   55       elif source == GDK.SOURCE_PEN:
   56           gc = widget.get_style().black_gc
   57       elif source == GDK.SOURCE_ERASER:
   58           gc = widget.get_style().white_gc
   59       else:
   60           gc = widget.get_style().light_gc[gtk.STATE_NORMAL]
   61   
   62       rect = (x-10*pressure, y-10*pressure, 20*pressure, 20*pressure)
   63       gtk.draw_rectangle (pixmap, gc, gtk.TRUE,
   64                           rect[0], rect[1], rect[2], rect[3])
   65       widget.draw(rect)

 
Our print_button_press() function simply prints the value of the device_id.
 
   67   def print_button_press(deviceid):
   68       print "Button press on device '%s'" % deviceid
   69       return

That completes the changes to "XInputize" our program; the result is scribblexinput.py.

23.4.3. Further sophistications

A major omission that we have mentioned above is the lack of cursor drawing. Platforms other than XFree86 currently do not allow simultaneously using a device as both the core pointer and directly by an application. See the XInput-HOWTO for more information about this. This means that applications that want to support the widest audience need to draw their own cursor.

An application that draws its own cursor needs to do two things: determine if the current device needs a cursor drawn or not, and determine if the current device is in proximity. (If the current device is a drawing tablet, it's a nice touch to make the cursor disappear when the stylus is lifted from the tablet. When the device is touching the stylus, that is called "in proximity.") The first is done by searching the device list, as we did to find out the device name. The second is achieved by selecting "proximity_out" events.