[gtk+/gestures] Implement a gestures interpreter



commit a82167eb2166977934e61bd8ee5d86526a9b725c
Author: Carlos Garnacho <carlosg gnome org>
Date:   Fri Jan 14 10:26:29 2011 +0100

    Implement a gestures interpreter
    
    GtkGesturesInterpreter is a private object, the GtkWidget methods
    gtk_widget_enable_gesture() and disable_gesture() are the only
    entry points, the interpreter itself is hooked into the event
    delivery mechanism.
    
    The algorithm used to determine strokes similarity is a variation
    of weighted Levenshtein, where variable weights are applied
    depending on the torsion effort needed to transform one stroke
    into another.

 gtk/Makefile.am              |    2 +
 gtk/gtkenums.h               |    9 +
 gtk/gtkgesturesinterpreter.c |  599 ++++++++++++++++++++++++++++++++++++++++++
 gtk/gtkgesturesinterpreter.h |   64 +++++
 gtk/gtkmain.c                |   12 +
 gtk/gtkwidget.c              |   94 +++++++
 gtk/gtkwidget.h              |    9 +
 gtk/gtkwidgetprivate.h       |    5 +
 tests/Makefile.am            |    6 +
 tests/testgestures.c         |  233 ++++++++++++++++
 10 files changed, 1033 insertions(+), 0 deletions(-)
---
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index a81ea5c..b7fe884 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -391,6 +391,7 @@ gtk_private_h_sources =		\
 	gtkfilechooserutils.h	\
 	gtkfilesystem.h		\
 	gtkfilesystemmodel.h	\
+	gtkgesturesinterpreter.h \
 	gtkiconcache.h		\
 	gtkimcontextsimpleseqs.h \
 	gtkintl.h		\
@@ -525,6 +526,7 @@ gtk_base_c_sources = 		\
 	gtkfontbutton.c		\
 	gtkfontsel.c		\
 	gtkframe.c		\
+	gtkgesturesinterpreter.c \
 	gtkgradient.c		\
 	gtkgrid.c		\
 	gtkhandlebox.c		\
diff --git a/gtk/gtkenums.h b/gtk/gtkenums.h
index 1eb3b9f..827656a 100644
--- a/gtk/gtkenums.h
+++ b/gtk/gtkenums.h
@@ -654,6 +654,15 @@ typedef enum {
   GTK_BORDER_STYLE_OUTSET
 } GtkBorderStyle;
 
+typedef enum {
+  GTK_GESTURE_SWIPE_LEFT,
+  GTK_GESTURE_SWIPE_RIGHT,
+  GTK_GESTURE_SWIPE_UP,
+  GTK_GESTURE_SWIPE_DOWN,
+  GTK_GESTURE_CIRCULAR_CLOCKWISE,
+  GTK_GESTURE_CIRCULAR_COUNTERCLOCKWISE
+} GtkGestureType;
+
 G_END_DECLS
 
 
