/* GTK - The GIMP Toolkit * Copyright (C) 2017, Red Hat, Inc. * * 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, see <http://www.gnu.org/licenses/>. * * Author(s): Carlos Garnacho <carlosg@gnome.org> */ /** * GtkEventControllerScroll: * * `GtkEventControllerScroll` is an event controller that handles scroll * events. * * It is capable of handling both discrete and continuous scroll * events from mice or touchpads, abstracting them both with the * [signal@Gtk.EventControllerScroll::scroll] signal. Deltas in * the discrete case are multiples of 1. * * In the case of continuous scroll events, `GtkEventControllerScroll` * encloses all [signal@Gtk.EventControllerScroll::scroll] emissions * between two [signal@Gtk.EventControllerScroll::scroll-begin] and * [signal@Gtk.EventControllerScroll::scroll-end] signals. * * The behavior of the event controller can be modified by the flags * given at creation time, or modified at a later point through * [method@Gtk.EventControllerScroll.set_flags] (e.g. because the scrolling * conditions of the widget changed). * * The controller can be set up to emit motion for either/both vertical * and horizontal scroll events through %GTK_EVENT_CONTROLLER_SCROLL_VERTICAL, * %GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL and %GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES. * If any axis is disabled, the respective [signal@Gtk.EventControllerScroll::scroll] * delta will be 0. Vertical scroll events will be translated to horizontal * motion for the devices incapable of horizontal scrolling. * * The event controller can also be forced to emit discrete events on all * devices through %GTK_EVENT_CONTROLLER_SCROLL_DISCRETE. This can be used * to implement discrete actions triggered through scroll events (e.g. * switching across combobox options). * * The %GTK_EVENT_CONTROLLER_SCROLL_KINETIC flag toggles the emission of the * [signal@Gtk.EventControllerScroll::decelerate] signal, emitted at the end * of scrolling with two X/Y velocity arguments that are consistent with the * motion that was received. */ #include "config.h" #include "gtkintl.h" #include "gtkwidget.h" #include "gtkeventcontrollerprivate.h" #include "gtkeventcontrollerscroll.h" #include "gtktypebuiltins.h" #include "gtkmarshalers.h" #include "gtkprivate.h" #define SCROLL_CAPTURE_THRESHOLD_MS 150 #define HOLD_TIMEOUT_MS 50 typedef struct { double dx; double dy; guint32 evtime; } ScrollHistoryElem; struct _GtkEventControllerScroll { GtkEventController parent_instance; GtkEventControllerScrollFlags flags; GArray *scroll_history; /* For discrete event coalescing */ double cur_dx; double cur_dy; guint hold_timeout_id; guint active : 1; }; struct _GtkEventControllerScrollClass { GtkEventControllerClass parent_class; }; enum { SCROLL_BEGIN, SCROLL, SCROLL_END, DECELERATE, N_SIGNALS }; enum { PROP_0, PROP_FLAGS, N_PROPS }; static GParamSpec *pspecs[N_PROPS] = { NULL }; static guint signals[N_SIGNALS] = { 0 }; G_DEFINE_TYPE (GtkEventControllerScroll, gtk_event_controller_scroll, GTK_TYPE_EVENT_CONTROLLER) static void scroll_history_push (GtkEventControllerScroll *scroll, double delta_x, double delta_y, guint32 evtime) { ScrollHistoryElem new_item; guint i; for (i = 0; i < scroll->scroll_history->len; i++) { ScrollHistoryElem *elem; elem = &g_array_index (scroll->scroll_history, ScrollHistoryElem, i); if (elem->evtime >= evtime - SCROLL_CAPTURE_THRESHOLD_MS) break; } if (i > 0) g_array_remove_range (scroll->scroll_history, 0, i); new_item.dx = delta_x; new_item.dy = delta_y; new_item.evtime = evtime; g_array_append_val (scroll->scroll_history, new_item); } static void scroll_history_reset (GtkEventControllerScroll *scroll) { if (scroll->scroll_history->len == 0) return; g_array_remove_range (scroll->scroll_history, 0, scroll->scroll_history->len); } static void scroll_history_finish (GtkEventControllerScroll *scroll, double *velocity_x, double *velocity_y) { double accum_dx = 0, accum_dy = 0; guint32 first = 0, last = 0; guint i; *velocity_x = 0; *velocity_y = 0; if (scroll->scroll_history->len == 0) return; for (i = 0; i < scroll->scroll_history->len; i++) { ScrollHistoryElem *elem; elem = &g_array_index (scroll->scroll_history, ScrollHistoryElem, i); accum_dx += elem->dx; accum_dy += elem->dy; last = elem->evtime; if (i == 0) first = elem->evtime; } if (last != first) { *velocity_x = (accum_dx * 1000) / (last - first); *velocity_y = (accum_dy * 1000) / (last - first); } scroll_history_reset (scroll); } static void gtk_event_controller_scroll_finalize (GObject *object) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (object); g_array_unref (scroll->scroll_history); g_clear_handle_id (&scroll->hold_timeout_id, g_source_remove); G_OBJECT_CLASS (gtk_event_controller_scroll_parent_class)->finalize (object); } static void gtk_event_controller_scroll_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (object); switch (prop_id) { case PROP_FLAGS: gtk_event_controller_scroll_set_flags (scroll, g_value_get_flags (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gtk_event_controller_scroll_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (object); switch (prop_id) { case PROP_FLAGS: g_value_set_flags (value, scroll->flags); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static gboolean gtk_event_controller_scroll_begin (GtkEventController *controller) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller); if (scroll->active) return FALSE; g_signal_emit (controller, signals[SCROLL_BEGIN], 0); scroll_history_reset (scroll); scroll->active = TRUE; return TRUE; } static gboolean gtk_event_controller_scroll_end (GtkEventController *controller) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller); if (!scroll->active) return FALSE; g_signal_emit (controller, signals[SCROLL_END], 0); scroll->active = FALSE; if (scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_KINETIC) { double vel_x, vel_y; scroll_history_finish (scroll, &vel_x, &vel_y); g_signal_emit (controller, signals[DECELERATE], 0, vel_x, vel_y); } return TRUE; } static gboolean gtk_event_controller_scroll_hold_timeout (gpointer user_data) { GtkEventController *controller; GtkEventControllerScroll *scroll; controller = user_data; scroll = GTK_EVENT_CONTROLLER_SCROLL (controller); gtk_event_controller_scroll_end (controller); scroll->hold_timeout_id = 0; return G_SOURCE_REMOVE; } static gboolean gtk_event_controller_scroll_handle_hold_event (GtkEventController *controller, GdkEvent *event) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller); gboolean handled = GDK_EVENT_PROPAGATE; GdkTouchpadGesturePhase phase; guint n_fingers = 0; if (gdk_event_get_event_type (event) != GDK_TOUCHPAD_HOLD) return handled; n_fingers = gdk_touchpad_event_get_n_fingers (event); if (n_fingers != 1 && n_fingers != 2) return handled; if (scroll->hold_timeout_id != 0) return handled; phase = gdk_touchpad_event_get_gesture_phase (event); switch (phase) { case GDK_TOUCHPAD_GESTURE_PHASE_BEGIN: handled = gtk_event_controller_scroll_begin (controller); break; case GDK_TOUCHPAD_GESTURE_PHASE_END: handled = gtk_event_controller_scroll_end (controller); break; case GDK_TOUCHPAD_GESTURE_PHASE_CANCEL: if (scroll->hold_timeout_id == 0) { scroll->hold_timeout_id = g_timeout_add (HOLD_TIMEOUT_MS, gtk_event_controller_scroll_hold_timeout, controller); } break; case GDK_TOUCHPAD_GESTURE_PHASE_UPDATE: default: break; } return handled; } static gboolean gtk_event_controller_scroll_handle_event (GtkEventController *controller, GdkEvent *event, double x, double y) { GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller); GdkScrollDirection direction = GDK_SCROLL_SMOOTH; double dx = 0, dy = 0; gboolean handled = GDK_EVENT_PROPAGATE; GdkEventType event_type; event_type = gdk_event_get_event_type (event); if (event_type == GDK_TOUCHPAD_HOLD) return gtk_event_controller_scroll_handle_hold_event (controller, event); if (event_type != GDK_SCROLL) return FALSE; if ((scroll->flags & (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL)) == 0) return FALSE; g_clear_handle_id (&scroll->hold_timeout_id, g_source_remove); /* FIXME: Handle device changes */ direction = gdk_scroll_event_get_direction (event); if (direction == GDK_SCROLL_SMOOTH) { gdk_scroll_event_get_deltas (event, &dx, &dy); gtk_event_controller_scroll_begin (controller); if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_VERTICAL) == 0) dy = 0; if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL) == 0) dx = 0; if (scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_DISCRETE) { int steps; scroll->cur_dx += dx; scroll->cur_dy += dy; dx = dy = 0; if (ABS (scroll->cur_dx) >= 1) { steps = trunc (scroll->cur_dx); scroll->cur_dx -= steps; dx = steps; } if (ABS (scroll->cur_dy) >= 1) { steps = trunc (scroll->cur_dy); scroll->cur_dy -= steps; dy = steps; } } } else { switch (direction) { case GDK_SCROLL_UP: dy -= 1; break; case GDK_SCROLL_DOWN: dy += 1; break; case GDK_SCROLL_LEFT: dx -= 1; break; case GDK_SCROLL_RIGHT: dx += 1; break; case GDK_SCROLL_SMOOTH: default: g_assert_not_reached (); break; } if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_VERTICAL) == 0) dy = 0; if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL) == 0) dx = 0; } if (dx != 0 || dy != 0) g_signal_emit (controller, signals[SCROLL], 0, dx, dy, &handled); else if (direction == GDK_SCROLL_SMOOTH && (scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_DISCRETE) != 0) handled = scroll->active; if (direction == GDK_SCROLL_SMOOTH && scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_KINETIC) scroll_history_push (scroll, dx, dy, gdk_event_get_time (event)); if (scroll->active && gdk_scroll_event_is_stop (event)) { gtk_event_controller_scroll_end (controller); handled = FALSE; } return handled; } static void gtk_event_controller_scroll_class_init (GtkEventControllerScrollClass *klass) { GtkEventControllerClass *controller_class = GTK_EVENT_CONTROLLER_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gtk_event_controller_scroll_finalize; object_class->set_property = gtk_event_controller_scroll_set_property; object_class->get_property = gtk_event_controller_scroll_get_property; controller_class->handle_event = gtk_event_controller_scroll_handle_event; /** * GtkEventControllerScroll:flags: (attributes org.gtk.Property.get=gtk_event_controller_scroll_get_flags org.gtk.Property.set=gtk_event_controller_scroll_set_flags) * * The flags affecting event controller behavior. */ pspecs[PROP_FLAGS] = g_param_spec_flags ("flags", P_("Flags"), P_("Flags"), GTK_TYPE_EVENT_CONTROLLER_SCROLL_FLAGS, GTK_EVENT_CONTROLLER_SCROLL_NONE, GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); /** * GtkEventControllerScroll::scroll-begin: * @controller: The object that received the signal * * Signals that a new scrolling operation has begun. * * It will only be emitted on devices capable of it. */ signals[SCROLL_BEGIN] = g_signal_new (I_("scroll-begin"), GTK_TYPE_EVENT_CONTROLLER_SCROLL, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkEventControllerScroll::scroll: * @controller: The object that received the signal * @dx: X delta * @dy: Y delta * * Signals that the widget should scroll by the * amount specified by @dx and @dy. * * Returns: %TRUE if the scroll event was handled, * %FALSE otherwise. */ signals[SCROLL] = g_signal_new (I_("scroll"), GTK_TYPE_EVENT_CONTROLLER_SCROLL, G_SIGNAL_RUN_LAST, 0, NULL, NULL, _gtk_marshal_BOOLEAN__DOUBLE_DOUBLE, G_TYPE_BOOLEAN, 2, G_TYPE_DOUBLE, G_TYPE_DOUBLE); g_signal_set_va_marshaller (signals[SCROLL], G_TYPE_FROM_CLASS (klass), _gtk_marshal_BOOLEAN__DOUBLE_DOUBLEv); /** * GtkEventControllerScroll::scroll-end: * @controller: The object that received the signal * * Signals that a scrolling operation has finished. * * It will only be emitted on devices capable of it. */ signals[SCROLL_END] = g_signal_new (I_("scroll-end"), GTK_TYPE_EVENT_CONTROLLER_SCROLL, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); /** * GtkEventControllerScroll::decelerate: * @controller: The object that received the signal * @vel_x: X velocity * @vel_y: Y velocity * * Emitted after scroll is finished if the * %GTK_EVENT_CONTROLLER_SCROLL_KINETIC flag is set. * * @vel_x and @vel_y express the initial velocity that was * imprinted by the scroll events. @vel_x and @vel_y are expressed in * pixels/ms. */ signals[DECELERATE] = g_signal_new (I_("decelerate"), GTK_TYPE_EVENT_CONTROLLER_SCROLL, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, _gtk_marshal_VOID__DOUBLE_DOUBLE, G_TYPE_NONE, 2, G_TYPE_DOUBLE, G_TYPE_DOUBLE); g_signal_set_va_marshaller (signals[DECELERATE], G_TYPE_FROM_CLASS (klass), _gtk_marshal_VOID__DOUBLE_DOUBLEv); g_object_class_install_properties (object_class, N_PROPS, pspecs); } static void gtk_event_controller_scroll_init (GtkEventControllerScroll *scroll) { scroll->scroll_history = g_array_new (FALSE, FALSE, sizeof (ScrollHistoryElem)); scroll->hold_timeout_id = 0; } /** * gtk_event_controller_scroll_new: * @flags: flags affecting the controller behavior * * Creates a new event controller that will handle scroll events. * * Returns: a new `GtkEventControllerScroll` */ GtkEventController * gtk_event_controller_scroll_new (GtkEventControllerScrollFlags flags) { return g_object_new (GTK_TYPE_EVENT_CONTROLLER_SCROLL, "flags", flags, NULL); } /** * gtk_event_controller_scroll_set_flags: (attributes org.gtk.Method.set_property=flags) * @scroll: a `GtkEventControllerScroll` * @flags: flags affecting the controller behavior * * Sets the flags conditioning scroll controller behavior. */ void gtk_event_controller_scroll_set_flags (GtkEventControllerScroll *scroll, GtkEventControllerScrollFlags flags) { g_return_if_fail (GTK_IS_EVENT_CONTROLLER_SCROLL (scroll)); if (scroll->flags == flags) return; scroll->flags = flags; g_object_notify_by_pspec (G_OBJECT (scroll), pspecs[PROP_FLAGS]); } /** * gtk_event_controller_scroll_get_flags: (attributes org.gtk.Method.get_property=flags) * @scroll: a `GtkEventControllerScroll` * * Gets the flags conditioning the scroll controller behavior. * * Returns: the controller flags. */ GtkEventControllerScrollFlags gtk_event_controller_scroll_get_flags (GtkEventControllerScroll *scroll) { g_return_val_if_fail (GTK_IS_EVENT_CONTROLLER_SCROLL (scroll), GTK_EVENT_CONTROLLER_SCROLL_NONE); return scroll->flags; }