GtkGesture

GtkGesture is the base class that simplifies the handling of pointer and touch events, each GtkGesture subclass focuses on recognizing very specific sequences within the stream of events received by the widget, emitting higher-level signals when this happens. The class hierarchy is:

  • GtkGesture - generic parent class

    • GtkGestureRotate - detects rotation of 2 touchpoints

    • GtkGestureZoom - detects zoom/distance changes between 2 touchpoints

    • GtkGestureSingle - base class for pointer and single-touch event handling

      • GtkGestureLongPress - detects a quasi-static long press

      • GtkGestureMultiPress - detects a number of presses [1..n]

      • GtkGestureDrag - detects press/motion/motion.../release sequences, reports deltas from initial dragging point

      • GtkGestureSwipe - detects press/motion/motion.../release sequences, reports residual velocity after it is finished.

      • GtkGesturePan - detects panning motions along the X or Y axes

The most useful built-in features of GtkGesture are:

  • Pointer and touch events are handled indistinctly, making your widgets/apps work on touchscreens out of the box
  • Handling of broken GTK+/GDK grabs is built-in, connect to GtkGesture::cancel or check through gtk_gesture_handles_sequence() on GtkGesture::end if there is anything that really needs undoing.

  • By default, events are fed into GtkGestures automatically. It is a viable replacement for GtkWidgetClass event handlers.

  • Negotiation on the ownership of pointer events and touch sequences is greatly simplified for users, these just need to set a proper accepted/denied state on the GtkGesture or the individual sequences constituting a gesture

Using GtkGesture

Setting up gestures on a custom or stock widgets is pretty straightforward:

static void
my_widget_init (MyWidget *widget)
{
  MyWidgetPriv *priv;

  priv = my_widget_get_instance_private (widget);
  priv->drag_gesture = gtk_gesture_drag_new (GTK_WIDGET (widget));
  priv->press_gesture = gtk_gesture_multipress_gesture (GTK_WIDGET (widget));
}

static GtkWidget *
create_touch_friendly_entry (void)
{
  GtkGesture *gesture;
  GtkWidget *entry;

  entry = gtk_entry_new ();
  gesture = gtk_gesture_long_press_new (entry);
  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), TRUE);
  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture),
                                              GTK_PHASE_TARGET);
  g_signal_connect (gesture, "pressed",
                    G_CALLBACK (entry_long_press_pressed_cb), entry);

  /* Attach the gesture as widget data, or optionally unref when destroying the widget */
  g_object_set_data_full (G_OBJECT (entry), "foobar-custom-gesture",
                          gesture, g_object_unref);

  return entry;
}

Choosing the right propagation phase

Similarly to the DOM world, input events go through 3 different phases that gestures can hook to:

  • capture: events run downwards, from the toplevel to the target widget.
  • target : the event is run exclusively on the target widget.
  • bubble: events run upwards, from the target widget to the toplevel.

Choosing the right phase for your gesture depends on a few factors, a few rules of thumb are:

  • If your widget is a container, and your gesture is meant to react even when interacting on top of child widgets, you should use GTK_PHASE_CAPTURE
  • If setting up gestures on a non-container, or replacing functionality previously done through GtkWidgetClass methods, the gesture should remain at its default GTK_PHASE_BUBBLE.

  • GTK_PHASE_TARGET is more scarcely useful, definitely never for GtkContainers

  • GTK_PHASE_NONE should be seldomly used, only useful if:
    • Handling gestures through non-standard ::event signals, eg. GtkTextTag::event

    • It proved useful within GTK+ replacing code that used to run on g_signal_connect()ed handlers, as users in the wild may (and some do) rely on signal propagation ordering between these handlers and their own. This use case should remain exclusively useful to GTK+ though.

Gesture grouping

GtkGesture implementations are individually fairly simple, although it is possible to combine those in order to create higher level or widget-specific behavior.

Gestures can be grouped through gtk_gesture_group(). Grouped gestures will share the same state for all sequences being handled by these. This effectively means that an accepted sequence will be processed by all the gestures in the group, and denied sequences will be ignored in block too. In the first case, gestures in other groups that were tentatively handling the sequence will automatically deny the sequence, this will happen both within the widget claiming the sequence, and across other widgets in the propagation chain.