diff --git a/gtk/gtkgesturesinterpreter.c b/gtk/gtkgesturesinterpreter.c
new file mode 100644
index 0000000..266984b
--- /dev/null
+++ b/gtk/gtkgesturesinterpreter.c
@@ -0,0 +1,599 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 2011 Carlos Garnacho <carlos lanedo com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "gtkgesturesinterpreter.h"
+#include <gdk/gdk.h>
+#include <math.h>
+
+#define GRID_SIZE 100
+#define POINT_DISTANCE 5
+#define N_CIRCULAR_SIDES 6
+
+#undef DEBUG_WEIGHTS
+
+G_DEFINE_TYPE (GtkGesturesInterpreter, gtk_gestures_interpreter, G_TYPE_OBJECT)
+
+typedef struct _GtkGesturesInterpreterPrivate GtkGesturesInterpreterPrivate;
+typedef struct _GestureNode GestureNode;
+typedef struct _StockGesture StockGesture;
+typedef struct _RecordedGesture RecordedGesture;
+
+struct _GtkGesturesInterpreterPrivate
+{
+  GHashTable *events;
+};
+
+struct _GestureNode
+{
+  gint angle;
+  gint distance;
+};
+
+struct _StockGesture
+{
+  GArray *gesture_data;
+  GtkGestureType gesture;
+};
+
+struct _RecordedGesture
+{
+  GArray *coordinates;
+  gint min_x;
+  gint max_x;
+  gint min_y;
+  gint max_y;
+};
+
+static GList *gestures_stock = NULL;
+
+static void
+_gtk_gestures_interpreter_register_gesture (GArray         *gesture,
+                                            GtkGestureType  gesture_type)
+{
+  StockGesture *stock_gesture;
+
+  stock_gesture = g_slice_new0 (StockGesture);
+  stock_gesture->gesture_data = gesture;
+  stock_gesture->gesture = gesture_type;
+
+  gestures_stock = g_list_prepend (gestures_stock, stock_gesture);
+}
+
+static void
+register_basic_gestures (void)
+{
+  GestureNode node;
+  GArray *gesture;
+  gint n, angle;
+
+  /* Swipe right */
+  gesture = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+  node.distance = 100;
+  node.angle = 90;
+  g_array_append_val (gesture, node);
+  _gtk_gestures_interpreter_register_gesture (gesture, GTK_GESTURE_SWIPE_RIGHT);
+
+  /* Swipe left */
+  gesture = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+  node.distance = 100;
+  node.angle = 270;
+  g_array_append_val (gesture, node);
+  _gtk_gestures_interpreter_register_gesture (gesture, GTK_GESTURE_SWIPE_LEFT);
+
+  /* Swipe up */
+  gesture = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+  node.distance = 100;
+  node.angle = 0;
+  g_array_append_val (gesture, node);
+  _gtk_gestures_interpreter_register_gesture (gesture, GTK_GESTURE_SWIPE_UP);
+
+  /* Swipe down */
+  gesture = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+  node.distance = 100;
+  node.angle = 180;
+  g_array_append_val (gesture, node);
+  _gtk_gestures_interpreter_register_gesture (gesture, GTK_GESTURE_SWIPE_DOWN);
+
+  /* Circular clockwise */
+  gesture = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+  angle = 90;
+
+  for (n = 0; n < N_CIRCULAR_SIDES; n++)
+    {
+      node.distance = 50;
+      node.angle = angle % 360;
+
+      g_array_append_val (gesture, node);
+      angle += 360 / N_CIRCULAR_SIDES;
+    }
+
+  _gtk_gestures_interpreter_register_gesture (gesture, GTK_GESTURE_CIRCULAR_CLOCKWISE);
+
+  /* Circular counterclockwise */
+  gesture = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+  angle = 270;
+
+  for (n = 0; n < N_CIRCULAR_SIDES; n++)
+    {
+      node.distance = 50;
+      node.angle = angle % 360;
+
+      g_array_append_val (gesture, node);
+      angle -= 360 / N_CIRCULAR_SIDES;
+
+      if (angle < 0)
+        angle += 360;
+    }
+
+  _gtk_gestures_interpreter_register_gesture (gesture, GTK_GESTURE_CIRCULAR_COUNTERCLOCKWISE);
+}
+
+static void
+gtk_gestures_interpreter_class_init (GtkGesturesInterpreterClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  register_basic_gestures ();
+
+  g_type_class_add_private (object_class, sizeof (GtkGesturesInterpreterPrivate));
+}
+
+static RecordedGesture *
+recorded_gesture_new (void)
+{
+  RecordedGesture *recorded;
+
+  recorded = g_slice_new0 (RecordedGesture);
+  recorded->coordinates = g_array_new (FALSE, FALSE, sizeof (GdkPoint));
+  recorded->min_x = G_MAXINT;
+  recorded->max_x = G_MININT;
+  recorded->min_y = G_MAXINT;
+  recorded->max_y = G_MININT;
+
+  return recorded;
+}
+
+static void
+recorded_gesture_free (RecordedGesture *recorded)
+{
+  g_array_free (recorded->coordinates, TRUE);
+  g_slice_free (RecordedGesture, recorded);
+}
+
+static void
+recorded_gesture_append_coordinate (RecordedGesture *recorded,
+                                    gint             x,
+                                    gint             y)
+{
+  GdkPoint point;
+
+  point.x = x;
+  point.y = y;
+
+  g_array_append_val (recorded->coordinates, point);
+
+  recorded->min_x = MIN (recorded->min_x, x);
+  recorded->max_x = MAX (recorded->max_x, x);
+  recorded->min_y = MIN (recorded->min_y, y);
+  recorded->max_y = MAX (recorded->max_y, y);
+}
+
+static void
+find_angle_and_distance (GdkPoint *point0,
+			 GdkPoint *point1,
+			 gint     *angle,
+			 gint     *distance)
+{
+  gdouble ang;
+  gint x, y;
+
+  x = point1->x - point0->x;
+  y = point1->y - point0->y;
+
+  ang = (atan2 (x, y) + G_PI) * (360 / (2 * G_PI));
+  *angle = ((gint) ang) % 360;
+  *angle = 360 - *angle;
+
+  *distance = (gint) sqrt ((x * x) + (y * y));
+}
+
+static GArray *
+recorded_gesture_vectorize (RecordedGesture *recorded)
+{
+  GdkPoint *point, *prev = NULL;
+  GArray *vector_data;
+  gint i, diff;
+
+  if ((recorded->max_x - recorded->min_x) == 0)
+    return NULL;
+
+  if ((recorded->max_y - recorded->min_y) == 0)
+    return NULL;
+
+  diff = ((recorded->max_x - recorded->min_x) - (recorded->max_y - recorded->min_y));
+
+  if (diff > 0)
+    {
+      /* Sample is more wide than high */
+      recorded->min_y -= diff / 2;
+      recorded->max_y += diff / 2;
+    }
+  else
+    {
+      /* Sample is more high than wide */
+      recorded->min_x -= ABS (diff) / 2;
+      recorded->max_x += ABS (diff) / 2;
+    }
+
+  vector_data = g_array_new (FALSE, FALSE, sizeof (GestureNode));
+
+  for (i = 0; i < recorded->coordinates->len; i++)
+    {
+      point = &g_array_index (recorded->coordinates, GdkPoint, i);
+
+      /* Resample coordinates so they fit in a GRID_SIZExGRID_SIZE grid */
+      point->x = ((point->x - recorded->min_x) * GRID_SIZE) / (recorded->max_x - recorded->min_x);
+      point->y = ((point->y - recorded->min_y) * GRID_SIZE) / (recorded->max_y - recorded->min_y);
+
+      /* Transform the coordinates into a vector array */
+      if (prev)
+        {
+          gint angle, distance;
+
+          find_angle_and_distance (prev, point, &angle, &distance);
+
+          if (distance > 0)
+            {
+              GestureNode *node, new;
+
+              if (vector_data->len > 0)
+                node = &g_array_index (vector_data, GestureNode, vector_data->len - 1);
+              else
+                node = NULL;
+
+              if (node && node->angle == angle)
+                node->distance += distance;
+              else
+                {
+                  new.angle = angle;
+                  new.distance = distance;
+                  g_array_append_val (vector_data, new);
+                }
+            }
+        }
+
+      prev = point;
+    }
+
+  return vector_data;
+}
+
+static void
+gtk_gestures_interpreter_init (GtkGesturesInterpreter *interpreter)
+{
+  GtkGesturesInterpreterPrivate *priv;
+
+  priv = interpreter->priv = G_TYPE_INSTANCE_GET_PRIVATE (interpreter,
+                                                          GTK_TYPE_GESTURES_INTERPRETER,
+                                                          GtkGesturesInterpreterPrivate);
+  priv->events = g_hash_table_new_full (NULL, NULL,
+                                        (GDestroyNotify) g_object_unref,
+                                        (GDestroyNotify) recorded_gesture_free);
+}
+
+GtkGesturesInterpreter *
+_gtk_gestures_interpreter_new (void)
+{
+  return g_object_new (GTK_TYPE_GESTURES_INTERPRETER, NULL);
+}
+
+gboolean
+_gtk_gestures_interpreter_feed_event (GtkGesturesInterpreter *interpreter,
+                                      GdkEvent               *event)
+{
+  GtkGesturesInterpreterPrivate *priv;
+  RecordedGesture *recorded;
+  GdkDevice *device;
+  gdouble x, y;
+
+  g_return_val_if_fail (GTK_IS_GESTURES_INTERPRETER (interpreter), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  priv = interpreter->priv;
+  device = gdk_event_get_source_device (event);
+
+  if (!device)
+    return FALSE;
+
+  if (!gdk_event_get_coords (event, &x, &y))
+    return FALSE;
+
+  recorded = g_hash_table_lookup (priv->events, device);
+
+  if (!recorded)
+    {
+      recorded = recorded_gesture_new ();
+      g_hash_table_insert (priv->events,
+                           g_object_ref (device),
+                           recorded);
+    }
+
+  recorded_gesture_append_coordinate (recorded, x, y);
+
+  return TRUE;
+}
+
+static GArray *
+get_gesture_data (GArray *gesture)
+{
+  GArray *data;
+  gint i, j;
+
+  data = g_array_new (FALSE, FALSE, sizeof (guint));
+
+  for (i = 0; i < gesture->len; i++)
+    {
+      GestureNode *node;
+      gint n_points;
+
+      node = &g_array_index (gesture, GestureNode, i);
+      n_points = MAX (1, node->distance / POINT_DISTANCE);
+
+      for (j = 0; j < n_points; j++)
+        g_array_append_val (data, node->angle);
+    }
+
+  return data;
+}
+
+static guint
+get_angle_diff (gint angle1,
+                gint angle2)
+{
+  gint diff;
+
+  diff = ABS (angle1 - angle2);
+
+  if (diff > 180)
+    diff = 360 - diff;
+
+  return diff;
+}
+
+/* This is a Levenshtein algorithm modification to have variable
+ * weights for modifications, gesture data is splitted and
+ * transformed into a list of angles, defining points at a
+ * more or less regular distance.
+ */
+static guint
+similarity_weight (GArray *gesture,
+                   GArray *stock)
+{
+  GArray *gesture_data, *stock_data;
+  guint *cur, *prev;
+  gint cost, i, j;
+#ifdef DEBUG_WEIGHTS
+  gchar chr;
+#endif
+
+  gesture_data = get_gesture_data (gesture);
+  stock_data = get_gesture_data (stock);
+
+  cur = g_new0 (guint, stock_data->len);
+  prev = g_new0 (guint, stock_data->len);
+
+#ifdef DEBUG_WEIGHTS
+  for (j = 0; j < stock_data->len; j++)
+    {
+      if (j == 0)
+        g_print (" %4d  ", 0);
+      else
+        g_print (" %4d  ", g_array_index (stock_data, guint, j - 1));
+    }
+
+  g_print ("\n");
+#endif
+
+  for (i = 0; i < gesture_data->len; i++)
+    {
+      guint *tmp;
+
+#ifdef DEBUG_WEIGHTS
+      if (i == 0)
+        g_print ("%4d  ", 0);
+      else
+        g_print ("%4d  ", g_array_index (gesture_data, guint, i - 1));
+#endif
+
+      for (j = 0; j < stock_data->len; j++)
+        {
+          gint diff;
+
+#ifdef DEBUG_WEIGHTS
+          chr = ' ';
+#endif
+
+          if (i == 0)
+            {
+              /* First iteration, fill in initial costs */
+              if (j == 0)
+                {
+                  diff = get_angle_diff (g_array_index (stock_data, guint, 1),
+                                         g_array_index (gesture_data, guint, 1));
+                  cur[j] = diff;
+                }
+              else
+                {
+                  diff = get_angle_diff (g_array_index (stock_data, guint, j),
+                                         g_array_index (stock_data, guint, j - 1));
+
+                  cur[j] = cur[j - 1] + diff;
+                }
+            }
+          else
+            {
+              if (j == 0)
+                {
+                  diff = get_angle_diff (g_array_index (gesture_data, guint, i),
+                                         g_array_index (gesture_data, guint, i - 1));
+
+                  cur[j] = prev[j] + diff;
+                }
+              else
+                {
+                  guint add_cost, del_cost, change_cost;
+
+                  /* Calculate add cost */
+                  if (i + 1 < gesture_data->len)
+                    diff =
+                      get_angle_diff (g_array_index (stock_data, guint, j),
+                                      g_array_index (gesture_data, guint, i)) +
+                      get_angle_diff (g_array_index (stock_data, guint, j),
+                                      g_array_index (gesture_data, guint, i + 1));
+                  else
+                    diff = 360;
+
+                  add_cost = cur[j - 1] + diff;
+
+                  /* Calculate deletion cost */
+                  if (j + 1 < stock_data->len)
+                    diff =
+                      get_angle_diff (g_array_index (stock_data, guint, j),
+                                      g_array_index (gesture_data, guint, i)) +
+                      get_angle_diff (g_array_index (stock_data, guint, j + 1),
+                                      g_array_index (gesture_data, guint, i));
+                  else
+                    diff = 0;
+
+                  del_cost = prev[j] + (360 - diff);
+
+                  /* Calculate change cost */
+                  diff = get_angle_diff (g_array_index (stock_data, guint, j),
+                                         g_array_index (gesture_data, guint, i));
+                  change_cost = prev[j - 1] + diff;
+
+                  cur[j] = MIN (add_cost, MIN (del_cost, change_cost));
+
+#ifdef DEBUG_WEIGHTS
+                  if (cur[j] == add_cost)
+                    chr = '-';
+                  else if (cur[j] == del_cost)
+                    chr = '|';
+                  else
+                    chr = '\\';
+#endif
+                }
+            }
+
+#ifdef DEBUG_WEIGHTS
+          g_print ("%c%4d ", chr, cur[j]);
+#endif
+        }
+
+#ifdef DEBUG_WEIGHTS
+      g_print ("\n");
+#endif
+
+      /* swap arrays for the next iteration, this also means it's
+       * the prev array which will contain the final score
+       */
+      tmp = prev;
+      prev = cur;
+      cur = tmp;
+    }
+
+  cost = prev[stock_data->len - 1] * MAX (1, ABS ((gint) stock_data->len - (gint) gesture_data->len));
+
+  g_array_free (gesture_data, TRUE);
+  g_array_free (stock_data, TRUE);
+  g_free (cur);
+  g_free (prev);
+
+  return cost;
+}
+
+gboolean
+_gtk_gestures_interpreter_finish (GtkGesturesInterpreter *interpreter,
+                                  GdkEvent               *event,
+                                  GSList                 *recognized_gestures,
+                                  GtkGestureType         *gesture)
+{
+  GtkGesturesInterpreterPrivate *priv;
+  RecordedGesture *recorded;
+  GtkGestureType gesture_type = 0;
+  GdkDevice *device;
+  GArray *vector_data;
+  guint weight, min_weight = G_MAXUINT;
+  gboolean found = FALSE;
+  GList *l;
+
+  g_return_val_if_fail (GTK_IS_GESTURES_INTERPRETER (interpreter), FALSE);
+
+  priv = interpreter->priv;
+  device = gdk_event_get_source_device (event);
+
+  if (!device)
+    return FALSE;
+
+  recorded = g_hash_table_lookup (priv->events, device);
+
+  if (!recorded)
+    return FALSE;
+
+  /* Any other considerations to
+   * discard a gesture event goes here
+   */
+
+  vector_data = recorded_gesture_vectorize (recorded);
+
+  if (!vector_data)
+    return FALSE;
+
+  /* Find out gesture data similarity with each gesture,
+   * the lower the number, the more similar is the just
+   * finished gesture with the one from the stock.
+   */
+  for (l = gestures_stock; l; l = l->next)
+    {
+      StockGesture *stock_gesture;
+
+      stock_gesture = l->data;
+      weight = similarity_weight (vector_data, stock_gesture->gesture_data);
+
+      if (weight < min_weight)
+        {
+          min_weight = weight;
+          gesture_type = stock_gesture->gesture;
+          found = TRUE;
+        }
+    }
+
+  g_hash_table_remove (priv->events, device);
+  g_array_free (vector_data, TRUE);
+
+  if (found &&
+      g_slist_find (recognized_gestures,
+                    GUINT_TO_POINTER (gesture_type)))
+    {
+      *gesture = gesture_type;
+      return TRUE;
+    }
+
+  return FALSE;
+}
diff --git a/gtk/gtkgesturesinterpreter.h b/gtk/gtkgesturesinterpreter.h
new file mode 100644
index 0000000..122866a
--- /dev/null
+++ b/gtk/gtkgesturesinterpreter.h
@@ -0,0 +1,64 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 2011 Carlos Garnacho <carlosg gnome org>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#ifndef __GTK_GESTURES_INTERPRETER_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_GESTURES_INTERPRETER         (gtk_gestures_interpreter_get_type ())
+#define GTK_GESTURES_INTERPRETER(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), GTK_TYPE_GESTURES_INTERPRETER, GtkGesturesInterpreter))
+#define GTK_GESTURES_INTERPRETER_CLASS(c)     (G_TYPE_CHECK_CLASS_CAST    ((c), GTK_TYPE_GESTURES_INTERPRETER, GtkGesturesInterpreterClass))
+#define GTK_IS_GESTURES_INTERPRETER(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), GTK_TYPE_GESTURES_INTERPRETER))
+#define GTK_IS_GESTURES_INTERPRETER_CLASS(c)  (G_TYPE_CHECK_CLASS_TYPE    ((c), GTK_TYPE_GESTURES_INTERPRETER))
+#define GTK_GESTURES_INTERPRETER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS  ((o), GTK_TYPE_GESTURES_INTERPRETER, GtkGesturesInterpreterClass))
+
+typedef struct _GtkGesturesInterpreter GtkGesturesInterpreter;
+typedef struct _GtkGesturesInterpreterClass GtkGesturesInterpreterClass;
+
+struct _GtkGesturesInterpreter
+{
+  GObject parent_object;
+  gpointer priv;
+};
+
+struct _GtkGesturesInterpreterClass
+{
+  GObjectClass parent_class;
+};
+
+GType gtk_gestures_interpreter_get_type (void) G_GNUC_CONST;
+
+GtkGesturesInterpreter * _gtk_gestures_interpreter_new (void);
+
+gboolean _gtk_gestures_interpreter_feed_event (GtkGesturesInterpreter *interpreter,
+                                               GdkEvent               *event);
+gboolean _gtk_gestures_interpreter_finish     (GtkGesturesInterpreter *interpreter,
+                                               GdkEvent               *event,
+                                               GSList                 *recognized_gestures,
+                                               GtkGestureType         *gesture);
+
+G_END_DECLS
+
+#endif /* __GTK_GESTURES_INTERPRETER_H__ */
diff --git a/gtk/gtkmain.c b/gtk/gtkmain.c
index 63376bf..af9b27f 100644
--- a/gtk/gtkmain.c
+++ b/gtk/gtkmain.c
@@ -1888,6 +1888,18 @@ gtk_main_do_event (GdkEvent *event)
       _gtk_tooltip_handle_event (event);
     }
 
