1/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */
2/* gtkpathbar.c
3 * Copyright (C) 2004 Red Hat, Inc., Jonathan Blandford <jrb@gnome.org>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public
16 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#include "config.h"
20
21#include "gtkpathbar.h"
22
23#include <string.h>
24
25#include "gtkbox.h"
26#include "gtkdragsource.h"
27#include "gtkicontheme.h"
28#include "gtkimage.h"
29#include "gtkintl.h"
30#include "gtklabel.h"
31#include "gtkmain.h"
32#include "gtkmarshalers.h"
33#include "gtksettings.h"
34#include "gtktogglebutton.h"
35#include "gtkwidgetprivate.h"
36#include "gtkeventcontrollerscroll.h"
37#include "gtkdragsource.h"
38
39struct _GtkPathBar
40{
41 GtkWidget parent_instance;
42
43 GFile *root_file;
44 GFile *home_file;
45 GFile *desktop_file;
46
47 /* List of running GCancellable. When we cancel one, we remove it from this list.
48 * The pathbar cancels all outstanding cancellables when it is disposed.
49 *
50 * In code that queues async I/O operations:
51 *
52 * - Obtain a cancellable from the async I/O APIs, and call add_cancellable().
53 *
54 * To cancel a cancellable:
55 *
56 * - Call cancel_cancellable().
57 *
58 * In async I/O callbacks:
59 *
60 * - Check right away if g_cancellable_is_cancelled(): if true, just
61 * g_object_unref() the cancellable and return early (also free your
62 * closure data if you have one).
63 *
64 * - If it was not cancelled, call cancellable_async_done(). This will
65 * unref the cancellable and unqueue it from the pathbar's outstanding
66 * cancellables. Do your normal work to process the async result and free
67 * your closure data if you have one.
68 */
69 GList *cancellables;
70
71 GCancellable *get_info_cancellable;
72
73 GIcon *root_icon;
74 GIcon *home_icon;
75 GIcon *desktop_icon;
76
77 GList *button_list;
78 GList *first_scrolled_button;
79 GList *fake_root;
80 GtkWidget *up_slider_button;
81 GtkWidget *down_slider_button;
82 gint16 slider_width;
83};
84
85typedef struct _GtkPathBarClass GtkPathBarClass;
86
87struct _GtkPathBarClass
88{
89 GtkWidgetClass parent_class;
90
91 void (* path_clicked) (GtkPathBar *path_bar,
92 GFile *file,
93 GFile *child_file,
94 gboolean child_is_hidden);
95};
96
97enum {
98 PATH_CLICKED,
99 LAST_SIGNAL
100};
101
102typedef enum {
103 NORMAL_BUTTON,
104 ROOT_BUTTON,
105 HOME_BUTTON,
106 DESKTOP_BUTTON
107} ButtonType;
108
109#define BUTTON_DATA(x) ((ButtonData *)(x))
110
111static guint path_bar_signals [LAST_SIGNAL] = { 0 };
112
113typedef struct _ButtonData ButtonData;
114
115struct _ButtonData
116{
117 GtkWidget *button;
118 ButtonType type;
119 char *dir_name;
120 GFile *file;
121 GtkWidget *image;
122 GtkWidget *label;
123 GCancellable *cancellable;
124 guint ignore_changes : 1;
125 guint file_is_hidden : 1;
126};
127/* This macro is used to check if a button can be used as a fake root.
128 * All buttons in front of a fake root are automatically hidden when in a
129 * directory below a fake root and replaced with the "<" arrow button.
130 */
131#define BUTTON_IS_FAKE_ROOT(button) ((button)->type == HOME_BUTTON)
132
133G_DEFINE_TYPE (GtkPathBar, gtk_path_bar, GTK_TYPE_WIDGET)
134
135static void gtk_path_bar_finalize (GObject *object);
136static void gtk_path_bar_dispose (GObject *object);
137static void gtk_path_bar_measure (GtkWidget *widget,
138 GtkOrientation orientation,
139 int for_size,
140 int *minimum,
141 int *natural,
142 int *minimum_baseline,
143 int *natural_baseline);
144static void gtk_path_bar_size_allocate (GtkWidget *widget,
145 int width,
146 int height,
147 int baseline);
148static void gtk_path_bar_scroll_up (GtkPathBar *path_bar);
149static void gtk_path_bar_scroll_down (GtkPathBar *path_bar);
150static void gtk_path_bar_update_button_appearance (GtkPathBar *path_bar,
151 ButtonData *button_data,
152 gboolean current_dir);
153
154static gboolean gtk_path_bar_scroll_controller_scroll (GtkEventControllerScroll *scroll,
155 double dx,
156 double dy,
157 GtkPathBar *path_bar);
158
159static void
160add_cancellable (GtkPathBar *path_bar,
161 GCancellable *cancellable)
162{
163 g_assert (g_list_find (path_bar->cancellables, cancellable) == NULL);
164 path_bar->cancellables = g_list_prepend (list: path_bar->cancellables, data: cancellable);
165}
166
167static void
168drop_node_for_cancellable (GtkPathBar *path_bar,
169 GCancellable *cancellable)
170{
171 GList *node;
172
173 node = g_list_find (list: path_bar->cancellables, data: cancellable);
174 g_assert (node != NULL);
175 node->data = NULL;
176 path_bar->cancellables = g_list_delete_link (list: path_bar->cancellables, link_: node);
177}
178
179static void
180cancel_cancellable (GtkPathBar *path_bar,
181 GCancellable *cancellable)
182{
183 drop_node_for_cancellable (path_bar, cancellable);
184 g_cancellable_cancel (cancellable);
185}
186
187static void
188cancellable_async_done (GtkPathBar *path_bar,
189 GCancellable *cancellable)
190{
191 drop_node_for_cancellable (path_bar, cancellable);
192 g_object_unref (object: cancellable);
193}
194
195static void
196cancel_all_cancellables (GtkPathBar *path_bar)
197{
198 while (path_bar->cancellables)
199 {
200 GCancellable *cancellable = path_bar->cancellables->data;
201 cancel_cancellable (path_bar, cancellable);
202 }
203}
204
205static void
206gtk_path_bar_init (GtkPathBar *path_bar)
207{
208 GtkEventController *controller;
209 const char *home;
210
211 path_bar->up_slider_button = gtk_button_new_from_icon_name (icon_name: "pan-start-symbolic");
212 gtk_widget_add_css_class (widget: path_bar->up_slider_button, css_class: "slider-button");
213 gtk_widget_set_parent (widget: path_bar->up_slider_button, GTK_WIDGET (path_bar));
214
215 path_bar->down_slider_button = gtk_button_new_from_icon_name (icon_name: "pan-end-symbolic");
216 gtk_widget_add_css_class (widget: path_bar->down_slider_button, css_class: "slider-button");
217 gtk_widget_set_parent (widget: path_bar->down_slider_button, GTK_WIDGET (path_bar));
218
219 /* GtkBuilder won't let us connect 'swapped' without specifying the signal's
220 * user data in the .ui file
221 */
222 g_signal_connect_swapped (path_bar->up_slider_button, "clicked",
223 G_CALLBACK (gtk_path_bar_scroll_up), path_bar);
224 g_signal_connect_swapped (path_bar->down_slider_button, "clicked",
225 G_CALLBACK (gtk_path_bar_scroll_down), path_bar);
226
227 gtk_widget_add_css_class (GTK_WIDGET (path_bar), css_class: "linked");
228
229 path_bar->get_info_cancellable = NULL;
230 path_bar->cancellables = NULL;
231
232 controller = gtk_event_controller_scroll_new (flags: GTK_EVENT_CONTROLLER_SCROLL_VERTICAL |
233 GTK_EVENT_CONTROLLER_SCROLL_DISCRETE);
234 g_signal_connect (controller, "scroll",
235 G_CALLBACK (gtk_path_bar_scroll_controller_scroll),
236 path_bar);
237 gtk_widget_add_controller (GTK_WIDGET (path_bar), controller);
238
239 home = g_get_home_dir ();
240 if (home != NULL)
241 {
242 const char *desktop;
243
244 path_bar->home_file = g_file_new_for_path (path: home);
245 /* FIXME: Need file system backend specific way of getting the
246 * Desktop path.
247 */
248 desktop = g_get_user_special_dir (directory: G_USER_DIRECTORY_DESKTOP);
249 if (desktop != NULL)
250 path_bar->desktop_file = g_file_new_for_path (path: desktop);
251 else
252 path_bar->desktop_file = NULL;
253 }
254 else
255 {
256 path_bar->home_file = NULL;
257 path_bar->desktop_file = NULL;
258 }
259 path_bar->root_file = g_file_new_for_path (path: "/");
260}
261
262static void
263gtk_path_bar_class_init (GtkPathBarClass *path_bar_class)
264{
265 GObjectClass *gobject_class;
266 GtkWidgetClass *widget_class;
267
268 gobject_class = (GObjectClass *) path_bar_class;
269 widget_class = (GtkWidgetClass *) path_bar_class;
270
271 gobject_class->finalize = gtk_path_bar_finalize;
272 gobject_class->dispose = gtk_path_bar_dispose;
273
274 widget_class->measure = gtk_path_bar_measure;
275 widget_class->size_allocate = gtk_path_bar_size_allocate;
276
277 path_bar_signals [PATH_CLICKED] =
278 g_signal_new (I_("path-clicked"),
279 G_OBJECT_CLASS_TYPE (gobject_class),
280 signal_flags: G_SIGNAL_RUN_FIRST,
281 G_STRUCT_OFFSET (GtkPathBarClass, path_clicked),
282 NULL, NULL,
283 c_marshaller: _gtk_marshal_VOID__OBJECT_OBJECT_BOOLEAN,
284 G_TYPE_NONE, n_params: 3,
285 G_TYPE_FILE,
286 G_TYPE_FILE,
287 G_TYPE_BOOLEAN);
288
289 gtk_widget_class_set_css_name (widget_class, name: "pathbar");
290}
291
292static void
293gtk_path_bar_finalize (GObject *object)
294{
295 GtkPathBar *path_bar = GTK_PATH_BAR (object);
296
297 cancel_all_cancellables (path_bar);
298
299 g_list_free (list: path_bar->button_list);
300 g_clear_object (&path_bar->root_file);
301 g_clear_object (&path_bar->home_file);
302 g_clear_object (&path_bar->desktop_file);
303
304 g_clear_object (&path_bar->root_icon);
305 g_clear_object (&path_bar->home_icon);
306 g_clear_object (&path_bar->desktop_icon);
307
308 G_OBJECT_CLASS (gtk_path_bar_parent_class)->finalize (object);
309}
310
311static void
312gtk_path_bar_dispose (GObject *object)
313{
314 GtkPathBar *path_bar = GTK_PATH_BAR (object);
315 GtkWidget *w;
316
317 while ((w = gtk_widget_get_first_child (GTK_WIDGET (path_bar))) != NULL)
318 gtk_widget_unparent (widget: w);
319
320 path_bar->get_info_cancellable = NULL;
321 cancel_all_cancellables (path_bar);
322
323 G_OBJECT_CLASS (gtk_path_bar_parent_class)->dispose (object);
324}
325
326static void
327gtk_path_bar_measure (GtkWidget *widget,
328 GtkOrientation orientation,
329 int for_size,
330 int *minimum,
331 int *natural,
332 int *minimum_baseline,
333 int *natural_baseline)
334{
335 GtkPathBar *path_bar = GTK_PATH_BAR (widget);
336 ButtonData *button_data;
337 GList *list;
338 int child_size;
339 int size = 0;
340 int child_min, child_nat;
341
342 *minimum = 0;
343 *natural = 0;
344
345 if (orientation == GTK_ORIENTATION_HORIZONTAL)
346 {
347 for (list = path_bar->button_list; list; list = list->next)
348 {
349 button_data = BUTTON_DATA (list->data);
350 gtk_widget_measure (widget: button_data->button, orientation: GTK_ORIENTATION_HORIZONTAL, for_size: -1,
351 minimum: &child_min, natural: &child_nat, NULL, NULL);
352 gtk_widget_measure (widget: button_data->button, orientation: GTK_ORIENTATION_VERTICAL, for_size: -1,
353 minimum: &child_size, NULL, NULL, NULL);
354 size = MAX (size, child_size);
355
356 if (button_data->type == NORMAL_BUTTON)
357 {
358 /* Use 2*Height as button width because of ellipsized label. */
359 child_min = MAX (child_min, child_size * 2);
360 child_nat = MAX (child_min, child_size * 2);
361 }
362
363 *minimum = MAX (*minimum, child_min);
364 *natural = *natural + child_nat;
365 }
366
367 /* Add space for slider, if we have more than one path */
368 /* Theoretically, the slider could be bigger than the other button. But we're
369 * not going to worry about that now.
370 */
371 path_bar->slider_width = 0;
372
373 gtk_widget_measure (widget: path_bar->up_slider_button, orientation: GTK_ORIENTATION_HORIZONTAL, for_size: -1,
374 minimum: &child_min, natural: &child_nat, NULL, NULL);
375 if (path_bar->button_list && path_bar->button_list->next != NULL)
376 {
377 *minimum += child_min;
378 *natural += child_nat;
379 }
380 path_bar->slider_width = MAX (path_bar->slider_width, child_min);
381
382 gtk_widget_measure (widget: path_bar->down_slider_button, orientation: GTK_ORIENTATION_HORIZONTAL, for_size: -1,
383 minimum: &child_min, natural: &child_nat, NULL, NULL);
384 if (path_bar->button_list && path_bar->button_list->next != NULL)
385 {
386 *minimum += child_min;
387 *natural += child_nat;
388 }
389 path_bar->slider_width = MAX (path_bar->slider_width, child_min);
390
391 }
392 else /* VERTICAL */
393 {
394 for (list = path_bar->button_list; list; list = list->next)
395 {
396 button_data = BUTTON_DATA (list->data);
397 gtk_widget_measure (widget: button_data->button, orientation: GTK_ORIENTATION_VERTICAL, for_size: -1,
398 minimum: &child_min, natural: &child_nat, NULL, NULL);
399
400 *minimum = MAX (*minimum, child_min);
401 *natural = MAX (*natural, child_nat);
402 }
403
404 gtk_widget_measure (widget: path_bar->up_slider_button, orientation: GTK_ORIENTATION_VERTICAL, for_size: -1,
405 minimum: &child_min, natural: &child_nat, NULL, NULL);
406 *minimum = MAX (*minimum, child_min);
407 *natural = MAX (*natural, child_nat);
408
409 gtk_widget_measure (widget: path_bar->up_slider_button, orientation: GTK_ORIENTATION_VERTICAL, for_size: -1,
410 minimum: &child_min, natural: &child_nat, NULL, NULL);
411 *minimum = MAX (*minimum, child_min);
412 *natural = MAX (*natural, child_nat);
413 }
414}
415
416static void
417gtk_path_bar_update_slider_buttons (GtkPathBar *path_bar)
418{
419 if (path_bar->button_list)
420 {
421 GtkWidget *button;
422
423 button = BUTTON_DATA (path_bar->button_list->data)->button;
424 if (gtk_widget_get_child_visible (widget: button))
425 gtk_widget_set_sensitive (widget: path_bar->down_slider_button, FALSE);
426 else
427 gtk_widget_set_sensitive (widget: path_bar->down_slider_button, TRUE);
428
429 button = BUTTON_DATA (g_list_last (path_bar->button_list)->data)->button;
430 if (gtk_widget_get_child_visible (widget: button))
431 gtk_widget_set_sensitive (widget: path_bar->up_slider_button, FALSE);
432 else
433 gtk_widget_set_sensitive (widget: path_bar->up_slider_button, TRUE);
434 }
435}
436
437/* This is a tad complicated
438 */
439static void
440gtk_path_bar_size_allocate (GtkWidget *widget,
441 int widget_width,
442 int widget_height,
443 int baseline)
444{
445 GtkPathBar *path_bar = GTK_PATH_BAR (widget);
446 GtkWidget *child;
447 GtkTextDirection direction;
448 GtkAllocation child_allocation;
449 GList *list, *first_button;
450 int width;
451 int allocation_width;
452 gboolean need_sliders = TRUE;
453 int up_slider_offset = 0;
454 int down_slider_offset = 0;
455 GtkRequisition child_requisition;
456
457 /* No path is set; we don't have to allocate anything. */
458 if (path_bar->button_list == NULL)
459 return;
460
461 direction = gtk_widget_get_direction (widget);
462 allocation_width = widget_width;
463
464 /* First, we check to see if we need the scrollbars. */
465 if (path_bar->fake_root)
466 width = path_bar->slider_width;
467 else
468 width = 0;
469
470 for (list = path_bar->button_list; list; list = list->next)
471 {
472 child = BUTTON_DATA (list->data)->button;
473
474 gtk_widget_get_preferred_size (widget: child, minimum_size: &child_requisition, NULL);
475
476 width += child_requisition.width;
477 if (list == path_bar->fake_root)
478 break;
479 }
480
481 if (width <= allocation_width)
482 {
483 if (path_bar->fake_root)
484 first_button = path_bar->fake_root;
485 else
486 first_button = g_list_last (list: path_bar->button_list);
487 }
488 else
489 {
490 gboolean reached_end = FALSE;
491 int slider_space = 2 * path_bar->slider_width;
492
493 if (path_bar->first_scrolled_button)
494 first_button = path_bar->first_scrolled_button;
495 else
496 first_button = path_bar->button_list;
497 need_sliders = TRUE;
498
499 /* To see how much space we have, and how many buttons we can display.
500 * We start at the first button, count forward until hit the new
501 * button, then count backwards.
502 */
503 /* Count down the path chain towards the end. */
504 gtk_widget_get_preferred_size (BUTTON_DATA (first_button->data)->button,
505 minimum_size: &child_requisition, NULL);
506
507 width = child_requisition.width;
508 list = first_button->prev;
509 while (list && !reached_end)
510 {
511 child = BUTTON_DATA (list->data)->button;
512
513 gtk_widget_get_preferred_size (widget: child, minimum_size: &child_requisition, NULL);
514
515 if (width + child_requisition.width + slider_space > allocation_width)
516 reached_end = TRUE;
517 else if (list == path_bar->fake_root)
518 break;
519 else
520 width += child_requisition.width;
521
522 list = list->prev;
523 }
524
525 /* Finally, we walk up, seeing how many of the previous buttons we can
526 * add */
527 while (first_button->next && !reached_end)
528 {
529 child = BUTTON_DATA (first_button->next->data)->button;
530
531 gtk_widget_get_preferred_size (widget: child, minimum_size: &child_requisition, NULL);
532
533 if (width + child_requisition.width + slider_space > allocation_width)
534 {
535 reached_end = TRUE;
536 }
537 else
538 {
539 width += child_requisition.width;
540 if (first_button == path_bar->fake_root)
541 break;
542 first_button = first_button->next;
543 }
544 }
545 }
546
547 /* Now, we allocate space to the buttons */
548 child_allocation.y = 0;
549 child_allocation.height = widget_height;
550
551 if (direction == GTK_TEXT_DIR_RTL)
552 {
553 child_allocation.x = widget_width;
554 if (need_sliders || path_bar->fake_root)
555 {
556 child_allocation.x -= path_bar->slider_width;
557 up_slider_offset = widget_width - path_bar->slider_width;
558 }
559 }
560 else
561 {
562 child_allocation.x = 0;
563 if (need_sliders || path_bar->fake_root)
564 {
565 up_slider_offset = 0;
566 child_allocation.x += path_bar->slider_width;
567 }
568 }
569
570 for (list = first_button; list; list = list->prev)
571 {
572 GtkAllocation widget_allocation;
573 ButtonData *button_data;
574
575 button_data = BUTTON_DATA (list->data);
576 child = button_data->button;
577
578 gtk_widget_get_preferred_size (widget: child, minimum_size: &child_requisition, NULL);
579
580 child_allocation.width = MIN (child_requisition.width,
581 allocation_width - 2 * path_bar->slider_width);
582
583 if (direction == GTK_TEXT_DIR_RTL)
584 child_allocation.x -= child_allocation.width;
585
586 /* Check to see if we've don't have any more space to allocate buttons */
587 if (need_sliders && direction == GTK_TEXT_DIR_RTL)
588 {
589 gtk_widget_get_allocation (widget, allocation: &widget_allocation);
590 if (child_allocation.x - path_bar->slider_width < widget_allocation.x)
591 break;
592 }
593 else if (need_sliders && direction == GTK_TEXT_DIR_LTR)
594 {
595 gtk_widget_get_allocation (widget, allocation: &widget_allocation);
596 if (child_allocation.x + child_allocation.width + path_bar->slider_width >
597 widget_allocation.x + allocation_width)
598 break;
599 }
600
601 if (child_allocation.width < child_requisition.width)
602 {
603 if (!gtk_widget_get_has_tooltip (widget: child))
604 gtk_widget_set_tooltip_text (widget: child, text: button_data->dir_name);
605 }
606 else if (gtk_widget_get_has_tooltip (widget: child))
607 gtk_widget_set_tooltip_text (widget: child, NULL);
608
609 gtk_widget_set_child_visible (widget: child, TRUE);
610 gtk_widget_size_allocate (widget: child, allocation: &child_allocation, baseline);
611
612 if (direction == GTK_TEXT_DIR_RTL)
613 {
614 down_slider_offset = child_allocation.x - path_bar->slider_width;
615 }
616 else
617 {
618 down_slider_offset += child_allocation.width;
619 child_allocation.x += child_allocation.width;
620 }
621 }
622 /* Now we go hide all the widgets that don't fit */
623 while (list)
624 {
625 child = BUTTON_DATA (list->data)->button;
626 gtk_widget_set_child_visible (widget: child, FALSE);
627 list = list->prev;
628 }
629 for (list = first_button->next; list; list = list->next)
630 {
631 child = BUTTON_DATA (list->data)->button;
632 gtk_widget_set_child_visible (widget: child, FALSE);
633 }
634
635 if (need_sliders || path_bar->fake_root)
636 {
637 child_allocation.width = path_bar->slider_width;
638 child_allocation.x = up_slider_offset;
639 gtk_widget_size_allocate (widget: path_bar->up_slider_button,
640 allocation: &child_allocation,
641 baseline: -1);
642
643 gtk_widget_set_child_visible (widget: path_bar->up_slider_button, TRUE);
644 gtk_widget_show (widget: path_bar->up_slider_button);
645
646 if (direction == GTK_TEXT_DIR_LTR)
647 down_slider_offset += path_bar->slider_width;
648 }
649 else
650 {
651 gtk_widget_set_child_visible (widget: path_bar->up_slider_button, FALSE);
652 }
653
654 if (need_sliders)
655 {
656 child_allocation.width = path_bar->slider_width;
657 child_allocation.x = down_slider_offset;
658
659 gtk_widget_size_allocate (widget: path_bar->down_slider_button,
660 allocation: &child_allocation,
661 baseline: -1);
662
663 gtk_widget_set_child_visible (widget: path_bar->down_slider_button, TRUE);
664 gtk_widget_show (widget: path_bar->down_slider_button);
665 gtk_path_bar_update_slider_buttons (path_bar);
666 }
667 else
668 {
669 gtk_widget_set_child_visible (widget: path_bar->down_slider_button, FALSE);
670 }
671}
672
673static gboolean
674gtk_path_bar_scroll_controller_scroll (GtkEventControllerScroll *scroll,
675 double dx,
676 double dy,
677 GtkPathBar *path_bar)
678{
679 if (dy > 0)
680 gtk_path_bar_scroll_down (path_bar);
681 else if (dy < 0)
682 gtk_path_bar_scroll_up (path_bar);
683
684 return GDK_EVENT_STOP;
685}
686
687static void
688gtk_path_bar_scroll_down (GtkPathBar *path_bar)
689{
690 GtkAllocation allocation, button_allocation;
691 GList *list;
692 GList *down_button = NULL;
693 int space_available;
694
695 if (gtk_widget_get_child_visible (BUTTON_DATA (path_bar->button_list->data)->button))
696 {
697 /* Return if the last button is already visible */
698 return;
699 }
700
701 gtk_widget_queue_resize (GTK_WIDGET (path_bar));
702
703 /* We find the button at the 'down' end that we have to make
704 * visible */
705 for (list = path_bar->button_list; list; list = list->next)
706 {
707 if (list->next && gtk_widget_get_child_visible (BUTTON_DATA (list->next->data)->button))
708 {
709 down_button = list;
710 break;
711 }
712 }
713 g_assert (down_button);
714
715 gtk_widget_get_allocation (GTK_WIDGET (path_bar), allocation: &allocation);
716 gtk_widget_get_allocation (BUTTON_DATA (down_button->data)->button, allocation: &button_allocation);
717
718 space_available = (allocation.width
719 - 2 * path_bar->slider_width
720 - button_allocation.width);
721 path_bar->first_scrolled_button = down_button;
722
723 /* We have space_available free space that's not being used.
724 * So we walk down from the end, adding buttons until we use all free space.
725 */
726 while (space_available > 0)
727 {
728 path_bar->first_scrolled_button = down_button;
729 down_button = down_button->next;
730 if (!down_button)
731 break;
732 space_available -= button_allocation.width;
733 }
734}
735
736static void
737gtk_path_bar_scroll_up (GtkPathBar *path_bar)
738{
739 GList *list;
740
741 list = g_list_last (list: path_bar->button_list);
742
743 if (gtk_widget_get_child_visible (BUTTON_DATA (list->data)->button))
744 {
745 /* Return if the first button is already visible */
746 return;
747 }
748
749 gtk_widget_queue_resize (GTK_WIDGET (path_bar));
750
751 for ( ; list; list = list->prev)
752 {
753 if (list->prev && gtk_widget_get_child_visible (BUTTON_DATA (list->prev->data)->button))
754 {
755 if (list->prev == path_bar->fake_root)
756 path_bar->fake_root = NULL;
757 path_bar->first_scrolled_button = list;
758 return;
759 }
760 }
761}
762
763static void
764gtk_path_bar_clear_buttons (GtkPathBar *path_bar)
765{
766 GtkWidget *w;
767
768 w = gtk_widget_get_first_child (GTK_WIDGET (path_bar));
769 while (w)
770 {
771 GtkWidget *next = gtk_widget_get_next_sibling (widget: w);
772
773 if (w != path_bar->up_slider_button && w != path_bar->down_slider_button)
774 {
775 gtk_widget_unparent (widget: w);
776 }
777
778 w = next;
779 }
780
781 path_bar->first_scrolled_button = NULL;
782 path_bar->fake_root = NULL;
783}
784
785static void
786button_clicked_cb (GtkWidget *button,
787 gpointer data)
788{
789 GtkPathBar *path_bar;
790 ButtonData *button_data;
791 GList *button_list;
792 gboolean child_is_hidden;
793 GFile *child_file;
794
795 button_data = BUTTON_DATA (data);
796 if (button_data->ignore_changes)
797 return;
798
799 path_bar = GTK_PATH_BAR (gtk_widget_get_parent (button));
800
801 button_list = g_list_find (list: path_bar->button_list, data: button_data);
802 g_assert (button_list != NULL);
803
804 g_signal_handlers_block_by_func (button,
805 G_CALLBACK (button_clicked_cb), data);
806 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
807 g_signal_handlers_unblock_by_func (button,
808 G_CALLBACK (button_clicked_cb), data);
809
810 if (button_list->prev)
811 {
812 ButtonData *child_data;
813
814 child_data = BUTTON_DATA (button_list->prev->data);
815 child_file = child_data->file;
816 child_is_hidden = child_data->file_is_hidden;
817 }
818 else
819 {
820 child_file = NULL;
821 child_is_hidden = FALSE;
822 }
823
824 g_signal_emit (instance: path_bar, signal_id: path_bar_signals [PATH_CLICKED], detail: 0,
825 button_data->file, child_file, child_is_hidden);
826}
827
828struct SetButtonImageData
829{
830 GtkPathBar *path_bar;
831 ButtonData *button_data;
832};
833
834static void
835set_button_image_get_info_cb (GObject *source,
836 GAsyncResult *result,
837 gpointer user_data)
838{
839 GFile *file = G_FILE (source);
840 struct SetButtonImageData *data = user_data;
841 GFileInfo *info;
842 GIcon *icon;
843
844 info = g_file_query_info_finish (file, res: result, NULL);
845 if (!info)
846 goto out;
847
848 g_assert (GTK_IS_PATH_BAR (data->path_bar));
849 g_assert (G_OBJECT (data->path_bar)->ref_count > 0);
850
851 cancellable_async_done (path_bar: data->path_bar, cancellable: data->button_data->cancellable);
852 data->button_data->cancellable = NULL;
853
854 icon = g_file_info_get_symbolic_icon (info);
855 gtk_image_set_from_gicon (GTK_IMAGE (data->button_data->image), icon);
856
857 switch (data->button_data->type)
858 {
859 case HOME_BUTTON:
860 g_set_object (&data->path_bar->home_icon, icon);
861 break;
862
863 case DESKTOP_BUTTON:
864 g_set_object (&data->path_bar->desktop_icon, icon);
865 break;
866
867 case NORMAL_BUTTON:
868 case ROOT_BUTTON:
869 default:
870 break;
871 };
872
873out:
874 g_free (mem: data);
875}
876
877static void
878set_button_image (GtkPathBar *path_bar,
879 ButtonData *button_data)
880{
881 struct SetButtonImageData *data;
882 GMount *mount;
883
884 switch (button_data->type)
885 {
886 case ROOT_BUTTON:
887
888 if (path_bar->root_icon != NULL)
889 {
890 gtk_image_set_from_gicon (GTK_IMAGE (button_data->image), icon: path_bar->root_icon);
891 break;
892 }
893
894 mount = g_file_find_enclosing_mount (file: button_data->file, NULL, NULL);
895
896 if (!mount && g_file_is_native (file: button_data->file))
897 path_bar->root_icon = g_themed_icon_new (iconname: "drive-harddisk-symbolic");
898 else if (mount)
899 path_bar->root_icon = g_mount_get_symbolic_icon (mount);
900 else
901 path_bar->root_icon = NULL;
902
903 g_clear_object (&mount);
904
905 gtk_image_set_from_gicon (GTK_IMAGE (button_data->image), icon: path_bar->root_icon);
906
907 break;
908
909 case HOME_BUTTON:
910 if (path_bar->home_icon != NULL)
911 {
912 gtk_image_set_from_gicon (GTK_IMAGE (button_data->image), icon: path_bar->home_icon);
913 break;
914 }
915
916 data = g_new0 (struct SetButtonImageData, 1);
917 data->path_bar = path_bar;
918 data->button_data = button_data;
919
920 if (button_data->cancellable)
921 {
922 cancel_cancellable (path_bar, cancellable: button_data->cancellable);
923 g_clear_object (&button_data->cancellable);
924 }
925
926 button_data->cancellable = g_cancellable_new ();
927 g_file_query_info_async (file: path_bar->home_file,
928 attributes: "standard::symbolic-icon",
929 flags: G_FILE_QUERY_INFO_NONE,
930 G_PRIORITY_DEFAULT,
931 cancellable: button_data->cancellable,
932 callback: set_button_image_get_info_cb,
933 user_data: data);
934 add_cancellable (path_bar, cancellable: button_data->cancellable);
935 break;
936
937 case DESKTOP_BUTTON:
938 if (path_bar->desktop_icon != NULL)
939 {
940 gtk_image_set_from_gicon (GTK_IMAGE (button_data->image), icon: path_bar->desktop_icon);
941 break;
942 }
943
944 data = g_new0 (struct SetButtonImageData, 1);
945 data->path_bar = path_bar;
946 data->button_data = button_data;
947
948 if (button_data->cancellable)
949 {
950 cancel_cancellable (path_bar, cancellable: button_data->cancellable);
951 g_clear_object (&button_data->cancellable);
952 }
953
954 button_data->cancellable = g_cancellable_new ();
955 g_file_query_info_async (file: path_bar->desktop_file,
956 attributes: "standard::symbolic-icon",
957 flags: G_FILE_QUERY_INFO_NONE,
958 G_PRIORITY_DEFAULT,
959 cancellable: button_data->cancellable,
960 callback: set_button_image_get_info_cb,
961 user_data: data);
962 add_cancellable (path_bar, cancellable: button_data->cancellable);
963 break;
964
965 case NORMAL_BUTTON:
966 default:
967 break;
968 }
969}
970
971static void
972button_data_free (ButtonData *button_data)
973{
974 g_clear_object (&button_data->file);
975 g_free (mem: button_data->dir_name);
976 g_free (mem: button_data);
977}
978
979static const char *
980get_dir_name (ButtonData *button_data)
981{
982 return button_data->dir_name;
983}
984
985static void
986gtk_path_bar_update_button_appearance (GtkPathBar *path_bar,
987 ButtonData *button_data,
988 gboolean current_dir)
989{
990 const char *dir_name = get_dir_name (button_data);
991
992 gtk_widget_remove_css_class (widget: button_data->button, css_class: "text-button");
993 gtk_widget_remove_css_class (widget: button_data->button, css_class: "image-button");
994
995 if (button_data->label != NULL)
996 {
997 gtk_label_set_text (GTK_LABEL (button_data->label), str: dir_name);
998 if (button_data->image == NULL)
999 gtk_widget_add_css_class (widget: button_data->button, css_class: "text-button");
1000 }
1001
1002 if (button_data->image != NULL)
1003 {
1004 set_button_image (path_bar, button_data);
1005 if (button_data->label == NULL)
1006 gtk_widget_add_css_class (widget: button_data->button, css_class: "image-button");
1007 }
1008
1009 if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button_data->button)) != current_dir)
1010 {
1011 button_data->ignore_changes = TRUE;
1012 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button_data->button), is_active: current_dir);
1013 button_data->ignore_changes = FALSE;
1014 }
1015}
1016
1017static ButtonType
1018find_button_type (GtkPathBar *path_bar,
1019 GFile *file)
1020{
1021 if (path_bar->root_file != NULL &&
1022 g_file_equal (file1: file, file2: path_bar->root_file))
1023 return ROOT_BUTTON;
1024 if (path_bar->home_file != NULL &&
1025 g_file_equal (file1: file, file2: path_bar->home_file))
1026 return HOME_BUTTON;
1027 if (path_bar->desktop_file != NULL &&
1028 g_file_equal (file1: file, file2: path_bar->desktop_file))
1029 return DESKTOP_BUTTON;
1030
1031 return NORMAL_BUTTON;
1032}
1033
1034static ButtonData *
1035make_directory_button (GtkPathBar *path_bar,
1036 const char *dir_name,
1037 GFile *file,
1038 gboolean current_dir,
1039 gboolean file_is_hidden)
1040{
1041 GtkWidget *child = NULL;
1042 ButtonData *button_data;
1043 GdkContentProvider *content;
1044 GtkDragSource *source;
1045
1046 file_is_hidden = !! file_is_hidden;
1047 /* Is it a special button? */
1048 button_data = g_new0 (ButtonData, 1);
1049 button_data->type = find_button_type (path_bar, file);
1050 button_data->button = gtk_toggle_button_new ();
1051 gtk_widget_set_focus_on_click (widget: button_data->button, FALSE);
1052
1053 switch (button_data->type)
1054 {
1055 case ROOT_BUTTON:
1056 button_data->image = gtk_image_new ();
1057 child = button_data->image;
1058 button_data->label = NULL;
1059 break;
1060 case HOME_BUTTON:
1061 case DESKTOP_BUTTON:
1062 button_data->image = gtk_image_new ();
1063 button_data->label = gtk_label_new (NULL);
1064 child = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 0);
1065 gtk_box_append (GTK_BOX (child), child: button_data->image);
1066 gtk_box_append (GTK_BOX (child), child: button_data->label);
1067 break;
1068 case NORMAL_BUTTON:
1069 default:
1070 button_data->label = gtk_label_new (NULL);
1071 child = button_data->label;
1072 button_data->image = NULL;
1073 }
1074
1075 button_data->dir_name = g_strdup (str: dir_name);
1076 button_data->file = g_object_ref (file);
1077 button_data->file_is_hidden = file_is_hidden;
1078
1079 gtk_button_set_child (GTK_BUTTON (button_data->button), child);
1080
1081 gtk_path_bar_update_button_appearance (path_bar, button_data, current_dir);
1082
1083 g_signal_connect (button_data->button, "clicked",
1084 G_CALLBACK (button_clicked_cb),
1085 button_data);
1086 g_object_weak_ref (G_OBJECT (button_data->button),
1087 notify: (GWeakNotify) button_data_free, data: button_data);
1088
1089 source = gtk_drag_source_new ();
1090 content = gdk_content_provider_new_typed (G_TYPE_FILE, button_data->file);
1091 gtk_drag_source_set_content (source, content);
1092 g_object_unref (object: content);
1093 gtk_widget_add_controller (widget: button_data->button, GTK_EVENT_CONTROLLER (source));
1094
1095 return button_data;
1096}
1097
1098static gboolean
1099gtk_path_bar_check_parent_path (GtkPathBar *path_bar,
1100 GFile *file)
1101{
1102 GList *list;
1103 GList *current_path = NULL;
1104 gboolean need_new_fake_root = FALSE;
1105
1106 for (list = path_bar->button_list; list; list = list->next)
1107 {
1108 ButtonData *button_data;
1109
1110 button_data = list->data;
1111 if (g_file_equal (file1: file, file2: button_data->file))
1112 {
1113 current_path = list;
1114 break;
1115 }
1116 if (list == path_bar->fake_root)
1117 need_new_fake_root = TRUE;
1118 }
1119
1120 if (current_path)
1121 {
1122 if (need_new_fake_root)
1123 {
1124 path_bar->fake_root = NULL;
1125 for (list = current_path; list; list = list->next)
1126 {
1127 ButtonData *button_data;
1128
1129 button_data = list->data;
1130 if (BUTTON_IS_FAKE_ROOT (button_data))
1131 {
1132 path_bar->fake_root = list;
1133 break;
1134 }
1135 }
1136 }
1137
1138 for (list = path_bar->button_list; list; list = list->next)
1139 {
1140 gtk_path_bar_update_button_appearance (path_bar,
1141 BUTTON_DATA (list->data),
1142 current_dir: (list == current_path) ? TRUE : FALSE);
1143 }
1144
1145 if (!gtk_widget_get_child_visible (BUTTON_DATA (current_path->data)->button))
1146 {
1147 path_bar->first_scrolled_button = current_path;
1148 gtk_widget_queue_resize (GTK_WIDGET (path_bar));
1149 }
1150
1151 return TRUE;
1152 }
1153 return FALSE;
1154}
1155
1156
1157struct SetFileInfo
1158{
1159 GFile *file;
1160 GFile *parent_file;
1161 GtkPathBar *path_bar;
1162 GList *new_buttons;
1163 GList *fake_root;
1164 GCancellable *cancellable;
1165 gboolean first_directory;
1166};
1167
1168static void
1169gtk_path_bar_set_file_finish (struct SetFileInfo *info,
1170 gboolean result)
1171{
1172 if (result)
1173 {
1174 GList *l;
1175
1176 gtk_path_bar_clear_buttons (path_bar: info->path_bar);
1177 info->path_bar->button_list = g_list_reverse (list: info->new_buttons);
1178 info->path_bar->fake_root = info->fake_root;
1179
1180 for (l = info->path_bar->button_list; l; l = l->next)
1181 {
1182 GtkWidget *button = BUTTON_DATA (l->data)->button;
1183
1184 gtk_widget_insert_after (widget: button, GTK_WIDGET (info->path_bar), previous_sibling: info->path_bar->up_slider_button);
1185 }
1186 }
1187 else
1188 {
1189 GList *l;
1190
1191 for (l = info->new_buttons; l; l = l->next)
1192 {
1193 ButtonData *button_data = BUTTON_DATA (l->data);
1194
1195 gtk_widget_unparent (widget: button_data->button);
1196 }
1197
1198 g_list_free (list: info->new_buttons);
1199 }
1200
1201 if (info->file)
1202 g_object_unref (object: info->file);
1203 if (info->parent_file)
1204 g_object_unref (object: info->parent_file);
1205
1206 g_free (mem: info);
1207}
1208
1209static void
1210gtk_path_bar_get_info_callback (GObject *source,
1211 GAsyncResult *result,
1212 gpointer data)
1213{
1214 GFile *file = G_FILE (source);
1215 struct SetFileInfo *file_info = data;
1216 GFileInfo *info;
1217 ButtonData *button_data;
1218 const char *display_name;
1219 gboolean is_hidden;
1220
1221 info = g_file_query_info_finish (file, res: result, NULL);
1222 if (!info)
1223 {
1224 gtk_path_bar_set_file_finish (info: file_info, FALSE);
1225 return;
1226 }
1227
1228 g_assert (GTK_IS_PATH_BAR (file_info->path_bar));
1229 g_assert (G_OBJECT (file_info->path_bar)->ref_count > 0);
1230
1231 cancellable_async_done (path_bar: file_info->path_bar, cancellable: file_info->cancellable);
1232 if (file_info->path_bar->get_info_cancellable == file_info->cancellable)
1233 file_info->path_bar->get_info_cancellable = NULL;
1234 file_info->cancellable = NULL;
1235
1236 display_name = g_file_info_get_display_name (info);
1237 is_hidden = g_file_info_get_is_hidden (info) || g_file_info_get_is_backup (info);
1238
1239 button_data = make_directory_button (path_bar: file_info->path_bar, dir_name: display_name,
1240 file: file_info->file,
1241 current_dir: file_info->first_directory, file_is_hidden: is_hidden);
1242 g_clear_object (&file_info->file);
1243
1244 file_info->new_buttons = g_list_prepend (list: file_info->new_buttons, data: button_data);
1245
1246 if (BUTTON_IS_FAKE_ROOT (button_data))
1247 file_info->fake_root = file_info->new_buttons;
1248
1249 /* We have assigned the info for the innermost button, i.e. the deepest directory.
1250 * Now, go on to fetch the info for this directory's parent.
1251 */
1252
1253 file_info->file = file_info->parent_file;
1254 file_info->first_directory = FALSE;
1255
1256 if (!file_info->file)
1257 {
1258 /* No parent? Okay, we are done. */
1259 gtk_path_bar_set_file_finish (info: file_info, TRUE);
1260 return;
1261 }
1262
1263 file_info->parent_file = g_file_get_parent (file: file_info->file);
1264
1265 /* Recurse asynchronously */
1266 file_info->cancellable = g_cancellable_new ();
1267 file_info->path_bar->get_info_cancellable = file_info->cancellable;
1268 g_file_query_info_async (file: file_info->file,
1269 attributes: "standard::display-name,"
1270 "standard::is-hidden,"
1271 "standard::is-backup",
1272 flags: G_FILE_QUERY_INFO_NONE,
1273 G_PRIORITY_DEFAULT,
1274 cancellable: file_info->cancellable,
1275 callback: gtk_path_bar_get_info_callback,
1276 user_data: file_info);
1277 add_cancellable (path_bar: file_info->path_bar, cancellable: file_info->cancellable);
1278}
1279
1280void
1281_gtk_path_bar_set_file (GtkPathBar *path_bar,
1282 GFile *file,
1283 gboolean keep_trail)
1284{
1285 struct SetFileInfo *info;
1286
1287 g_return_if_fail (GTK_IS_PATH_BAR (path_bar));
1288 g_return_if_fail (G_IS_FILE (file));
1289
1290 /* Check whether the new path is already present in the pathbar as buttons.
1291 * This could be a parent directory or a previous selected subdirectory.
1292 */
1293 if (keep_trail && gtk_path_bar_check_parent_path (path_bar, file))
1294 return;
1295
1296 info = g_new0 (struct SetFileInfo, 1);
1297 info->file = g_object_ref (file);
1298 info->path_bar = path_bar;
1299 info->first_directory = TRUE;
1300 info->parent_file = g_file_get_parent (file: info->file);
1301
1302 if (path_bar->get_info_cancellable)
1303 cancel_cancellable (path_bar, cancellable: path_bar->get_info_cancellable);
1304
1305 info->cancellable = g_cancellable_new ();
1306 path_bar->get_info_cancellable = info->cancellable;
1307 g_file_query_info_async (file: info->file,
1308 attributes: "standard::display-name,"
1309 "standard::is-hidden,"
1310 "standard::is-backup",
1311 flags: G_FILE_QUERY_INFO_NONE,
1312 G_PRIORITY_DEFAULT,
1313 cancellable: info->cancellable,
1314 callback: gtk_path_bar_get_info_callback,
1315 user_data: info);
1316 add_cancellable (path_bar, cancellable: info->cancellable);
1317}
1318
1319/**
1320 * _gtk_path_bar_up:
1321 * @path_bar: a `GtkPathBar`
1322 *
1323 * If the selected button in the pathbar is not the furthest button “up” (in the
1324 * root direction), act as if the user clicked on the next button up.
1325 **/
1326void
1327_gtk_path_bar_up (GtkPathBar *path_bar)
1328{
1329 GList *l;
1330
1331 for (l = path_bar->button_list; l; l = l->next)
1332 {
1333 GtkWidget *button = BUTTON_DATA (l->data)->button;
1334 if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
1335 {
1336 if (l->next)
1337 {
1338 GtkWidget *next_button = BUTTON_DATA (l->next->data)->button;
1339 button_clicked_cb (button: next_button, data: l->next->data);
1340 }
1341 break;
1342 }
1343 }
1344}
1345
1346/**
1347 * _gtk_path_bar_down:
1348 * @path_bar: a `GtkPathBar`
1349 *
1350 * If the selected button in the pathbar is not the furthest button “down” (in the
1351 * leaf direction), act as if the user clicked on the next button down.
1352 **/
1353void
1354_gtk_path_bar_down (GtkPathBar *path_bar)
1355{
1356 GList *l;
1357
1358 for (l = path_bar->button_list; l; l = l->next)
1359 {
1360 GtkWidget *button = BUTTON_DATA (l->data)->button;
1361 if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
1362 {
1363 if (l->prev)
1364 {
1365 GtkWidget *prev_button = BUTTON_DATA (l->prev->data)->button;
1366 button_clicked_cb (button: prev_button, data: l->prev->data);
1367 }
1368 break;
1369 }
1370 }
1371}
1372

source code of gtk/gtk/gtkpathbar.c