By default, each gesture starts out in its own isolated group; gestures can only be grouped with other gestures within the same widget.

Example: combining zoom/rotate

This example groups zoom and rotate gestures, so both react simultaneously to the same 2 touchpoints:

typedef struct {
  GtkDrawingArea parent_instance;
  cairo_surface_t *surface;
  GtkGesture *zoom;
  GtkGesture *rotate;
  gdouble initial_angle;
  gdouble angle;
  gdouble initial_zoom;
  gdouble zoom;
} MyWidget;

static void
zoom_begin_cb (GtkGesture       *gesture,
               GdkEventSequence *sequence,
               MyWidget         *widget)
{
  MyWidgetPrivate *priv;

  priv = my_widget_get_instance_private (widget);
  priv->initial_zoom = priv->zoom;
}

static void
zoom_scale_changed_cb (GtkGestureZoom *zoom,
                       gdouble         scale,
                       MyWidget       *widget)
{
  MyWidgetPrivate *priv;

  priv = my_widget_get_instance_private (widget);
  priv->zoom = priv->initial_zoom * scale;
  gtk_widget_queue_draw (GTK_WIDGET (widget));
}

static void
rotate_begin_cb (GtkGesture       *gesture,
                 GdkEventSequence *sequence,
                 MyWidget         *widget)
{
  MyWidgetPrivate *priv;

  priv = my_widget_get_instance_private (widget);
  priv->initial_angle = priv->angle;
}

static void
rotate_angle_changed_cb (GtkGestureZoom *zoom,
                         gdouble         angle,
                         MyWidget       *widget)
{
  MyWidgetPrivate *priv;

  priv = my_widget_get_instance_private (widget);
  priv->angle = priv->initial_angle + angle;
  gtk_widget_queue_draw (GTK_WIDGET (widget));
}

static gboolean
image_widget_draw (GtkWidget *widget,
                   cairo_t   *cr)
{
  MyWidgetPrivate *priv;
  GtkAllocation allocation;

  priv = my_widget_get_instance_private (MY_WIDGET (widget));
  gtk_widget_get_allocation (widget, &allocation);

  cairo_save (cr);
  cairo_translate (cr, allocation.width / 2, allocation.height / 2);
  cairo_rotate (cr, priv->angle);
  cairo_scale (cr, priv->zoom, priv->zoom);
  cairo_translate (cr, - cairo_image_surface_get_width (priv->surface) / 2,
                   - cairo_image_surface_get_height (priv->surface) / 2);

  cairo_set_source_surface (cr, priv->surface, 0, 0)
  cairo_paint (cr);
  cairo_restore (cr);

  return TRUE;
}

...

static void
my_widget_init (MyWidget *widget)
{
  MyWidgetPrivate *priv;

  priv = my_widget_get_instance_private (widget);
  priv->zoom = gtk_gesture_zoom_new (GTK_WIDGET (widget));
  g_signal_connect (priv->zoom, "begin",
                    G_CALLBACK (zoom_begin_cb), widget);
  g_signal_connect (priv->zoom, "scale-changed",
                    G_CALLBACK (zoom_scale_changed_cb), widget);

  priv->rotate = gtk_gesture_rotate_new (GTK_WIDGET (widget));
  g_signal_connect (priv->rotate, "begin",
                    G_CALLBACK (rotate_begin_cb), widget);
  g_signal_connect (priv->rotate, "angle-changed",
                    G_CALLBACK (rotate_angle_changed_cb), widget);

  gtk_gesture_group (priv->zoom, priv->rotate);

  priv->angle = priv->initial_angle = 0;
  priv->zoom = priv->initial_zoom = 1;
}

Handling gesture status

As mentioned briefly earlier, gestures can specify whether pointer/touchpoints are accepted or denied by it, either individually or for all interacting sequences at once.

By default, gestures neither claim nor reject ownership on mouse/touch input, so events would conceivably run unstopped from the toplevel to the target widget and back, although still triggering all gestures attached along the propagation chain. In practice, widgets using GtkGesture will either accept or deny the sequence more or less promptly, usually when the gesture is actually initiating an implementation-defined action (eg. moving past a threshold on DnD, showing a menu on long press, ...).

