1 /*
2  * Copyright © 2019 Benjamin Otte
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.1 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  * Authors: Benjamin Otte <otte@gnome.org>
18  */
19 
20 #include "config.h"
21 
22 #include "gtklistbaseprivate.h"
23 
24 #include "gtkadjustment.h"
25 #include "gtkbitset.h"
26 #include "gtkdragsourceprivate.h"
27 #include "gtkdropcontrollermotion.h"
28 #include "gtkgesturedrag.h"
29 #include "gtkgizmoprivate.h"
30 #include "gtkintl.h"
31 #include "gtklistitemwidgetprivate.h"
32 #include "gtkmultiselection.h"
33 #include "gtkorientable.h"
34 #include "gtkscrollable.h"
35 #include "gtksingleselection.h"
36 #include "gtksnapshot.h"
37 #include "gtktypebuiltins.h"
38 #include "gtkwidgetprivate.h"
39 
40 typedef struct _RubberbandData RubberbandData;
41 
42 struct _RubberbandData
43 {
44   GtkWidget *widget;                            /* The rubberband widget */
45 
46   GtkListItemTracker *start_tracker;            /* The item we started dragging on */
47   double              start_align_across;       /* alignment in horizontal direction */
48   double              start_align_along;        /* alignment in vertical direction */
49 
50   double pointer_x, pointer_y;                  /* mouse coordinates in widget space */
51 };
52 
53 typedef struct _GtkListBasePrivate GtkListBasePrivate;
54 
55 struct _GtkListBasePrivate
56 {
57   GtkListItemManager *item_manager;
58   GtkSelectionModel *model;
59   GtkOrientation orientation;
60   GtkAdjustment *adjustment[2];
61   GtkScrollablePolicy scroll_policy[2];
62 
63   GtkListItemTracker *anchor;
64   double anchor_align_along;
65   double anchor_align_across;
66   GtkPackType anchor_side_along;
67   GtkPackType anchor_side_across;
68   guint center_widgets;
69   guint above_below_widgets;
70   /* the last item that was selected - basically the location to extend selections from */
71   GtkListItemTracker *selected;
72   /* the item that has input focus */
73   GtkListItemTracker *focus;
74 
75   gboolean enable_rubberband;
76   GtkGesture *drag_gesture;
77   RubberbandData *rubberband;
78 
79   guint autoscroll_id;
80   double autoscroll_delta_x;
81   double autoscroll_delta_y;
82 };
83 
84 enum
85 {
86   PROP_0,
87   PROP_HADJUSTMENT,
88   PROP_HSCROLL_POLICY,
89   PROP_ORIENTATION,
90   PROP_VADJUSTMENT,
91   PROP_VSCROLL_POLICY,
92 
93   N_PROPS
94 };
95 
96 /* HACK: We want the g_class argument in our instance init func and G_DEFINE_TYPE() won't let us */
97 static void gtk_list_base_init_real (GtkListBase *self, GtkListBaseClass *g_class);
98 #define g_type_register_static_simple(a,b,c,d,e,evil,f) g_type_register_static_simple(a,b,c,d,e, (GInstanceInitFunc) gtk_list_base_init_real, f);
G_DEFINE_ABSTRACT_TYPE_WITH_CODE(GtkListBase,gtk_list_base,GTK_TYPE_WIDGET,G_ADD_PRIVATE (GtkListBase)G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE,NULL)G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE,NULL))99 G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GtkListBase, gtk_list_base, GTK_TYPE_WIDGET,
100                                                               G_ADD_PRIVATE (GtkListBase)
101                                                               G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
102                                                               G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
103 #undef g_type_register_static_simple
104 G_GNUC_UNUSED static void gtk_list_base_init (GtkListBase *self) { }
105 
106 static GParamSpec *properties[N_PROPS] = { NULL, };
107 
108 /*
109  * gtk_list_base_get_position_from_allocation:
110  * @self: a `GtkListBase`
111  * @across: position in pixels in the direction cross to the list
112  * @along:  position in pixels in the direction of the list
113  * @pos: (out caller-allocates): set to the looked up position
114  * @area: (out caller-allocates) (optional): set to the area occupied
115  *   by the returned position
116  *
117  * Given a coordinate in list coordinates, determine the position of the
118  * item that occupies that position.
119  *
120  * It is possible for @area to not include the point given by (across, along).
121  * This will happen for example in the last row of a gridview, where the
122  * last item will be returned for the whole width, even if there are empty
123  * cells.
124  *
125  * Returns: %TRUE on success or %FALSE if no position occupies the given offset.
126  **/
127 static guint
gtk_list_base_get_position_from_allocation(GtkListBase * self,int across,int along,guint * pos,cairo_rectangle_int_t * area)128 gtk_list_base_get_position_from_allocation (GtkListBase           *self,
129                                             int                    across,
130                                             int                    along,
131                                             guint                 *pos,
132                                             cairo_rectangle_int_t *area)
133 {
134   return GTK_LIST_BASE_GET_CLASS (self)->get_position_from_allocation (self, across, along, pos, area);
135 }
136 
137 static gboolean
gtk_list_base_adjustment_is_flipped(GtkListBase * self,GtkOrientation orientation)138 gtk_list_base_adjustment_is_flipped (GtkListBase    *self,
139                                      GtkOrientation  orientation)
140 {
141   if (orientation == GTK_ORIENTATION_VERTICAL)
142     return FALSE;
143 
144   return gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
145 }
146 
147 static void
gtk_list_base_get_adjustment_values(GtkListBase * self,GtkOrientation orientation,int * value,int * size,int * page_size)148 gtk_list_base_get_adjustment_values (GtkListBase    *self,
149                                      GtkOrientation  orientation,
150                                      int            *value,
151                                      int            *size,
152                                      int            *page_size)
153 {
154   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
155   int val, upper, ps;
156 
157   val = gtk_adjustment_get_value (priv->adjustment[orientation]);
158   upper = gtk_adjustment_get_upper (priv->adjustment[orientation]);
159   ps = gtk_adjustment_get_page_size (priv->adjustment[orientation]);
160   if (gtk_list_base_adjustment_is_flipped (self, orientation))
161     val = upper - ps - val;
162 
163   if (value)
164     *value = val;
165   if (size)
166     *size = upper;
167   if (page_size)
168     *page_size = ps;
169 }
170 
171 static void
gtk_list_base_adjustment_value_changed_cb(GtkAdjustment * adjustment,GtkListBase * self)172 gtk_list_base_adjustment_value_changed_cb (GtkAdjustment *adjustment,
173                                            GtkListBase   *self)
174 {
175   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
176   cairo_rectangle_int_t area, cell_area;
177   int along, across, total_size;
178   double align_across, align_along;
179   GtkPackType side_across, side_along;
180   guint pos;
181 
182   gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), &area.x, &total_size, &area.width);
183   if (total_size == area.width)
184     align_across = 0.5;
185   else if (adjustment != priv->adjustment[priv->orientation])
186     align_across = CLAMP (priv->anchor_align_across, 0, 1);
187   else
188     align_across = (double) area.x / (total_size - area.width);
189   across = area.x + round (align_across * area.width);
190   across = CLAMP (across, 0, total_size - 1);
191 
192   gtk_list_base_get_adjustment_values (self, priv->orientation, &area.y, &total_size, &area.height);
193   if (total_size == area.height)
194     align_along = 0.5;
195   else if (adjustment != priv->adjustment[OPPOSITE_ORIENTATION(priv->orientation)])
196     align_along = CLAMP (priv->anchor_align_along, 0, 1);
197   else
198     align_along = (double) area.y / (total_size - area.height);
199   along = area.y + round (align_along * area.height);
200   along = CLAMP (along, 0, total_size - 1);
201 
202   if (!gtk_list_base_get_position_from_allocation (self,
203                                                    across, along,
204                                                    &pos,
205                                                    &cell_area))
206     {
207       g_warning ("%s failed to scroll to given position. Ignoring...", G_OBJECT_TYPE_NAME (self));
208       return;
209     }
210 
211   /* find an anchor that is in the visible area */
212   if (cell_area.x < area.x && cell_area.x + cell_area.width <= area.x + area.width)
213     side_across = GTK_PACK_END;
214   else if (cell_area.x >= area.x && cell_area.x + cell_area.width > area.x + area.width)
215     side_across = GTK_PACK_START;
216   else if (cell_area.x + cell_area.width / 2 > across)
217     side_across = GTK_PACK_END;
218   else
219     side_across = GTK_PACK_START;
220 
221   if (cell_area.y < area.y && cell_area.y + cell_area.height <= area.y + area.height)
222     side_along = GTK_PACK_END;
223   else if (cell_area.y >= area.y && cell_area.y + cell_area.height > area.y + area.height)
224     side_along = GTK_PACK_START;
225   else if (cell_area.y + cell_area.height / 2 > along)
226     side_along = GTK_PACK_END;
227   else
228     side_along = GTK_PACK_START;
229 
230   /* Compute the align based on side to keep the values identical */
231   if (side_across == GTK_PACK_START)
232     align_across = (double) (cell_area.x - area.x) / area.width;
233   else
234     align_across = (double) (cell_area.x + cell_area.width - area.x) / area.width;
235   if (side_along == GTK_PACK_START)
236     align_along = (double) (cell_area.y - area.y) / area.height;
237   else
238     align_along = (double) (cell_area.y + cell_area.height - area.y) / area.height;
239 
240   gtk_list_base_set_anchor (self,
241                             pos,
242                             align_across, side_across,
243                             align_along, side_along);
244 
245   gtk_widget_queue_allocate (GTK_WIDGET (self));
246 }
247 
248 static void
gtk_list_base_clear_adjustment(GtkListBase * self,GtkOrientation orientation)249 gtk_list_base_clear_adjustment (GtkListBase    *self,
250                                 GtkOrientation  orientation)
251 {
252   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
253 
254   if (priv->adjustment[orientation] == NULL)
255     return;
256 
257   g_signal_handlers_disconnect_by_func (priv->adjustment[orientation],
258                                         gtk_list_base_adjustment_value_changed_cb,
259                                         self);
260   g_clear_object (&priv->adjustment[orientation]);
261 }
262 
263 /*
264  * gtk_list_base_move_focus_along:
265  * @self: a `GtkListBase`
266  * @pos: position from which to move focus
267  * @steps: steps to move focus - negative numbers move focus backwards
268  *
269  * Moves focus @steps in the direction of the list.
270  * If focus cannot be moved, @pos is returned.
271  * If focus should be moved out of the widget, %GTK_INVALID_LIST_POSITION
272  * is returned.
273  *
274  * Returns: new focus position
275  **/
276 static guint
gtk_list_base_move_focus_along(GtkListBase * self,guint pos,int steps)277 gtk_list_base_move_focus_along (GtkListBase *self,
278                                 guint        pos,
279                                 int          steps)
280 {
281   return GTK_LIST_BASE_GET_CLASS (self)->move_focus_along (self, pos, steps);
282 }
283 
284 /*
285  * gtk_list_base_move_focus_across:
286  * @self: a `GtkListBase`
287  * @pos: position from which to move focus
288  * @steps: steps to move focus - negative numbers move focus backwards
289  *
290  * Moves focus @steps in the direction across the list.
291  * If focus cannot be moved, @pos is returned.
292  * If focus should be moved out of the widget, %GTK_INVALID_LIST_POSITION
293  * is returned.
294  *
295  * Returns: new focus position
296  **/
297 static guint
gtk_list_base_move_focus_across(GtkListBase * self,guint pos,int steps)298 gtk_list_base_move_focus_across (GtkListBase *self,
299                                  guint        pos,
300                                  int          steps)
301 {
302   return GTK_LIST_BASE_GET_CLASS (self)->move_focus_across (self, pos, steps);
303 }
304 
305 static guint
gtk_list_base_move_focus(GtkListBase * self,guint pos,GtkOrientation orientation,int steps)306 gtk_list_base_move_focus (GtkListBase    *self,
307                           guint           pos,
308                           GtkOrientation  orientation,
309                           int             steps)
310 {
311   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
312 
313   if (orientation == GTK_ORIENTATION_HORIZONTAL &&
314       gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
315     steps = -steps;
316 
317   if (orientation == priv->orientation)
318     return gtk_list_base_move_focus_along (self, pos, steps);
319   else
320     return gtk_list_base_move_focus_across (self, pos, steps);
321 }
322 
323 /*
324  * gtk_list_base_get_allocation_along:
325  * @self: a `GtkListBase`
326  * @pos: item to get the size of
327  * @offset: (out caller-allocates) (optional): set to the offset
328  *   of the top/left of the item
329  * @size: (out caller-allocates) (optional): set to the size of
330  *   the item in the direction
331  *
332  * Computes the allocation of the item in the direction along the sizing
333  * axis.
334  *
335  * Returns: %TRUE if the item exists and has an allocation, %FALSE otherwise
336  **/
337 static gboolean
gtk_list_base_get_allocation_along(GtkListBase * self,guint pos,int * offset,int * size)338 gtk_list_base_get_allocation_along (GtkListBase *self,
339                                     guint        pos,
340                                     int         *offset,
341                                     int         *size)
342 {
343   return GTK_LIST_BASE_GET_CLASS (self)->get_allocation_along (self, pos, offset, size);
344 }
345 
346 /*
347  * gtk_list_base_get_allocation_across:
348  * @self: a `GtkListBase`
349  * @pos: item to get the size of
350  * @offset: (out caller-allocates) (optional): set to the offset
351  *   of the top/left of the item
352  * @size: (out caller-allocates) (optional): set to the size of
353  *   the item in the direction
354  *
355  * Computes the allocation of the item in the direction across to the sizing
356  * axis.
357  *
358  * Returns: %TRUE if the item exists and has an allocation, %FALSE otherwise
359  **/
360 static gboolean
gtk_list_base_get_allocation_across(GtkListBase * self,guint pos,int * offset,int * size)361 gtk_list_base_get_allocation_across (GtkListBase *self,
362                                      guint        pos,
363                                      int         *offset,
364                                      int         *size)
365 {
366   return GTK_LIST_BASE_GET_CLASS (self)->get_allocation_across (self, pos, offset, size);
367 }
368 
369 /*
370  * gtk_list_base_select_item:
371  * @self: a `GtkListBase`
372  * @pos: item to select
373  * @modify: %TRUE if the selection should be modified, %FALSE
374  *   if a new selection should be done. This is usually set
375  *   to %TRUE if the user keeps the <Shift> key pressed.
376  * @extend_pos: %TRUE if the selection should be extended.
377  *   Selections are usually extended from the last selected
378  *   position if the user presses the <Ctrl> key.
379  *
380  * Selects the item at @pos according to how GTK list widgets modify
381  * selections, both when clicking rows with the mouse or when using
382  * the keyboard.
383  **/
384 void
gtk_list_base_select_item(GtkListBase * self,guint pos,gboolean modify,gboolean extend)385 gtk_list_base_select_item (GtkListBase *self,
386                            guint        pos,
387                            gboolean     modify,
388                            gboolean     extend)
389 {
390   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
391   GtkSelectionModel *model;
392   gboolean success = FALSE;
393   guint n_items;
394 
395   model = gtk_list_item_manager_get_model (priv->item_manager);
396   if (model == NULL)
397     return;
398 
399   n_items = g_list_model_get_n_items (G_LIST_MODEL (model));
400   if (pos >= n_items)
401     return;
402 
403   if (extend)
404     {
405       guint extend_pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->selected);
406 
407       if (extend_pos < n_items)
408         {
409           guint max = MAX (extend_pos, pos);
410           guint min = MIN (extend_pos, pos);
411 
412           if (modify)
413             {
414               if (gtk_selection_model_is_selected (model, extend_pos))
415                 {
416                   success = gtk_selection_model_select_range (model,
417                                                               min,
418                                                               max - min + 1,
419                                                               FALSE);
420                 }
421               else
422                 {
423                   success = gtk_selection_model_unselect_range (model,
424                                                                 min,
425                                                                 max - min + 1);
426                 }
427             }
428           else
429             {
430               success = gtk_selection_model_select_range (model,
431                                                           min,
432                                                           max - min + 1,
433                                                           TRUE);
434             }
435         }
436       /* If there's no range to select or selecting ranges isn't supported
437        * by the model, fall through to normal setting.
438        */
439     }
440 
441   if (success)
442     return;
443 
444   if (modify)
445     {
446       if (gtk_selection_model_is_selected (model, pos))
447         gtk_selection_model_unselect_item (model, pos);
448       else
449         gtk_selection_model_select_item (model, pos, FALSE);
450     }
451   else
452     {
453       gtk_selection_model_select_item (model, pos, TRUE);
454     }
455 
456   gtk_list_item_tracker_set_position (priv->item_manager,
457                                       priv->selected,
458                                       pos,
459                                       0, 0);
460 }
461 
462 guint
gtk_list_base_get_n_items(GtkListBase * self)463 gtk_list_base_get_n_items (GtkListBase *self)
464 {
465   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
466 
467   if (priv->model == NULL)
468     return 0;
469 
470   return g_list_model_get_n_items (G_LIST_MODEL (priv->model));
471 }
472 
473 guint
gtk_list_base_get_focus_position(GtkListBase * self)474 gtk_list_base_get_focus_position (GtkListBase *self)
475 {
476   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
477 
478   return gtk_list_item_tracker_get_position (priv->item_manager, priv->focus);
479 }
480 
481 static gboolean
gtk_list_base_focus(GtkWidget * widget,GtkDirectionType direction)482 gtk_list_base_focus (GtkWidget        *widget,
483                      GtkDirectionType  direction)
484 {
485   GtkListBase *self = GTK_LIST_BASE (widget);
486   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
487   guint old, pos, n_items;
488   GtkWidget *focus_child;
489   GtkListItemManagerItem *item;
490 
491   focus_child = gtk_widget_get_focus_child (widget);
492   /* focus is moving around fine inside the focus child, don't disturb it */
493   if (focus_child && gtk_widget_child_focus (focus_child, direction))
494     return TRUE;
495 
496   pos = gtk_list_base_get_focus_position (self);
497   n_items = gtk_list_base_get_n_items (self);
498   old = pos;
499 
500   if (pos >= n_items)
501     {
502       if (n_items == 0)
503         return FALSE;
504 
505       pos = 0;
506     }
507   else if (focus_child == NULL)
508     {
509       /* Focus was outside the list, just grab the old focus item
510        * while keeping the selection intact.
511        */
512       old = GTK_INVALID_LIST_POSITION;
513     }
514   else
515     {
516       switch (direction)
517         {
518         case GTK_DIR_TAB_FORWARD:
519           pos++;
520           if (pos >= n_items)
521             return FALSE;
522           break;
523 
524         case GTK_DIR_TAB_BACKWARD:
525           if (pos == 0)
526             return FALSE;
527           pos--;
528           break;
529 
530         case GTK_DIR_UP:
531           pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_VERTICAL, -1);
532           break;
533 
534         case GTK_DIR_DOWN:
535           pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_VERTICAL, 1);
536           break;
537 
538         case GTK_DIR_LEFT:
539           pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_HORIZONTAL, -1);
540           break;
541 
542         case GTK_DIR_RIGHT:
543           pos = gtk_list_base_move_focus (self, pos, GTK_ORIENTATION_HORIZONTAL, 1);
544           break;
545 
546         default:
547           g_assert_not_reached ();
548           return TRUE;
549         }
550     }
551 
552   if (old == pos)
553     return TRUE;
554 
555   item = gtk_list_item_manager_get_nth (priv->item_manager, pos, NULL);
556   if (item == NULL)
557     return FALSE;
558 
559   /* This shouldn't really happen, but if it does, oh well */
560   if (item->widget == NULL)
561     return gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos, TRUE, FALSE, FALSE);
562 
563   return gtk_widget_child_focus (item->widget, direction);
564 }
565 
566 static void
gtk_list_base_dispose(GObject * object)567 gtk_list_base_dispose (GObject *object)
568 {
569   GtkListBase *self = GTK_LIST_BASE (object);
570   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
571 
572   gtk_list_base_clear_adjustment (self, GTK_ORIENTATION_HORIZONTAL);
573   gtk_list_base_clear_adjustment (self, GTK_ORIENTATION_VERTICAL);
574 
575   if (priv->anchor)
576     {
577       gtk_list_item_tracker_free (priv->item_manager, priv->anchor);
578       priv->anchor = NULL;
579     }
580   if (priv->selected)
581     {
582       gtk_list_item_tracker_free (priv->item_manager, priv->selected);
583       priv->selected = NULL;
584     }
585   if (priv->focus)
586     {
587       gtk_list_item_tracker_free (priv->item_manager, priv->focus);
588       priv->focus = NULL;
589     }
590   g_clear_object (&priv->item_manager);
591 
592   g_clear_object (&priv->model);
593 
594   G_OBJECT_CLASS (gtk_list_base_parent_class)->dispose (object);
595 }
596 
597 static void
gtk_list_base_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)598 gtk_list_base_get_property (GObject    *object,
599                             guint       property_id,
600                             GValue     *value,
601                             GParamSpec *pspec)
602 {
603   GtkListBase *self = GTK_LIST_BASE (object);
604   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
605 
606   switch (property_id)
607     {
608     case PROP_HADJUSTMENT:
609       g_value_set_object (value, priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
610       break;
611 
612     case PROP_HSCROLL_POLICY:
613       g_value_set_enum (value, priv->scroll_policy[GTK_ORIENTATION_HORIZONTAL]);
614       break;
615 
616     case PROP_ORIENTATION:
617       g_value_set_enum (value, priv->orientation);
618       break;
619 
620     case PROP_VADJUSTMENT:
621       g_value_set_object (value, priv->adjustment[GTK_ORIENTATION_VERTICAL]);
622       break;
623 
624     case PROP_VSCROLL_POLICY:
625       g_value_set_enum (value, priv->scroll_policy[GTK_ORIENTATION_VERTICAL]);
626       break;
627 
628     default:
629       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
630       break;
631     }
632 }
633 
634 static void
gtk_list_base_set_adjustment(GtkListBase * self,GtkOrientation orientation,GtkAdjustment * adjustment)635 gtk_list_base_set_adjustment (GtkListBase    *self,
636                               GtkOrientation  orientation,
637                               GtkAdjustment  *adjustment)
638 {
639   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
640 
641   if (priv->adjustment[orientation] == adjustment)
642     return;
643 
644   if (adjustment == NULL)
645     adjustment = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
646   g_object_ref_sink (adjustment);
647 
648   gtk_list_base_clear_adjustment (self, orientation);
649 
650   priv->adjustment[orientation] = adjustment;
651 
652   g_signal_connect (adjustment, "value-changed",
653 		    G_CALLBACK (gtk_list_base_adjustment_value_changed_cb),
654 		    self);
655 
656   gtk_widget_queue_allocate (GTK_WIDGET (self));
657 }
658 
659 static void
gtk_list_base_set_scroll_policy(GtkListBase * self,GtkOrientation orientation,GtkScrollablePolicy scroll_policy)660 gtk_list_base_set_scroll_policy (GtkListBase         *self,
661                                  GtkOrientation       orientation,
662                                  GtkScrollablePolicy  scroll_policy)
663 {
664   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
665 
666   if (priv->scroll_policy[orientation] == scroll_policy)
667     return;
668 
669   priv->scroll_policy[orientation] = scroll_policy;
670 
671   gtk_widget_queue_resize (GTK_WIDGET (self));
672 
673   g_object_notify_by_pspec (G_OBJECT (self),
674                             orientation == GTK_ORIENTATION_HORIZONTAL
675                             ? properties[PROP_HSCROLL_POLICY]
676                             : properties[PROP_VSCROLL_POLICY]);
677 }
678 
679 static void
gtk_list_base_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)680 gtk_list_base_set_property (GObject      *object,
681                             guint         property_id,
682                             const GValue *value,
683                             GParamSpec   *pspec)
684 {
685   GtkListBase *self = GTK_LIST_BASE (object);
686   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
687 
688   switch (property_id)
689     {
690     case PROP_HADJUSTMENT:
691       gtk_list_base_set_adjustment (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_object (value));
692       break;
693 
694     case PROP_HSCROLL_POLICY:
695       gtk_list_base_set_scroll_policy (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_enum (value));
696       break;
697 
698     case PROP_ORIENTATION:
699       {
700         GtkOrientation orientation = g_value_get_enum (value);
701         if (priv->orientation != orientation)
702           {
703             priv->orientation = orientation;
704             gtk_widget_update_orientation (GTK_WIDGET (self), priv->orientation);
705             gtk_widget_queue_resize (GTK_WIDGET (self));
706             g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORIENTATION]);
707           }
708       }
709       break;
710 
711     case PROP_VADJUSTMENT:
712       gtk_list_base_set_adjustment (self, GTK_ORIENTATION_VERTICAL, g_value_get_object (value));
713       break;
714 
715     case PROP_VSCROLL_POLICY:
716       gtk_list_base_set_scroll_policy (self, GTK_ORIENTATION_VERTICAL, g_value_get_enum (value));
717       break;
718 
719     default:
720       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
721       break;
722     }
723 }
724 
725 static void
gtk_list_base_compute_scroll_align(GtkListBase * self,GtkOrientation orientation,int cell_start,int cell_end,double current_align,GtkPackType current_side,double * new_align,GtkPackType * new_side)726 gtk_list_base_compute_scroll_align (GtkListBase   *self,
727                                     GtkOrientation orientation,
728                                     int            cell_start,
729                                     int            cell_end,
730                                     double         current_align,
731                                     GtkPackType    current_side,
732                                     double        *new_align,
733                                     GtkPackType   *new_side)
734 {
735   int visible_start, visible_size, visible_end;
736   int cell_size;
737 
738   gtk_list_base_get_adjustment_values (GTK_LIST_BASE (self),
739                                        orientation,
740                                        &visible_start, NULL, &visible_size);
741   visible_end = visible_start + visible_size;
742   cell_size = cell_end - cell_start;
743 
744   if (cell_size <= visible_size)
745     {
746       if (cell_start < visible_start)
747         {
748           *new_align = 0.0;
749           *new_side = GTK_PACK_START;
750         }
751       else if (cell_end > visible_end)
752         {
753           *new_align = 1.0;
754           *new_side = GTK_PACK_END;
755         }
756       else
757         {
758           /* XXX: start or end here? */
759           *new_side = GTK_PACK_START;
760           *new_align = (double) (cell_start - visible_start) / visible_size;
761         }
762     }
763   else
764     {
765       /* This is the unlikely case of the cell being higher than the visible area */
766       if (cell_start > visible_start)
767         {
768           *new_align = 0.0;
769           *new_side = GTK_PACK_START;
770         }
771       else if (cell_end < visible_end)
772         {
773           *new_align = 1.0;
774           *new_side = GTK_PACK_END;
775         }
776       else
777         {
778           /* the cell already covers the whole screen */
779           *new_align = current_align;
780           *new_side = current_side;
781         }
782     }
783 }
784 
785 static void
gtk_list_base_update_focus_tracker(GtkListBase * self)786 gtk_list_base_update_focus_tracker (GtkListBase *self)
787 {
788   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
789   GtkWidget *focus_child;
790   guint pos;
791 
792   focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self));
793   if (!GTK_IS_LIST_ITEM_WIDGET (focus_child))
794     return;
795 
796   pos = gtk_list_item_widget_get_position (GTK_LIST_ITEM_WIDGET (focus_child));
797   if (pos != gtk_list_item_tracker_get_position (priv->item_manager, priv->focus))
798     {
799       gtk_list_item_tracker_set_position (priv->item_manager,
800                                           priv->focus,
801                                           pos,
802                                           0,
803                                           0);
804     }
805 }
806 
807 static void
gtk_list_base_scroll_to_item(GtkWidget * widget,const char * action_name,GVariant * parameter)808 gtk_list_base_scroll_to_item (GtkWidget  *widget,
809                               const char *action_name,
810                               GVariant   *parameter)
811 {
812   GtkListBase *self = GTK_LIST_BASE (widget);
813   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
814   int start, end;
815   double align_along, align_across;
816   GtkPackType side_along, side_across;
817   guint pos;
818 
819   if (!g_variant_check_format_string (parameter, "u", FALSE))
820     return;
821 
822   g_variant_get (parameter, "u", &pos);
823 
824   /* figure out primary orientation and if position is valid */
825   if (!gtk_list_base_get_allocation_along (GTK_LIST_BASE (self), pos, &start, &end))
826     return;
827 
828   end += start;
829   gtk_list_base_compute_scroll_align (self,
830                                       gtk_list_base_get_orientation (GTK_LIST_BASE (self)),
831                                       start, end,
832                                       priv->anchor_align_along, priv->anchor_side_along,
833                                       &align_along, &side_along);
834 
835   /* now do the same thing with the other orientation */
836   if (!gtk_list_base_get_allocation_across (GTK_LIST_BASE (self), pos, &start, &end))
837     return;
838 
839   end += start;
840   gtk_list_base_compute_scroll_align (self,
841                                       gtk_list_base_get_opposite_orientation (GTK_LIST_BASE (self)),
842                                       start, end,
843                                       priv->anchor_align_across, priv->anchor_side_across,
844                                       &align_across, &side_across);
845 
846   gtk_list_base_set_anchor (self,
847                             pos,
848                             align_across, side_across,
849                             align_along, side_along);
850 
851   /* HACK HACK HACK
852    *
853    * GTK has no way to track the focused child. But we now that when a listitem
854    * gets focus, it calls this action. So we update our focus tracker from here
855    * because it's the closest we can get to accurate tracking.
856    */
857   gtk_list_base_update_focus_tracker (self);
858 }
859 
860 static void
gtk_list_base_select_item_action(GtkWidget * widget,const char * action_name,GVariant * parameter)861 gtk_list_base_select_item_action (GtkWidget  *widget,
862                                   const char *action_name,
863                                   GVariant   *parameter)
864 {
865   GtkListBase *self = GTK_LIST_BASE (widget);
866   guint pos;
867   gboolean modify, extend;
868 
869   g_variant_get (parameter, "(ubb)", &pos, &modify, &extend);
870 
871   gtk_list_base_select_item (self, pos, modify, extend);
872 }
873 
874 static void
gtk_list_base_select_all(GtkWidget * widget,const char * action_name,GVariant * parameter)875 gtk_list_base_select_all (GtkWidget  *widget,
876                           const char *action_name,
877                           GVariant   *parameter)
878 {
879   GtkListBase *self = GTK_LIST_BASE (widget);
880   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
881   GtkSelectionModel *selection_model;
882 
883   selection_model = gtk_list_item_manager_get_model (priv->item_manager);
884   if (selection_model == NULL)
885     return;
886 
887   gtk_selection_model_select_all (selection_model);
888 }
889 
890 static void
gtk_list_base_unselect_all(GtkWidget * widget,const char * action_name,GVariant * parameter)891 gtk_list_base_unselect_all (GtkWidget  *widget,
892                             const char *action_name,
893                             GVariant   *parameter)
894 {
895   GtkListBase *self = GTK_LIST_BASE (widget);
896   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
897   GtkSelectionModel *selection_model;
898 
899   selection_model = gtk_list_item_manager_get_model (priv->item_manager);
900   if (selection_model == NULL)
901     return;
902 
903   gtk_selection_model_unselect_all (selection_model);
904 }
905 
906 static gboolean
gtk_list_base_move_cursor_to_start(GtkWidget * widget,GVariant * args,gpointer unused)907 gtk_list_base_move_cursor_to_start (GtkWidget *widget,
908                                     GVariant  *args,
909                                     gpointer   unused)
910 {
911   GtkListBase *self = GTK_LIST_BASE (widget);
912   gboolean select, modify, extend;
913 
914   if (gtk_list_base_get_n_items (self) == 0)
915     return TRUE;
916 
917   g_variant_get (args, "(bbb)", &select, &modify, &extend);
918 
919   gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), 0, select, modify, extend);
920 
921   return TRUE;
922 }
923 
924 static gboolean
gtk_list_base_move_cursor_page_up(GtkWidget * widget,GVariant * args,gpointer unused)925 gtk_list_base_move_cursor_page_up (GtkWidget *widget,
926                                    GVariant  *args,
927                                    gpointer   unused)
928 {
929   GtkListBase *self = GTK_LIST_BASE (widget);
930   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
931   gboolean select, modify, extend;
932   cairo_rectangle_int_t area, new_area;
933   int page_size;
934   guint pos, new_pos;
935 
936   pos = gtk_list_base_get_focus_position (self);
937   page_size = gtk_adjustment_get_page_size (priv->adjustment[priv->orientation]);
938   if (!gtk_list_base_get_allocation_along (self, pos, &area.y, &area.height) ||
939       !gtk_list_base_get_allocation_across (self, pos, &area.x, &area.width))
940     return TRUE;
941   if (!gtk_list_base_get_position_from_allocation (self,
942                                                    area.x + area.width / 2,
943                                                    MAX (0, area.y + area.height - page_size),
944                                                    &new_pos,
945                                                    &new_area))
946     return TRUE;
947 
948   /* We want the whole row to be visible */
949   if (new_area.y < MAX (0, area.y + area.height - page_size))
950     new_pos = gtk_list_base_move_focus_along (self, new_pos, 1);
951   /* But we definitely want to move if we can */
952   if (new_pos >= pos)
953     {
954       new_pos = gtk_list_base_move_focus_along (self, new_pos, -1);
955       if (new_pos == pos)
956         return TRUE;
957     }
958 
959   g_variant_get (args, "(bbb)", &select, &modify, &extend);
960 
961   gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), new_pos, select, modify, extend);
962 
963   return TRUE;
964 }
965 
966 static gboolean
gtk_list_base_move_cursor_page_down(GtkWidget * widget,GVariant * args,gpointer unused)967 gtk_list_base_move_cursor_page_down (GtkWidget *widget,
968                                      GVariant  *args,
969                                      gpointer   unused)
970 {
971   GtkListBase *self = GTK_LIST_BASE (widget);
972   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
973   gboolean select, modify, extend;
974   cairo_rectangle_int_t area, new_area;
975   int page_size, end;
976   guint pos, new_pos;
977 
978   pos = gtk_list_base_get_focus_position (self);
979   page_size = gtk_adjustment_get_page_size (priv->adjustment[priv->orientation]);
980   end = gtk_adjustment_get_upper (priv->adjustment[priv->orientation]);
981   if (end == 0)
982     return TRUE;
983 
984   if (!gtk_list_base_get_allocation_along (self, pos, &area.y, &area.height) ||
985       !gtk_list_base_get_allocation_across (self, pos, &area.x, &area.width))
986     return TRUE;
987 
988   if (!gtk_list_base_get_position_from_allocation (self,
989                                                    area.x + area.width / 2,
990                                                    MIN (end, area.y + page_size) - 1,
991                                                    &new_pos,
992                                                    &new_area))
993     return TRUE;
994 
995   /* We want the whole row to be visible */
996   if (new_area.y + new_area.height > MIN (end, area.y + page_size))
997     new_pos = gtk_list_base_move_focus_along (self, new_pos, -1);
998   /* But we definitely want to move if we can */
999   if (new_pos <= pos)
1000     {
1001       new_pos = gtk_list_base_move_focus_along (self, new_pos, 1);
1002       if (new_pos == pos)
1003         return TRUE;
1004     }
1005 
1006   g_variant_get (args, "(bbb)", &select, &modify, &extend);
1007 
1008   gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), new_pos, select, modify, extend);
1009 
1010   return TRUE;
1011 }
1012 
1013 static gboolean
gtk_list_base_move_cursor_to_end(GtkWidget * widget,GVariant * args,gpointer unused)1014 gtk_list_base_move_cursor_to_end (GtkWidget *widget,
1015                                   GVariant  *args,
1016                                   gpointer   unused)
1017 {
1018   GtkListBase *self = GTK_LIST_BASE (widget);
1019   gboolean select, modify, extend;
1020   guint n_items;
1021 
1022   n_items = gtk_list_base_get_n_items (self);
1023   if (n_items == 0)
1024     return TRUE;
1025 
1026   g_variant_get (args, "(bbb)", &select, &modify, &extend);
1027 
1028   gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), n_items - 1, select, modify, extend);
1029 
1030   return TRUE;
1031 }
1032 
1033 static gboolean
gtk_list_base_move_cursor(GtkWidget * widget,GVariant * args,gpointer unused)1034 gtk_list_base_move_cursor (GtkWidget *widget,
1035                            GVariant  *args,
1036                            gpointer   unused)
1037 {
1038   GtkListBase *self = GTK_LIST_BASE (widget);
1039   int amount;
1040   guint orientation;
1041   guint pos;
1042   gboolean select, modify, extend;
1043 
1044   g_variant_get (args, "(ubbbi)", &orientation, &select, &modify, &extend, &amount);
1045 
1046   pos = gtk_list_base_get_focus_position (self);
1047   pos = gtk_list_base_move_focus (self, pos, orientation, amount);
1048 
1049   gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos, select, modify, extend);
1050 
1051   return TRUE;
1052 }
1053 
1054 static void
gtk_list_base_add_move_binding(GtkWidgetClass * widget_class,guint keyval,GtkOrientation orientation,int amount)1055 gtk_list_base_add_move_binding (GtkWidgetClass *widget_class,
1056                                 guint           keyval,
1057                                 GtkOrientation  orientation,
1058                                 int             amount)
1059 {
1060   gtk_widget_class_add_binding (widget_class,
1061                                 keyval,
1062                                 0,
1063                                 gtk_list_base_move_cursor,
1064                                 "(ubbbi)", orientation, TRUE, FALSE, FALSE, amount);
1065   gtk_widget_class_add_binding (widget_class,
1066                                 keyval,
1067                                 GDK_CONTROL_MASK,
1068                                 gtk_list_base_move_cursor,
1069                                 "(ubbbi)", orientation, FALSE, FALSE, FALSE, amount);
1070   gtk_widget_class_add_binding (widget_class,
1071                                 keyval,
1072                                 GDK_SHIFT_MASK,
1073                                 gtk_list_base_move_cursor,
1074                                 "(ubbbi)", orientation, TRUE, FALSE, TRUE, amount);
1075   gtk_widget_class_add_binding (widget_class,
1076                                 keyval,
1077                                 GDK_CONTROL_MASK | GDK_SHIFT_MASK,
1078                                 gtk_list_base_move_cursor,
1079                                 "(ubbbi)", orientation, TRUE, TRUE, TRUE, amount);
1080 }
1081 
1082 static void
gtk_list_base_add_custom_move_binding(GtkWidgetClass * widget_class,guint keyval,GtkShortcutFunc callback)1083 gtk_list_base_add_custom_move_binding (GtkWidgetClass  *widget_class,
1084                                        guint            keyval,
1085                                        GtkShortcutFunc  callback)
1086 {
1087   gtk_widget_class_add_binding (widget_class,
1088                                 keyval,
1089                                 0,
1090                                 callback,
1091                                 "(bbb)", TRUE, FALSE, FALSE);
1092   gtk_widget_class_add_binding (widget_class,
1093                                 keyval,
1094                                 GDK_CONTROL_MASK,
1095                                 callback,
1096                                 "(bbb)", FALSE, FALSE, FALSE);
1097   gtk_widget_class_add_binding (widget_class,
1098                                 keyval,
1099                                 GDK_SHIFT_MASK,
1100                                 callback,
1101                                 "(bbb)", TRUE, FALSE, TRUE);
1102   gtk_widget_class_add_binding (widget_class,
1103                                 keyval,
1104                                 GDK_CONTROL_MASK | GDK_SHIFT_MASK,
1105                                 callback,
1106                                 "(bbb)", TRUE, TRUE, TRUE);
1107 }
1108 
1109 static void
gtk_list_base_class_init(GtkListBaseClass * klass)1110 gtk_list_base_class_init (GtkListBaseClass *klass)
1111 {
1112   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
1113   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
1114   gpointer iface;
1115 
1116   widget_class->focus = gtk_list_base_focus;
1117 
1118   gobject_class->dispose = gtk_list_base_dispose;
1119   gobject_class->get_property = gtk_list_base_get_property;
1120   gobject_class->set_property = gtk_list_base_set_property;
1121 
1122   /* GtkScrollable implementation */
1123   iface = g_type_default_interface_peek (GTK_TYPE_SCROLLABLE);
1124   properties[PROP_HADJUSTMENT] =
1125       g_param_spec_override ("hadjustment",
1126                              g_object_interface_find_property (iface, "hadjustment"));
1127   properties[PROP_HSCROLL_POLICY] =
1128       g_param_spec_override ("hscroll-policy",
1129                              g_object_interface_find_property (iface, "hscroll-policy"));
1130   properties[PROP_VADJUSTMENT] =
1131       g_param_spec_override ("vadjustment",
1132                              g_object_interface_find_property (iface, "vadjustment"));
1133   properties[PROP_VSCROLL_POLICY] =
1134       g_param_spec_override ("vscroll-policy",
1135                              g_object_interface_find_property (iface, "vscroll-policy"));
1136 
1137   /**
1138    * GtkListBase:orientation:
1139    *
1140    * The orientation of the list. See GtkOrientable:orientation
1141    * for details.
1142    */
1143   properties[PROP_ORIENTATION] =
1144     g_param_spec_enum ("orientation",
1145                        P_("Orientation"),
1146                        P_("The orientation of the orientable"),
1147                        GTK_TYPE_ORIENTATION,
1148                        GTK_ORIENTATION_VERTICAL,
1149                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1150 
1151   g_object_class_install_properties (gobject_class, N_PROPS, properties);
1152 
1153   /**
1154    * GtkListBase|list.scroll-to-item:
1155    * @position: position of item to scroll to
1156    *
1157    * Moves the visible area to the item given in @position with the minimum amount
1158    * of scrolling required. If the item is already visible, nothing happens.
1159    */
1160   gtk_widget_class_install_action (widget_class,
1161                                    "list.scroll-to-item",
1162                                    "u",
1163                                    gtk_list_base_scroll_to_item);
1164 
1165   /**
1166    * GtkListBase|list.select-item:
1167    * @position: position of item to select
1168    * @modify: %TRUE to toggle the existing selection, %FALSE to select
1169    * @extend: %TRUE to extend the selection
1170    *
1171    * Changes selection.
1172    *
1173    * If @extend is %TRUE and the model supports selecting ranges, the
1174    * affected items are all items from the last selected item to the item
1175    * in @position.
1176    * If @extend is %FALSE or selecting ranges is not supported, only the
1177    * item in @position is affected.
1178    *
1179    * If @modify is %TRUE, the affected items will be set to the same state.
1180    * If @modify is %FALSE, the affected items will be selected and
1181    * all other items will be deselected.
1182    */
1183   gtk_widget_class_install_action (widget_class,
1184                                    "list.select-item",
1185                                    "(ubb)",
1186                                    gtk_list_base_select_item_action);
1187 
1188   /**
1189    * GtkListBase|list.select-all:
1190    *
1191    * If the selection model supports it, select all items in the model.
1192    * If not, do nothing.
1193    */
1194   gtk_widget_class_install_action (widget_class,
1195                                    "list.select-all",
1196                                    NULL,
1197                                    gtk_list_base_select_all);
1198 
1199   /**
1200    * GtkListBase|list.unselect-all:
1201    *
1202    * If the selection model supports it, unselect all items in the model.
1203    * If not, do nothing.
1204    */
1205   gtk_widget_class_install_action (widget_class,
1206                                    "list.unselect-all",
1207                                    NULL,
1208                                    gtk_list_base_unselect_all);
1209 
1210   gtk_list_base_add_move_binding (widget_class, GDK_KEY_Up, GTK_ORIENTATION_VERTICAL, -1);
1211   gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Up, GTK_ORIENTATION_VERTICAL, -1);
1212   gtk_list_base_add_move_binding (widget_class, GDK_KEY_Down, GTK_ORIENTATION_VERTICAL, 1);
1213   gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Down, GTK_ORIENTATION_VERTICAL, 1);
1214   gtk_list_base_add_move_binding (widget_class, GDK_KEY_Left, GTK_ORIENTATION_HORIZONTAL, -1);
1215   gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Left, GTK_ORIENTATION_HORIZONTAL, -1);
1216   gtk_list_base_add_move_binding (widget_class, GDK_KEY_Right, GTK_ORIENTATION_HORIZONTAL, 1);
1217   gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Right, GTK_ORIENTATION_HORIZONTAL, 1);
1218 
1219   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Home, gtk_list_base_move_cursor_to_start);
1220   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Home, gtk_list_base_move_cursor_to_start);
1221   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_End, gtk_list_base_move_cursor_to_end);
1222   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_End, gtk_list_base_move_cursor_to_end);
1223   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Page_Up, gtk_list_base_move_cursor_page_up);
1224   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Page_Up, gtk_list_base_move_cursor_page_up);
1225   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Page_Down, gtk_list_base_move_cursor_page_down);
1226   gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Page_Down, gtk_list_base_move_cursor_page_down);
1227 
1228   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_a, GDK_CONTROL_MASK, "list.select-all", NULL);
1229   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_slash, GDK_CONTROL_MASK, "list.select-all", NULL);
1230   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_A, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "list.unselect-all", NULL);
1231   gtk_widget_class_add_binding_action (widget_class, GDK_KEY_backslash, GDK_CONTROL_MASK, "list.unselect-all", NULL);
1232 }
1233 
1234 static gboolean
autoscroll_cb(GtkWidget * widget,GdkFrameClock * frame_clock,gpointer data)1235 autoscroll_cb (GtkWidget     *widget,
1236                GdkFrameClock *frame_clock,
1237                gpointer       data)
1238 {
1239   GtkListBase *self = data;
1240   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1241   double value;
1242   double delta_x, delta_y;
1243 
1244   value = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
1245   gtk_adjustment_set_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL], value + priv->autoscroll_delta_x);
1246 
1247   delta_x = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]) - value;
1248 
1249   value = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
1250   gtk_adjustment_set_value (priv->adjustment[GTK_ORIENTATION_VERTICAL], value + priv->autoscroll_delta_y);
1251 
1252   delta_y = gtk_adjustment_get_value (priv->adjustment[GTK_ORIENTATION_VERTICAL]) - value;
1253 
1254   if (delta_x != 0 || delta_y != 0)
1255     {
1256       return G_SOURCE_CONTINUE;
1257     }
1258   else
1259     {
1260       priv->autoscroll_id = 0;
1261       return G_SOURCE_REMOVE;
1262     }
1263 }
1264 
1265 static void
add_autoscroll(GtkListBase * self,double delta_x,double delta_y)1266 add_autoscroll (GtkListBase *self,
1267                 double       delta_x,
1268                 double       delta_y)
1269 {
1270   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1271 
1272   if (gtk_list_base_adjustment_is_flipped (self, GTK_ORIENTATION_HORIZONTAL))
1273     priv->autoscroll_delta_x = -delta_x;
1274   else
1275     priv->autoscroll_delta_x = delta_x;
1276   if (gtk_list_base_adjustment_is_flipped (self, GTK_ORIENTATION_VERTICAL))
1277     priv->autoscroll_delta_y = -delta_y;
1278   else
1279     priv->autoscroll_delta_y = delta_y;
1280 
1281   if (priv->autoscroll_id == 0)
1282     priv->autoscroll_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), autoscroll_cb, self, NULL);
1283 }
1284 
1285 static void
remove_autoscroll(GtkListBase * self)1286 remove_autoscroll (GtkListBase *self)
1287 {
1288   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1289 
1290   if (priv->autoscroll_id != 0)
1291     {
1292       gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->autoscroll_id);
1293       priv->autoscroll_id = 0;
1294     }
1295 }
1296 
1297 #define SCROLL_EDGE_SIZE 30
1298 
1299 static void
update_autoscroll(GtkListBase * self,double x,double y)1300 update_autoscroll (GtkListBase *self,
1301                    double       x,
1302                    double       y)
1303 {
1304   double width, height;
1305   double delta_x, delta_y;
1306 
1307   width = gtk_widget_get_width (GTK_WIDGET (self));
1308 
1309   if (x < SCROLL_EDGE_SIZE)
1310     delta_x = - (SCROLL_EDGE_SIZE - x)/3.0;
1311   else if (width - x < SCROLL_EDGE_SIZE)
1312     delta_x = (SCROLL_EDGE_SIZE - (width - x))/3.0;
1313   else
1314     delta_x = 0;
1315 
1316   if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
1317     delta_x = - delta_x;
1318 
1319   height = gtk_widget_get_height (GTK_WIDGET (self));
1320 
1321   if (y < SCROLL_EDGE_SIZE)
1322     delta_y = - (SCROLL_EDGE_SIZE - y)/3.0;
1323   else if (height - y < SCROLL_EDGE_SIZE)
1324     delta_y = (SCROLL_EDGE_SIZE - (height - y))/3.0;
1325   else
1326     delta_y = 0;
1327 
1328   if (delta_x != 0 || delta_y != 0)
1329     add_autoscroll (self, delta_x, delta_y);
1330   else
1331     remove_autoscroll (self);
1332 }
1333 
1334 /**
1335  * gtk_list_base_size_allocate_child:
1336  * @self: The listbase
1337  * @child: The child
1338  * @x: top left coordinate in the across direction
1339  * @y: top right coordinate in the along direction
1340  * @width: size in the across direction
1341  * @height: size in the along direction
1342  *
1343  * Allocates a child widget in the list coordinate system,
1344  * but with the coordinates already offset by the scroll
1345  * offset.
1346  **/
1347 void
gtk_list_base_size_allocate_child(GtkListBase * self,GtkWidget * child,int x,int y,int width,int height)1348 gtk_list_base_size_allocate_child (GtkListBase *self,
1349                                    GtkWidget   *child,
1350                                    int          x,
1351                                    int          y,
1352                                    int          width,
1353                                    int          height)
1354 {
1355   GtkAllocation child_allocation;
1356 
1357   if (gtk_list_base_get_orientation (GTK_LIST_BASE (self)) == GTK_ORIENTATION_VERTICAL)
1358     {
1359       if (_gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR)
1360         {
1361           child_allocation.x = x;
1362           child_allocation.y = y;
1363           child_allocation.width = width;
1364           child_allocation.height = height;
1365         }
1366       else
1367         {
1368           int mirror_point = gtk_widget_get_width (GTK_WIDGET (self));
1369 
1370           child_allocation.x = mirror_point - x - width;
1371           child_allocation.y = y;
1372           child_allocation.width = width;
1373           child_allocation.height = height;
1374         }
1375     }
1376   else
1377     {
1378       if (_gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR)
1379         {
1380           child_allocation.x = y;
1381           child_allocation.y = x;
1382           child_allocation.width = height;
1383           child_allocation.height = width;
1384         }
1385       else
1386         {
1387           int mirror_point = gtk_widget_get_width (GTK_WIDGET (self));
1388 
1389           child_allocation.x = mirror_point - y - height;
1390           child_allocation.y = x;
1391           child_allocation.width = height;
1392           child_allocation.height = width;
1393         }
1394     }
1395 
1396   gtk_widget_size_allocate (child, &child_allocation, -1);
1397 }
1398 
1399 static void
gtk_list_base_widget_to_list(GtkListBase * self,double x_widget,double y_widget,int * across_out,int * along_out)1400 gtk_list_base_widget_to_list (GtkListBase *self,
1401                               double       x_widget,
1402                               double       y_widget,
1403                               int         *across_out,
1404                               int         *along_out)
1405 {
1406   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1407   GtkWidget *widget = GTK_WIDGET (self);
1408 
1409   if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
1410     x_widget = gtk_widget_get_width (widget) - x_widget;
1411 
1412   gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), across_out, NULL, NULL);
1413   gtk_list_base_get_adjustment_values (self, priv->orientation, along_out, NULL, NULL);
1414 
1415   if (priv->orientation == GTK_ORIENTATION_VERTICAL)
1416     {
1417       *across_out += x_widget;
1418       *along_out += y_widget;
1419     }
1420   else
1421     {
1422       *across_out += y_widget;
1423       *along_out += x_widget;
1424     }
1425 }
1426 
1427 static GtkBitset *
gtk_list_base_get_items_in_rect(GtkListBase * self,const GdkRectangle * rect)1428 gtk_list_base_get_items_in_rect (GtkListBase        *self,
1429                                  const GdkRectangle *rect)
1430 {
1431   return GTK_LIST_BASE_GET_CLASS (self)->get_items_in_rect (self, rect);
1432 }
1433 
1434 static gboolean
gtk_list_base_get_rubberband_coords(GtkListBase * self,GdkRectangle * rect)1435 gtk_list_base_get_rubberband_coords (GtkListBase  *self,
1436                                      GdkRectangle *rect)
1437 {
1438   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1439   int x1, x2, y1, y2;
1440 
1441   if (!priv->rubberband)
1442     return FALSE;
1443 
1444   if (priv->rubberband->start_tracker == NULL)
1445     {
1446       x1 = 0;
1447       y1 = 0;
1448     }
1449   else
1450     {
1451       guint pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->rubberband->start_tracker);
1452 
1453       if (gtk_list_base_get_allocation_along (self, pos, &y1, &y2) &&
1454           gtk_list_base_get_allocation_across (self, pos, &x1, &x2))
1455         {
1456           x1 += x2 * priv->rubberband->start_align_across;
1457           y1 += y2 * priv->rubberband->start_align_along;
1458         }
1459       else
1460         {
1461           x1 = 0;
1462           y1 = 0;
1463         }
1464     }
1465 
1466   gtk_list_base_widget_to_list (self,
1467                                 priv->rubberband->pointer_x, priv->rubberband->pointer_y,
1468                                 &x2, &y2);
1469 
1470   rect->x = MIN (x1, x2);
1471   rect->y = MIN (y1, y2);
1472   rect->width = ABS (x1 - x2) + 1;
1473   rect->height = ABS (y1 - y2) + 1;
1474 
1475   return TRUE;
1476 }
1477 
1478 void
gtk_list_base_allocate_rubberband(GtkListBase * self)1479 gtk_list_base_allocate_rubberband (GtkListBase *self)
1480 {
1481   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1482   GtkRequisition min_size;
1483   GdkRectangle rect;
1484   int offset_x, offset_y;
1485 
1486   if (!gtk_list_base_get_rubberband_coords (self, &rect))
1487     return;
1488 
1489   gtk_widget_get_preferred_size (priv->rubberband->widget, &min_size, NULL);
1490   rect.width = MAX (min_size.width, rect.width);
1491   rect.height = MAX (min_size.height, rect.height);
1492 
1493   gtk_list_base_get_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), &offset_x, NULL, NULL);
1494   gtk_list_base_get_adjustment_values (self, priv->orientation, &offset_y, NULL, NULL);
1495   rect.x -= offset_x;
1496   rect.y -= offset_y;
1497 
1498   gtk_list_base_size_allocate_child (self,
1499                                      priv->rubberband->widget,
1500                                      rect.x, rect.y, rect.width, rect.height);
1501 }
1502 
1503 static void
gtk_list_base_start_rubberband(GtkListBase * self,double x,double y)1504 gtk_list_base_start_rubberband (GtkListBase *self,
1505                                 double       x,
1506                                 double       y)
1507 {
1508   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1509   cairo_rectangle_int_t item_area;
1510   int list_x, list_y;
1511   guint pos;
1512 
1513   if (priv->rubberband)
1514     return;
1515 
1516   gtk_list_base_widget_to_list (self, x, y, &list_x, &list_y);
1517   if (!gtk_list_base_get_position_from_allocation (self, list_x, list_y, &pos, &item_area))
1518     {
1519       g_warning ("Could not start rubberbanding: No item\n");
1520       return;
1521     }
1522 
1523   priv->rubberband = g_new0 (RubberbandData, 1);
1524 
1525   priv->rubberband->start_tracker = gtk_list_item_tracker_new (priv->item_manager);
1526   gtk_list_item_tracker_set_position (priv->item_manager, priv->rubberband->start_tracker, pos, 0, 0);
1527   priv->rubberband->start_align_across = (double) (list_x - item_area.x) / item_area.width;
1528   priv->rubberband->start_align_along = (double) (list_y - item_area.y) / item_area.height;
1529 
1530   priv->rubberband->pointer_x = x;
1531   priv->rubberband->pointer_y = y;
1532 
1533   priv->rubberband->widget = gtk_gizmo_new ("rubberband",
1534                                             NULL, NULL, NULL, NULL, NULL, NULL);
1535   gtk_widget_set_parent (priv->rubberband->widget, GTK_WIDGET (self));
1536 }
1537 
1538 static void
gtk_list_base_stop_rubberband(GtkListBase * self,gboolean modify,gboolean extend)1539 gtk_list_base_stop_rubberband (GtkListBase *self,
1540                                gboolean     modify,
1541                                gboolean     extend)
1542 {
1543   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1544   GtkListItemManagerItem *item;
1545   GtkSelectionModel *model;
1546 
1547   if (!priv->rubberband)
1548     return;
1549 
1550   for (item = gtk_list_item_manager_get_first (priv->item_manager);
1551        item != NULL;
1552        item = gtk_rb_tree_node_get_next (item))
1553     {
1554       if (item->widget)
1555         gtk_widget_unset_state_flags (item->widget, GTK_STATE_FLAG_ACTIVE);
1556     }
1557 
1558   model = gtk_list_item_manager_get_model (priv->item_manager);
1559   if (model != NULL)
1560     {
1561       GtkBitset *selected, *mask;
1562       GdkRectangle rect;
1563       GtkBitset *rubberband_selection;
1564 
1565       if (!gtk_list_base_get_rubberband_coords (self, &rect))
1566         return;
1567 
1568       rubberband_selection = gtk_list_base_get_items_in_rect (self, &rect);
1569       if (gtk_bitset_is_empty (rubberband_selection))
1570         {
1571           gtk_bitset_unref (rubberband_selection);
1572           return;
1573         }
1574 
1575       if (modify && extend) /* Ctrl + Shift */
1576         {
1577           GtkBitset *current;
1578           guint min = gtk_bitset_get_minimum (rubberband_selection);
1579           guint max = gtk_bitset_get_maximum (rubberband_selection);
1580           /* toggle the rubberband, keep the rest */
1581           current = gtk_selection_model_get_selection_in_range (model, min, max - min + 1);
1582           selected = gtk_bitset_copy (current);
1583           gtk_bitset_unref (current);
1584           gtk_bitset_intersect (selected, rubberband_selection);
1585           gtk_bitset_difference (selected, rubberband_selection);
1586 
1587           mask = gtk_bitset_ref (rubberband_selection);
1588         }
1589       else if (modify) /* Ctrl */
1590         {
1591           /* select the rubberband, keep the rest */
1592           selected = gtk_bitset_ref (rubberband_selection);
1593           mask = gtk_bitset_ref (rubberband_selection);
1594         }
1595       else if (extend) /* Shift */
1596         {
1597           /* unselect the rubberband, keep the rest */
1598           selected = gtk_bitset_new_empty ();
1599           mask = gtk_bitset_ref (rubberband_selection);
1600         }
1601       else /* no modifier */
1602         {
1603           /* select the rubberband, clear the rest */
1604           selected = gtk_bitset_ref (rubberband_selection);
1605           mask = gtk_bitset_new_empty ();
1606           gtk_bitset_add_range (mask, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
1607         }
1608 
1609       gtk_selection_model_set_selection (model, selected, mask);
1610 
1611       gtk_bitset_unref (selected);
1612       gtk_bitset_unref (mask);
1613       gtk_bitset_unref (rubberband_selection);
1614     }
1615 
1616   gtk_list_item_tracker_free (priv->item_manager, priv->rubberband->start_tracker);
1617   g_clear_pointer (&priv->rubberband->widget, gtk_widget_unparent);
1618   g_free (priv->rubberband);
1619   priv->rubberband = NULL;
1620 
1621   remove_autoscroll (self);
1622 }
1623 
1624 static void
gtk_list_base_update_rubberband_selection(GtkListBase * self)1625 gtk_list_base_update_rubberband_selection (GtkListBase *self)
1626 {
1627   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1628   GtkListItemManagerItem *item;
1629   GdkRectangle rect;
1630   guint pos;
1631   GtkBitset *rubberband_selection;
1632 
1633   if (!gtk_list_base_get_rubberband_coords (self, &rect))
1634     return;
1635 
1636   rubberband_selection = gtk_list_base_get_items_in_rect (self, &rect);
1637 
1638   pos = 0;
1639   for (item = gtk_list_item_manager_get_first (priv->item_manager);
1640        item != NULL;
1641        item = gtk_rb_tree_node_get_next (item))
1642     {
1643       if (item->widget)
1644         {
1645           if (gtk_bitset_contains (rubberband_selection, pos))
1646             gtk_widget_set_state_flags (item->widget, GTK_STATE_FLAG_ACTIVE, FALSE);
1647           else
1648             gtk_widget_unset_state_flags (item->widget, GTK_STATE_FLAG_ACTIVE);
1649         }
1650 
1651       pos += item->n_items;
1652     }
1653 
1654   gtk_bitset_unref (rubberband_selection);
1655 }
1656 
1657 static void
gtk_list_base_update_rubberband(GtkListBase * self,double x,double y)1658 gtk_list_base_update_rubberband (GtkListBase *self,
1659                                  double       x,
1660                                  double       y)
1661 {
1662   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1663 
1664   if (!priv->rubberband)
1665     return;
1666 
1667   priv->rubberband->pointer_x = x;
1668   priv->rubberband->pointer_y = y;
1669 
1670   gtk_list_base_update_rubberband_selection (self);
1671 
1672   update_autoscroll (self, x, y);
1673 
1674   gtk_widget_queue_allocate (GTK_WIDGET (self));
1675 }
1676 
1677 static void
get_selection_modifiers(GtkGesture * gesture,gboolean * modify,gboolean * extend)1678 get_selection_modifiers (GtkGesture *gesture,
1679                          gboolean   *modify,
1680                          gboolean   *extend)
1681 {
1682   GdkEventSequence *sequence;
1683   GdkEvent *event;
1684   GdkModifierType state;
1685 
1686   *modify = FALSE;
1687   *extend = FALSE;
1688 
1689   sequence = gtk_gesture_get_last_updated_sequence (gesture);
1690   event = gtk_gesture_get_last_event (gesture, sequence);
1691   state = gdk_event_get_modifier_state (event);
1692   if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK)
1693     *modify = TRUE;
1694   if ((state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK)
1695     *extend = TRUE;
1696 }
1697 
1698 static void
gtk_list_base_drag_update(GtkGestureDrag * gesture,double offset_x,double offset_y,GtkListBase * self)1699 gtk_list_base_drag_update (GtkGestureDrag *gesture,
1700                            double          offset_x,
1701                            double          offset_y,
1702                            GtkListBase    *self)
1703 {
1704   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1705   double start_x, start_y;
1706 
1707   gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
1708 
1709   if (!priv->rubberband)
1710     {
1711       if (!gtk_drag_check_threshold_double (GTK_WIDGET (self), 0, 0, offset_x, offset_y))
1712         return;
1713 
1714       gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
1715       gtk_list_base_start_rubberband (self, start_x, start_y);
1716     }
1717   gtk_list_base_update_rubberband (self, start_x + offset_x, start_y + offset_y);
1718 }
1719 
1720 static void
gtk_list_base_drag_end(GtkGestureDrag * gesture,double offset_x,double offset_y,GtkListBase * self)1721 gtk_list_base_drag_end (GtkGestureDrag *gesture,
1722                         double          offset_x,
1723                         double          offset_y,
1724                         GtkListBase    *self)
1725 {
1726   gboolean modify, extend;
1727 
1728   gtk_list_base_drag_update (gesture, offset_x, offset_y, self);
1729   get_selection_modifiers (GTK_GESTURE (gesture), &modify, &extend);
1730   gtk_list_base_stop_rubberband (self, modify, extend);
1731 }
1732 
1733 void
gtk_list_base_set_enable_rubberband(GtkListBase * self,gboolean enable)1734 gtk_list_base_set_enable_rubberband (GtkListBase *self,
1735                                      gboolean     enable)
1736 {
1737   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1738 
1739   if (priv->enable_rubberband == enable)
1740     return;
1741 
1742   priv->enable_rubberband = enable;
1743 
1744   if (enable)
1745     {
1746       priv->drag_gesture = gtk_gesture_drag_new ();
1747       g_signal_connect (priv->drag_gesture, "drag-update", G_CALLBACK (gtk_list_base_drag_update), self);
1748       g_signal_connect (priv->drag_gesture, "drag-end", G_CALLBACK (gtk_list_base_drag_end), self);
1749       gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
1750     }
1751   else
1752     {
1753       gtk_widget_remove_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
1754       priv->drag_gesture = NULL;
1755     }
1756 }
1757 
1758 gboolean
gtk_list_base_get_enable_rubberband(GtkListBase * self)1759 gtk_list_base_get_enable_rubberband (GtkListBase *self)
1760 {
1761   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1762 
1763   return priv->enable_rubberband;
1764 }
1765 
1766 static void
gtk_list_base_drag_motion(GtkDropControllerMotion * motion,double x,double y,gpointer unused)1767 gtk_list_base_drag_motion (GtkDropControllerMotion *motion,
1768                            double                   x,
1769                            double                   y,
1770                            gpointer                 unused)
1771 {
1772   GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion));
1773 
1774   update_autoscroll (GTK_LIST_BASE (widget), x, y);
1775 }
1776 
1777 static void
gtk_list_base_drag_leave(GtkDropControllerMotion * motion,gpointer unused)1778 gtk_list_base_drag_leave (GtkDropControllerMotion *motion,
1779                           gpointer                 unused)
1780 {
1781   GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion));
1782 
1783   remove_autoscroll (GTK_LIST_BASE (widget));
1784 }
1785 
1786 static void
gtk_list_base_init_real(GtkListBase * self,GtkListBaseClass * g_class)1787 gtk_list_base_init_real (GtkListBase      *self,
1788                          GtkListBaseClass *g_class)
1789 {
1790   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1791   GtkEventController *controller;
1792 
1793   priv->item_manager = gtk_list_item_manager_new_for_size (GTK_WIDGET (self),
1794                                                            g_class->list_item_name,
1795                                                            g_class->list_item_role,
1796                                                            g_class->list_item_size,
1797                                                            g_class->list_item_augment_size,
1798                                                            g_class->list_item_augment_func);
1799   priv->anchor = gtk_list_item_tracker_new (priv->item_manager);
1800   priv->anchor_side_along = GTK_PACK_START;
1801   priv->anchor_side_across = GTK_PACK_START;
1802   priv->selected = gtk_list_item_tracker_new (priv->item_manager);
1803   priv->focus = gtk_list_item_tracker_new (priv->item_manager);
1804 
1805   priv->adjustment[GTK_ORIENTATION_HORIZONTAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1806   priv->adjustment[GTK_ORIENTATION_VERTICAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1807 
1808   priv->orientation = GTK_ORIENTATION_VERTICAL;
1809 
1810   gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
1811   gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
1812 
1813   controller = gtk_drop_controller_motion_new ();
1814   g_signal_connect (controller, "motion", G_CALLBACK (gtk_list_base_drag_motion), NULL);
1815   g_signal_connect (controller, "leave", G_CALLBACK (gtk_list_base_drag_leave), NULL);
1816   gtk_widget_add_controller (GTK_WIDGET (self), controller);
1817 }
1818 
1819 static int
gtk_list_base_set_adjustment_values(GtkListBase * self,GtkOrientation orientation,int value,int size,int page_size)1820 gtk_list_base_set_adjustment_values (GtkListBase    *self,
1821                                      GtkOrientation  orientation,
1822                                      int             value,
1823                                      int             size,
1824                                      int             page_size)
1825 {
1826   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1827 
1828   size = MAX (size, page_size);
1829   value = MAX (value, 0);
1830   value = MIN (value, size - page_size);
1831 
1832   g_signal_handlers_block_by_func (priv->adjustment[orientation],
1833                                    gtk_list_base_adjustment_value_changed_cb,
1834                                    self);
1835   gtk_adjustment_configure (priv->adjustment[orientation],
1836                             gtk_list_base_adjustment_is_flipped (self, orientation)
1837                               ? size - page_size - value
1838                               : value,
1839                             0,
1840                             size,
1841                             page_size * 0.1,
1842                             page_size * 0.9,
1843                             page_size);
1844   g_signal_handlers_unblock_by_func (priv->adjustment[orientation],
1845                                      gtk_list_base_adjustment_value_changed_cb,
1846                                      self);
1847 
1848   return value;
1849 }
1850 
1851 void
gtk_list_base_update_adjustments(GtkListBase * self,int total_across,int total_along,int page_across,int page_along,int * across,int * along)1852 gtk_list_base_update_adjustments (GtkListBase *self,
1853                                   int          total_across,
1854                                   int          total_along,
1855                                   int          page_across,
1856                                   int          page_along,
1857                                   int         *across,
1858                                   int         *along)
1859 {
1860   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1861   int value_along, value_across, size;
1862   guint pos;
1863 
1864   pos = gtk_list_item_tracker_get_position (priv->item_manager, priv->anchor);
1865   if (pos == GTK_INVALID_LIST_POSITION)
1866     {
1867       value_across = 0;
1868       value_along = 0;
1869     }
1870   else
1871     {
1872       if (gtk_list_base_get_allocation_across (self, pos, &value_across, &size))
1873         {
1874           if (priv->anchor_side_across == GTK_PACK_END)
1875             value_across += size;
1876           value_across -= priv->anchor_align_across * page_across;
1877         }
1878       else
1879         {
1880           value_along = 0;
1881         }
1882       if (gtk_list_base_get_allocation_along (self, pos, &value_along, &size))
1883         {
1884           if (priv->anchor_side_along == GTK_PACK_END)
1885             value_along += size;
1886           value_along -= priv->anchor_align_along * page_along;
1887         }
1888       else
1889         {
1890           value_along = 0;
1891         }
1892     }
1893 
1894   *across = gtk_list_base_set_adjustment_values (self,
1895                                                  OPPOSITE_ORIENTATION (priv->orientation),
1896                                                  value_across,
1897                                                  total_across,
1898                                                  page_across);
1899   *along = gtk_list_base_set_adjustment_values (self,
1900                                                 priv->orientation,
1901                                                 value_along,
1902                                                 total_along,
1903                                                 page_along);
1904 }
1905 
1906 GtkScrollablePolicy
gtk_list_base_get_scroll_policy(GtkListBase * self,GtkOrientation orientation)1907 gtk_list_base_get_scroll_policy (GtkListBase    *self,
1908                                  GtkOrientation  orientation)
1909 {
1910   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1911 
1912   return priv->scroll_policy[orientation];
1913 }
1914 
1915 GtkOrientation
gtk_list_base_get_orientation(GtkListBase * self)1916 gtk_list_base_get_orientation (GtkListBase *self)
1917 {
1918   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1919 
1920   return priv->orientation;
1921 }
1922 
1923 GtkListItemManager *
gtk_list_base_get_manager(GtkListBase * self)1924 gtk_list_base_get_manager (GtkListBase *self)
1925 {
1926   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1927 
1928   return priv->item_manager;
1929 }
1930 
1931 guint
gtk_list_base_get_anchor(GtkListBase * self)1932 gtk_list_base_get_anchor (GtkListBase *self)
1933 {
1934   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1935 
1936   return gtk_list_item_tracker_get_position (priv->item_manager,
1937                                              priv->anchor);
1938 }
1939 
1940 /*
1941  * gtk_list_base_set_anchor:
1942  * @self: a `GtkListBase`
1943  * @anchor_pos: position of the item to anchor
1944  * @anchor_align_across: how far in the across direction to anchor
1945  * @anchor_side_across: if the anchor should side to start or end of item
1946  * @anchor_align_along: how far in the along direction to anchor
1947  * @anchor_side_along: if the anchor should side to start or end of item
1948  *
1949  * Sets the anchor.
1950  * The anchor is the item that is always kept on screen.
1951  *
1952  * In each dimension, anchoring uses 2 variables: The side of the
1953  * item that gets anchored - either start or end - and where in
1954  * the widget's allocation it should get anchored - here 0.0 means
1955  * the start of the widget and 1.0 is the end of the widget.
1956  * It is allowed to use values outside of this range. In particular,
1957  * this is necessary when the items are larger than the list's
1958  * allocation.
1959  *
1960  * Using this information, the adjustment's value and in turn widget
1961  * offsets will then be computed. If the anchor is too far off, it
1962  * will be clamped so that there are always visible items on screen.
1963  *
1964  * Making anchoring this complicated ensures that one item - one
1965  * corner of one item to be exact - always stays at the same place
1966  * (usually this item is the focused item). So when the list undergoes
1967  * heavy changes (like sorting, filtering, removals, additions), this
1968  * item will stay in place while everything around it will shuffle
1969  * around.
1970  *
1971  * The anchor will also ensure that enough widgets are created according
1972  * to gtk_list_base_set_anchor_max_widgets().
1973  **/
1974 void
gtk_list_base_set_anchor(GtkListBase * self,guint anchor_pos,double anchor_align_across,GtkPackType anchor_side_across,double anchor_align_along,GtkPackType anchor_side_along)1975 gtk_list_base_set_anchor (GtkListBase *self,
1976                           guint        anchor_pos,
1977                           double       anchor_align_across,
1978                           GtkPackType  anchor_side_across,
1979                           double       anchor_align_along,
1980                           GtkPackType  anchor_side_along)
1981 {
1982   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1983   guint items_before;
1984 
1985   items_before = round (priv->center_widgets * CLAMP (anchor_align_along, 0, 1));
1986   gtk_list_item_tracker_set_position (priv->item_manager,
1987                                       priv->anchor,
1988                                       anchor_pos,
1989                                       items_before + priv->above_below_widgets,
1990                                       priv->center_widgets - items_before + priv->above_below_widgets);
1991 
1992   priv->anchor_align_across = anchor_align_across;
1993   priv->anchor_side_across = anchor_side_across;
1994   priv->anchor_align_along = anchor_align_along;
1995   priv->anchor_side_along = anchor_side_along;
1996 
1997   gtk_widget_queue_allocate (GTK_WIDGET (self));
1998 }
1999 
2000 /**
2001  * gtk_list_base_set_anchor_max_widgets:
2002  * @self: a `GtkListBase`
2003  * @center: the number of widgets in the middle
2004  * @above_below: extra widgets above and below
2005  *
2006  * Sets how many widgets should be kept alive around the anchor.
2007  * The number of these widgets determines how many items can be
2008  * displayed and must be chosen to be large enough to cover the
2009  * allocation but should be kept as small as possible for
2010  * performance reasons.
2011  *
2012  * There will be @center widgets allocated around the anchor
2013  * evenly distributed according to the anchor's alignment - if
2014  * the anchor is at the start, all these widgets will be allocated
2015  * behind it, if it's at the end, all the widgets will be allocated
2016  * in front of it.
2017  *
2018  * Addditionally, there will be @above_below widgets allocated both
2019  * before and after the sencter widgets, so the total number of
2020  * widgets kept alive is 2 * above_below + center + 1.
2021  **/
2022 void
gtk_list_base_set_anchor_max_widgets(GtkListBase * self,guint n_center,guint n_above_below)2023 gtk_list_base_set_anchor_max_widgets (GtkListBase *self,
2024                                       guint        n_center,
2025                                       guint        n_above_below)
2026 {
2027   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2028 
2029   priv->center_widgets = n_center;
2030   priv->above_below_widgets = n_above_below;
2031 
2032   gtk_list_base_set_anchor (self,
2033                             gtk_list_item_tracker_get_position (priv->item_manager, priv->anchor),
2034                             priv->anchor_align_across,
2035                             priv->anchor_side_across,
2036                             priv->anchor_align_along,
2037                             priv->anchor_side_along);
2038 }
2039 
2040 /*
2041  * gtk_list_base_grab_focus_on_item:
2042  * @self: a `GtkListBase`
2043  * @pos: position of the item to focus
2044  * @select: %TRUE to select the item
2045  * @modify: if selecting, %TRUE to modify the selected
2046  *   state, %FALSE to always select
2047  * @extend: if selecting, %TRUE to extend the selection,
2048  *   %FALSE to only operate on this item
2049  *
2050  * Tries to grab focus on the given item. If there is no item
2051  * at this position or grabbing focus failed, %FALSE will be
2052  * returned.
2053  *
2054  * Returns: %TRUE if focusing the item succeeded
2055  **/
2056 gboolean
gtk_list_base_grab_focus_on_item(GtkListBase * self,guint pos,gboolean select,gboolean modify,gboolean extend)2057 gtk_list_base_grab_focus_on_item (GtkListBase *self,
2058                                   guint        pos,
2059                                   gboolean     select,
2060                                   gboolean     modify,
2061                                   gboolean     extend)
2062 {
2063   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2064   GtkListItemManagerItem *item;
2065   gboolean success;
2066 
2067   item = gtk_list_item_manager_get_nth (priv->item_manager, pos, NULL);
2068   if (item == NULL)
2069     return FALSE;
2070 
2071   if (!item->widget)
2072     {
2073       GtkListItemTracker *tracker = gtk_list_item_tracker_new (priv->item_manager);
2074 
2075       /* We need a tracker here to create the widget.
2076        * That needs to have happened or we can't grab it.
2077        * And we can't use a different tracker, because they manage important rows,
2078        * so we create a temporary one. */
2079       gtk_list_item_tracker_set_position (priv->item_manager, tracker, pos, 0, 0);
2080 
2081       item = gtk_list_item_manager_get_nth (priv->item_manager, pos, NULL);
2082       g_assert (item->widget);
2083 
2084       success = gtk_widget_grab_focus (item->widget);
2085 
2086       gtk_list_item_tracker_free (priv->item_manager, tracker);
2087     }
2088   else
2089     {
2090       success = gtk_widget_grab_focus (item->widget);
2091     }
2092 
2093   if (!success)
2094     return FALSE;
2095 
2096   if (select)
2097     gtk_list_base_select_item (self, pos, modify, extend);
2098 
2099   return TRUE;
2100 }
2101 
2102 GtkSelectionModel *
gtk_list_base_get_model(GtkListBase * self)2103 gtk_list_base_get_model (GtkListBase *self)
2104 {
2105   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2106 
2107   return priv->model;
2108 }
2109 
2110 gboolean
gtk_list_base_set_model(GtkListBase * self,GtkSelectionModel * model)2111 gtk_list_base_set_model (GtkListBase       *self,
2112                          GtkSelectionModel *model)
2113 {
2114   GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2115 
2116   if (priv->model == model)
2117     return FALSE;
2118 
2119   g_clear_object (&priv->model);
2120 
2121   if (model)
2122     {
2123       priv->model = g_object_ref (model);
2124       gtk_list_item_manager_set_model (priv->item_manager, model);
2125       gtk_list_base_set_anchor (self, 0, 0.0, GTK_PACK_START, 0.0, GTK_PACK_START);
2126     }
2127   else
2128     {
2129       gtk_list_item_manager_set_model (priv->item_manager, NULL);
2130     }
2131 
2132   return TRUE;
2133 }
2134