1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 /*
3  * st-scroll-bar.c: Scroll bar actor
4  *
5  * Copyright 2008 OpenedHand
6  * Copyright 2008, 2009 Intel Corporation.
7  * Copyright 2009, 2010 Red Hat, Inc.
8  * Copyright 2010 Maxim Ermilov
9  *
10  * This program is free software; you can redistribute it and/or modify it
11  * under the terms and conditions of the GNU Lesser General Public License,
12  * version 2.1, as published by the Free Software Foundation.
13  *
14  * This program is distributed in the hope it will be useful, but WITHOUT ANY
15  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
17  * more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public License
20  * along with this program. If not, see <http://www.gnu.org/licenses/>.
21  */
22 
23 /**
24  * SECTION:st-scroll-bar
25  * @short_description: a user interface element to control scrollable areas.
26  *
27  * The #StScrollBar allows users to scroll scrollable actors, either by
28  * the step or page amount, or by manually dragging the handle.
29  */
30 
31 #ifdef HAVE_CONFIG_H
32 #include "config.h"
33 #endif
34 
35 #include <math.h>
36 #include <clutter/clutter.h>
37 
38 #include "st-scroll-bar.h"
39 #include "st-bin.h"
40 #include "st-enum-types.h"
41 #include "st-private.h"
42 #include "st-button.h"
43 #include "st-settings.h"
44 
45 #define PAGING_INITIAL_REPEAT_TIMEOUT 500
46 #define PAGING_SUBSEQUENT_REPEAT_TIMEOUT 200
47 
48 typedef struct _StScrollBarPrivate   StScrollBarPrivate;
49 struct _StScrollBarPrivate
50 {
51   StAdjustment *adjustment;
52 
53   gfloat        x_origin;
54   gfloat        y_origin;
55 
56   ClutterInputDevice *grab_device;
57 
58   ClutterActor *trough;
59   ClutterActor *handle;
60 
61   gfloat        move_x;
62   gfloat        move_y;
63 
64   /* Trough-click handling. */
65   enum { NONE, UP, DOWN }  paging_direction;
66   guint             paging_source_id;
67   guint             paging_event_no;
68 
69   guint             vertical : 1;
70 };
71 
72 G_DEFINE_TYPE_WITH_PRIVATE (StScrollBar, st_scroll_bar, ST_TYPE_WIDGET)
73 
74 #define ST_SCROLL_BAR_PRIVATE(sb) st_scroll_bar_get_instance_private (ST_SCROLL_BAR (sb))
75 
76 enum
77 {
78   PROP_0,
79 
80   PROP_ADJUSTMENT,
81   PROP_VERTICAL,
82 
83   N_PROPS
84 };
85 
86 static GParamSpec *props[N_PROPS] = { NULL, };
87 
88 enum
89 {
90   SCROLL_START,
91   SCROLL_STOP,
92 
93   LAST_SIGNAL
94 };
95 
96 static guint signals[LAST_SIGNAL] = { 0, };
97 
98 static gboolean
99 handle_button_press_event_cb (ClutterActor       *actor,
100                               ClutterButtonEvent *event,
101                               StScrollBar        *bar);
102 
103 static void stop_scrolling (StScrollBar *bar);
104 
105 static void
st_scroll_bar_get_property(GObject * gobject,guint prop_id,GValue * value,GParamSpec * pspec)106 st_scroll_bar_get_property (GObject    *gobject,
107                             guint       prop_id,
108                             GValue     *value,
109                             GParamSpec *pspec)
110 {
111   StScrollBarPrivate *priv = ST_SCROLL_BAR_PRIVATE (gobject);
112 
113   switch (prop_id)
114     {
115     case PROP_ADJUSTMENT:
116       g_value_set_object (value, priv->adjustment);
117       break;
118 
119     case PROP_VERTICAL:
120       g_value_set_boolean (value, priv->vertical);
121       break;
122 
123     default:
124       G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
125       break;
126     }
127 }
128 
129 static void
st_scroll_bar_set_property(GObject * gobject,guint prop_id,const GValue * value,GParamSpec * pspec)130 st_scroll_bar_set_property (GObject      *gobject,
131                             guint         prop_id,
132                             const GValue *value,
133                             GParamSpec   *pspec)
134 {
135   StScrollBar *bar = ST_SCROLL_BAR (gobject);
136   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
137 
138   switch (prop_id)
139     {
140     case PROP_ADJUSTMENT:
141       st_scroll_bar_set_adjustment (bar, g_value_get_object (value));
142       break;
143 
144     case PROP_VERTICAL:
145       priv->vertical = g_value_get_boolean (value);
146       if (priv->vertical)
147         clutter_actor_set_name (CLUTTER_ACTOR (priv->handle),
148                                 "vhandle");
149       else
150         clutter_actor_set_name (CLUTTER_ACTOR (priv->handle),
151                                 "hhandle");
152       clutter_actor_queue_relayout ((ClutterActor*) gobject);
153       break;
154 
155     default:
156       G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
157       break;
158     }
159 }
160 
161 static void
st_scroll_bar_dispose(GObject * gobject)162 st_scroll_bar_dispose (GObject *gobject)
163 {
164   StScrollBar *bar = ST_SCROLL_BAR (gobject);
165   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
166 
167   if (priv->adjustment)
168     st_scroll_bar_set_adjustment (bar, NULL);
169 
170   if (priv->handle)
171     {
172       clutter_actor_destroy (priv->handle);
173       priv->handle = NULL;
174     }
175 
176   if (priv->trough)
177     {
178       clutter_actor_destroy (priv->trough);
179       priv->trough = NULL;
180     }
181 
182   G_OBJECT_CLASS (st_scroll_bar_parent_class)->dispose (gobject);
183 }
184 
185 static void
st_scroll_bar_unmap(ClutterActor * actor)186 st_scroll_bar_unmap (ClutterActor *actor)
187 {
188   CLUTTER_ACTOR_CLASS (st_scroll_bar_parent_class)->unmap (actor);
189 
190   stop_scrolling (ST_SCROLL_BAR (actor));
191 }
192 
193 static void
scroll_bar_allocate_children(StScrollBar * bar,const ClutterActorBox * box)194 scroll_bar_allocate_children (StScrollBar           *bar,
195                               const ClutterActorBox *box)
196 {
197   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
198   StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (bar));
199   ClutterActorBox content_box, trough_box;
200 
201   st_theme_node_get_content_box (theme_node, box, &content_box);
202 
203   trough_box.x1 = content_box.x1;
204   trough_box.y1 = content_box.y1;
205   trough_box.x2 = content_box.x2;
206   trough_box.y2 = content_box.y2;
207   clutter_actor_allocate (priv->trough, &trough_box);
208 
209   if (priv->adjustment)
210     {
211       float handle_size, position, avail_size;
212       gdouble value, lower, upper, page_size, increment, min_size, max_size;
213       ClutterActorBox handle_box = { 0, };
214 
215       st_adjustment_get_values (priv->adjustment,
216                                 &value,
217                                 &lower,
218                                 &upper,
219                                 NULL,
220                                 NULL,
221                                 &page_size);
222 
223       if ((upper == lower)
224           || (page_size >= (upper - lower)))
225         increment = 1.0;
226       else
227         increment = page_size / (upper - lower);
228 
229       min_size = 32.;
230       st_theme_node_lookup_length (theme_node, "min-size", FALSE, &min_size);
231       max_size = G_MAXINT16;
232       st_theme_node_lookup_length (theme_node, "max-size", FALSE, &max_size);
233 
234       if (upper - lower - page_size <= 0)
235         position = 0;
236       else
237         position = (value - lower) / (upper - lower - page_size);
238 
239       if (priv->vertical)
240         {
241           avail_size = content_box.y2 - content_box.y1;
242           handle_size = increment * avail_size;
243           handle_size = CLAMP (handle_size, min_size, max_size);
244 
245           handle_box.x1 = content_box.x1;
246           handle_box.y1 = content_box.y1 + position * (avail_size - handle_size);
247 
248           handle_box.x2 = content_box.x2;
249           handle_box.y2 = handle_box.y1 + handle_size;
250         }
251       else
252         {
253           ClutterTextDirection direction;
254 
255           avail_size = content_box.x2 - content_box.x1;
256           handle_size = increment * avail_size;
257           handle_size = CLAMP (handle_size, min_size, max_size);
258 
259           direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (bar));
260           if (direction == CLUTTER_TEXT_DIRECTION_RTL)
261             {
262               handle_box.x2 = content_box.x2 - position * (avail_size - handle_size);
263               handle_box.x1 = handle_box.x2 - handle_size;
264             }
265           else
266             {
267               handle_box.x1 = content_box.x1 + position * (avail_size - handle_size);
268               handle_box.x2 = handle_box.x1 + handle_size;
269             }
270 
271           handle_box.y1 = content_box.y1;
272           handle_box.y2 = content_box.y2;
273         }
274 
275       clutter_actor_allocate (priv->handle, &handle_box);
276     }
277 }
278 
279 static void
st_scroll_bar_get_preferred_width(ClutterActor * self,gfloat for_height,gfloat * min_width_p,gfloat * natural_width_p)280 st_scroll_bar_get_preferred_width (ClutterActor *self,
281                                    gfloat        for_height,
282                                    gfloat       *min_width_p,
283                                    gfloat       *natural_width_p)
284 {
285   StScrollBar *bar = ST_SCROLL_BAR (self);
286   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
287   StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
288   gfloat trough_min_width, trough_natural_width;
289   gfloat handle_min_width, handle_natural_width;
290 
291   st_theme_node_adjust_for_height (theme_node, &for_height);
292 
293   _st_actor_get_preferred_width (priv->trough, for_height, TRUE,
294                                  &trough_min_width, &trough_natural_width);
295 
296   _st_actor_get_preferred_width (priv->handle, for_height, TRUE,
297                                  &handle_min_width, &handle_natural_width);
298 
299   if (priv->vertical)
300     {
301       if (min_width_p)
302         *min_width_p = MAX (trough_min_width, handle_min_width);
303 
304       if (natural_width_p)
305         *natural_width_p = MAX (trough_natural_width, handle_natural_width);
306     }
307   else
308     {
309       if (min_width_p)
310         *min_width_p = trough_min_width + handle_min_width;
311 
312       if (natural_width_p)
313         *natural_width_p = trough_natural_width + handle_natural_width;
314     }
315 
316   st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p);
317 }
318 
319 static void
st_scroll_bar_get_preferred_height(ClutterActor * self,gfloat for_width,gfloat * min_height_p,gfloat * natural_height_p)320 st_scroll_bar_get_preferred_height (ClutterActor *self,
321                                     gfloat        for_width,
322                                     gfloat       *min_height_p,
323                                     gfloat       *natural_height_p)
324 {
325   StScrollBar *bar = ST_SCROLL_BAR (self);
326   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
327   StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
328   gfloat trough_min_height, trough_natural_height;
329   gfloat handle_min_height, handle_natural_height;
330 
331   st_theme_node_adjust_for_width (theme_node, &for_width);
332 
333   _st_actor_get_preferred_height (priv->trough, for_width, TRUE,
334                                   &trough_min_height, &trough_natural_height);
335 
336   _st_actor_get_preferred_height (priv->handle, for_width, TRUE,
337                                   &handle_min_height, &handle_natural_height);
338 
339   if (priv->vertical)
340     {
341       if (min_height_p)
342         *min_height_p = trough_min_height + handle_min_height;
343 
344       if (natural_height_p)
345         *natural_height_p = trough_natural_height + handle_natural_height;
346     }
347   else
348     {
349       if (min_height_p)
350         *min_height_p = MAX (trough_min_height, handle_min_height);
351 
352       if (natural_height_p)
353         *natural_height_p = MAX (trough_natural_height, handle_natural_height);
354     }
355 
356   st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p);
357 }
358 
359 static void
st_scroll_bar_allocate(ClutterActor * actor,const ClutterActorBox * box)360 st_scroll_bar_allocate (ClutterActor          *actor,
361                         const ClutterActorBox *box)
362 {
363   StScrollBar *bar = ST_SCROLL_BAR (actor);
364 
365   clutter_actor_set_allocation (actor, box);
366 
367   scroll_bar_allocate_children (bar, box);
368 }
369 
370 static void
scroll_bar_update_positions(StScrollBar * bar)371 scroll_bar_update_positions (StScrollBar *bar)
372 {
373   ClutterActorBox box;
374 
375   /* Due to a change in the adjustments, we need to reposition our
376    * children; since adjustments changes can come from allocation
377    * changes in the scrolled area, we can't just queue a new relayout -
378    * we may already be in a relayout cycle. On the other hand, if
379    * a relayout is already queued, we can't just go ahead and allocate
380    * our children, since we don't have a valid allocation, and calling
381    * clutter_actor_get_allocation_box() will trigger an immediate
382    * stage relayout. So what we do is go ahead and immediately
383    * allocate our children if we already have a valid allocation, and
384    * otherwise just wait for the queued relayout.
385    */
386   if (!clutter_actor_has_allocation (CLUTTER_ACTOR (bar)))
387     return;
388 
389   clutter_actor_get_allocation_box (CLUTTER_ACTOR (bar), &box);
390   scroll_bar_allocate_children (bar, &box);
391 }
392 
393 static void
bar_reactive_notify_cb(GObject * gobject,GParamSpec * arg1,gpointer user_data)394 bar_reactive_notify_cb (GObject    *gobject,
395                         GParamSpec *arg1,
396                         gpointer    user_data)
397 {
398   StScrollBar *bar = ST_SCROLL_BAR (gobject);
399   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
400 
401   clutter_actor_set_reactive (priv->handle,
402                               clutter_actor_get_reactive (CLUTTER_ACTOR (bar)));
403 }
404 
405 static GObject*
st_scroll_bar_constructor(GType type,guint n_properties,GObjectConstructParam * properties)406 st_scroll_bar_constructor (GType                  type,
407                            guint                  n_properties,
408                            GObjectConstructParam *properties)
409 {
410   GObjectClass *gobject_class;
411   GObject *obj;
412   StScrollBar *bar;
413 
414   gobject_class = G_OBJECT_CLASS (st_scroll_bar_parent_class);
415   obj = gobject_class->constructor (type, n_properties, properties);
416 
417   bar  = ST_SCROLL_BAR (obj);
418 
419   g_signal_connect (bar, "notify::reactive",
420                     G_CALLBACK (bar_reactive_notify_cb), NULL);
421 
422   return obj;
423 }
424 
425 static void
adjust_with_direction(StAdjustment * adj,ClutterScrollDirection direction)426 adjust_with_direction (StAdjustment           *adj,
427                        ClutterScrollDirection  direction)
428 {
429   gdouble delta;
430 
431   switch (direction)
432     {
433     case CLUTTER_SCROLL_UP:
434     case CLUTTER_SCROLL_LEFT:
435       delta = -1.0;
436       break;
437     case CLUTTER_SCROLL_RIGHT:
438     case CLUTTER_SCROLL_DOWN:
439       delta = 1.0;
440       break;
441     case CLUTTER_SCROLL_SMOOTH:
442     default:
443       g_assert_not_reached ();
444       break;
445     }
446 
447   st_adjustment_adjust_for_scroll_event (adj, delta);
448 }
449 
450 static gboolean
st_scroll_bar_scroll_event(ClutterActor * actor,ClutterScrollEvent * event)451 st_scroll_bar_scroll_event (ClutterActor       *actor,
452                             ClutterScrollEvent *event)
453 {
454   StScrollBarPrivate *priv = ST_SCROLL_BAR_PRIVATE (actor);
455   ClutterTextDirection direction;
456   ClutterScrollDirection scroll_dir;
457 
458   if (clutter_event_is_pointer_emulated ((ClutterEvent *) event))
459     return TRUE;
460 
461   direction = clutter_actor_get_text_direction (actor);
462   scroll_dir = event->direction;
463 
464   switch (scroll_dir)
465     {
466     case CLUTTER_SCROLL_SMOOTH:
467       {
468         gdouble delta_x, delta_y;
469         clutter_event_get_scroll_delta ((ClutterEvent *)event, &delta_x, &delta_y);
470 
471         if (direction == CLUTTER_TEXT_DIRECTION_RTL)
472           delta_x *= -1;
473 
474         if (priv->vertical)
475           st_adjustment_adjust_for_scroll_event (priv->adjustment, delta_y);
476         else
477           st_adjustment_adjust_for_scroll_event (priv->adjustment, delta_x);
478       }
479       break;
480     case CLUTTER_SCROLL_LEFT:
481     case CLUTTER_SCROLL_RIGHT:
482       if (direction == CLUTTER_TEXT_DIRECTION_RTL)
483           scroll_dir = scroll_dir == CLUTTER_SCROLL_LEFT ? CLUTTER_SCROLL_RIGHT
484                                                          : CLUTTER_SCROLL_LEFT;
485     /* Fall through */
486     case CLUTTER_SCROLL_UP:
487     case CLUTTER_SCROLL_DOWN:
488       adjust_with_direction (priv->adjustment, scroll_dir);
489       break;
490     default:
491       g_return_val_if_reached (FALSE);
492       break;
493     }
494 
495   return TRUE;
496 }
497 
498 static void
st_scroll_bar_class_init(StScrollBarClass * klass)499 st_scroll_bar_class_init (StScrollBarClass *klass)
500 {
501   GObjectClass *object_class = G_OBJECT_CLASS (klass);
502   ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
503 
504   object_class->get_property = st_scroll_bar_get_property;
505   object_class->set_property = st_scroll_bar_set_property;
506   object_class->dispose      = st_scroll_bar_dispose;
507   object_class->constructor  = st_scroll_bar_constructor;
508 
509   actor_class->get_preferred_width  = st_scroll_bar_get_preferred_width;
510   actor_class->get_preferred_height = st_scroll_bar_get_preferred_height;
511   actor_class->allocate       = st_scroll_bar_allocate;
512   actor_class->scroll_event   = st_scroll_bar_scroll_event;
513   actor_class->unmap          = st_scroll_bar_unmap;
514 
515   /**
516    * StScrollBar:adjustment:
517    *
518    * The #StAdjustment controlling the #StScrollBar.
519    */
520   props[PROP_ADJUSTMENT] =
521     g_param_spec_object ("adjustment", "Adjustment", "The adjustment",
522                          ST_TYPE_ADJUSTMENT,
523                          ST_PARAM_READWRITE);
524 
525   /**
526    * StScrollBar:vertical:
527    *
528    * Whether the #StScrollBar is vertical. If %FALSE it is horizontal.
529    */
530   props[PROP_VERTICAL] =
531     g_param_spec_boolean ("vertical",
532                           "Vertical Orientation",
533                           "Vertical Orientation",
534                           FALSE,
535                           ST_PARAM_READWRITE);
536 
537   g_object_class_install_properties (object_class, N_PROPS, props);
538 
539 
540   /**
541    * StScrollBar::scroll-start:
542    * @bar: a #StScrollBar
543    *
544    * Emitted when the #StScrollBar begins scrolling.
545    */
546   signals[SCROLL_START] =
547     g_signal_new ("scroll-start",
548                   G_TYPE_FROM_CLASS (klass),
549                   G_SIGNAL_RUN_LAST,
550                   G_STRUCT_OFFSET (StScrollBarClass, scroll_start),
551                   NULL, NULL, NULL,
552                   G_TYPE_NONE, 0);
553 
554   /**
555    * StScrollBar::scroll-stop:
556    * @bar: a #StScrollBar
557    *
558    * Emitted when the #StScrollBar finishes scrolling.
559    */
560   signals[SCROLL_STOP] =
561     g_signal_new ("scroll-stop",
562                   G_TYPE_FROM_CLASS (klass),
563                   G_SIGNAL_RUN_LAST,
564                   G_STRUCT_OFFSET (StScrollBarClass, scroll_stop),
565                   NULL, NULL, NULL,
566                   G_TYPE_NONE, 0);
567 }
568 
569 static void
move_slider(StScrollBar * bar,gfloat x,gfloat y)570 move_slider (StScrollBar *bar,
571              gfloat       x,
572              gfloat       y)
573 {
574   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
575   ClutterTextDirection direction;
576   gdouble position, lower, upper, page_size;
577   gfloat ux, uy, pos, size;
578 
579   if (!priv->adjustment)
580     return;
581 
582   if (!clutter_actor_transform_stage_point (priv->trough, x, y, &ux, &uy))
583     return;
584 
585   if (priv->vertical)
586     size = clutter_actor_get_height (priv->trough)
587            - clutter_actor_get_height (priv->handle);
588   else
589     size = clutter_actor_get_width (priv->trough)
590            - clutter_actor_get_width (priv->handle);
591 
592   if (size == 0)
593     return;
594 
595   if (priv->vertical)
596     pos = uy - priv->y_origin;
597   else
598     pos = ux - priv->x_origin;
599   pos = CLAMP (pos, 0, size);
600 
601   st_adjustment_get_values (priv->adjustment,
602                             NULL,
603                             &lower,
604                             &upper,
605                             NULL,
606                             NULL,
607                             &page_size);
608 
609   direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (bar));
610   if (!priv->vertical && direction == CLUTTER_TEXT_DIRECTION_RTL)
611     pos = size - pos;
612 
613   position = ((pos / size)
614               * (upper - lower - page_size))
615              + lower;
616 
617   st_adjustment_set_value (priv->adjustment, position);
618 }
619 
620 static void
stop_scrolling(StScrollBar * bar)621 stop_scrolling (StScrollBar *bar)
622 {
623   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
624   if (!priv->grab_device)
625     return;
626 
627   st_widget_remove_style_pseudo_class (ST_WIDGET (priv->handle), "active");
628 
629   clutter_input_device_ungrab (priv->grab_device);
630   priv->grab_device = NULL;
631   g_signal_emit (bar, signals[SCROLL_STOP], 0);
632 }
633 
634 static gboolean
handle_motion_event_cb(ClutterActor * trough,ClutterMotionEvent * event,StScrollBar * bar)635 handle_motion_event_cb (ClutterActor       *trough,
636                         ClutterMotionEvent *event,
637                         StScrollBar        *bar)
638 {
639   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
640   if (!priv->grab_device)
641     return FALSE;
642 
643   move_slider (bar, event->x, event->y);
644   return TRUE;
645 }
646 
647 static gboolean
handle_button_release_event_cb(ClutterActor * trough,ClutterButtonEvent * event,StScrollBar * bar)648 handle_button_release_event_cb (ClutterActor       *trough,
649                                 ClutterButtonEvent *event,
650                                 StScrollBar        *bar)
651 {
652   if (event->button != 1)
653     return FALSE;
654 
655   stop_scrolling (bar);
656   return TRUE;
657 }
658 
659 static gboolean
handle_button_press_event_cb(ClutterActor * actor,ClutterButtonEvent * event,StScrollBar * bar)660 handle_button_press_event_cb (ClutterActor       *actor,
661                               ClutterButtonEvent *event,
662                               StScrollBar        *bar)
663 {
664   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar);
665   ClutterInputDevice *device = clutter_event_get_device ((ClutterEvent*) event);
666 
667   if (event->button != 1)
668     return FALSE;
669 
670   if (!clutter_actor_transform_stage_point (priv->handle,
671                                             event->x,
672                                             event->y,
673                                             &priv->x_origin,
674                                             &priv->y_origin))
675     return FALSE;
676 
677   st_widget_add_style_pseudo_class (ST_WIDGET (priv->handle), "active");
678 
679   /* Account for the scrollbar-trough-handle nesting. */
680   priv->x_origin += clutter_actor_get_x (priv->trough);
681   priv->y_origin += clutter_actor_get_y (priv->trough);
682 
683   g_assert (!priv->grab_device);
684 
685   clutter_input_device_grab (device, priv->handle);
686   priv->grab_device = device;
687   g_signal_emit (bar, signals[SCROLL_START], 0);
688 
689   return TRUE;
690 }
691 
692 static gboolean
trough_paging_cb(StScrollBar * self)693 trough_paging_cb (StScrollBar *self)
694 {
695   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self);
696   ClutterTextDirection direction;
697   g_autoptr (ClutterTransition) transition = NULL;
698   StSettings *settings;
699   gfloat handle_pos, event_pos, tx, ty;
700   gdouble value, new_value;
701   gdouble page_increment;
702   gdouble slow_down_factor;
703   gboolean ret;
704 
705   gulong mode;
706 
707   if (priv->paging_event_no == 0)
708     {
709       /* Scroll on after initial timeout. */
710       mode = CLUTTER_EASE_OUT_CUBIC;
711       ret = FALSE;
712       priv->paging_event_no = 1;
713       priv->paging_source_id = g_timeout_add (
714         PAGING_INITIAL_REPEAT_TIMEOUT,
715         (GSourceFunc) trough_paging_cb,
716         self);
717       g_source_set_name_by_id (priv->paging_source_id, "[gnome-shell] trough_paging_cb");
718     }
719   else if (priv->paging_event_no == 1)
720     {
721       /* Scroll on after subsequent timeout. */
722       ret = FALSE;
723       mode = CLUTTER_EASE_IN_CUBIC;
724       priv->paging_event_no = 2;
725       priv->paging_source_id = g_timeout_add (
726         PAGING_SUBSEQUENT_REPEAT_TIMEOUT,
727         (GSourceFunc) trough_paging_cb,
728         self);
729       g_source_set_name_by_id (priv->paging_source_id, "[gnome-shell] trough_paging_cb");
730     }
731   else
732     {
733       /* Keep scrolling. */
734       ret = TRUE;
735       mode = CLUTTER_LINEAR;
736       priv->paging_event_no++;
737     }
738 
739   /* Do the scrolling */
740   st_adjustment_get_values (priv->adjustment,
741                             &value, NULL, NULL,
742                             NULL, &page_increment, NULL);
743 
744   if (priv->vertical)
745     handle_pos = clutter_actor_get_y (priv->handle);
746   else
747     handle_pos = clutter_actor_get_x (priv->handle);
748 
749   clutter_actor_transform_stage_point (CLUTTER_ACTOR (priv->trough),
750                                        priv->move_x,
751                                        priv->move_y,
752                                        &tx, &ty);
753 
754   direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (self));
755   if (!priv->vertical && direction == CLUTTER_TEXT_DIRECTION_RTL)
756     page_increment *= -1;
757 
758   if (priv->vertical)
759     event_pos = ty;
760   else
761     event_pos = tx;
762 
763   if (event_pos > handle_pos)
764     {
765       if (priv->paging_direction == NONE)
766         {
767           /* Remember direction. */
768           priv->paging_direction = DOWN;
769         }
770       if (priv->paging_direction == UP)
771         {
772           /* Scrolled far enough. */
773           return FALSE;
774         }
775       new_value = value + page_increment;
776     }
777   else
778     {
779       if (priv->paging_direction == NONE)
780         {
781           /* Remember direction. */
782           priv->paging_direction = UP;
783         }
784       if (priv->paging_direction == DOWN)
785         {
786           /* Scrolled far enough. */
787           return FALSE;
788         }
789       new_value = value - page_increment;
790     }
791 
792   /* Stop existing transition, if one exists */
793   st_adjustment_remove_transition (priv->adjustment, "value");
794 
795   settings = st_settings_get ();
796   g_object_get (settings, "slow-down-factor", &slow_down_factor, NULL);
797 
798   /* FIXME: Creating a new transition for each scroll is probably not the best
799   * idea, but it's a lot less involved than extending the current animation */
800   transition = g_object_new (CLUTTER_TYPE_PROPERTY_TRANSITION,
801                              "property-name", "value",
802                              "interval", clutter_interval_new (G_TYPE_DOUBLE, value, new_value),
803                              "duration", (guint)(PAGING_SUBSEQUENT_REPEAT_TIMEOUT * slow_down_factor),
804                              "progress-mode", mode,
805                              "remove-on-complete", TRUE,
806                              NULL);
807   st_adjustment_add_transition (priv->adjustment, "value", transition);
808 
809   return ret;
810 }
811 
812 static gboolean
trough_button_press_event_cb(ClutterActor * actor,ClutterButtonEvent * event,StScrollBar * self)813 trough_button_press_event_cb (ClutterActor       *actor,
814                               ClutterButtonEvent *event,
815                               StScrollBar        *self)
816 {
817   StScrollBarPrivate *priv;
818 
819   g_return_val_if_fail (self, FALSE);
820 
821   if (event->button != 1)
822     return FALSE;
823 
824   priv = st_scroll_bar_get_instance_private (self);
825   if (priv->adjustment == NULL)
826     return FALSE;
827 
828   priv->move_x = event->x;
829   priv->move_y = event->y;
830   priv->paging_direction = NONE;
831   priv->paging_event_no = 0;
832   trough_paging_cb (self);
833 
834   return TRUE;
835 }
836 
837 static gboolean
trough_button_release_event_cb(ClutterActor * actor,ClutterButtonEvent * event,StScrollBar * self)838 trough_button_release_event_cb (ClutterActor       *actor,
839                                 ClutterButtonEvent *event,
840                                 StScrollBar        *self)
841 {
842   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self);
843 
844   if (event->button != 1)
845     return FALSE;
846 
847   g_clear_handle_id (&priv->paging_source_id, g_source_remove);
848 
849   return TRUE;
850 }
851 
852 static gboolean
trough_leave_event_cb(ClutterActor * actor,ClutterEvent * event,StScrollBar * self)853 trough_leave_event_cb (ClutterActor *actor,
854                        ClutterEvent *event,
855                        StScrollBar  *self)
856 {
857   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self);
858 
859   if (priv->paging_source_id)
860     {
861       g_clear_handle_id (&priv->paging_source_id, g_source_remove);
862       return TRUE;
863     }
864 
865   return FALSE;
866 }
867 
868 static void
st_scroll_bar_notify_reactive(StScrollBar * self)869 st_scroll_bar_notify_reactive (StScrollBar *self)
870 {
871   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self);
872 
873   gboolean reactive = clutter_actor_get_reactive (CLUTTER_ACTOR (self));
874 
875   clutter_actor_set_reactive (CLUTTER_ACTOR (priv->trough), reactive);
876   clutter_actor_set_reactive (CLUTTER_ACTOR (priv->handle), reactive);
877 }
878 
879 static void
st_scroll_bar_init(StScrollBar * self)880 st_scroll_bar_init (StScrollBar *self)
881 {
882   StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self);
883 
884   priv->trough = (ClutterActor *) st_bin_new ();
885   clutter_actor_set_reactive ((ClutterActor *) priv->trough, TRUE);
886   clutter_actor_set_name (CLUTTER_ACTOR (priv->trough), "trough");
887   clutter_actor_add_child (CLUTTER_ACTOR (self),
888                            CLUTTER_ACTOR (priv->trough));
889   g_signal_connect (priv->trough, "button-press-event",
890                     G_CALLBACK (trough_button_press_event_cb), self);
891   g_signal_connect (priv->trough, "button-release-event",
892                     G_CALLBACK (trough_button_release_event_cb), self);
893   g_signal_connect (priv->trough, "leave-event",
894                     G_CALLBACK (trough_leave_event_cb), self);
895 
896   priv->handle = (ClutterActor *) st_button_new ();
897   clutter_actor_set_name (CLUTTER_ACTOR (priv->handle), "hhandle");
898   clutter_actor_add_child (CLUTTER_ACTOR (self),
899                            CLUTTER_ACTOR (priv->handle));
900   g_signal_connect (priv->handle, "button-press-event",
901                     G_CALLBACK (handle_button_press_event_cb), self);
902   g_signal_connect (priv->handle, "button-release-event",
903                     G_CALLBACK (handle_button_release_event_cb), self);
904   g_signal_connect (priv->handle, "motion-event",
905                     G_CALLBACK (handle_motion_event_cb), self);
906 
907   clutter_actor_set_reactive (CLUTTER_ACTOR (self), TRUE);
908 
909   g_signal_connect (self, "notify::reactive",
910                     G_CALLBACK (st_scroll_bar_notify_reactive), NULL);
911 }
912 
913 StWidget *
st_scroll_bar_new(StAdjustment * adjustment)914 st_scroll_bar_new (StAdjustment *adjustment)
915 {
916   return g_object_new (ST_TYPE_SCROLL_BAR,
917                        "adjustment", adjustment,
918                        NULL);
919 }
920 
921 static void
on_notify_value(GObject * object,GParamSpec * pspec,StScrollBar * bar)922 on_notify_value (GObject     *object,
923                  GParamSpec  *pspec,
924                  StScrollBar *bar)
925 {
926   scroll_bar_update_positions (bar);
927 }
928 
929 static void
on_changed(StAdjustment * adjustment,StScrollBar * bar)930 on_changed (StAdjustment *adjustment,
931             StScrollBar  *bar)
932 {
933   scroll_bar_update_positions (bar);
934 }
935 
936 void
st_scroll_bar_set_adjustment(StScrollBar * bar,StAdjustment * adjustment)937 st_scroll_bar_set_adjustment (StScrollBar  *bar,
938                               StAdjustment *adjustment)
939 {
940   StScrollBarPrivate *priv;
941 
942   g_return_if_fail (ST_IS_SCROLL_BAR (bar));
943 
944   priv = st_scroll_bar_get_instance_private (bar);
945 
946   if (adjustment == priv->adjustment)
947     return;
948 
949   if (priv->adjustment)
950     {
951       g_signal_handlers_disconnect_by_func (priv->adjustment,
952                                             on_notify_value,
953                                             bar);
954       g_signal_handlers_disconnect_by_func (priv->adjustment,
955                                             on_changed,
956                                             bar);
957       g_object_unref (priv->adjustment);
958       priv->adjustment = NULL;
959     }
960 
961   if (adjustment)
962     {
963       priv->adjustment = g_object_ref (adjustment);
964 
965       g_signal_connect (priv->adjustment, "notify::value",
966                         G_CALLBACK (on_notify_value),
967                         bar);
968       g_signal_connect (priv->adjustment, "changed",
969                         G_CALLBACK (on_changed),
970                         bar);
971 
972       clutter_actor_queue_relayout (CLUTTER_ACTOR (bar));
973     }
974 
975   g_object_notify_by_pspec (G_OBJECT (bar), props[PROP_ADJUSTMENT]);
976 }
977 
978 /**
979  * st_scroll_bar_get_adjustment:
980  * @bar: a #StScrollbar
981  *
982  * Gets the #StAdjustment that controls the current position of @bar.
983  *
984  * Returns: (transfer none): an #StAdjustment
985  */
986 StAdjustment *
st_scroll_bar_get_adjustment(StScrollBar * bar)987 st_scroll_bar_get_adjustment (StScrollBar *bar)
988 {
989   g_return_val_if_fail (ST_IS_SCROLL_BAR (bar), NULL);
990 
991   return ((StScrollBarPrivate *)ST_SCROLL_BAR_PRIVATE (bar))->adjustment;
992 }
993 
994