This site has been retired. For up to date information, see handbook.gnome.org or gitlab.gnome.org.


[Home] [TitleIndex] [WordIndex

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:

The most useful built-in features of GtkGesture are:

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:

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

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:

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);
}

2024-10-23 11:12