+  /* Handle gestures */
+  if (event->type == GDK_MOTION_NOTIFY)
+    {
+      GdkModifierType state;
+
+      if (gdk_event_get_state (event, &state) &&
+          state & GDK_BUTTON1_MASK)
+        _gtk_widget_gesture_stroke (grab_widget, event);
+    }
+  else if (event->type == GDK_BUTTON_RELEASE)
+    _gtk_widget_gesture_finish (grab_widget, event);
+
   tmp_list = current_events;
   current_events = g_list_remove_link (current_events, tmp_list);
   g_list_free_1 (tmp_list);
diff --git a/gtk/gtkwidget.c b/gtk/gtkwidget.c
index 24cc272..2320635 100644
--- a/gtk/gtkwidget.c
+++ b/gtk/gtkwidget.c
@@ -61,6 +61,7 @@
 #include "gtkcssprovider.h"
 #include "gtkanimationdescription.h"
 #include "gtkmodifierstyle.h"
+#include "gtkgesturesinterpreter.h"
 #include "gtkversion.h"
 #include "gtkdebug.h"
 #include "gtktypebuiltins.h"
@@ -377,6 +378,8 @@ struct _GtkWidgetPrivate
 
   /* Widget's path for styling */
   GtkWidgetPath *path;
+
+  GSList *gestures;
 };
 
 enum {
@@ -450,6 +453,7 @@ enum {
   QUERY_TOOLTIP,
   DRAG_FAILED,
   STYLE_UPDATED,
+  GESTURE,
   LAST_SIGNAL
 };
 
@@ -691,6 +695,7 @@ static GQuark		quark_has_tooltip = 0;
 static GQuark		quark_tooltip_window = 0;
 static GQuark		quark_visual = 0;
 static GQuark           quark_modifier_style = 0;
+static GQuark           quark_gestures_interpreter = 0;
 GParamSpecPool         *_gtk_widget_child_property_pool = NULL;
 GObjectNotifyContext   *_gtk_widget_child_property_notify_context = NULL;
 
@@ -804,6 +809,7 @@ gtk_widget_class_init (GtkWidgetClass *klass)
   quark_tooltip_window = g_quark_from_static_string ("gtk-tooltip-window");
   quark_visual = g_quark_from_static_string ("gtk-widget-visual");
   quark_modifier_style = g_quark_from_static_string ("gtk-widget-modifier-style");
+  quark_gestures_interpreter = g_quark_from_static_string ("gtk-widget-gestures-interpreter");
 
   style_property_spec_pool = g_param_spec_pool_new (FALSE);
   _gtk_widget_child_property_pool = g_param_spec_pool_new (TRUE);
@@ -2966,6 +2972,15 @@ gtk_widget_class_init (GtkWidgetClass *klass)
 		  _gtk_marshal_BOOLEAN__UINT,
                   G_TYPE_BOOLEAN, 1, G_TYPE_UINT);
 
