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
40typedef struct _RubberbandData RubberbandData;
41
42struct _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
53typedef struct _GtkListBasePrivate GtkListBasePrivate;
54
55struct _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
84enum
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 */
97static 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);
99G_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
104G_GNUC_UNUSED static void gtk_list_base_init (GtkListBase *self) { }
105
106static 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 **/
127static guint
128gtk_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
137static gboolean
138gtk_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
147static void
148gtk_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 (adjustment: priv->adjustment[orientation]);
158 upper = gtk_adjustment_get_upper (adjustment: priv->adjustment[orientation]);
159 ps = gtk_adjustment_get_page_size (adjustment: 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
171static void
172gtk_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), value: &area.x, size: &total_size, page_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 (x: align_across * area.width);
190 across = CLAMP (across, 0, total_size - 1);
191
192 gtk_list_base_get_adjustment_values (self, orientation: priv->orientation, value: &area.y, size: &total_size, page_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 (x: 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: &pos,
205 area: &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 anchor_pos: pos,
242 anchor_align_across: align_across, anchor_side_across: side_across,
243 anchor_align_along: align_along, anchor_side_along: side_along);
244
245 gtk_widget_queue_allocate (GTK_WIDGET (self));
246}
247
248static void
249gtk_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 **/
276static guint
277gtk_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 **/
297static guint
298gtk_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
305static guint
306gtk_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 **/
337static gboolean
338gtk_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 **/
360static gboolean
361gtk_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 **/
384void
385gtk_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 (self: priv->item_manager);
396 if (model == NULL)
397 return;
398
399 n_items = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: model));
400 if (pos >= n_items)
401 return;
402
403 if (extend)
404 {
405 guint extend_pos = gtk_list_item_tracker_get_position (self: priv->item_manager, tracker: 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, position: extend_pos))
415 {
416 success = gtk_selection_model_select_range (model,
417 position: min,
418 n_items: max - min + 1,
419 FALSE);
420 }
421 else
422 {
423 success = gtk_selection_model_unselect_range (model,
424 position: min,
425 n_items: max - min + 1);
426 }
427 }
428 else
429 {
430 success = gtk_selection_model_select_range (model,
431 position: min,
432 n_items: 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, position: pos))
447 gtk_selection_model_unselect_item (model, position: pos);
448 else
449 gtk_selection_model_select_item (model, position: pos, FALSE);
450 }
451 else
452 {
453 gtk_selection_model_select_item (model, position: pos, TRUE);
454 }
455
456 gtk_list_item_tracker_set_position (self: priv->item_manager,
457 tracker: priv->selected,
458 position: pos,
459 n_before: 0, n_after: 0);
460}
461
462guint
463gtk_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 (list: G_LIST_MODEL (ptr: priv->model));
471}
472
473guint
474gtk_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 (self: priv->item_manager, tracker: priv->focus);
479}
480
481static gboolean
482gtk_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 (widget: 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, orientation: GTK_ORIENTATION_VERTICAL, steps: -1);
532 break;
533
534 case GTK_DIR_DOWN:
535 pos = gtk_list_base_move_focus (self, pos, orientation: GTK_ORIENTATION_VERTICAL, steps: 1);
536 break;
537
538 case GTK_DIR_LEFT:
539 pos = gtk_list_base_move_focus (self, pos, orientation: GTK_ORIENTATION_HORIZONTAL, steps: -1);
540 break;
541
542 case GTK_DIR_RIGHT:
543 pos = gtk_list_base_move_focus (self, pos, orientation: GTK_ORIENTATION_HORIZONTAL, steps: 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 (self: priv->item_manager, position: 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 (widget: item->widget, direction);
564}
565
566static void
567gtk_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, orientation: GTK_ORIENTATION_HORIZONTAL);
573 gtk_list_base_clear_adjustment (self, orientation: GTK_ORIENTATION_VERTICAL);
574
575 if (priv->anchor)
576 {
577 gtk_list_item_tracker_free (self: priv->item_manager, tracker: priv->anchor);
578 priv->anchor = NULL;
579 }
580 if (priv->selected)
581 {
582 gtk_list_item_tracker_free (self: priv->item_manager, tracker: priv->selected);
583 priv->selected = NULL;
584 }
585 if (priv->focus)
586 {
587 gtk_list_item_tracker_free (self: priv->item_manager, tracker: 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
597static void
598gtk_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, v_object: priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
610 break;
611
612 case PROP_HSCROLL_POLICY:
613 g_value_set_enum (value, v_enum: priv->scroll_policy[GTK_ORIENTATION_HORIZONTAL]);
614 break;
615
616 case PROP_ORIENTATION:
617 g_value_set_enum (value, v_enum: priv->orientation);
618 break;
619
620 case PROP_VADJUSTMENT:
621 g_value_set_object (value, v_object: priv->adjustment[GTK_ORIENTATION_VERTICAL]);
622 break;
623
624 case PROP_VSCROLL_POLICY:
625 g_value_set_enum (value, v_enum: 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
634static void
635gtk_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 (value: 0.0, lower: 0.0, upper: 0.0, step_increment: 0.0, page_increment: 0.0, page_size: 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
659static void
660gtk_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 pspec: orientation == GTK_ORIENTATION_HORIZONTAL
675 ? properties[PROP_HSCROLL_POLICY]
676 : properties[PROP_VSCROLL_POLICY]);
677}
678
679static void
680gtk_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, orientation: GTK_ORIENTATION_HORIZONTAL, adjustment: g_value_get_object (value));
692 break;
693
694 case PROP_HSCROLL_POLICY:
695 gtk_list_base_set_scroll_policy (self, orientation: GTK_ORIENTATION_HORIZONTAL, scroll_policy: 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), orientation: priv->orientation);
705 gtk_widget_queue_resize (GTK_WIDGET (self));
706 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ORIENTATION]);
707 }
708 }
709 break;
710
711 case PROP_VADJUSTMENT:
712 gtk_list_base_set_adjustment (self, orientation: GTK_ORIENTATION_VERTICAL, adjustment: g_value_get_object (value));
713 break;
714
715 case PROP_VSCROLL_POLICY:
716 gtk_list_base_set_scroll_policy (self, orientation: GTK_ORIENTATION_VERTICAL, scroll_policy: 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
725static void
726gtk_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 value: &visible_start, NULL, page_size: &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
785static void
786gtk_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 (self: priv->item_manager, tracker: priv->focus))
798 {
799 gtk_list_item_tracker_set_position (self: priv->item_manager,
800 tracker: priv->focus,
801 position: pos,
802 n_before: 0,
803 n_after: 0);
804 }
805}
806
807static void
808gtk_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 (value: parameter, format_string: "u", FALSE))
820 return;
821
822 g_variant_get (value: parameter, format_string: "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, offset: &start, size: &end))
826 return;
827
828 end += start;
829 gtk_list_base_compute_scroll_align (self,
830 orientation: gtk_list_base_get_orientation (GTK_LIST_BASE (self)),
831 cell_start: start, cell_end: end,
832 current_align: priv->anchor_align_along, current_side: priv->anchor_side_along,
833 new_align: &align_along, new_side: &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, offset: &start, size: &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 cell_start: start, cell_end: end,
843 current_align: priv->anchor_align_across, current_side: priv->anchor_side_across,
844 new_align: &align_across, new_side: &side_across);
845
846 gtk_list_base_set_anchor (self,
847 anchor_pos: pos,
848 anchor_align_across: align_across, anchor_side_across: side_across,
849 anchor_align_along: align_along, anchor_side_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
860static void
861gtk_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 (value: parameter, format_string: "(ubb)", &pos, &modify, &extend);
870
871 gtk_list_base_select_item (self, pos, modify, extend);
872}
873
874static void
875gtk_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 (self: priv->item_manager);
884 if (selection_model == NULL)
885 return;
886
887 gtk_selection_model_select_all (model: selection_model);
888}
889
890static void
891gtk_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 (self: priv->item_manager);
900 if (selection_model == NULL)
901 return;
902
903 gtk_selection_model_unselect_all (model: selection_model);
904}
905
906static gboolean
907gtk_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 (value: args, format_string: "(bbb)", &select, &modify, &extend);
918
919 gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos: 0, select, modify, extend);
920
921 return TRUE;
922}
923
924static gboolean
925gtk_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 (adjustment: priv->adjustment[priv->orientation]);
938 if (!gtk_list_base_get_allocation_along (self, pos, offset: &area.y, size: &area.height) ||
939 !gtk_list_base_get_allocation_across (self, pos, offset: &area.x, size: &area.width))
940 return TRUE;
941 if (!gtk_list_base_get_position_from_allocation (self,
942 across: area.x + area.width / 2,
943 MAX (0, area.y + area.height - page_size),
944 pos: &new_pos,
945 area: &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, pos: new_pos, steps: 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, pos: new_pos, steps: -1);
955 if (new_pos == pos)
956 return TRUE;
957 }
958
959 g_variant_get (value: args, format_string: "(bbb)", &select, &modify, &extend);
960
961 gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos: new_pos, select, modify, extend);
962
963 return TRUE;
964}
965
966static gboolean
967gtk_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 (adjustment: priv->adjustment[priv->orientation]);
980 end = gtk_adjustment_get_upper (adjustment: priv->adjustment[priv->orientation]);
981 if (end == 0)
982 return TRUE;
983
984 if (!gtk_list_base_get_allocation_along (self, pos, offset: &area.y, size: &area.height) ||
985 !gtk_list_base_get_allocation_across (self, pos, offset: &area.x, size: &area.width))
986 return TRUE;
987
988 if (!gtk_list_base_get_position_from_allocation (self,
989 across: area.x + area.width / 2,
990 MIN (end, area.y + page_size) - 1,
991 pos: &new_pos,
992 area: &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, pos: new_pos, steps: -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, pos: new_pos, steps: 1);
1002 if (new_pos == pos)
1003 return TRUE;
1004 }
1005
1006 g_variant_get (value: args, format_string: "(bbb)", &select, &modify, &extend);
1007
1008 gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos: new_pos, select, modify, extend);
1009
1010 return TRUE;
1011}
1012
1013static gboolean
1014gtk_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 (value: args, format_string: "(bbb)", &select, &modify, &extend);
1027
1028 gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos: n_items - 1, select, modify, extend);
1029
1030 return TRUE;
1031}
1032
1033static gboolean
1034gtk_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 (value: args, format_string: "(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, steps: amount);
1048
1049 gtk_list_base_grab_focus_on_item (GTK_LIST_BASE (self), pos, select, modify, extend);
1050
1051 return TRUE;
1052}
1053
1054static void
1055gtk_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 mods: 0,
1063 callback: gtk_list_base_move_cursor,
1064 format_string: "(ubbbi)", orientation, TRUE, FALSE, FALSE, amount);
1065 gtk_widget_class_add_binding (widget_class,
1066 keyval,
1067 mods: GDK_CONTROL_MASK,
1068 callback: gtk_list_base_move_cursor,
1069 format_string: "(ubbbi)", orientation, FALSE, FALSE, FALSE, amount);
1070 gtk_widget_class_add_binding (widget_class,
1071 keyval,
1072 mods: GDK_SHIFT_MASK,
1073 callback: gtk_list_base_move_cursor,
1074 format_string: "(ubbbi)", orientation, TRUE, FALSE, TRUE, amount);
1075 gtk_widget_class_add_binding (widget_class,
1076 keyval,
1077 mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK,
1078 callback: gtk_list_base_move_cursor,
1079 format_string: "(ubbbi)", orientation, TRUE, TRUE, TRUE, amount);
1080}
1081
1082static void
1083gtk_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 mods: 0,
1090 callback,
1091 format_string: "(bbb)", TRUE, FALSE, FALSE);
1092 gtk_widget_class_add_binding (widget_class,
1093 keyval,
1094 mods: GDK_CONTROL_MASK,
1095 callback,
1096 format_string: "(bbb)", FALSE, FALSE, FALSE);
1097 gtk_widget_class_add_binding (widget_class,
1098 keyval,
1099 mods: GDK_SHIFT_MASK,
1100 callback,
1101 format_string: "(bbb)", TRUE, FALSE, TRUE);
1102 gtk_widget_class_add_binding (widget_class,
1103 keyval,
1104 mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK,
1105 callback,
1106 format_string: "(bbb)", TRUE, TRUE, TRUE);
1107}
1108
1109static void
1110gtk_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 (name: "hadjustment",
1126 overridden: g_object_interface_find_property (g_iface: iface, property_name: "hadjustment"));
1127 properties[PROP_HSCROLL_POLICY] =
1128 g_param_spec_override (name: "hscroll-policy",
1129 overridden: g_object_interface_find_property (g_iface: iface, property_name: "hscroll-policy"));
1130 properties[PROP_VADJUSTMENT] =
1131 g_param_spec_override (name: "vadjustment",
1132 overridden: g_object_interface_find_property (g_iface: iface, property_name: "vadjustment"));
1133 properties[PROP_VSCROLL_POLICY] =
1134 g_param_spec_override (name: "vscroll-policy",
1135 overridden: g_object_interface_find_property (g_iface: iface, property_name: "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 (name: "orientation",
1145 P_("Orientation"),
1146 P_("The orientation of the orientable"),
1147 enum_type: GTK_TYPE_ORIENTATION,
1148 default_value: GTK_ORIENTATION_VERTICAL,
1149 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
1150
1151 g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: 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 action_name: "list.scroll-to-item",
1162 parameter_type: "u",
1163 activate: 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 action_name: "list.select-item",
1185 parameter_type: "(ubb)",
1186 activate: 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 action_name: "list.select-all",
1196 NULL,
1197 activate: 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 action_name: "list.unselect-all",
1207 NULL,
1208 activate: gtk_list_base_unselect_all);
1209
1210 gtk_list_base_add_move_binding (widget_class, GDK_KEY_Up, orientation: GTK_ORIENTATION_VERTICAL, amount: -1);
1211 gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Up, orientation: GTK_ORIENTATION_VERTICAL, amount: -1);
1212 gtk_list_base_add_move_binding (widget_class, GDK_KEY_Down, orientation: GTK_ORIENTATION_VERTICAL, amount: 1);
1213 gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Down, orientation: GTK_ORIENTATION_VERTICAL, amount: 1);
1214 gtk_list_base_add_move_binding (widget_class, GDK_KEY_Left, orientation: GTK_ORIENTATION_HORIZONTAL, amount: -1);
1215 gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Left, orientation: GTK_ORIENTATION_HORIZONTAL, amount: -1);
1216 gtk_list_base_add_move_binding (widget_class, GDK_KEY_Right, orientation: GTK_ORIENTATION_HORIZONTAL, amount: 1);
1217 gtk_list_base_add_move_binding (widget_class, GDK_KEY_KP_Right, orientation: GTK_ORIENTATION_HORIZONTAL, amount: 1);
1218
1219 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Home, callback: gtk_list_base_move_cursor_to_start);
1220 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Home, callback: gtk_list_base_move_cursor_to_start);
1221 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_End, callback: gtk_list_base_move_cursor_to_end);
1222 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_End, callback: gtk_list_base_move_cursor_to_end);
1223 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Page_Up, callback: gtk_list_base_move_cursor_page_up);
1224 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Page_Up, callback: gtk_list_base_move_cursor_page_up);
1225 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_Page_Down, callback: gtk_list_base_move_cursor_page_down);
1226 gtk_list_base_add_custom_move_binding (widget_class, GDK_KEY_KP_Page_Down, callback: gtk_list_base_move_cursor_page_down);
1227
1228 gtk_widget_class_add_binding_action (widget_class, GDK_KEY_a, mods: GDK_CONTROL_MASK, action_name: "list.select-all", NULL);
1229 gtk_widget_class_add_binding_action (widget_class, GDK_KEY_slash, mods: GDK_CONTROL_MASK, action_name: "list.select-all", NULL);
1230 gtk_widget_class_add_binding_action (widget_class, GDK_KEY_A, mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK, action_name: "list.unselect-all", NULL);
1231 gtk_widget_class_add_binding_action (widget_class, GDK_KEY_backslash, mods: GDK_CONTROL_MASK, action_name: "list.unselect-all", NULL);
1232}
1233
1234static gboolean
1235autoscroll_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 (adjustment: priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
1245 gtk_adjustment_set_value (adjustment: priv->adjustment[GTK_ORIENTATION_HORIZONTAL], value: value + priv->autoscroll_delta_x);
1246
1247 delta_x = gtk_adjustment_get_value (adjustment: priv->adjustment[GTK_ORIENTATION_HORIZONTAL]) - value;
1248
1249 value = gtk_adjustment_get_value (adjustment: priv->adjustment[GTK_ORIENTATION_VERTICAL]);
1250 gtk_adjustment_set_value (adjustment: priv->adjustment[GTK_ORIENTATION_VERTICAL], value: value + priv->autoscroll_delta_y);
1251
1252 delta_y = gtk_adjustment_get_value (adjustment: 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
1265static void
1266add_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, orientation: 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, orientation: 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), callback: autoscroll_cb, user_data: self, NULL);
1283}
1284
1285static void
1286remove_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), id: priv->autoscroll_id);
1293 priv->autoscroll_id = 0;
1294 }
1295}
1296
1297#define SCROLL_EDGE_SIZE 30
1298
1299static void
1300update_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 **/
1347void
1348gtk_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 (widget: child, allocation: &child_allocation, baseline: -1);
1397}
1398
1399static void
1400gtk_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), value: across_out, NULL, NULL);
1413 gtk_list_base_get_adjustment_values (self, orientation: priv->orientation, value: 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
1427static GtkBitset *
1428gtk_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
1434static gboolean
1435gtk_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 (self: priv->item_manager, tracker: priv->rubberband->start_tracker);
1452
1453 if (gtk_list_base_get_allocation_along (self, pos, offset: &y1, size: &y2) &&
1454 gtk_list_base_get_allocation_across (self, pos, offset: &x1, size: &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 x_widget: priv->rubberband->pointer_x, y_widget: priv->rubberband->pointer_y,
1468 across_out: &x2, along_out: &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
1478void
1479gtk_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: &rect))
1487 return;
1488
1489 gtk_widget_get_preferred_size (widget: priv->rubberband->widget, minimum_size: &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), value: &offset_x, NULL, NULL);
1494 gtk_list_base_get_adjustment_values (self, orientation: priv->orientation, value: &offset_y, NULL, NULL);
1495 rect.x -= offset_x;
1496 rect.y -= offset_y;
1497
1498 gtk_list_base_size_allocate_child (self,
1499 child: priv->rubberband->widget,
1500 x: rect.x, y: rect.y, width: rect.width, height: rect.height);
1501}
1502
1503static void
1504gtk_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_widget: x, y_widget: y, across_out: &list_x, along_out: &list_y);
1517 if (!gtk_list_base_get_position_from_allocation (self, across: list_x, along: list_y, pos: &pos, area: &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 (self: priv->item_manager);
1526 gtk_list_item_tracker_set_position (self: priv->item_manager, tracker: priv->rubberband->start_tracker, position: pos, n_before: 0, n_after: 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 (css_name: "rubberband",
1534 NULL, NULL, NULL, NULL, NULL, NULL);
1535 gtk_widget_set_parent (widget: priv->rubberband->widget, GTK_WIDGET (self));
1536}
1537
1538static void
1539gtk_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 (self: priv->item_manager);
1551 item != NULL;
1552 item = gtk_rb_tree_node_get_next (node: item))
1553 {
1554 if (item->widget)
1555 gtk_widget_unset_state_flags (widget: item->widget, flags: GTK_STATE_FLAG_ACTIVE);
1556 }
1557
1558 model = gtk_list_item_manager_get_model (self: 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: &rect))
1566 return;
1567
1568 rubberband_selection = gtk_list_base_get_items_in_rect (self, rect: &rect);
1569
1570 if (modify && extend) /* Ctrl + Shift */
1571 {
1572 if (gtk_bitset_is_empty (self: rubberband_selection))
1573 {
1574 selected = gtk_bitset_ref (self: rubberband_selection);
1575 mask = gtk_bitset_ref (self: rubberband_selection);
1576 }
1577 else
1578 {
1579 GtkBitset *current;
1580 guint min = gtk_bitset_get_minimum (self: rubberband_selection);
1581 guint max = gtk_bitset_get_maximum (self: rubberband_selection);
1582 /* toggle the rubberband, keep the rest */
1583 current = gtk_selection_model_get_selection_in_range (model, position: min, n_items: max - min + 1);
1584 selected = gtk_bitset_copy (self: current);
1585 gtk_bitset_unref (self: current);
1586 gtk_bitset_intersect (self: selected, other: rubberband_selection);
1587 gtk_bitset_difference (self: selected, other: rubberband_selection);
1588
1589 mask = gtk_bitset_ref (self: rubberband_selection);
1590 }
1591 }
1592 else if (modify) /* Ctrl */
1593 {
1594 /* select the rubberband, keep the rest */
1595 selected = gtk_bitset_ref (self: rubberband_selection);
1596 mask = gtk_bitset_ref (self: rubberband_selection);
1597 }
1598 else if (extend) /* Shift */
1599 {
1600 /* unselect the rubberband, keep the rest */
1601 selected = gtk_bitset_new_empty ();
1602 mask = gtk_bitset_ref (self: rubberband_selection);
1603 }
1604 else /* no modifier */
1605 {
1606 /* select the rubberband, clear the rest */
1607 selected = gtk_bitset_ref (self: rubberband_selection);
1608 mask = gtk_bitset_new_empty ();
1609 gtk_bitset_add_range (self: mask, start: 0, n_items: g_list_model_get_n_items (list: G_LIST_MODEL (ptr: model)));
1610 }
1611
1612 gtk_selection_model_set_selection (model, selected, mask);
1613
1614 gtk_bitset_unref (self: selected);
1615 gtk_bitset_unref (self: mask);
1616 gtk_bitset_unref (self: rubberband_selection);
1617 }
1618
1619 gtk_list_item_tracker_free (self: priv->item_manager, tracker: priv->rubberband->start_tracker);
1620 g_clear_pointer (&priv->rubberband->widget, gtk_widget_unparent);
1621 g_free (mem: priv->rubberband);
1622 priv->rubberband = NULL;
1623
1624 remove_autoscroll (self);
1625}
1626
1627static void
1628gtk_list_base_update_rubberband_selection (GtkListBase *self)
1629{
1630 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1631 GtkListItemManagerItem *item;
1632 GdkRectangle rect;
1633 guint pos;
1634 GtkBitset *rubberband_selection;
1635
1636 if (!gtk_list_base_get_rubberband_coords (self, rect: &rect))
1637 return;
1638
1639 rubberband_selection = gtk_list_base_get_items_in_rect (self, rect: &rect);
1640
1641 pos = 0;
1642 for (item = gtk_list_item_manager_get_first (self: priv->item_manager);
1643 item != NULL;
1644 item = gtk_rb_tree_node_get_next (node: item))
1645 {
1646 if (item->widget)
1647 {
1648 if (gtk_bitset_contains (self: rubberband_selection, value: pos))
1649 gtk_widget_set_state_flags (widget: item->widget, flags: GTK_STATE_FLAG_ACTIVE, FALSE);
1650 else
1651 gtk_widget_unset_state_flags (widget: item->widget, flags: GTK_STATE_FLAG_ACTIVE);
1652 }
1653
1654 pos += item->n_items;
1655 }
1656
1657 gtk_bitset_unref (self: rubberband_selection);
1658}
1659
1660static void
1661gtk_list_base_update_rubberband (GtkListBase *self,
1662 double x,
1663 double y)
1664{
1665 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1666
1667 if (!priv->rubberband)
1668 return;
1669
1670 priv->rubberband->pointer_x = x;
1671 priv->rubberband->pointer_y = y;
1672
1673 gtk_list_base_update_rubberband_selection (self);
1674
1675 update_autoscroll (self, x, y);
1676
1677 gtk_widget_queue_allocate (GTK_WIDGET (self));
1678}
1679
1680static void
1681get_selection_modifiers (GtkGesture *gesture,
1682 gboolean *modify,
1683 gboolean *extend)
1684{
1685 GdkEventSequence *sequence;
1686 GdkEvent *event;
1687 GdkModifierType state;
1688
1689 *modify = FALSE;
1690 *extend = FALSE;
1691
1692 sequence = gtk_gesture_get_last_updated_sequence (gesture);
1693 event = gtk_gesture_get_last_event (gesture, sequence);
1694 state = gdk_event_get_modifier_state (event);
1695 if ((state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK)
1696 *modify = TRUE;
1697 if ((state & GDK_SHIFT_MASK) == GDK_SHIFT_MASK)
1698 *extend = TRUE;
1699}
1700
1701static void
1702gtk_list_base_drag_update (GtkGestureDrag *gesture,
1703 double offset_x,
1704 double offset_y,
1705 GtkListBase *self)
1706{
1707 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1708 double start_x, start_y;
1709
1710 gtk_gesture_drag_get_start_point (gesture, x: &start_x, y: &start_y);
1711
1712 if (!priv->rubberband)
1713 {
1714 if (!gtk_drag_check_threshold_double (GTK_WIDGET (self), start_x: 0, start_y: 0, current_x: offset_x, current_y: offset_y))
1715 return;
1716
1717 gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED);
1718 gtk_list_base_start_rubberband (self, x: start_x, y: start_y);
1719 }
1720 gtk_list_base_update_rubberband (self, x: start_x + offset_x, y: start_y + offset_y);
1721}
1722
1723static void
1724gtk_list_base_drag_end (GtkGestureDrag *gesture,
1725 double offset_x,
1726 double offset_y,
1727 GtkListBase *self)
1728{
1729 gboolean modify, extend;
1730
1731 gtk_list_base_drag_update (gesture, offset_x, offset_y, self);
1732 get_selection_modifiers (GTK_GESTURE (gesture), modify: &modify, extend: &extend);
1733 gtk_list_base_stop_rubberband (self, modify, extend);
1734}
1735
1736void
1737gtk_list_base_set_enable_rubberband (GtkListBase *self,
1738 gboolean enable)
1739{
1740 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1741
1742 if (priv->enable_rubberband == enable)
1743 return;
1744
1745 priv->enable_rubberband = enable;
1746
1747 if (enable)
1748 {
1749 priv->drag_gesture = gtk_gesture_drag_new ();
1750 g_signal_connect (priv->drag_gesture, "drag-update", G_CALLBACK (gtk_list_base_drag_update), self);
1751 g_signal_connect (priv->drag_gesture, "drag-end", G_CALLBACK (gtk_list_base_drag_end), self);
1752 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
1753 }
1754 else
1755 {
1756 gtk_widget_remove_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
1757 priv->drag_gesture = NULL;
1758 }
1759}
1760
1761gboolean
1762gtk_list_base_get_enable_rubberband (GtkListBase *self)
1763{
1764 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1765
1766 return priv->enable_rubberband;
1767}
1768
1769static void
1770gtk_list_base_drag_motion (GtkDropControllerMotion *motion,
1771 double x,
1772 double y,
1773 gpointer unused)
1774{
1775 GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion));
1776
1777 update_autoscroll (GTK_LIST_BASE (widget), x, y);
1778}
1779
1780static void
1781gtk_list_base_drag_leave (GtkDropControllerMotion *motion,
1782 gpointer unused)
1783{
1784 GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion));
1785
1786 remove_autoscroll (GTK_LIST_BASE (widget));
1787}
1788
1789static void
1790gtk_list_base_init_real (GtkListBase *self,
1791 GtkListBaseClass *g_class)
1792{
1793 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1794 GtkEventController *controller;
1795
1796 priv->item_manager = gtk_list_item_manager_new_for_size (GTK_WIDGET (self),
1797 item_css_name: g_class->list_item_name,
1798 item_role: g_class->list_item_role,
1799 element_size: g_class->list_item_size,
1800 augment_size: g_class->list_item_augment_size,
1801 augment_func: g_class->list_item_augment_func);
1802 priv->anchor = gtk_list_item_tracker_new (self: priv->item_manager);
1803 priv->anchor_side_along = GTK_PACK_START;
1804 priv->anchor_side_across = GTK_PACK_START;
1805 priv->selected = gtk_list_item_tracker_new (self: priv->item_manager);
1806 priv->focus = gtk_list_item_tracker_new (self: priv->item_manager);
1807
1808 priv->adjustment[GTK_ORIENTATION_HORIZONTAL] = gtk_adjustment_new (value: 0.0, lower: 0.0, upper: 0.0, step_increment: 0.0, page_increment: 0.0, page_size: 0.0);
1809 g_object_ref_sink (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]);
1810 priv->adjustment[GTK_ORIENTATION_VERTICAL] = gtk_adjustment_new (value: 0.0, lower: 0.0, upper: 0.0, step_increment: 0.0, page_increment: 0.0, page_size: 0.0);
1811 g_object_ref_sink (priv->adjustment[GTK_ORIENTATION_VERTICAL]);
1812
1813 priv->orientation = GTK_ORIENTATION_VERTICAL;
1814
1815 gtk_widget_set_overflow (GTK_WIDGET (self), overflow: GTK_OVERFLOW_HIDDEN);
1816 gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
1817
1818 controller = gtk_drop_controller_motion_new ();
1819 g_signal_connect (controller, "motion", G_CALLBACK (gtk_list_base_drag_motion), NULL);
1820 g_signal_connect (controller, "leave", G_CALLBACK (gtk_list_base_drag_leave), NULL);
1821 gtk_widget_add_controller (GTK_WIDGET (self), controller);
1822}
1823
1824static int
1825gtk_list_base_set_adjustment_values (GtkListBase *self,
1826 GtkOrientation orientation,
1827 int value,
1828 int size,
1829 int page_size)
1830{
1831 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1832
1833 size = MAX (size, page_size);
1834 value = MAX (value, 0);
1835 value = MIN (value, size - page_size);
1836
1837 g_signal_handlers_block_by_func (priv->adjustment[orientation],
1838 gtk_list_base_adjustment_value_changed_cb,
1839 self);
1840 gtk_adjustment_configure (adjustment: priv->adjustment[orientation],
1841 value: gtk_list_base_adjustment_is_flipped (self, orientation)
1842 ? size - page_size - value
1843 : value,
1844 lower: 0,
1845 upper: size,
1846 step_increment: page_size * 0.1,
1847 page_increment: page_size * 0.9,
1848 page_size);
1849 g_signal_handlers_unblock_by_func (priv->adjustment[orientation],
1850 gtk_list_base_adjustment_value_changed_cb,
1851 self);
1852
1853 return value;
1854}
1855
1856void
1857gtk_list_base_update_adjustments (GtkListBase *self,
1858 int total_across,
1859 int total_along,
1860 int page_across,
1861 int page_along,
1862 int *across,
1863 int *along)
1864{
1865 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1866 int value_along, value_across, size;
1867 guint pos;
1868
1869 pos = gtk_list_item_tracker_get_position (self: priv->item_manager, tracker: priv->anchor);
1870 if (pos == GTK_INVALID_LIST_POSITION)
1871 {
1872 value_across = 0;
1873 value_along = 0;
1874 }
1875 else
1876 {
1877 if (gtk_list_base_get_allocation_across (self, pos, offset: &value_across, size: &size))
1878 {
1879 if (priv->anchor_side_across == GTK_PACK_END)
1880 value_across += size;
1881 value_across -= priv->anchor_align_across * page_across;
1882 }
1883 else
1884 {
1885 value_along = 0;
1886 }
1887 if (gtk_list_base_get_allocation_along (self, pos, offset: &value_along, size: &size))
1888 {
1889 if (priv->anchor_side_along == GTK_PACK_END)
1890 value_along += size;
1891 value_along -= priv->anchor_align_along * page_along;
1892 }
1893 else
1894 {
1895 value_along = 0;
1896 }
1897 }
1898
1899 *across = gtk_list_base_set_adjustment_values (self,
1900 OPPOSITE_ORIENTATION (priv->orientation),
1901 value: value_across,
1902 size: total_across,
1903 page_size: page_across);
1904 *along = gtk_list_base_set_adjustment_values (self,
1905 orientation: priv->orientation,
1906 value: value_along,
1907 size: total_along,
1908 page_size: page_along);
1909}
1910
1911GtkScrollablePolicy
1912gtk_list_base_get_scroll_policy (GtkListBase *self,
1913 GtkOrientation orientation)
1914{
1915 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1916
1917 return priv->scroll_policy[orientation];
1918}
1919
1920GtkOrientation
1921gtk_list_base_get_orientation (GtkListBase *self)
1922{
1923 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1924
1925 return priv->orientation;
1926}
1927
1928GtkListItemManager *
1929gtk_list_base_get_manager (GtkListBase *self)
1930{
1931 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1932
1933 return priv->item_manager;
1934}
1935
1936guint
1937gtk_list_base_get_anchor (GtkListBase *self)
1938{
1939 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1940
1941 return gtk_list_item_tracker_get_position (self: priv->item_manager,
1942 tracker: priv->anchor);
1943}
1944
1945/*
1946 * gtk_list_base_set_anchor:
1947 * @self: a `GtkListBase`
1948 * @anchor_pos: position of the item to anchor
1949 * @anchor_align_across: how far in the across direction to anchor
1950 * @anchor_side_across: if the anchor should side to start or end of item
1951 * @anchor_align_along: how far in the along direction to anchor
1952 * @anchor_side_along: if the anchor should side to start or end of item
1953 *
1954 * Sets the anchor.
1955 * The anchor is the item that is always kept on screen.
1956 *
1957 * In each dimension, anchoring uses 2 variables: The side of the
1958 * item that gets anchored - either start or end - and where in
1959 * the widget's allocation it should get anchored - here 0.0 means
1960 * the start of the widget and 1.0 is the end of the widget.
1961 * It is allowed to use values outside of this range. In particular,
1962 * this is necessary when the items are larger than the list's
1963 * allocation.
1964 *
1965 * Using this information, the adjustment's value and in turn widget
1966 * offsets will then be computed. If the anchor is too far off, it
1967 * will be clamped so that there are always visible items on screen.
1968 *
1969 * Making anchoring this complicated ensures that one item - one
1970 * corner of one item to be exact - always stays at the same place
1971 * (usually this item is the focused item). So when the list undergoes
1972 * heavy changes (like sorting, filtering, removals, additions), this
1973 * item will stay in place while everything around it will shuffle
1974 * around.
1975 *
1976 * The anchor will also ensure that enough widgets are created according
1977 * to gtk_list_base_set_anchor_max_widgets().
1978 **/
1979void
1980gtk_list_base_set_anchor (GtkListBase *self,
1981 guint anchor_pos,
1982 double anchor_align_across,
1983 GtkPackType anchor_side_across,
1984 double anchor_align_along,
1985 GtkPackType anchor_side_along)
1986{
1987 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
1988 guint items_before;
1989
1990 items_before = round (x: priv->center_widgets * CLAMP (anchor_align_along, 0, 1));
1991 gtk_list_item_tracker_set_position (self: priv->item_manager,
1992 tracker: priv->anchor,
1993 position: anchor_pos,
1994 n_before: items_before + priv->above_below_widgets,
1995 n_after: priv->center_widgets - items_before + priv->above_below_widgets);
1996
1997 priv->anchor_align_across = anchor_align_across;
1998 priv->anchor_side_across = anchor_side_across;
1999 priv->anchor_align_along = anchor_align_along;
2000 priv->anchor_side_along = anchor_side_along;
2001
2002 gtk_widget_queue_allocate (GTK_WIDGET (self));
2003}
2004
2005/**
2006 * gtk_list_base_set_anchor_max_widgets:
2007 * @self: a `GtkListBase`
2008 * @center: the number of widgets in the middle
2009 * @above_below: extra widgets above and below
2010 *
2011 * Sets how many widgets should be kept alive around the anchor.
2012 * The number of these widgets determines how many items can be
2013 * displayed and must be chosen to be large enough to cover the
2014 * allocation but should be kept as small as possible for
2015 * performance reasons.
2016 *
2017 * There will be @center widgets allocated around the anchor
2018 * evenly distributed according to the anchor's alignment - if
2019 * the anchor is at the start, all these widgets will be allocated
2020 * behind it, if it's at the end, all the widgets will be allocated
2021 * in front of it.
2022 *
2023 * Addditionally, there will be @above_below widgets allocated both
2024 * before and after the sencter widgets, so the total number of
2025 * widgets kept alive is 2 * above_below + center + 1.
2026 **/
2027void
2028gtk_list_base_set_anchor_max_widgets (GtkListBase *self,
2029 guint n_center,
2030 guint n_above_below)
2031{
2032 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2033
2034 priv->center_widgets = n_center;
2035 priv->above_below_widgets = n_above_below;
2036
2037 gtk_list_base_set_anchor (self,
2038 anchor_pos: gtk_list_item_tracker_get_position (self: priv->item_manager, tracker: priv->anchor),
2039 anchor_align_across: priv->anchor_align_across,
2040 anchor_side_across: priv->anchor_side_across,
2041 anchor_align_along: priv->anchor_align_along,
2042 anchor_side_along: priv->anchor_side_along);
2043}
2044
2045/*
2046 * gtk_list_base_grab_focus_on_item:
2047 * @self: a `GtkListBase`
2048 * @pos: position of the item to focus
2049 * @select: %TRUE to select the item
2050 * @modify: if selecting, %TRUE to modify the selected
2051 * state, %FALSE to always select
2052 * @extend: if selecting, %TRUE to extend the selection,
2053 * %FALSE to only operate on this item
2054 *
2055 * Tries to grab focus on the given item. If there is no item
2056 * at this position or grabbing focus failed, %FALSE will be
2057 * returned.
2058 *
2059 * Returns: %TRUE if focusing the item succeeded
2060 **/
2061gboolean
2062gtk_list_base_grab_focus_on_item (GtkListBase *self,
2063 guint pos,
2064 gboolean select,
2065 gboolean modify,
2066 gboolean extend)
2067{
2068 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2069 GtkListItemManagerItem *item;
2070 gboolean success;
2071
2072 item = gtk_list_item_manager_get_nth (self: priv->item_manager, position: pos, NULL);
2073 if (item == NULL)
2074 return FALSE;
2075
2076 if (!item->widget)
2077 {
2078 GtkListItemTracker *tracker = gtk_list_item_tracker_new (self: priv->item_manager);
2079
2080 /* We need a tracker here to create the widget.
2081 * That needs to have happened or we can't grab it.
2082 * And we can't use a different tracker, because they manage important rows,
2083 * so we create a temporary one. */
2084 gtk_list_item_tracker_set_position (self: priv->item_manager, tracker, position: pos, n_before: 0, n_after: 0);
2085
2086 item = gtk_list_item_manager_get_nth (self: priv->item_manager, position: pos, NULL);
2087 g_assert (item->widget);
2088
2089 success = gtk_widget_grab_focus (widget: item->widget);
2090
2091 gtk_list_item_tracker_free (self: priv->item_manager, tracker);
2092 }
2093 else
2094 {
2095 success = gtk_widget_grab_focus (widget: item->widget);
2096 }
2097
2098 if (!success)
2099 return FALSE;
2100
2101 if (select)
2102 gtk_list_base_select_item (self, pos, modify, extend);
2103
2104 return TRUE;
2105}
2106
2107GtkSelectionModel *
2108gtk_list_base_get_model (GtkListBase *self)
2109{
2110 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2111
2112 return priv->model;
2113}
2114
2115gboolean
2116gtk_list_base_set_model (GtkListBase *self,
2117 GtkSelectionModel *model)
2118{
2119 GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self);
2120
2121 if (priv->model == model)
2122 return FALSE;
2123
2124 g_clear_object (&priv->model);
2125
2126 if (model)
2127 {
2128 priv->model = g_object_ref (model);
2129 gtk_list_item_manager_set_model (self: priv->item_manager, model);
2130 gtk_list_base_set_anchor (self, anchor_pos: 0, anchor_align_across: 0.0, anchor_side_across: GTK_PACK_START, anchor_align_along: 0.0, anchor_side_along: GTK_PACK_START);
2131 }
2132 else
2133 {
2134 gtk_list_item_manager_set_model (self: priv->item_manager, NULL);
2135 }
2136
2137 return TRUE;
2138}
2139

source code of gtk/gtk/gtklistbase.c