When a sequence is accepted by a gesture, that sequence will be cancelled and forgotten downwards the propagation chain (ie. towards the target widget), and event propagation will be stopped at this phase and widget for as long the sequence is active and accepted by the gesture group. In this stage, parent widgets can still capture and eventually accept themselves the sequence, resulting in it being cancelled downwards again. Only one gesture group can effectively accept a sequence at a given time.

Denying a sequence has the opposite effect, the gesture group will become no longer reactive to this sequence. Initially accepted sequences can also be eventually denied, allowing widgets towards the target widget to receive and possibly handle the sequence again.

State changes depend largely on the action you are implementing through GtkGesture, it is very rare that gestures trigger automatically state changes, two notable exceptions are:

  • Any just started touchpoint that exceeds GtkGesture:n-points is rejected right away, the GtkGesture::end signal will also follow soon after.

  • GtkGesturePan self-denies the sequence when panning doesn't happen along the expected axis.

Example: cancelling drag after a long press

static void
long_press_pressed_cb (GtkGestureMultiPress *gesture,
                       gdouble               x,
                       gdouble               y,
                       MyWidget             *widget)
{
  /* Deny gesture, as both drag/long-press are grouped, both will be cancelled */
  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
}

static void
long_press_cancelled_cb (GtkGestureMultiPress *gesture,
                         MyWidget             *widget)
{
  /* The pointer/touchpoint moved past the threshold. accept the
   * gesture nonetheless to let the drag gesture take over.
   *
   * NB: This will also trigger when releasing too early, accepting
   * an ending sequence should have no effect though.
   */
  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}

static void
drag_update_cb (GtkGestureDrag *gesture,
                gdouble         dx,
                gdouble         dy,
                MyWidget       *widget)
{
  GdkEventSequence *sequence;

  sequence = gtk_gesture_get_last_updated_sequence (GTK_GESTURE (gesture));

  if (gtk_gesture_get_sequence_state (GTK_GESTURE (gesture), sequence) == GTK_EVENT_SEQUENCE_CLAIMED)
    {
      /* Only trigger visible actions after the sequence is accepted */
      priv->dx = dx;
      priv->dy = dy;

      ...

      gtk_widget_queue_draw (GTK_WIDGET (widget));
    }
}

static void
my_widget_init (MyWidget *widget)
{
  MyContainerPrivate *priv;

  priv = my_widget_get_instance_private (widget);

  /* This gesture triggers the user-visible drag action */
  priv->drag = gtk_gesture_drag_new (GTK_WIDGET (widget));
  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->drag),
                                              GTK_PHASE_CAPTURE);
  g_signal_connect (priv->drag, "drag-update",
                    G_CALLBACK (drag_update_cb), widget);

  /* This gesture basically controls the state of the drag gesture */
  priv->long_press = gtk_gesture_long_press_new (GTK_WIDGET (widget));
  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->long_press),
                                              GTK_PHASE_CAPTURE);
  g_signal_connect (priv->long_press, "pressed",
                    G_CALLBACK (long_press_pressed_cb), widget);
  g_signal_connect (priv->long_press, "cancelled",
                    G_CALLBACK (long_press_cancelled_cb), widget);

  gtk_gesture_group (priv->drag, priv->long_press);
}

Example: Allowing only horizontal swipes

static void
swipe_cb (GtkGestureSwipe *gesture,
          gdouble          velocity_x,
          gdouble          velocity_y,
          MyWidget        *widget)
{
  my_widget_start_action (widget, velocity_x);
}

static void
my_widget_init (MyWidget *widget)
{
  MyContainerPrivate *priv;

  priv = my_widget_get_instance_private (widget);

  priv->swipe = gtk_gesture_swipe_new (GTK_WIDGET (widget));
  g_signal_connect (priv->swipe, "swipe",
                    G_CALLBACK (swipe_cb), widget);

  /* We rely on the pan gesture cancelling itself on non-horizontal panning, the
   * swipe gesture will also get to deny the sequence, so ::swipe will never happen.
   */
  priv->pan = gtk_gesture_pan_new (GTK_WIDGET (widget),
                                   GTK_ORIENTATION_HORIZONTAL);

  gtk_gesture_group (priv->swipe, priv->pan);
}