+  widget_signals[GESTURE] =
+    g_signal_new (I_("gesture"),
+                  G_TYPE_FROM_CLASS (klass),
+		  G_SIGNAL_RUN_LAST,
+		  G_STRUCT_OFFSET (GtkWidgetClass, gesture),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__ENUM,
+                  G_TYPE_NONE, 1, GTK_TYPE_GESTURE_TYPE);
+
   binding_set = gtk_binding_set_by_class (klass);
   gtk_binding_entry_add_signal (binding_set, GDK_KEY_F10, GDK_SHIFT_MASK,
                                 "popup-menu", 0);
@@ -14025,3 +14040,82 @@ gtk_widget_get_style_context (GtkWidget *widget)
 
   return widget->priv->context;
 }
+
+void
+_gtk_widget_gesture_stroke (GtkWidget *widget,
+                            GdkEvent  *event)
+{
+  GtkGesturesInterpreter *interpreter;
+  GtkWidgetPrivate *priv;
+
+  priv = widget->priv;
+
+  if (!priv->gestures)
+    return;
+
+  interpreter = g_object_get_qdata (G_OBJECT (widget), quark_gestures_interpreter);
+
+  if (!interpreter)
+    {
+      interpreter = _gtk_gestures_interpreter_new ();
+      g_object_set_qdata_full (G_OBJECT (widget), quark_gestures_interpreter,
+                               interpreter, (GDestroyNotify) g_object_unref);
+    }
+
+  _gtk_gestures_interpreter_feed_event (interpreter, event);
+}
+
+void
+_gtk_widget_gesture_finish (GtkWidget *widget,
+                            GdkEvent  *event)
+{
+  GtkGesturesInterpreter *interpreter;
+  GtkWidgetPrivate *priv;
+  GtkGestureType gesture;
+
+  priv = widget->priv;
+
+  if (!priv->gestures)
+    return;
+
+  interpreter = g_object_get_qdata (G_OBJECT (widget), quark_gestures_interpreter);
+
+  if (!interpreter)
+    return;
+
+  if (_gtk_gestures_interpreter_finish (interpreter, event, priv->gestures, &gesture))
+    g_signal_emit (widget, widget_signals[GESTURE], 0, gesture);
+}
+
+void
+gtk_widget_enable_gesture (GtkWidget       *widget,
+                           GtkGestureType   gesture)
+{
+  GtkWidgetPrivate *priv;
+
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  priv = widget->priv;
+
+  if (!g_slist_find (priv->gestures, GUINT_TO_POINTER (gesture)))
+    priv->gestures = g_slist_prepend (priv->gestures,
+                                      GUINT_TO_POINTER (gesture));
+}
+
+void
+gtk_widget_disable_gesture (GtkWidget      *widget,
+                            GtkGestureType  gesture)
+{
+  GtkWidgetPrivate *priv;
+
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  priv = widget->priv;
+  priv->gestures = g_slist_remove (priv->gestures,
+                                   GUINT_TO_POINTER (gesture));
+
+  if (!priv->gestures)
+    {
+      /* Cancel current interpreter operation */
+    }
+}
diff --git a/gtk/gtkwidget.h b/gtk/gtkwidget.h
index a2d4d5b..fc0a4cd 100644
--- a/gtk/gtkwidget.h
+++ b/gtk/gtkwidget.h
@@ -425,6 +425,9 @@ struct _GtkWidgetClass
 
   void         (* style_updated)          (GtkWidget *widget);
 
