Background

GTK+ uses XInput2 for input events, which caters for a fully dynamic device hierarchy, and support for multiple pointer/keyboard pairs.

carlos@sacarino:~$ xinput list
⎡ Virtual core pointer                          id=2    [master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
⎜   ↳ Wacom ISDv4 E6 Pen stylus                 id=10   [slave  pointer  (2)]
⎜   ↳ Wacom ISDv4 E6 Finger touch               id=11   [slave  pointer  (2)]
⎜   ↳ SynPS/2 Synaptics TouchPad                id=13   [slave  pointer  (2)]
⎜   ↳ TPPS/2 IBM TrackPoint                     id=14   [slave  pointer  (2)]
⎜   ↳ Wacom ISDv4 E6 Pen eraser                 id=16   [slave  pointer  (2)]
⎣ Virtual core keyboard                         id=3    [master keyboard (2)]
    ↳ Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    ↳ Power Button                              id=6    [slave  keyboard (3)]
    ↳ Video Bus                                 id=7    [slave  keyboard (3)]
    ↳ Sleep Button                              id=8    [slave  keyboard (3)]
    ↳ Integrated Camera                         id=9    [slave  keyboard (3)]
    ↳ AT Translated Set 2 keyboard              id=12   [slave  keyboard (3)]
    ↳ ThinkPad Extra Buttons                    id=15   [slave  keyboard (3)]
carlos@sacarino:~$ xinput create-master eek
carlos@sacarino:~$ xinput list
  ...
⎡ eek pointer                                   id=17   [master pointer  (18)]
⎜   ↳ eek XTEST pointer                         id=19   [slave  pointer  (17)]
⎣ eek keyboard                                  id=18   [master keyboard (17)]
    ↳ eek XTEST keyboard                        id=20   [slave  keyboard (18)]
carlos@sacarino:~$ xinput reattach 10 17
carlos@sacarino:~$ xinput list
  ...
⎡ eek pointer                                   id=17   [master pointer  (18)]
⎜   ↳ Wacom ISDv4 E6 Pen stylus                 id=10   [slave  pointer  (17)]
⎜   ↳ eek XTEST pointer                         id=19   [slave  pointer  (17)]
⎣ eek keyboard                                  id=18   [master keyboard (17)]
    ↳ eek XTEST keyboard                        id=20   [slave  keyboard (18)]

Listing and modifying the device hierarchy

The device hierarchy is formed by master and slave devices. Master devices are those represented in the screen (ie. mouse cursor and keyboard focus indicator), while slave devices represent the hardware connected to the computer. Slave devices can be either connected to a master (thus being able to drive them) or floating.

The client pointer

Under the presence of multiple pointers, XInput2 uses the "client pointer" principle to allow several legacy applications to interact simultaneously with different pointer/keyboard pairs, it would be usually set by the window manager for a focused window, so different applications could have different client pointers.

Under the hood, X11 uses the client pointer (or its paired keyboard) to satisfy core calls such as XGrabPointer/Keyboard, XQueryPointer and others that have been superseded by XInput2

Apps that don't want to deal with devices

There are applications that could have little gain in being multidevice aware, but there are still stituations where a device could be needed (eg. popping up a menu on the pointer coordinates).

For such applications, the client pointer may be a good approximation for these operations.

GdkDisplay *display;
GdkDeviceManager *device_manager;
GdkDevice *client_pointer, client_keyboard;

display = gdk_display_get_default ();
device_manager = gdk_display_get_device_manager (display);
client_pointer = gdk_device_manager_get_client_pointer (device_manager);

/* Or if we need a keyboard too */
client_keyboard = gdk_device_get_associated_device (client_pointer);

Getting the client pointer and keyboard

With multiple pointers, this gives a behavior most similar to that of legacy applications (i.e. gtk+2)

Dealing with multiple devices

There may be several usecases to deal with multiple devices, but the patterns to make them work are about the same

Event handling

  • Each device will emit its own event stream, this means that you will need

    to check the GdkEvent you get in your event handlers

      static gboolean
      my_widget_motion_notify (GtkWidget      *widget,
                               GdkEventMotion *event)
      {
        GdkDevice *device, *source_device;
    
        device = gdk_event_get_device ((GdkEvent *) event);
        source_device = gdk_event_get_source_device ((GdkEvent *) event);
    
        g_print ("Motion event by '%s', coming from HW device '%s'\n",
                 gdk_device_get_name (device),
                 gdk_device_get_name (source_device));
    
        /* Some fictional specialized handling based on the event source */
        if (gdk_device_get_source (source_device) == GDK_SOURCE_ERASER)
          {
            ...
          }
        else
          {
            ...
          }
        return TRUE;
      }

Reacting differently to devices

  • The mechanism above could also be used for fine grained event discarding (so rubberband selection doesn't jump to another pointer entering the widget for example)
      static gboolean
      my_widget_button_press (Gtkwidget      *widget,
                              GdkEventButton *event)
      {
        GET_PRIV(widget)->current_pointer = gdk_event_get_device ((GdkEvent *) event);
        ...
      }
    
      static gboolean
      my_widget_button_release (Gtkwidget      *widget,
                                GdkEventButton *event)
      {
        GET_PRIV(widget)->current_pointer = NULL;
        ...
      }
    
      static gboolean
      my_widget_motion_notify (Gtkwidget      *widget,
                               GdkEventMotion *event)
      {
        if (gdk_event_get_device (event) !=
            GET_PRIV(widget)->current_pointer)
          return FALSE;
    
        ...
      }

Grabs

  • Grabs are a mechanism to coerce a device into sending events to a window, but with multidevice there's an other side of the coin, how other devices are supposed to interact while the grab is in effect.

    The GdkGrabOwnership enum passed to gdk_device_grab() may be used to block other devices' interaction. GDK_OWNERSHIP_NONE applies no restrictions, allowing other devices to interact, even with the grab window. GDK_OWNERSHIP_WINDOW blocks other devices from interacting with the grab window, but they'll still be able to interact with the rest of the application, whereas GDK_OWNERSHIP_APPLICATION will render the whole application insensitive to other devices. Different devices may have simultaneous grabs on the same or different windows.

      gboolean
      my_widget_button_press (GtkWidget      *widget,
                              GdkEventButton *event)
      {
        GdkDevice *pointer, *keyboard;
    
        pointer = gdk_event_get_device ((GdkEvent *) event);
        keyboard = gdk_device_get_associated_device (pointer);
    
        /* Grab both keyboard/pointer, other devices will be
         * unable to interact with the widget window meanwhile
         */
        gdk_device_grab (pointer,
                         gtk_widget_get_window (widget),
                         GDK_OWNERSHIP_WINDOW,
                         ...);
        gdk_device_grab (keyboard,
                         gtk_widget_get_window (widget),
                         GDK_OWNERSHIP_WINDOW,
                         ...);
    
        return FALSE;
      }
    For GTK+ grabs, there's only a boolean value, equivalent to GDK_OWNERSHIP_NONE/WINDOW, but the mechanism is quite similar. Once the device is grabbed, there may be different situations that could break the

    grabs, so the widget needs to listen to GdkGrabBrokenEvent and ::grab-notify to handle these situations.

      static gboolean
      my_widget_grab_broken (GtkWidget          *widget,
                             GdkEventGrabBroken *event)
      {
        MyWidgetPrivate *priv = GET_PRIV (widget);
    
        if (gdk_event_get_device (event) == priv->grab_pointer)
          {
            /* Undo state */
            ...
            priv->grab_pointer = NULL;
            return TRUE;
          }
    
        return FALSE;
      }
    
      static void
      my_widget_grab_notify (GtkWidget *widget,
                             gboolean   was_grabbed)
      {
        MyWidgetPrivate *priv = GET_PRIV (widget);
    
        if (gtk_widget_device_is_shadowed (widget, priv->grab_device))
          {
            /* Device was "shadowed" by another widget's grab,
             * release and undo state
             */
            ...
            priv->grab_pointer = NULL;
          }
      }

Handling broken grabs

Handling multipointer

  • Widgets do react by default to every master device, although most (all?) are set in a compatibility mode that make them behave better with multiple pointers, without necessarily being multipointer aware. This compatibility mode most notably disables per-device enter/leave events, so these are stacked, and the event is only emitted when the first/last pointer enters/leaves the window.
      /* Prepare for enter/leave crazyness */
      gtk_widget_set_support_multidevice (widget, TRUE);

Reading device axes

  • Button and motion events provide further information about the device axes' current state. Note the device axes are HW and driver dependent, therefore the set of axes is far from set in stone, although there are a few more common ones.
      carlos@sacarino:~$ xinput list "Wacom ISDv4 E6 Pen stylus"  |grep "Label"
                      Label: Abs X
                      Label: Abs Y
                      Label: Abs Pressure
                      Label: Abs Tilt X
                      Label: Abs Tilt Y
                      Label: Abs Wheel

Getting to know a device axes

  •   gboolean
      my_widget_motion_notify (GtkWidget      *widget,
                               GdkEventMotion *event)
      {
        GdkAtom *label_atom;
        gdouble pressure;
    
        label_atom = gdk_atom_intern_static_string ("Abs Pressure");
        gdk_device_get_axis_value (gdk_event_get_device ((GdkEvent *) event),
                                   event->axes, label_atom, &pressure);
    
        /* Do something with pressure */
        ...
      }

Getting an axis value

  • All pointer devices report axes information, master and slave. to achieve this, master pointers modify their list of axes at runtime to reflect those of the currently routed slave, emitting ::changed on the way.

Dealing directly with slave (or floating) devices

  • GTK+ listens by default to all master devices, although some applications may want to interact in detail with a hardware device, such as a tablet stylus. To do so, the device must be enabled, and the widget wanting its events needs to add the event mask.
      GdkDevice *device;
    
      /* Gets the first device found with the given GdkInputSource */
      device = get_device (gtk_widget_get_display (widget),
                           GDK_SOURCE_PEN);
      gdk_device_set_mode (device, GDK_MODE_SCREEN);
      gtk_widget_add_device_events (widget, device,
                                    GDK_BUTTON_PRESS_MASK |
                                    GDK_BUTTON_RELEASE_MASK |
                                    GDK_POINTER_MOTION_MASK);

enabling events for a slave device

  • After these calls, the widget would specifically receive events from the stylus, regardless of it being floating or connected to a master device, in this second case, and if you want exclusive control of the device, you can temporarily detach the stylus device from its master by doing a GDK grab on it. For events coming directly from slave devices, both gdk_event_get_device() and gdk_event_get_source_device() will return the same device of type GDK_DEVICE_TYPE_SLAVE/FLOATING. Note that this is less useful than it used to be in GTK+2/XInput1, at least for attached slaves, as there is gdk_event_get_source_device(), and master devices' events provide axes information.

Recommendations

  • Device operations often come up as a result of input events,
    • favor gdk_event_get_device() and gtk_get_current_event_device() before gdk_device_manager_get_client_pointer()
  • Store the devices the widget is currently interacting with, handle

Projects/GTK/DeviceInteractionPatterns (last edited 2018-12-05 15:46:11 by EmmanueleBassi)