summaryrefslogtreecommitdiffstats
path: root/subprojects/libhandy/src/hdy-swipe-tracker.c
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/libhandy/src/hdy-swipe-tracker.c')
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker.c1113
1 files changed, 1113 insertions, 0 deletions
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.c b/subprojects/libhandy/src/hdy-swipe-tracker.c
new file mode 100644
index 0000000..0cbf4a4
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker.c
@@ -0,0 +1,1113 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-swipe-tracker-private.h"
+#include "hdy-navigation-direction.h"
+
+#include <math.h>
+
+#define TOUCHPAD_BASE_DISTANCE_H 400
+#define TOUCHPAD_BASE_DISTANCE_V 300
+#define SCROLL_MULTIPLIER 10
+#define MIN_ANIMATION_DURATION 100
+#define MAX_ANIMATION_DURATION 400
+#define VELOCITY_THRESHOLD 0.4
+#define DURATION_MULTIPLIER 3
+#define ANIMATION_BASE_VELOCITY 0.002
+#define DRAG_THRESHOLD_DISTANCE 5
+
+/**
+ * SECTION:hdy-swipe-tracker
+ * @short_description: Swipe tracker used in #HdyCarousel and #HdyLeaflet
+ * @title: HdySwipeTracker
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable
+ *
+ * The HdySwipeTracker object can be used for implementing widgets with swipe
+ * gestures. It supports touch-based swipes, pointer dragging, and touchpad
+ * scrolling.
+ *
+ * The widgets will probably want to expose #HdySwipeTracker:enabled property.
+ * If they expect to use horizontal orientation, #HdySwipeTracker:reversed
+ * property can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+
+typedef enum {
+ HDY_SWIPE_TRACKER_STATE_NONE,
+ HDY_SWIPE_TRACKER_STATE_PENDING,
+ HDY_SWIPE_TRACKER_STATE_SCROLLING,
+ HDY_SWIPE_TRACKER_STATE_FINISHING,
+ HDY_SWIPE_TRACKER_STATE_REJECTED,
+} HdySwipeTrackerState;
+
+struct _HdySwipeTracker
+{
+ GObject parent_instance;
+
+ HdySwipeable *swipeable;
+ gboolean enabled;
+ gboolean reversed;
+ gboolean allow_mouse_drag;
+ GtkOrientation orientation;
+
+ gint start_x;
+ gint start_y;
+
+ guint32 prev_time;
+ gdouble velocity;
+
+ gdouble initial_progress;
+ gdouble progress;
+ gboolean cancelled;
+
+ gdouble prev_offset;
+
+ gboolean is_scrolling;
+
+ HdySwipeTrackerState state;
+ GtkGesture *touch_gesture;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdySwipeTracker, hdy_swipe_tracker, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL));
+
+enum {
+ PROP_0,
+ PROP_SWIPEABLE,
+ PROP_ENABLED,
+ PROP_REVERSED,
+ PROP_ALLOW_MOUSE_DRAG,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ALLOW_MOUSE_DRAG + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_BEGIN_SWIPE,
+ SIGNAL_UPDATE_SWIPE,
+ SIGNAL_END_SWIPE,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+reset (HdySwipeTracker *self)
+{
+ self->state = HDY_SWIPE_TRACKER_STATE_NONE;
+
+ self->prev_offset = 0;
+
+ self->initial_progress = 0;
+ self->progress = 0;
+
+ self->start_x = 0;
+ self->start_y = 0;
+
+ self->prev_time = 0;
+ self->velocity = 0;
+
+ self->cancelled = FALSE;
+
+ if (self->swipeable)
+ gtk_grab_remove (GTK_WIDGET (self->swipeable));
+}
+
+static void
+get_range (HdySwipeTracker *self,
+ gdouble *first,
+ gdouble *last)
+{
+ g_autofree gdouble *points = NULL;
+ gint n;
+
+ points = hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+ *first = points[0];
+ *last = points[n - 1];
+}
+
+static void
+gesture_prepare (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean is_drag)
+{
+ GdkRectangle rect;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_NONE)
+ return;
+
+ hdy_swipeable_get_swipe_area (self->swipeable, direction, is_drag, &rect);
+
+ if (self->start_x < rect.x ||
+ self->start_x >= rect.x + rect.width ||
+ self->start_y < rect.y ||
+ self->start_y >= rect.y + rect.height) {
+ self->state = HDY_SWIPE_TRACKER_STATE_REJECTED;
+
+ return;
+ }
+
+ hdy_swipe_tracker_emit_begin_swipe (self, direction, TRUE);
+
+ self->initial_progress = hdy_swipeable_get_progress (self->swipeable);
+ self->progress = self->initial_progress;
+ self->velocity = 0;
+ self->state = HDY_SWIPE_TRACKER_STATE_PENDING;
+}
+
+static void
+gesture_begin (HdySwipeTracker *self)
+{
+ GdkEvent *event;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING)
+ return;
+
+ event = gtk_get_current_event ();
+ self->prev_time = gdk_event_get_time (event);
+ self->state = HDY_SWIPE_TRACKER_STATE_SCROLLING;
+
+ gtk_grab_add (GTK_WIDGET (self->swipeable));
+}
+
+static void
+gesture_update (HdySwipeTracker *self,
+ gdouble delta)
+{
+ GdkEvent *event;
+ guint32 time;
+ gdouble progress;
+ gdouble first_point, last_point;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ event = gtk_get_current_event ();
+ time = gdk_event_get_time (event);
+ if (time != self->prev_time)
+ self->velocity = delta / (time - self->prev_time);
+
+ get_range (self, &first_point, &last_point);
+
+ progress = self->progress + delta;
+ progress = CLAMP (progress, first_point, last_point);
+
+ /* FIXME: this is a hack to prevent swiping more than 1 page at once */
+ progress = CLAMP (progress, self->initial_progress - 1, self->initial_progress + 1);
+
+ self->progress = progress;
+
+ hdy_swipe_tracker_emit_update_swipe (self, progress);
+
+ self->prev_time = time;
+}
+
+static void
+get_closest_snap_points (HdySwipeTracker *self,
+ gdouble *upper,
+ gdouble *lower)
+{
+ gint i, n;
+ gdouble *points;
+
+ *upper = 0;
+ *lower = 0;
+
+ points = hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+ for (i = 0; i < n; i++) {
+ if (points[i] >= self->progress) {
+ *upper = points[i];
+ break;
+ }
+ }
+
+ for (i = n - 1; i >= 0; i--) {
+ if (points[i] <= self->progress) {
+ *lower = points[i];
+ break;
+ }
+ }
+
+ g_free (points);
+}
+
+static gdouble
+get_end_progress (HdySwipeTracker *self,
+ gdouble distance)
+{
+ gdouble upper, lower, middle;
+
+ if (self->cancelled)
+ return hdy_swipeable_get_cancel_progress (self->swipeable);
+
+ get_closest_snap_points (self, &upper, &lower);
+ middle = (upper + lower) / 2;
+
+ if (self->progress > middle)
+ return (self->velocity * distance > -VELOCITY_THRESHOLD ||
+ self->initial_progress > upper) ? upper : lower;
+
+ return (self->velocity * distance < VELOCITY_THRESHOLD ||
+ self->initial_progress < lower) ? lower : upper;
+}
+
+static void
+gesture_end (HdySwipeTracker *self,
+ gdouble distance)
+{
+ gdouble end_progress, velocity;
+ gint64 duration;
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE)
+ return;
+
+ end_progress = get_end_progress (self, distance);
+
+ velocity = ANIMATION_BASE_VELOCITY;
+ if ((end_progress - self->progress) * self->velocity > 0)
+ velocity = self->velocity;
+
+ duration = ABS ((self->progress - end_progress) / velocity * DURATION_MULTIPLIER);
+ if (self->progress != end_progress)
+ duration = CLAMP (duration, MIN_ANIMATION_DURATION, MAX_ANIMATION_DURATION);
+
+ hdy_swipe_tracker_emit_end_swipe (self, duration, end_progress);
+
+ if (self->cancelled)
+ reset (self);
+ else
+ self->state = HDY_SWIPE_TRACKER_STATE_FINISHING;
+}
+
+static void
+gesture_cancel (HdySwipeTracker *self,
+ gdouble distance)
+{
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING &&
+ self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ self->cancelled = TRUE;
+ gesture_end (self, distance);
+}
+
+static void
+drag_begin_cb (HdySwipeTracker *self,
+ gdouble start_x,
+ gdouble start_y,
+ GtkGestureDrag *gesture)
+{
+ if (self->state != HDY_SWIPE_TRACKER_STATE_NONE)
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ self->start_x = start_x;
+ self->start_y = start_y;
+}
+
+static void
+drag_update_cb (HdySwipeTracker *self,
+ gdouble offset_x,
+ gdouble offset_y,
+ GtkGestureDrag *gesture)
+{
+ gdouble offset, distance;
+ gboolean is_vertical, is_offset_vertical;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+ if (is_vertical)
+ offset = -offset_y / distance;
+ else
+ offset = -offset_x / distance;
+
+ if (self->reversed)
+ offset = -offset;
+
+ is_offset_vertical = (ABS (offset_y) > ABS (offset_x));
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) {
+ if (is_vertical == is_offset_vertical)
+ gesture_prepare (self, offset > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, TRUE);
+ else
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_PENDING) {
+ gdouble drag_distance;
+ gdouble first_point, last_point;
+ gboolean is_overshooting;
+
+ get_range (self, &first_point, &last_point);
+
+ drag_distance = sqrt (offset_x * offset_x + offset_y * offset_y);
+ is_overshooting = (offset < 0 && self->progress <= first_point) ||
+ (offset > 0 && self->progress >= last_point);
+
+ if (drag_distance >= DRAG_THRESHOLD_DISTANCE) {
+ if ((is_vertical == is_offset_vertical) && !is_overshooting) {
+ gesture_begin (self);
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ } else {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ }
+ }
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ gesture_update (self, offset - self->prev_offset);
+ self->prev_offset = offset;
+ }
+}
+
+static void
+drag_end_cb (HdySwipeTracker *self,
+ gdouble offset_x,
+ gdouble offset_y,
+ GtkGestureDrag *gesture)
+{
+ gdouble distance;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ reset (self);
+ return;
+ }
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ gesture_cancel (self, distance);
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ gesture_end (self, distance);
+}
+
+static void
+drag_cancel_cb (HdySwipeTracker *self,
+ GdkEventSequence *sequence,
+ GtkGesture *gesture)
+{
+ gdouble distance;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ gesture_cancel (self, distance);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+}
+
+static gboolean
+handle_scroll_event (HdySwipeTracker *self,
+ GdkEvent *event,
+ gboolean capture)
+{
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+ gdouble dx, dy, delta, distance;
+ gboolean is_vertical;
+ gboolean is_delta_vertical;
+
+ is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+ distance = is_vertical ? TOUCHPAD_BASE_DISTANCE_V : TOUCHPAD_BASE_DISTANCE_H;
+
+ if (gdk_event_get_scroll_direction (event, NULL))
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gdk_event_get_source_device (event);
+ input_source = gdk_device_get_source (source_device);
+ if (input_source != GDK_SOURCE_TOUCHPAD)
+ return GDK_EVENT_PROPAGATE;
+
+ gdk_event_get_scroll_deltas (event, &dx, &dy);
+ delta = is_vertical ? dy : dx;
+ if (self->reversed)
+ delta = -delta;
+
+ is_delta_vertical = (ABS (dy) > ABS (dx));
+
+ if (self->is_scrolling) {
+ gesture_cancel (self, distance);
+
+ if (gdk_event_is_scroll_stop_event (event))
+ self->is_scrolling = FALSE;
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ if (gdk_event_is_scroll_stop_event (event))
+ reset (self);
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) {
+ if (gdk_event_is_scroll_stop_event (event))
+ return GDK_EVENT_PROPAGATE;
+
+ if (is_vertical == is_delta_vertical) {
+ if (!capture) {
+ GtkWidget *widget = gtk_get_event_widget (event);
+ gdouble event_x, event_y;
+
+ gdk_event_get_coords (event, &event_x, &event_y);
+ gtk_widget_translate_coordinates (widget, GTK_WIDGET (self->swipeable),
+ event_x, event_y,
+ &self->start_x, &self->start_y);
+
+ gesture_prepare (self, delta > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, FALSE);
+ }
+ } else {
+ self->is_scrolling = TRUE;
+ return GDK_EVENT_PROPAGATE;
+ }
+ }
+
+ if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_PENDING) {
+ gboolean is_overshooting;
+ gdouble first_point, last_point;
+
+ get_range (self, &first_point, &last_point);
+
+ is_overshooting = (delta < 0 && self->progress <= first_point) ||
+ (delta > 0 && self->progress >= last_point);
+
+ if ((is_vertical == is_delta_vertical) && !is_overshooting)
+ gesture_begin (self);
+ else
+ gesture_cancel (self, distance);
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ if (gdk_event_is_scroll_stop_event (event)) {
+ gesture_end (self, distance);
+ } else {
+ gesture_update (self, delta / distance * SCROLL_MULTIPLIER);
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_FINISHING)
+ reset (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+is_window_handle (GtkWidget *widget)
+{
+ gboolean window_dragging;
+ GtkWidget *parent, *window, *titlebar;
+
+ gtk_widget_style_get (widget, "window-dragging", &window_dragging, NULL);
+
+ if (window_dragging)
+ return TRUE;
+
+ /* Window titlebar area is always draggable, so check if we're inside. */
+ window = gtk_widget_get_toplevel (widget);
+ if (!GTK_IS_WINDOW (window))
+ return FALSE;
+
+ titlebar = gtk_window_get_titlebar (GTK_WINDOW (window));
+ if (!titlebar)
+ return FALSE;
+
+ parent = widget;
+ while (parent && parent != titlebar)
+ parent = gtk_widget_get_parent (parent);
+
+ return parent == titlebar;
+}
+
+static gboolean
+handle_event_cb (HdySwipeTracker *self,
+ GdkEvent *event)
+{
+ GdkEventSequence *sequence;
+ gboolean retval;
+ GtkEventSequenceState state;
+ GtkWidget *widget;
+
+ if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type == GDK_SCROLL)
+ return handle_scroll_event (self, event, FALSE);
+
+ if (event->type != GDK_BUTTON_PRESS &&
+ event->type != GDK_BUTTON_RELEASE &&
+ event->type != GDK_MOTION_NOTIFY &&
+ event->type != GDK_TOUCH_BEGIN &&
+ event->type != GDK_TOUCH_END &&
+ event->type != GDK_TOUCH_UPDATE &&
+ event->type != GDK_TOUCH_CANCEL)
+ return GDK_EVENT_PROPAGATE;
+
+ widget = gtk_get_event_widget (event);
+ if (is_window_handle (widget))
+ return GDK_EVENT_PROPAGATE;
+
+ sequence = gdk_event_get_event_sequence (event);
+ retval = gtk_event_controller_handle_event (GTK_EVENT_CONTROLLER (self->touch_gesture), event);
+ state = gtk_gesture_get_sequence_state (self->touch_gesture, sequence);
+
+ if (state == GTK_EVENT_SEQUENCE_DENIED) {
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->touch_gesture));
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ return GDK_EVENT_STOP;
+ } else if (self->state == HDY_SWIPE_TRACKER_STATE_FINISHING) {
+ reset (self);
+ return GDK_EVENT_STOP;
+ }
+ return retval;
+}
+
+static gboolean
+captured_event_cb (HdySwipeable *swipeable,
+ GdkEvent *event)
+{
+ HdySwipeTracker *self = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ g_assert (HDY_IS_SWIPE_TRACKER (self));
+
+ if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type != GDK_SCROLL)
+ return GDK_EVENT_PROPAGATE;
+
+ return handle_scroll_event (self, event, TRUE);
+}
+
+static void
+hdy_swipe_tracker_constructed (GObject *object)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ g_assert (self->swipeable);
+
+ gtk_widget_add_events (GTK_WIDGET (self->swipeable),
+ GDK_SMOOTH_SCROLL_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_TOUCH_MASK);
+
+ self->touch_gesture = g_object_new (GTK_TYPE_GESTURE_DRAG,
+ "widget", self->swipeable,
+ "propagation-phase", GTK_PHASE_NONE,
+ "touch-only", !self->allow_mouse_drag,
+ NULL);
+
+ g_signal_connect_swapped (self->touch_gesture, "drag-begin", G_CALLBACK (drag_begin_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "drag-update", G_CALLBACK (drag_update_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "drag-end", G_CALLBACK (drag_end_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "cancel", G_CALLBACK (drag_cancel_cb), self);
+
+ g_signal_connect_object (self->swipeable, "event", G_CALLBACK (handle_event_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->swipeable, "unrealize", G_CALLBACK (reset), self, G_CONNECT_SWAPPED);
+
+ /*
+ * HACK: GTK3 has no other way to get events on capture phase.
+ * This is a reimplementation of _gtk_widget_set_captured_event_handler(),
+ * which is private. In GTK4 it can be replaced with GtkEventControllerLegacy
+ * with capture propagation phase
+ */
+ g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", captured_event_cb);
+
+ G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->constructed (object);
+}
+
+static void
+hdy_swipe_tracker_dispose (GObject *object)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ if (self->swipeable)
+ gtk_grab_remove (GTK_WIDGET (self->swipeable));
+
+ if (self->touch_gesture)
+ g_signal_handlers_disconnect_by_data (self->touch_gesture, self);
+
+ g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", NULL);
+
+ g_clear_object (&self->touch_gesture);
+ g_clear_object (&self->swipeable);
+
+ G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->dispose (object);
+}
+
+static void
+hdy_swipe_tracker_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ switch (prop_id) {
+ case PROP_SWIPEABLE:
+ g_value_set_object (value, hdy_swipe_tracker_get_swipeable (self));
+ break;
+
+ case PROP_ENABLED:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_enabled (self));
+ break;
+
+ case PROP_REVERSED:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_reversed (self));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_allow_mouse_drag (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_swipe_tracker_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ switch (prop_id) {
+ case PROP_SWIPEABLE:
+ self->swipeable = HDY_SWIPEABLE (g_object_ref (g_value_get_object (value)));
+ break;
+
+ case PROP_ENABLED:
+ hdy_swipe_tracker_set_enabled (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_REVERSED:
+ hdy_swipe_tracker_set_reversed (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ hdy_swipe_tracker_set_allow_mouse_drag (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = g_value_get_enum (value);
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_swipe_tracker_class_init (HdySwipeTrackerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = hdy_swipe_tracker_constructed;
+ object_class->dispose = hdy_swipe_tracker_dispose;
+ object_class->get_property = hdy_swipe_tracker_get_property;
+ object_class->set_property = hdy_swipe_tracker_set_property;
+
+ /**
+ * HdySwipeTracker:swipeable:
+ *
+ * The widget the swipe tracker is attached to. Must not be %NULL.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SWIPEABLE] =
+ g_param_spec_object ("swipeable",
+ _("Swipeable"),
+ _("The swipeable the swipe tracker is attached to"),
+ HDY_TYPE_SWIPEABLE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ /**
+ * HdySwipeTracker:enabled:
+ *
+ * Whether the swipe tracker is enabled. When it's not enabled, no events
+ * will be processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ENABLED] =
+ g_param_spec_boolean ("enabled",
+ _("Enabled"),
+ _("Whether the swipe tracker processes events"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySwipeTracker:reversed:
+ *
+ * Whether to reverse the swipe direction. If the swipe tracker is horizontal,
+ * it can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVERSED] =
+ g_param_spec_boolean ("reversed",
+ _("Reversed"),
+ _("Whether swipe direction is reversed"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySwipeTracker:allow-mouse-drag:
+ *
+ * Whether to allow dragging with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ALLOW_MOUSE_DRAG] =
+ g_param_spec_boolean ("allow-mouse-drag",
+ _("Allow mouse drag"),
+ _("Whether to allow dragging with mouse pointer"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdySwipeTracker::begin-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @direction: The direction of the swipe
+ * @direct: %TRUE if the swipe is directly triggered by a gesture,
+ * %FALSE if it's triggered via a #HdySwipeGroup
+ *
+ * This signal is emitted when a possible swipe is detected.
+ *
+ * The @direction value can be used to restrict the swipe to a certain
+ * direction.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_BEGIN_SWIPE] =
+ g_signal_new ("begin-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ HDY_TYPE_NAVIGATION_DIRECTION, G_TYPE_BOOLEAN);
+
+ /**
+ * HdySwipeTracker::update-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @progress: The current animation progress value
+ *
+ * This signal is emitted every time the progress value changes.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_UPDATE_SWIPE] =
+ g_signal_new ("update-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_DOUBLE);
+
+ /**
+ * HdySwipeTracker::end-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @duration: Snap-back animation duration in milliseconds
+ * @to: The progress value to animate to
+ *
+ * This signal is emitted as soon as the gesture has stopped.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_END_SWIPE] =
+ g_signal_new ("end-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_INT64, G_TYPE_DOUBLE);
+}
+
+static void
+hdy_swipe_tracker_init (HdySwipeTracker *self)
+{
+ reset (self);
+ self->orientation = GTK_ORIENTATION_HORIZONTAL;
+ self->enabled = TRUE;
+}
+
+/**
+ * hdy_swipe_tracker_new:
+ * @swipeable: a #GtkWidget to add the tracker on
+ *
+ * Create a new #HdySwipeTracker object on @widget.
+ *
+ * Returns: the newly created #HdySwipeTracker object
+ *
+ * Since: 1.0
+ */
+HdySwipeTracker *
+hdy_swipe_tracker_new (HdySwipeable *swipeable)
+{
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (swipeable), NULL);
+
+ return g_object_new (HDY_TYPE_SWIPE_TRACKER,
+ "swipeable", swipeable,
+ NULL);
+}
+
+/**
+ * hdy_swipe_tracker_get_swipeable:
+ * @self: a #HdySwipeTracker
+ *
+ * Get @self's swipeable widget.
+ *
+ * Returns: (transfer none): the swipeable widget
+ *
+ * Since: 1.0
+ */
+HdySwipeable *
+hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), NULL);
+
+ return self->swipeable;
+}
+
+/**
+ * hdy_swipe_tracker_get_enabled:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self is enabled. When it's not enabled, no events will be
+ * processed. Generally widgets will want to expose this via a property.
+ *
+ * Returns: %TRUE if @self is enabled
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_enabled (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->enabled;
+}
+
+/**
+ * hdy_swipe_tracker_set_enabled:
+ * @self: a #HdySwipeTracker
+ * @enabled: whether to enable to swipe tracker
+ *
+ * Set whether @self is enabled. When it's not enabled, no events will be
+ * processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_enabled (HdySwipeTracker *self,
+ gboolean enabled)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ enabled = !!enabled;
+
+ if (self->enabled == enabled)
+ return;
+
+ self->enabled = enabled;
+
+ if (!enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ reset (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]);
+}
+
+/**
+ * hdy_swipe_tracker_get_reversed:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self is reversing the swipe direction.
+ *
+ * Returns: %TRUE is the direction is reversed
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_reversed (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->reversed;
+}
+
+/**
+ * hdy_swipe_tracker_set_reversed:
+ * @self: a #HdySwipeTracker
+ * @reversed: whether to reverse the swipe direction
+ *
+ * Set whether to reverse the swipe direction. If @self is horizontal,
+ * can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_reversed (HdySwipeTracker *self,
+ gboolean reversed)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ reversed = !!reversed;
+
+ if (self->reversed == reversed)
+ return;
+
+ self->reversed = reversed;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVERSED]);
+}
+
+/**
+ * hdy_swipe_tracker_get_allow_mouse_drag:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self can be dragged with mouse pointer.
+ *
+ * Returns: %TRUE is mouse dragging is allowed
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->allow_mouse_drag;
+}
+
+/**
+ * hdy_swipe_tracker_set_allow_mouse_drag:
+ * @self: a #HdySwipeTracker
+ * @allow_mouse_drag: whether to allow mouse dragging
+ *
+ * Set whether @self can be dragged with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self,
+ gboolean allow_mouse_drag)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ allow_mouse_drag = !!allow_mouse_drag;
+
+ if (self->allow_mouse_drag == allow_mouse_drag)
+ return;
+
+ self->allow_mouse_drag = allow_mouse_drag;
+
+ if (self->touch_gesture)
+ g_object_set (self->touch_gesture, "touch-only", !allow_mouse_drag, NULL);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]);
+}
+
+/**
+ * hdy_swipe_tracker_shift_position:
+ * @self: a #HdySwipeTracker
+ * @delta: the position delta
+ *
+ * Move the current progress value by @delta. This can be used to adjust the
+ * current position if snap points move during the gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_shift_position (HdySwipeTracker *self,
+ gdouble delta)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING &&
+ self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ self->progress += delta;
+ self->initial_progress += delta;
+}
+
+void
+hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean direct)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_BEGIN_SWIPE], 0, direction, direct);
+}
+
+void
+hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self,
+ gdouble progress)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_UPDATE_SWIPE], 0, progress);
+}
+
+void
+hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self,
+ gint64 duration,
+ gdouble to)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_END_SWIPE], 0, duration, to);
+}