+  void         (* gesture)                (GtkWidget *widget,
+                                           GtkGestureType  gesture);
+
   /*< private >*/
 
   /* Padding for future expansion */
@@ -977,6 +980,12 @@ GtkStyleContext * gtk_widget_get_style_context (GtkWidget *widget);
 
 GtkWidgetPath *   gtk_widget_get_path (GtkWidget *widget);
 
+/* Gestures */
+void gtk_widget_enable_gesture (GtkWidget       *widget,
+                                GtkGestureType   gesture);
+void gtk_widget_disable_gesture (GtkWidget      *widget,
+                                 GtkGestureType  gesture);
+
 
 G_END_DECLS
 
diff --git a/gtk/gtkwidgetprivate.h b/gtk/gtkwidgetprivate.h
index c566646..be5044a 100644
--- a/gtk/gtkwidgetprivate.h
+++ b/gtk/gtkwidgetprivate.h
@@ -93,6 +93,11 @@ gboolean _gtk_widget_get_translation_to_window (GtkWidget      *widget,
 						int            *x,
 						int            *y);
 
+void _gtk_widget_gesture_stroke (GtkWidget *widget,
+                                 GdkEvent  *event);
+void _gtk_widget_gesture_finish (GtkWidget *widget,
+                                 GdkEvent  *event);
+
 G_END_DECLS
 
 #endif /* __GTK_WIDGET_PRIVATE_H__ */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 5364298..2157907 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -49,6 +49,7 @@ noinst_PROGRAMS =  $(TEST_PROGS)	\
 	testfilechooserbutton		\
 	testframe			\
 	testgeometry			\
