1/* GTK - The GIMP Toolkit
2 * Copyright (C) 2017, Red Hat, Inc.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Author(s): Carlos Garnacho <carlosg@gnome.org>
18 */
19
20/**
21 * GtkEventControllerScroll:
22 *
23 * `GtkEventControllerScroll` is an event controller that handles scroll
24 * events.
25 *
26 * It is capable of handling both discrete and continuous scroll
27 * events from mice or touchpads, abstracting them both with the
28 * [signal@Gtk.EventControllerScroll::scroll] signal. Deltas in
29 * the discrete case are multiples of 1.
30 *
31 * In the case of continuous scroll events, `GtkEventControllerScroll`
32 * encloses all [signal@Gtk.EventControllerScroll::scroll] emissions
33 * between two [signal@Gtk.EventControllerScroll::scroll-begin] and
34 * [signal@Gtk.EventControllerScroll::scroll-end] signals.
35 *
36 * The behavior of the event controller can be modified by the flags
37 * given at creation time, or modified at a later point through
38 * [method@Gtk.EventControllerScroll.set_flags] (e.g. because the scrolling
39 * conditions of the widget changed).
40 *
41 * The controller can be set up to emit motion for either/both vertical
42 * and horizontal scroll events through %GTK_EVENT_CONTROLLER_SCROLL_VERTICAL,
43 * %GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL and %GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES.
44 * If any axis is disabled, the respective [signal@Gtk.EventControllerScroll::scroll]
45 * delta will be 0. Vertical scroll events will be translated to horizontal
46 * motion for the devices incapable of horizontal scrolling.
47 *
48 * The event controller can also be forced to emit discrete events on all
49 * devices through %GTK_EVENT_CONTROLLER_SCROLL_DISCRETE. This can be used
50 * to implement discrete actions triggered through scroll events (e.g.
51 * switching across combobox options).
52 *
53 * The %GTK_EVENT_CONTROLLER_SCROLL_KINETIC flag toggles the emission of the
54 * [signal@Gtk.EventControllerScroll::decelerate] signal, emitted at the end
55 * of scrolling with two X/Y velocity arguments that are consistent with the
56 * motion that was received.
57 */
58#include "config.h"
59
60#include "gtkintl.h"
61#include "gtkwidget.h"
62#include "gtkeventcontrollerprivate.h"
63#include "gtkeventcontrollerscroll.h"
64#include "gtktypebuiltins.h"
65#include "gtkmarshalers.h"
66#include "gtkprivate.h"
67
68#define SCROLL_CAPTURE_THRESHOLD_MS 150
69#define HOLD_TIMEOUT_MS 50
70
71typedef struct
72{
73 double dx;
74 double dy;
75 guint32 evtime;
76} ScrollHistoryElem;
77
78struct _GtkEventControllerScroll
79{
80 GtkEventController parent_instance;
81 GtkEventControllerScrollFlags flags;
82 GArray *scroll_history;
83
84 /* For discrete event coalescing */
85 double cur_dx;
86 double cur_dy;
87
88 guint hold_timeout_id;
89 guint active : 1;
90};
91
92struct _GtkEventControllerScrollClass
93{
94 GtkEventControllerClass parent_class;
95};
96
97enum {
98 SCROLL_BEGIN,
99 SCROLL,
100 SCROLL_END,
101 DECELERATE,
102 N_SIGNALS
103};
104
105enum {
106 PROP_0,
107 PROP_FLAGS,
108 N_PROPS
109};
110
111static GParamSpec *pspecs[N_PROPS] = { NULL };
112static guint signals[N_SIGNALS] = { 0 };
113
114G_DEFINE_TYPE (GtkEventControllerScroll, gtk_event_controller_scroll,
115 GTK_TYPE_EVENT_CONTROLLER)
116
117static void
118scroll_history_push (GtkEventControllerScroll *scroll,
119 double delta_x,
120 double delta_y,
121 guint32 evtime)
122{
123 ScrollHistoryElem new_item;
124 guint i;
125
126 for (i = 0; i < scroll->scroll_history->len; i++)
127 {
128 ScrollHistoryElem *elem;
129
130 elem = &g_array_index (scroll->scroll_history, ScrollHistoryElem, i);
131
132 if (elem->evtime >= evtime - SCROLL_CAPTURE_THRESHOLD_MS)
133 break;
134 }
135
136 if (i > 0)
137 g_array_remove_range (array: scroll->scroll_history, index_: 0, length: i);
138
139 new_item.dx = delta_x;
140 new_item.dy = delta_y;
141 new_item.evtime = evtime;
142 g_array_append_val (scroll->scroll_history, new_item);
143}
144
145static void
146scroll_history_reset (GtkEventControllerScroll *scroll)
147{
148 if (scroll->scroll_history->len == 0)
149 return;
150
151 g_array_remove_range (array: scroll->scroll_history, index_: 0,
152 length: scroll->scroll_history->len);
153}
154
155static void
156scroll_history_finish (GtkEventControllerScroll *scroll,
157 double *velocity_x,
158 double *velocity_y)
159{
160 double accum_dx = 0, accum_dy = 0;
161 guint32 first = 0, last = 0;
162 guint i;
163
164 *velocity_x = 0;
165 *velocity_y = 0;
166
167 if (scroll->scroll_history->len == 0)
168 return;
169
170 for (i = 0; i < scroll->scroll_history->len; i++)
171 {
172 ScrollHistoryElem *elem;
173
174 elem = &g_array_index (scroll->scroll_history, ScrollHistoryElem, i);
175 accum_dx += elem->dx;
176 accum_dy += elem->dy;
177 last = elem->evtime;
178
179 if (i == 0)
180 first = elem->evtime;
181 }
182
183 if (last != first)
184 {
185 *velocity_x = (accum_dx * 1000) / (last - first);
186 *velocity_y = (accum_dy * 1000) / (last - first);
187 }
188
189 scroll_history_reset (scroll);
190}
191
192static void
193gtk_event_controller_scroll_finalize (GObject *object)
194{
195 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (object);
196
197 g_array_unref (array: scroll->scroll_history);
198 g_clear_handle_id (&scroll->hold_timeout_id, g_source_remove);
199
200 G_OBJECT_CLASS (gtk_event_controller_scroll_parent_class)->finalize (object);
201}
202
203static void
204gtk_event_controller_scroll_set_property (GObject *object,
205 guint prop_id,
206 const GValue *value,
207 GParamSpec *pspec)
208{
209 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (object);
210
211 switch (prop_id)
212 {
213 case PROP_FLAGS:
214 gtk_event_controller_scroll_set_flags (scroll, flags: g_value_get_flags (value));
215 break;
216 default:
217 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
218 break;
219 }
220}
221
222static void
223gtk_event_controller_scroll_get_property (GObject *object,
224 guint prop_id,
225 GValue *value,
226 GParamSpec *pspec)
227{
228 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (object);
229
230 switch (prop_id)
231 {
232 case PROP_FLAGS:
233 g_value_set_flags (value, v_flags: scroll->flags);
234 break;
235 default:
236 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
237 break;
238 }
239}
240
241static void
242gtk_event_controller_scroll_begin (GtkEventController *controller)
243{
244 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller);
245
246 if (scroll->active)
247 return;
248
249 g_signal_emit (instance: controller, signal_id: signals[SCROLL_BEGIN], detail: 0);
250 scroll_history_reset (scroll);
251 scroll->active = TRUE;
252}
253
254static void
255gtk_event_controller_scroll_end (GtkEventController *controller)
256{
257 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller);
258
259 if (!scroll->active)
260 return;
261
262 g_signal_emit (instance: controller, signal_id: signals[SCROLL_END], detail: 0);
263 scroll->active = FALSE;
264
265 if (scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_KINETIC)
266 {
267 double vel_x, vel_y;
268
269 scroll_history_finish (scroll, velocity_x: &vel_x, velocity_y: &vel_y);
270 g_signal_emit (instance: controller, signal_id: signals[DECELERATE], detail: 0, vel_x, vel_y);
271 }
272}
273
274static gboolean
275gtk_event_controller_scroll_hold_timeout (gpointer user_data)
276{
277 GtkEventController *controller;
278 GtkEventControllerScroll *scroll;
279
280 controller = user_data;
281 scroll = GTK_EVENT_CONTROLLER_SCROLL (controller);
282
283 gtk_event_controller_scroll_end (controller);
284 scroll->hold_timeout_id = 0;
285
286 return G_SOURCE_REMOVE;
287}
288
289static gboolean
290gtk_event_controller_scroll_handle_hold_event (GtkEventController *controller,
291 GdkEvent *event)
292{
293 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller);
294 GdkTouchpadGesturePhase phase;
295 guint n_fingers = 0;
296
297 if (gdk_event_get_event_type (event) != GDK_TOUCHPAD_HOLD)
298 return GDK_EVENT_PROPAGATE;
299
300 n_fingers = gdk_touchpad_event_get_n_fingers (event);
301 if (n_fingers != 1 && n_fingers != 2)
302 return GDK_EVENT_PROPAGATE;
303
304 if (scroll->hold_timeout_id != 0)
305 return GDK_EVENT_PROPAGATE;
306
307 phase = gdk_touchpad_event_get_gesture_phase (event);
308
309 switch (phase)
310 {
311 case GDK_TOUCHPAD_GESTURE_PHASE_BEGIN:
312 gtk_event_controller_scroll_begin (controller);
313 break;
314
315 case GDK_TOUCHPAD_GESTURE_PHASE_END:
316 gtk_event_controller_scroll_end (controller);
317 break;
318
319 case GDK_TOUCHPAD_GESTURE_PHASE_CANCEL:
320 if (scroll->hold_timeout_id == 0)
321 {
322 scroll->hold_timeout_id =
323 g_timeout_add (HOLD_TIMEOUT_MS,
324 function: gtk_event_controller_scroll_hold_timeout,
325 data: controller);
326 }
327 break;
328
329 case GDK_TOUCHPAD_GESTURE_PHASE_UPDATE:
330 default:
331 break;
332 }
333
334 return GDK_EVENT_PROPAGATE;
335}
336
337static gboolean
338gtk_event_controller_scroll_handle_event (GtkEventController *controller,
339 GdkEvent *event,
340 double x,
341 double y)
342{
343 GtkEventControllerScroll *scroll = GTK_EVENT_CONTROLLER_SCROLL (controller);
344 GdkScrollDirection direction = GDK_SCROLL_SMOOTH;
345 double dx = 0, dy = 0;
346 gboolean handled = GDK_EVENT_PROPAGATE;
347 GdkEventType event_type;
348
349 event_type = gdk_event_get_event_type (event);
350
351 if (event_type == GDK_TOUCHPAD_HOLD)
352 return gtk_event_controller_scroll_handle_hold_event (controller, event);
353
354 if (event_type != GDK_SCROLL)
355 return FALSE;
356
357 if ((scroll->flags & (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL |
358 GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL)) == 0)
359 return FALSE;
360
361 g_clear_handle_id (&scroll->hold_timeout_id, g_source_remove);
362
363 /* FIXME: Handle device changes */
364 direction = gdk_scroll_event_get_direction (event);
365 if (direction == GDK_SCROLL_SMOOTH)
366 {
367 gdk_scroll_event_get_deltas (event, delta_x: &dx, delta_y: &dy);
368 gtk_event_controller_scroll_begin (controller);
369
370 if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_VERTICAL) == 0)
371 dy = 0;
372 if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL) == 0)
373 dx = 0;
374
375 if (scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_DISCRETE)
376 {
377 int steps;
378
379 scroll->cur_dx += dx;
380 scroll->cur_dy += dy;
381 dx = dy = 0;
382
383 if (ABS (scroll->cur_dx) >= 1)
384 {
385 steps = trunc (x: scroll->cur_dx);
386 scroll->cur_dx -= steps;
387 dx = steps;
388 }
389
390 if (ABS (scroll->cur_dy) >= 1)
391 {
392 steps = trunc (x: scroll->cur_dy);
393 scroll->cur_dy -= steps;
394 dy = steps;
395 }
396 }
397 }
398 else
399 {
400 switch (direction)
401 {
402 case GDK_SCROLL_UP:
403 dy -= 1;
404 break;
405 case GDK_SCROLL_DOWN:
406 dy += 1;
407 break;
408 case GDK_SCROLL_LEFT:
409 dx -= 1;
410 break;
411 case GDK_SCROLL_RIGHT:
412 dx += 1;
413 break;
414 case GDK_SCROLL_SMOOTH:
415 default:
416 g_assert_not_reached ();
417 break;
418 }
419
420 if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_VERTICAL) == 0)
421 dy = 0;
422 if ((scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_HORIZONTAL) == 0)
423 dx = 0;
424 }
425
426 if (dx != 0 || dy != 0)
427 g_signal_emit (instance: controller, signal_id: signals[SCROLL], detail: 0, dx, dy, &handled);
428 else if (direction == GDK_SCROLL_SMOOTH &&
429 (scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_DISCRETE) != 0)
430 handled = scroll->active;
431
432 if (direction == GDK_SCROLL_SMOOTH &&
433 scroll->flags & GTK_EVENT_CONTROLLER_SCROLL_KINETIC)
434 scroll_history_push (scroll, delta_x: dx, delta_y: dy, evtime: gdk_event_get_time (event));
435
436 if (scroll->active && gdk_scroll_event_is_stop (event))
437 {
438 gtk_event_controller_scroll_end (controller);
439 handled = FALSE;
440 }
441
442 return handled;
443}
444
445static void
446gtk_event_controller_scroll_class_init (GtkEventControllerScrollClass *klass)
447{
448 GtkEventControllerClass *controller_class = GTK_EVENT_CONTROLLER_CLASS (klass);
449 GObjectClass *object_class = G_OBJECT_CLASS (klass);
450
451 object_class->finalize = gtk_event_controller_scroll_finalize;
452 object_class->set_property = gtk_event_controller_scroll_set_property;
453 object_class->get_property = gtk_event_controller_scroll_get_property;
454
455 controller_class->handle_event = gtk_event_controller_scroll_handle_event;
456
457 /**
458 * GtkEventControllerScroll:flags: (attributes org.gtk.Property.get=gtk_event_controller_scroll_get_flags org.gtk.Property.set=gtk_event_controller_scroll_set_flags)
459 *
460 * The flags affecting event controller behavior.
461 */
462 pspecs[PROP_FLAGS] =
463 g_param_spec_flags (name: "flags",
464 P_("Flags"),
465 P_("Flags"),
466 flags_type: GTK_TYPE_EVENT_CONTROLLER_SCROLL_FLAGS,
467 default_value: GTK_EVENT_CONTROLLER_SCROLL_NONE,
468 GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
469
470 /**
471 * GtkEventControllerScroll::scroll-begin:
472 * @controller: The object that received the signal
473 *
474 * Signals that a new scrolling operation has begun.
475 *
476 * It will only be emitted on devices capable of it.
477 */
478 signals[SCROLL_BEGIN] =
479 g_signal_new (I_("scroll-begin"),
480 GTK_TYPE_EVENT_CONTROLLER_SCROLL,
481 signal_flags: G_SIGNAL_RUN_FIRST,
482 class_offset: 0, NULL, NULL,
483 NULL,
484 G_TYPE_NONE, n_params: 0);
485
486 /**
487 * GtkEventControllerScroll::scroll:
488 * @controller: The object that received the signal
489 * @dx: X delta
490 * @dy: Y delta
491 *
492 * Signals that the widget should scroll by the
493 * amount specified by @dx and @dy.
494 *
495 * Returns: %TRUE if the scroll event was handled,
496 * %FALSE otherwise.
497 */
498 signals[SCROLL] =
499 g_signal_new (I_("scroll"),
500 GTK_TYPE_EVENT_CONTROLLER_SCROLL,
501 signal_flags: G_SIGNAL_RUN_LAST,
502 class_offset: 0, NULL, NULL,
503 c_marshaller: _gtk_marshal_BOOLEAN__DOUBLE_DOUBLE,
504 G_TYPE_BOOLEAN, n_params: 2, G_TYPE_DOUBLE, G_TYPE_DOUBLE);
505 g_signal_set_va_marshaller (signal_id: signals[SCROLL],
506 G_TYPE_FROM_CLASS (klass),
507 va_marshaller: _gtk_marshal_BOOLEAN__DOUBLE_DOUBLEv);
508
509 /**
510 * GtkEventControllerScroll::scroll-end:
511 * @controller: The object that received the signal
512 *
513 * Signals that a scrolling operation has finished.
514 *
515 * It will only be emitted on devices capable of it.
516 */
517 signals[SCROLL_END] =
518 g_signal_new (I_("scroll-end"),
519 GTK_TYPE_EVENT_CONTROLLER_SCROLL,
520 signal_flags: G_SIGNAL_RUN_FIRST,
521 class_offset: 0, NULL, NULL,
522 NULL,
523 G_TYPE_NONE, n_params: 0);
524
525 /**
526 * GtkEventControllerScroll::decelerate:
527 * @controller: The object that received the signal
528 * @vel_x: X velocity
529 * @vel_y: Y velocity
530 *
531 * Emitted after scroll is finished if the
532 * %GTK_EVENT_CONTROLLER_SCROLL_KINETIC flag is set.
533 *
534 * @vel_x and @vel_y express the initial velocity that was
535 * imprinted by the scroll events. @vel_x and @vel_y are expressed in
536 * pixels/ms.
537 */
538 signals[DECELERATE] =
539 g_signal_new (I_("decelerate"),
540 GTK_TYPE_EVENT_CONTROLLER_SCROLL,
541 signal_flags: G_SIGNAL_RUN_FIRST,
542 class_offset: 0, NULL, NULL,
543 c_marshaller: _gtk_marshal_VOID__DOUBLE_DOUBLE,
544 G_TYPE_NONE, n_params: 2, G_TYPE_DOUBLE, G_TYPE_DOUBLE);
545 g_signal_set_va_marshaller (signal_id: signals[DECELERATE],
546 G_TYPE_FROM_CLASS (klass),
547 va_marshaller: _gtk_marshal_VOID__DOUBLE_DOUBLEv);
548
549 g_object_class_install_properties (oclass: object_class, n_pspecs: N_PROPS, pspecs);
550}
551
552static void
553gtk_event_controller_scroll_init (GtkEventControllerScroll *scroll)
554{
555 scroll->scroll_history = g_array_new (FALSE, FALSE,
556 element_size: sizeof (ScrollHistoryElem));
557 scroll->hold_timeout_id = 0;
558}
559
560/**
561 * gtk_event_controller_scroll_new:
562 * @flags: flags affecting the controller behavior
563 *
564 * Creates a new event controller that will handle scroll events.
565 *
566 * Returns: a new `GtkEventControllerScroll`
567 */
568GtkEventController *
569gtk_event_controller_scroll_new (GtkEventControllerScrollFlags flags)
570{
571 return g_object_new (GTK_TYPE_EVENT_CONTROLLER_SCROLL,
572 first_property_name: "flags", flags,
573 NULL);
574}
575
576/**
577 * gtk_event_controller_scroll_set_flags: (attributes org.gtk.Method.set_property=flags)
578 * @scroll: a `GtkEventControllerScroll`
579 * @flags: flags affecting the controller behavior
580 *
581 * Sets the flags conditioning scroll controller behavior.
582 */
583void
584gtk_event_controller_scroll_set_flags (GtkEventControllerScroll *scroll,
585 GtkEventControllerScrollFlags flags)
586{
587 g_return_if_fail (GTK_IS_EVENT_CONTROLLER_SCROLL (scroll));
588
589 if (scroll->flags == flags)
590 return;
591
592 scroll->flags = flags;
593 g_object_notify_by_pspec (G_OBJECT (scroll), pspec: pspecs[PROP_FLAGS]);
594}
595
596/**
597 * gtk_event_controller_scroll_get_flags: (attributes org.gtk.Method.get_property=flags)
598 * @scroll: a `GtkEventControllerScroll`
599 *
600 * Gets the flags conditioning the scroll controller behavior.
601 *
602 * Returns: the controller flags.
603 */
604GtkEventControllerScrollFlags
605gtk_event_controller_scroll_get_flags (GtkEventControllerScroll *scroll)
606{
607 g_return_val_if_fail (GTK_IS_EVENT_CONTROLLER_SCROLL (scroll),
608 GTK_EVENT_CONTROLLER_SCROLL_NONE);
609
610 return scroll->flags;
611}
612

source code of gtk/gtk/gtkeventcontrollerscroll.c