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