+	testgestures			\
 	testgiconpixbuf			\
 	testgrid			\
 	testgtk				\
@@ -149,6 +150,7 @@ testfilechooser_DEPENDENCIES = $(TEST_DEPS)
 testfilechooserbutton_DEPENDENCIES = $(TEST_DEPS)
 testframe_DEPENDENCIES = $(TEST_DEPS)
 testgeometry_DEPENDENCIES = $(TEST_DEPS)
+testgestures_DEPENDENCIES = $(TEST_DEPS)
 testgiconpixbuf = $(TEST_DEPS)
 testgrid_DEPENDENCIES = $(TEST_DEPS)
 testgtk_DEPENDENCIES = $(TEST_DEPS)
@@ -225,6 +227,7 @@ testfilechooser_LDADD = $(LDADDS)
 testfilechooserbutton_LDADD = $(LDADDS)
 testframe_LDADD = $(LDADDS)
 testgeometry_LDADD = $(LDADDS)
+testgestures_LDADD = $(LDADDS)
 testgiconpixbuf_LDADD = $(LDADDS)
 testgrid_LDADD = $(LDADDS)
 testgtk_LDADD = $(LDADDS)
@@ -359,6 +362,9 @@ testframe_SOURCES = 		\
 testgeometry_SOURCES = 		\
 	testgeometry.c
 
