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); |
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 |
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 |
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 |
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 (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 | |
171 | static void |
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), 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 | |
248 | static void |
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 |
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 |
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 |
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 |
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 |
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 |
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 (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 | |
462 | guint |
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 (list: G_LIST_MODEL (ptr: priv->model)); |
471 | } |
472 | |
473 | guint |
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 (self: priv->item_manager, tracker: priv->focus); |
479 | } |
480 | |
481 | static gboolean |
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 (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 | |
566 | static void |
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, 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 | |
597 | static void |
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, 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 | |
634 | static void |
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 (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 | |
659 | static void |
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 | pspec: orientation == GTK_ORIENTATION_HORIZONTAL |
675 | ? properties[PROP_HSCROLL_POLICY] |
676 | : properties[PROP_VSCROLL_POLICY]); |
677 | } |
678 | |
679 | static void |
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, 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 | |
725 | static void |
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 | 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 | |
785 | static void |
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 (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 | |
807 | static void |
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 (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 | |
860 | static void |
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 (value: parameter, format_string: "(ubb)" , &pos, &modify, &extend); |
870 | |
871 | gtk_list_base_select_item (self, pos, modify, extend); |
872 | } |
873 | |
874 | static void |
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 (self: priv->item_manager); |
884 | if (selection_model == NULL) |
885 | return; |
886 | |
887 | gtk_selection_model_select_all (model: selection_model); |
888 | } |
889 | |
890 | static void |
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 (self: priv->item_manager); |
900 | if (selection_model == NULL) |
901 | return; |
902 | |
903 | gtk_selection_model_unselect_all (model: selection_model); |
904 | } |
905 | |
906 | static gboolean |
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 (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 | |
924 | static gboolean |
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 (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 | |
966 | static gboolean |
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 (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 | |
1013 | static gboolean |
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 (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 | |
1033 | static gboolean |
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 (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 | |
1054 | static void |
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 | 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 | |
1082 | static void |
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 | 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 | |
1109 | static void |
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 (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 | |
1234 | static gboolean |
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 (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 | |
1265 | static void |
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, 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 | |
1285 | static void |
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), id: priv->autoscroll_id); |
1293 | priv->autoscroll_id = 0; |
1294 | } |
1295 | } |
1296 | |
1297 | #define SCROLL_EDGE_SIZE 30 |
1298 | |
1299 | static void |
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 |
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 (widget: child, allocation: &child_allocation, baseline: -1); |
1397 | } |
1398 | |
1399 | static void |
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), 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 | |
1427 | static GtkBitset * |
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 |
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 (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 | |
1478 | void |
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: &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 | |
1503 | static void |
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_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 | |
1538 | static void |
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 (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 | |
1627 | static void |
1628 | gtk_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 | |
1660 | static void |
1661 | gtk_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 | |
1680 | static void |
1681 | get_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 | |
1701 | static void |
1702 | gtk_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 | |
1723 | static void |
1724 | gtk_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 | |
1736 | void |
1737 | gtk_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 | |
1761 | gboolean |
1762 | gtk_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 | |
1769 | static void |
1770 | gtk_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 | |
1780 | static void |
1781 | gtk_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 | |
1789 | static void |
1790 | gtk_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 | |
1824 | static int |
1825 | gtk_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 | |
1856 | void |
1857 | gtk_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 | |
1911 | GtkScrollablePolicy |
1912 | gtk_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 | |
1920 | GtkOrientation |
1921 | gtk_list_base_get_orientation (GtkListBase *self) |
1922 | { |
1923 | GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self); |
1924 | |
1925 | return priv->orientation; |
1926 | } |
1927 | |
1928 | GtkListItemManager * |
1929 | gtk_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 | |
1936 | guint |
1937 | gtk_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 | **/ |
1979 | void |
1980 | gtk_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 | **/ |
2027 | void |
2028 | gtk_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 | **/ |
2061 | gboolean |
2062 | gtk_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 | |
2107 | GtkSelectionModel * |
2108 | gtk_list_base_get_model (GtkListBase *self) |
2109 | { |
2110 | GtkListBasePrivate *priv = gtk_list_base_get_instance_private (self); |
2111 | |
2112 | return priv->model; |
2113 | } |
2114 | |
2115 | gboolean |
2116 | gtk_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 | |