+testgestures_SOURCES = 		\
+	testgestures.c
+
 testgiconpixbuf_SOURCES =	\
 	testgiconpixbuf.c
 
diff --git a/tests/testgestures.c b/tests/testgestures.c
new file mode 100644
index 0000000..dab0cec
--- /dev/null
+++ b/tests/testgestures.c
@@ -0,0 +1,233 @@
+#include <gtk/gtk.h>
+
+static GArray *points;
+static GtkGestureType shown_gesture = -1;
+static guint timeout_id = 0;
+
+static gboolean
+hide_gesture_cb (gpointer user_data)
+{
+  shown_gesture = -1;
+  gtk_widget_queue_draw (GTK_WIDGET (user_data));
+
+  timeout_id = 0;
+  return FALSE;
+}
+
+static void
+gesture_cb (GtkWidget      *widget,
+            GtkGestureType  gesture,
+            gpointer        user_data)
+{
+  shown_gesture = gesture;
+  gtk_widget_queue_draw (widget);
+
+  if (timeout_id != 0)
+    g_source_remove (timeout_id);
+
+  timeout_id = g_timeout_add (500, hide_gesture_cb, widget);
+}
+
+static gboolean
+draw_cb (GtkWidget *widget,
+         cairo_t   *cr,
+         gpointer   user_data)
+{
+  GdkPoint *point;
+  gint i;
+
+  if (shown_gesture != -1)
+    {
+      gint width, height, size;
+
+      cairo_save (cr);
+      width = gdk_window_get_width (gtk_widget_get_window (widget));
+      height = gdk_window_get_height (gtk_widget_get_window (widget));
+
+      size = MIN (width, height);
+      size -= size / 4;
+
+      cairo_translate (cr, width / 2, height / 2);
+
+      cairo_set_line_width (cr, 10);
+      cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
+      cairo_set_source_rgb (cr, 0.8, 0.8, 0.8);
+
+      switch (shown_gesture)
+        {
+        case GTK_GESTURE_SWIPE_LEFT:
+        case GTK_GESTURE_SWIPE_RIGHT:
+        case GTK_GESTURE_SWIPE_UP:
+        case GTK_GESTURE_SWIPE_DOWN:
+          if (shown_gesture == GTK_GESTURE_SWIPE_RIGHT)
+            cairo_rotate (cr, (G_PI * 3) / 2);
+          else if (shown_gesture == GTK_GESTURE_SWIPE_LEFT)
+            cairo_rotate (cr, G_PI / 2);
+          else if (shown_gesture == GTK_GESTURE_SWIPE_UP)
+            cairo_rotate (cr, G_PI);
+
+          cairo_move_to (cr, 0, - size / 2);
+          cairo_line_to (cr, 0, size / 2);
+
+          cairo_move_to (cr, 0, size / 2);
+          cairo_rel_line_to (cr, -size / 4, -size / 4);
+
+          cairo_move_to (cr, 0, size / 2);
+          cairo_rel_line_to (cr, size / 4, -size / 4);
+
+          cairo_stroke (cr);
+          break;
+        case GTK_GESTURE_CIRCULAR_COUNTERCLOCKWISE:
+          cairo_move_to (cr, 0, - size / 2);
+          cairo_rel_line_to (cr, size / 8, - size / 8);
+
+          cairo_move_to (cr, 0, - size / 2);
+          cairo_rel_line_to (cr, size / 8, size / 8);
+
+          cairo_new_sub_path (cr);
+          cairo_arc (cr, 0, 0, size / 2, (3 * G_PI) / 2, (5 * G_PI) / 4);
+
+          cairo_stroke (cr);
+          break;
+        case GTK_GESTURE_CIRCULAR_CLOCKWISE:
+          cairo_move_to (cr, 0, - size / 2);
+          cairo_rel_line_to (cr, - size / 8, - size / 8);
+
+          cairo_move_to (cr, 0, - size / 2);
+          cairo_rel_line_to (cr, - size / 8, size / 8);
+
+          cairo_new_sub_path (cr);
+          cairo_arc_negative (cr, 0, 0, size / 2, (3 * G_PI) / 2, (7* G_PI) / 4);
+
+          cairo_stroke (cr);
+          break;
+        default:
+          break;
+        }
+
+      cairo_restore (cr);
+    }
+  else if (points->len > 0)
+    {
+      cairo_set_line_width (cr, 2);
+
+      point = &g_array_index (points, GdkPoint, 0);
+      cairo_move_to (cr, point->x, point->y);
+
+      for (i = 0; i < points->len; i++)
+        {
+          point = &g_array_index (points, GdkPoint, i);
+          cairo_line_to (cr, point->x, point->y);
+        }
+
+      cairo_stroke (cr);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+motion_notify_cb (GtkWidget *widget,
+                  GdkEvent  *event,
+                  gpointer   user_data)
+{
+  GdkModifierType state;
+  gdouble x, y;
+  GdkPoint point;
+
+  if (!gdk_event_get_state (event, &state))
+    return FALSE;
+
+  if ((state & GDK_BUTTON1_MASK) == 0)
+    return FALSE;
+
+  gdk_event_get_coords (event, &x, &y);
+
+  point.x = x;
+  point.y = y;
+  g_array_append_val (points, point);
+
+  gtk_widget_queue_draw (widget);
+
+  return FALSE;
+}
+
+static gboolean
+button_release_cb (GtkWidget *widget,
+                   GdkEvent  *event,
+                   gpointer   user_data)
+{
+  if (points->len > 0)
+    {
+      g_array_remove_range (points, 0, points->len);
+      gtk_widget_queue_draw (widget);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+button_press_cb (GtkWidget *widget,
+                 GdkEvent  *event,
+                 gpointer   user_data)
+{
+  if (timeout_id != 0)
+    {
+      g_source_remove (timeout_id);
+      timeout_id = 0;
+    }
+
+  shown_gesture = -1;
+  gtk_widget_queue_draw (widget);
+
+  return FALSE;
+}
+
+static void
+create_window (void)
+{
+  GtkWidget *window;
+
+  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+  gtk_widget_set_app_paintable (window, TRUE);
+  gtk_widget_set_size_request (window, 400, 400);
+  gtk_window_set_title (GTK_WINDOW (window), "Gestures demo");
+
+  gtk_widget_add_events (window,
+                         GDK_BUTTON_PRESS_MASK |
+                         GDK_BUTTON_RELEASE_MASK |
+                         GDK_POINTER_MOTION_MASK);
+
+  gtk_widget_enable_gesture (window, GTK_GESTURE_SWIPE_LEFT);
+  gtk_widget_enable_gesture (window, GTK_GESTURE_SWIPE_RIGHT);
+  gtk_widget_enable_gesture (window, GTK_GESTURE_SWIPE_UP);
+  gtk_widget_enable_gesture (window, GTK_GESTURE_SWIPE_DOWN);
+  gtk_widget_enable_gesture (window, GTK_GESTURE_CIRCULAR_CLOCKWISE);
+  gtk_widget_enable_gesture (window, GTK_GESTURE_CIRCULAR_COUNTERCLOCKWISE);
+
+  g_signal_connect (window, "destroy",
+		    G_CALLBACK (gtk_main_quit), NULL);
+  g_signal_connect (window, "motion-notify-event",
+                    G_CALLBACK (motion_notify_cb), NULL);
+  g_signal_connect (window, "button-release-event",
+                    G_CALLBACK (button_release_cb), NULL);
+  g_signal_connect (window, "button-press-event",
+                    G_CALLBACK (button_press_cb), NULL);
+  g_signal_connect (window, "draw",
+                    G_CALLBACK (draw_cb), NULL);
+  g_signal_connect (window, "gesture",
+                    G_CALLBACK (gesture_cb), NULL);
+
+  gtk_widget_show (window);
+}
+
+int
+main (int argc, char *argv[])
+{
+  gtk_init (&argc, &argv);
+
+  points = g_array_new (FALSE, FALSE, sizeof (GdkPoint));
+  create_window ();
+
+  gtk_main ();
+}



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]