thunar-usagebars/thunar/thunar-location-buttons.c
4aiman e969f94310 thunar 4.20.2 upstream
Tree of tag thunar-4.20.2 (commit 11a519f) from https://gitlab.xfce.org/xfce/thunar.
Upstream history is not included here to keep the repository small.
2026-11-03 22:42:48 +03:00

1332 lines
47 KiB
C

/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
* Copyright (c) 2005-2006 Benedikt Meurer <benny@xfce.org>
* Copyright (c) 2009 Jannis Pohlmann <jannis@xfce.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 59 Temple
* Place, Suite 330, Boston, MA 02111-1307 USA
*
* Based on code written by Jonathan Blandford <jrb@gnome.org> for
* Red Hat, Inc.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "thunar/thunar-application.h"
#include "thunar/thunar-clipboard-manager.h"
#include "thunar/thunar-dialogs.h"
#include "thunar/thunar-gio-extensions.h"
#include "thunar/thunar-gobject-extensions.h"
#include "thunar/thunar-gtk-extensions.h"
#include "thunar/thunar-location-button.h"
#include "thunar/thunar-location-buttons.h"
#include "thunar/thunar-menu.h"
#include "thunar/thunar-preferences.h"
#include "thunar/thunar-private.h"
#include "thunar/thunar-properties-dialog.h"
#define THUNAR_LOCATION_BUTTON_MAX_WIDTH 350
#define THUNAR_LOCATION_BUTTON_FACTOR 0.9
#define THUNAR_LOCATION_BUTTONS_SCROLL_TIMEOUT 200
/* Property identifiers */
enum
{
PROP_0,
PROP_CURRENT_DIRECTORY,
};
static void
thunar_location_buttons_navigator_init (ThunarNavigatorIface *iface);
static void
thunar_location_buttons_finalize (GObject *object);
static void
thunar_location_buttons_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec);
static void
thunar_location_buttons_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec);
static ThunarFile *
thunar_location_buttons_get_current_directory (ThunarNavigator *navigator);
static void
thunar_location_buttons_set_current_directory (ThunarNavigator *navigator,
ThunarFile *current_directory);
static void
thunar_location_buttons_unmap (GtkWidget *widget);
static void
thunar_location_buttons_on_filler_clicked (ThunarLocationButtons *buttons);
static void
thunar_location_buttons_get_preferred_width (GtkWidget *widget,
gint *minimum,
gint *natural);
static void
thunar_location_buttons_get_preferred_height (GtkWidget *widget,
gint *minimum,
gint *natural);
static GtkWidgetPath *
thunar_location_buttons_get_path_for_child (GtkContainer *container,
GtkWidget *child);
static void
thunar_location_buttons_child_ordering_changed (ThunarLocationButtons *buttons);
static GList *
thunar_location_buttons_detect_first_visible_button (ThunarLocationButtons *buttons,
gint available_width,
gint *resulting_occupied_width);
static GList *
thunar_location_buttons_detect_last_visible_button (ThunarLocationButtons *buttons,
gint available_width,
gint *resulting_occupied_width);
static void
thunar_location_buttons_size_allocate (GtkWidget *widget,
GtkAllocation *allocation);
static void
thunar_location_buttons_state_changed (GtkWidget *widget,
GtkStateType previous_state);
static void
thunar_location_buttons_grab_notify (GtkWidget *widget,
gboolean was_grabbed);
static void
thunar_location_buttons_add (GtkContainer *container,
GtkWidget *widget);
static void
thunar_location_buttons_remove (GtkContainer *container,
GtkWidget *widget);
static void
thunar_location_buttons_forall (GtkContainer *container,
gboolean include_internals,
GtkCallback callback,
gpointer callback_data);
static GtkWidget *
thunar_location_buttons_make_button (ThunarLocationButtons *buttons,
ThunarFile *file);
static void
thunar_location_buttons_remove_1 (GtkContainer *container,
GtkWidget *widget);
static gboolean
thunar_location_buttons_draw (GtkWidget *buttons,
cairo_t *cr);
static gboolean
thunar_location_buttons_scroll_timeout (gpointer user_data);
static void
thunar_location_buttons_scroll_timeout_destroy (gpointer user_data);
static void
thunar_location_buttons_stop_scrolling (ThunarLocationButtons *buttons);
static void
thunar_location_buttons_update_sliders (ThunarLocationButtons *buttons);
static gboolean
thunar_location_buttons_slider_button_press (GtkWidget *button,
GdkEventButton *event,
ThunarLocationButtons *buttons);
static gboolean
thunar_location_buttons_slider_button_release (GtkWidget *button,
GdkEventButton *event,
ThunarLocationButtons *buttons);
static void
thunar_location_buttons_scroll_left (GtkWidget *button,
ThunarLocationButtons *buttons);
static void
thunar_location_buttons_scroll_right (GtkWidget *button,
ThunarLocationButtons *buttons);
static void
thunar_location_buttons_clicked (ThunarLocationButton *button,
gboolean open_in_tab,
ThunarLocationButtons *buttons);
static gboolean
thunar_location_buttons_context_menu (ThunarLocationButton *button,
ThunarLocationButtons *buttons);
static void
thunar_location_buttons_gone (ThunarLocationButton *button,
ThunarLocationButtons *buttons);
struct _ThunarLocationButtonsClass
{
GtkContainerClass __parent__;
void (*entry_requested) (const gchar *initial_text);
};
struct _ThunarLocationButtons
{
GtkContainer __parent__;
ThunarActionManager *action_mgr;
GtkWidget *left_slider;
GtkWidget *right_slider;
GtkWidget *filler_widget;
ThunarFile *current_directory;
gint button_max_width;
gint slider_width;
guint ignore_click : 1;
GList *list;
GList *fake_root_button;
GList *first_visible_button;
GList *last_visible_button;
guint scroll_timeout_id;
};
static GQuark thunar_file_quark = 0;
G_DEFINE_TYPE_WITH_CODE (ThunarLocationButtons, thunar_location_buttons, GTK_TYPE_CONTAINER,
G_IMPLEMENT_INTERFACE (THUNAR_TYPE_NAVIGATOR, thunar_location_buttons_navigator_init))
static void
thunar_location_buttons_class_init (ThunarLocationButtonsClass *klass)
{
GtkContainerClass *gtkcontainer_class;
GtkWidgetClass *gtkwidget_class;
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = thunar_location_buttons_finalize;
gobject_class->get_property = thunar_location_buttons_get_property;
gobject_class->set_property = thunar_location_buttons_set_property;
gtkwidget_class = GTK_WIDGET_CLASS (klass);
gtkwidget_class->draw = thunar_location_buttons_draw;
gtkwidget_class->unmap = thunar_location_buttons_unmap;
gtkwidget_class->get_preferred_width = thunar_location_buttons_get_preferred_width;
gtkwidget_class->get_preferred_height = thunar_location_buttons_get_preferred_height;
gtkwidget_class->size_allocate = thunar_location_buttons_size_allocate;
gtkwidget_class->state_changed = thunar_location_buttons_state_changed;
gtkwidget_class->grab_notify = thunar_location_buttons_grab_notify;
gtkcontainer_class = GTK_CONTAINER_CLASS (klass);
gtkcontainer_class->add = thunar_location_buttons_add;
gtkcontainer_class->remove = thunar_location_buttons_remove;
gtkcontainer_class->forall = thunar_location_buttons_forall;
gtkcontainer_class->get_path_for_child = thunar_location_buttons_get_path_for_child;
/* Override ThunarNavigator's properties */
g_object_class_override_property (gobject_class, PROP_CURRENT_DIRECTORY, "current-directory");
/* signals */
g_signal_new ("entry-requested",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ThunarLocationButtonsClass, entry_requested),
NULL, NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_STRING);
thunar_file_quark = g_quark_from_static_string ("button-thunar-file");
}
static void
thunar_location_buttons_navigator_init (ThunarNavigatorIface *iface)
{
iface->get_current_directory = thunar_location_buttons_get_current_directory;
iface->set_current_directory = thunar_location_buttons_set_current_directory;
}
static void
thunar_location_buttons_on_filler_clicked (ThunarLocationButtons *buttons)
{
g_signal_emit_by_name (buttons, "entry-requested", NULL);
}
static void
thunar_location_buttons_init (ThunarLocationButtons *buttons)
{
ThunarPreferences *preferences;
GtkWidget *icon;
gboolean use_symbolic_icons;
preferences = thunar_preferences_get ();
g_object_get (G_OBJECT (preferences), "misc-symbolic-icons-in-toolbar", &use_symbolic_icons, NULL);
g_object_unref (G_OBJECT (preferences));
gtk_widget_set_has_window (GTK_WIDGET (buttons), FALSE);
gtk_widget_set_redraw_on_allocate (GTK_WIDGET (buttons), FALSE);
buttons->button_max_width = THUNAR_LOCATION_BUTTON_MAX_WIDTH;
buttons->left_slider = gtk_button_new ();
g_signal_connect (G_OBJECT (buttons->left_slider), "button-press-event", G_CALLBACK (thunar_location_buttons_slider_button_press), buttons);
g_signal_connect (G_OBJECT (buttons->left_slider), "button-release-event", G_CALLBACK (thunar_location_buttons_slider_button_release), buttons);
g_signal_connect (G_OBJECT (buttons->left_slider), "clicked", G_CALLBACK (thunar_location_buttons_scroll_left), buttons);
gtk_container_add (GTK_CONTAINER (buttons), buttons->left_slider);
gtk_widget_show (buttons->left_slider);
icon = gtk_image_new_from_icon_name (use_symbolic_icons ? "pan-start-symbolic" : "pan-start", GTK_ICON_SIZE_BUTTON);
gtk_container_add (GTK_CONTAINER (buttons->left_slider), icon);
gtk_widget_show (icon);
buttons->right_slider = gtk_button_new ();
g_signal_connect (G_OBJECT (buttons->right_slider), "button-press-event", G_CALLBACK (thunar_location_buttons_slider_button_press), buttons);
g_signal_connect (G_OBJECT (buttons->right_slider), "button-release-event", G_CALLBACK (thunar_location_buttons_slider_button_release), buttons);
g_signal_connect (G_OBJECT (buttons->right_slider), "clicked", G_CALLBACK (thunar_location_buttons_scroll_right), buttons);
gtk_container_add (GTK_CONTAINER (buttons), buttons->right_slider);
gtk_widget_show (buttons->right_slider);
icon = gtk_image_new_from_icon_name (use_symbolic_icons ? "pan-end-symbolic" : "pan-end", GTK_ICON_SIZE_BUTTON);
gtk_container_add (GTK_CONTAINER (buttons->right_slider), icon);
gtk_widget_show (icon);
buttons->filler_widget = gtk_button_new ();
g_signal_connect_swapped (buttons->filler_widget, "clicked", G_CALLBACK (thunar_location_buttons_on_filler_clicked), buttons);
icon = gtk_image_new_from_icon_name (use_symbolic_icons ? "document-edit-symbolic" : "document-edit", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_halign (icon, GTK_ALIGN_END);
gtk_container_add (GTK_CONTAINER (buttons->filler_widget), icon);
gtk_widget_show (icon);
gtk_container_add (GTK_CONTAINER (buttons), buttons->filler_widget);
gtk_widget_show (buttons->filler_widget);
g_object_set (buttons, "valign", GTK_ALIGN_CENTER, NULL);
/* setup style classes */
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons)),
GTK_STYLE_CLASS_LINKED);
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons)),
"path-bar");
/* add sub-buttons to css class which matches all buttons in the path-bar */
/* keep as well the old name 'path-bar-button'. It might be used by themes */
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons->left_slider)),
"location-button");
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons->right_slider)),
"location-button");
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons->filler_widget)),
"location-button");
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons->left_slider)),
"path-bar-button");
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons->right_slider)),
"path-bar-button");
gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (buttons->filler_widget)),
"path-bar-button");
buttons->action_mgr = g_object_new (THUNAR_TYPE_ACTION_MANAGER, "widget", GTK_WIDGET (buttons), NULL);
g_signal_connect_swapped (G_OBJECT (buttons->action_mgr), "change-directory", G_CALLBACK (thunar_location_buttons_set_current_directory), buttons);
g_signal_connect_swapped (G_OBJECT (buttons->action_mgr), "open-new-tab", G_CALLBACK (thunar_navigator_open_new_tab), buttons);
}
static void
thunar_location_buttons_finalize (GObject *object)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (object);
_thunar_return_if_fail (THUNAR_IS_LOCATION_BUTTONS (buttons));
/* be sure to cancel the scrolling */
thunar_location_buttons_stop_scrolling (buttons);
/* release from the current_directory */
thunar_navigator_set_current_directory (THUNAR_NAVIGATOR (buttons), NULL);
(*G_OBJECT_CLASS (thunar_location_buttons_parent_class)->finalize) (object);
}
static void
thunar_location_buttons_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
switch (prop_id)
{
case PROP_CURRENT_DIRECTORY:
g_value_set_object (value, thunar_navigator_get_current_directory (THUNAR_NAVIGATOR (object)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
thunar_location_buttons_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
switch (prop_id)
{
case PROP_CURRENT_DIRECTORY:
thunar_navigator_set_current_directory (THUNAR_NAVIGATOR (object), g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static ThunarFile *
thunar_location_buttons_get_current_directory (ThunarNavigator *navigator)
{
return THUNAR_LOCATION_BUTTONS (navigator)->current_directory;
}
static inline gboolean
eglible_for_fake_root (ThunarFile *file)
{
/* use 'Home' as fake root button */
return thunar_file_is_home (file);
/* TODO: mounted devices */
}
static void
thunar_location_buttons_set_current_directory (ThunarNavigator *navigator,
ThunarFile *current_directory)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (navigator);
ThunarFile *file_parent;
ThunarFile *file;
GtkWidget *button;
GList *lp;
_thunar_return_if_fail (current_directory == NULL || THUNAR_IS_FILE (current_directory));
/* verify that we're not already using the new directory */
if (G_UNLIKELY (buttons->current_directory == current_directory))
return;
/* check if we already have a visible button for that directory */
for (lp = buttons->list; lp != NULL; lp = lp->next)
{
if (thunar_location_button_get_file (lp->data) == current_directory && gtk_widget_get_child_visible (GTK_WIDGET (lp->data)))
{
/* fake a "clicked" event for that button */
gtk_button_clicked (GTK_BUTTON (lp->data));
return;
}
}
if (G_LIKELY (buttons->current_directory != NULL))
{
/* remove all buttons */
g_object_unref (G_OBJECT (buttons->current_directory));
while (buttons->list != NULL)
gtk_container_remove (GTK_CONTAINER (buttons), buttons->list->data);
/* clear scroll positions and fake root button */
buttons->first_visible_button = NULL;
buttons->last_visible_button = NULL;
buttons->fake_root_button = NULL;
}
buttons->current_directory = current_directory;
/* regenerate the button list */
if (G_LIKELY (current_directory != NULL))
{
g_object_ref (G_OBJECT (current_directory));
/* add the new buttons */
for (file = current_directory; file != NULL; file = file_parent)
{
button = thunar_location_buttons_make_button (buttons, file);
buttons->list = g_list_append (buttons->list, button);
gtk_container_add (GTK_CONTAINER (buttons), button);
gtk_widget_show (button);
/* use 'Home' as fake root button */
if (!buttons->fake_root_button && eglible_for_fake_root (file))
buttons->fake_root_button = g_list_last (buttons->list);
/* continue with the parent (if any) */
file_parent = thunar_file_get_parent (file, NULL);
if (G_LIKELY (file != current_directory))
g_object_unref (G_OBJECT (file));
}
}
g_object_notify (G_OBJECT (buttons), "current-directory");
}
static void
thunar_location_buttons_unmap (GtkWidget *widget)
{
/* be sure to stop the scroll timer before the buttons are unmapped */
thunar_location_buttons_stop_scrolling (THUNAR_LOCATION_BUTTONS (widget));
/* do the real unmap */
(*GTK_WIDGET_CLASS (thunar_location_buttons_parent_class)->unmap) (widget);
}
static void
thunar_location_buttons_get_preferred_width (GtkWidget *widget,
gint *minimum,
gint *natural)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (widget);
gint width = 0, height = 0, child_width = 0, child_height = 0, filler_width = 0;
GList *lp;
/* calculate the size of the biggest button */
for (lp = buttons->list; lp != NULL; lp = lp->next)
{
gtk_widget_get_preferred_width (GTK_WIDGET (lp->data), &child_width, NULL);
gtk_widget_get_preferred_height (GTK_WIDGET (lp->data), &child_height, NULL);
width = MAX (width, child_width);
height = MAX (height, child_height);
}
/* add space for the sliders if we have more than one path */
buttons->slider_width = MIN (height * 2 / 3 + 5, height);
if (buttons->list != NULL && buttons->list->next != NULL)
width += (buttons->slider_width) * 2;
gtk_widget_get_preferred_width (buttons->filler_widget, &filler_width, NULL);
width += filler_width; // default is the minimum
*minimum = *natural = width;
}
static void
thunar_location_buttons_get_preferred_height (GtkWidget *widget,
gint *minimum,
gint *natural)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (widget);
gint height = 0, child_height = 0;
GList *lp;
/* calculate the size of the biggest button */
for (lp = buttons->list; lp != NULL; lp = lp->next)
{
gtk_widget_get_preferred_height (GTK_WIDGET (lp->data), &child_height, NULL);
height = MAX (height, child_height);
}
*minimum = *natural = height;
}
static GtkWidgetPath *
thunar_location_buttons_get_path_for_child (GtkContainer *container,
GtkWidget *child)
{
ThunarLocationButtons *path_bar = THUNAR_LOCATION_BUTTONS (container);
GtkWidgetPath *path;
path = gtk_widget_path_copy (gtk_widget_get_path (GTK_WIDGET (path_bar)));
if (gtk_widget_get_visible (child) && gtk_widget_get_child_visible (child))
{
GtkWidgetPath *sibling_path;
GList *visible_children;
GList *l;
int pos;
/* 1. Build the list of visible children, in visually left-to-right order
* (i.e. independently of the widget's direction). Note that our
* list is stored in innermost-to-outermost path order!
*/
visible_children = NULL;
visible_children = g_list_prepend (visible_children, path_bar->right_slider);
visible_children = g_list_prepend (visible_children, path_bar->filler_widget);
for (l = path_bar->list; l; l = l->next)
{
if (gtk_widget_get_visible (GTK_WIDGET (l->data)) && gtk_widget_get_child_visible (GTK_WIDGET (l->data)))
visible_children = g_list_prepend (visible_children, l->data);
}
visible_children = g_list_prepend (visible_children, path_bar->left_slider);
if (gtk_widget_get_direction (GTK_WIDGET (path_bar)) == GTK_TEXT_DIR_RTL)
{
visible_children = g_list_reverse (visible_children);
}
/* 2. Find the index of the child within that list */
pos = 0;
for (l = visible_children; l; l = l->next)
{
GtkWidget *button = l->data;
if (button == child)
break;
pos++;
}
/* 3. Build the path */
sibling_path = gtk_widget_path_new ();
for (l = visible_children; l; l = l->next)
{
gtk_widget_path_append_for_widget (sibling_path, l->data);
}
gtk_widget_path_append_with_siblings (path, sibling_path, pos);
g_list_free (visible_children);
gtk_widget_path_unref (sibling_path);
}
else
{
gtk_widget_path_append_for_widget (path, child);
}
return path;
}
static void
thunar_location_buttons_child_ordering_changed (ThunarLocationButtons *location_buttons)
{
gtk_widget_reset_style (GTK_WIDGET (location_buttons));
}
static GList *
thunar_location_buttons_detect_first_visible_button (ThunarLocationButtons *buttons,
gint available_width,
gint *resulting_occupied_width)
{
GList *lp;
gint button_width = 0;
gint resulting_width = 0;
/* walk all buttons starting by last, until no space is left */
for (lp = buttons->last_visible_button; lp != NULL; lp = lp->next)
{
gtk_widget_get_preferred_width (GTK_WIDGET (lp->data), &button_width, NULL);
if (resulting_width + button_width > available_width)
{
return lp->prev;
}
resulting_width += button_width;
if (resulting_occupied_width != NULL)
*resulting_occupied_width = resulting_width;
/* stop at the fake root button */
if (lp == buttons->fake_root_button)
return lp;
}
return g_list_last (buttons->list);
}
static GList *
thunar_location_buttons_detect_last_visible_button (ThunarLocationButtons *buttons,
gint available_width,
gint *resulting_occupied_width)
{
GList *lp;
gint button_width = 0;
gint resulting_width = 0;
/* walk all buttons starting by first, until no space is left */
for (lp = buttons->first_visible_button; lp != NULL; lp = lp->prev)
{
gtk_widget_get_preferred_width (GTK_WIDGET (lp->data), &button_width, NULL);
if (resulting_width + button_width > available_width)
{
return lp->next;
}
resulting_width += button_width;
if (resulting_occupied_width != NULL)
*resulting_occupied_width = resulting_width;
}
return buttons->list;
}
static void
thunar_location_buttons_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (widget);
GtkTextDirection direction;
GtkAllocation available_space;
GtkAllocation folder_button_allocation;
GtkAllocation first_slider_allocation; /* left slider if direction is LTR */
GtkAllocation second_slider_allocation; /* right slider if direction is LTR */
GtkAllocation filler_allocation;
GList *lp;
gint sliders_filler_width;
gint buttons_area_width;
gint buttons_total_width;
gint border_width;
gint temp_width;
gboolean need_reorder = FALSE;
gtk_widget_set_allocation (widget, allocation);
/* if no path is set, we don't have to allocate anything */
if (G_UNLIKELY (buttons->list == NULL))
return;
direction = gtk_widget_get_direction (widget);
border_width = gtk_container_get_border_width (GTK_CONTAINER (buttons));
available_space.x = allocation->x + border_width;
available_space.y = allocation->y + border_width;
available_space.width = allocation->width - 2 * border_width;
available_space.height = MAX (1, allocation->height - 2 * border_width);
first_slider_allocation.width = buttons->slider_width;
first_slider_allocation.height = available_space.height;
first_slider_allocation.y = available_space.y;
second_slider_allocation.width = buttons->slider_width;
second_slider_allocation.height = available_space.height;
second_slider_allocation.y = available_space.y;
if (G_LIKELY (direction == GTK_TEXT_DIR_LTR))
{
first_slider_allocation.x = available_space.x;
second_slider_allocation.x = available_space.x + available_space.width - buttons->slider_width;
}
else
{
first_slider_allocation.x = available_space.x + available_space.width - buttons->slider_width;
second_slider_allocation.x = available_space.x;
}
gtk_widget_get_preferred_width (buttons->filler_widget, &filler_allocation.width, NULL);
filler_allocation.height = available_space.height;
filler_allocation.y = available_space.y;
/* space used by sliders + filler */
sliders_filler_width = buttons->slider_width * 2 + filler_allocation.width;
/* space available for buttons */
buttons_area_width = available_space.width - sliders_filler_width;
/* dynamically calculate the maximum button width relative to the available space
* by using a factor between 0 and 1 to allow the button to shrink in size */
buttons->button_max_width = MIN (buttons_area_width * THUNAR_LOCATION_BUTTON_FACTOR,
THUNAR_LOCATION_BUTTON_MAX_WIDTH);
/* apply the value to all buttons */
for (lp = buttons->list; lp != NULL; lp = lp->next)
thunar_location_button_set_max_width (THUNAR_LOCATION_BUTTON (lp->data), buttons->button_max_width);
/* calculate the total width of all buttons */
buttons_total_width = 0;
for (lp = buttons->list; lp != NULL; lp = lp->next)
{
gtk_widget_get_preferred_width (GTK_WIDGET (lp->data), &temp_width, NULL);
buttons_total_width += temp_width;
if (lp == buttons->fake_root_button)
break;
}
/* determine how many buttons fit into the available area */
if (buttons_total_width <= buttons_area_width)
{
/* Show full list of buttons, starting by fake root, if available */
if (buttons->fake_root_button == NULL)
buttons->first_visible_button = g_list_last (buttons->list);
else
buttons->first_visible_button = buttons->fake_root_button;
buttons->last_visible_button = buttons->list;
}
else
{
/* reset calculation for buttons_total_width, because we need to cut off some buttons */
buttons_total_width = 0;
/* No scrolling defined ? Use last button as fixed start! */
if (buttons->last_visible_button == NULL && buttons->first_visible_button == NULL)
buttons->last_visible_button = buttons->list;
/* if the last visible button is the active one, do not cut it off */
if (buttons->last_visible_button != NULL
&& gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (buttons->last_visible_button->data)))
{
buttons->first_visible_button = thunar_location_buttons_detect_first_visible_button (buttons, buttons_area_width, NULL);
buttons->last_visible_button = thunar_location_buttons_detect_last_visible_button (buttons, buttons_area_width, &buttons_total_width);
}
else if (buttons->first_visible_button == NULL) /* last button is fixed defined */
buttons->first_visible_button = thunar_location_buttons_detect_first_visible_button (buttons, buttons_area_width, &buttons_total_width);
else /* first button is fixed defined */
buttons->last_visible_button = thunar_location_buttons_detect_last_visible_button (buttons, buttons_area_width, &buttons_total_width);
}
/* hide buttons before first_visible_button */
if (buttons->first_visible_button != NULL)
{
for (lp = buttons->first_visible_button->next; lp != NULL; lp = lp->next)
{
need_reorder |= gtk_widget_get_child_visible (GTK_WIDGET (lp->data)) == TRUE;
gtk_widget_set_child_visible (GTK_WIDGET (lp->data), FALSE);
}
}
/* hide buttons after last visible_button */
if (buttons->last_visible_button != NULL)
{
for (lp = buttons->last_visible_button->prev; lp != NULL; lp = lp->prev)
{
need_reorder |= gtk_widget_get_child_visible (GTK_WIDGET (lp->data)) == TRUE;
gtk_widget_set_child_visible (GTK_WIDGET (lp->data), FALSE);
}
}
/* allocate space for the buttons */
folder_button_allocation.y = available_space.y;
folder_button_allocation.height = available_space.height;
if (G_LIKELY (direction == GTK_TEXT_DIR_LTR))
folder_button_allocation.x = first_slider_allocation.x + first_slider_allocation.width;
else
{
if (first_slider_allocation.width == 0)
folder_button_allocation.x = available_space.x + available_space.width;
else
folder_button_allocation.x = first_slider_allocation.x;
}
for (lp = buttons->first_visible_button; lp != NULL; lp = lp->prev)
{
gtk_widget_get_preferred_width (GTK_WIDGET (lp->data), &folder_button_allocation.width, NULL);
if (G_UNLIKELY (direction == GTK_TEXT_DIR_RTL))
folder_button_allocation.x -= folder_button_allocation.width;
need_reorder |= gtk_widget_get_child_visible (GTK_WIDGET (lp->data)) == FALSE;
gtk_widget_set_child_visible (GTK_WIDGET (lp->data), TRUE);
gtk_widget_size_allocate (lp->data, &folder_button_allocation);
if (G_LIKELY (direction == GTK_TEXT_DIR_LTR))
folder_button_allocation.x += folder_button_allocation.width;
if (lp == buttons->last_visible_button)
break;
}
/* allocate the filler */
filler_allocation.width += available_space.width - sliders_filler_width - buttons_total_width;
filler_allocation.x = folder_button_allocation.x;
if (G_UNLIKELY (direction == GTK_TEXT_DIR_RTL))
filler_allocation.x -= filler_allocation.width;
gtk_widget_size_allocate (buttons->filler_widget, &filler_allocation);
/* allocate the sliders */
gtk_widget_get_preferred_width (GTK_WIDGET (buttons->left_slider), &temp_width, NULL); /* to avoid warnings in gtk >= 3.20 */
gtk_widget_get_preferred_width (GTK_WIDGET (buttons->right_slider), &temp_width, NULL); /* to avoid warnings in gtk >= 3.20 */
gtk_widget_size_allocate (buttons->left_slider, &first_slider_allocation);
gtk_widget_size_allocate (buttons->right_slider, &second_slider_allocation);
thunar_location_buttons_update_sliders (buttons);
if (need_reorder)
thunar_location_buttons_child_ordering_changed (buttons);
gtk_widget_queue_draw (GTK_WIDGET (buttons));
}
static gboolean
thunar_location_buttons_draw (GtkWidget *widget,
cairo_t *cr)
{
GtkStyleContext *context;
GtkAllocation alloc;
context = gtk_widget_get_style_context (widget);
gtk_widget_get_allocation (widget, &alloc);
gtk_render_background (context, cr, 0, 0, alloc.width, alloc.height);
gtk_render_frame (context, cr, 0, 0, alloc.width, alloc.height);
return GTK_WIDGET_CLASS (thunar_location_buttons_parent_class)->draw (widget, cr);
}
static void
thunar_location_buttons_state_changed (GtkWidget *widget,
GtkStateType previous_state)
{
if (!gtk_widget_is_sensitive (widget))
thunar_location_buttons_stop_scrolling (THUNAR_LOCATION_BUTTONS (widget));
}
static void
thunar_location_buttons_grab_notify (GtkWidget *widget,
gboolean was_grabbed)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (widget);
if (!was_grabbed)
thunar_location_buttons_stop_scrolling (buttons);
}
static void
thunar_location_buttons_add (GtkContainer *container,
GtkWidget *widget)
{
gtk_widget_set_parent (widget, GTK_WIDGET (container));
}
static void
thunar_location_buttons_remove (GtkContainer *container,
GtkWidget *widget)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (container);
GList *lp;
if (widget == buttons->left_slider)
{
thunar_location_buttons_remove_1 (container, widget);
buttons->left_slider = NULL;
return;
}
else if (widget == buttons->right_slider)
{
thunar_location_buttons_remove_1 (container, widget);
buttons->right_slider = NULL;
return;
}
for (lp = buttons->list; lp != NULL; lp = lp->next)
if (widget == GTK_WIDGET (lp->data))
{
thunar_location_buttons_remove_1 (container, widget);
buttons->list = g_list_remove_link (buttons->list, lp);
g_list_free (lp);
return;
}
/* fallback case */
thunar_location_buttons_remove_1 (container, widget);
}
static void
thunar_location_buttons_forall (GtkContainer *container,
gboolean include_internals,
GtkCallback callback,
gpointer callback_data)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (container);
GtkWidget *child;
GList *lp;
_thunar_return_if_fail (THUNAR_IS_LOCATION_BUTTONS (buttons));
_thunar_return_if_fail (callback != NULL);
for (lp = buttons->list; lp != NULL;)
{
child = GTK_WIDGET (lp->data);
lp = lp->next;
(*callback) (child, callback_data);
}
if (buttons->left_slider != NULL && include_internals)
(*callback) (buttons->left_slider, callback_data);
if (buttons->right_slider != NULL && include_internals)
(*callback) (buttons->right_slider, callback_data);
if (buttons->filler_widget != NULL && include_internals)
(*callback) (buttons->filler_widget, callback_data);
}
static GtkWidget *
thunar_location_buttons_make_button (ThunarLocationButtons *buttons,
ThunarFile *file)
{
GtkWidget *button;
/* allocate the button */
button = g_object_new (THUNAR_TYPE_LOCATION_BUTTON, "max-width", buttons->button_max_width, NULL);
/* connect to the file */
thunar_location_button_set_file (THUNAR_LOCATION_BUTTON (button), file);
/* the current directory is active */
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), (file == buttons->current_directory));
/* connect signal handlers */
g_signal_connect (G_OBJECT (button), "location-button-clicked", G_CALLBACK (thunar_location_buttons_clicked), buttons);
g_signal_connect (G_OBJECT (button), "gone", G_CALLBACK (thunar_location_buttons_gone), buttons);
g_signal_connect (G_OBJECT (button), "popup-menu", G_CALLBACK (thunar_location_buttons_context_menu), buttons);
return button;
}
static void
thunar_location_buttons_remove_1 (GtkContainer *container,
GtkWidget *widget)
{
gboolean need_resize = gtk_widget_get_visible (widget);
gtk_widget_unparent (widget);
if (G_LIKELY (need_resize))
gtk_widget_queue_resize (GTK_WIDGET (container));
}
static gboolean
thunar_location_buttons_scroll_timeout (gpointer user_data)
{
ThunarLocationButtons *buttons = THUNAR_LOCATION_BUTTONS (user_data);
if (gtk_widget_has_focus (buttons->left_slider))
thunar_location_buttons_scroll_left (buttons->left_slider, buttons);
else if (gtk_widget_has_focus (buttons->right_slider))
thunar_location_buttons_scroll_right (buttons->right_slider, buttons);
return TRUE;
}
static void
thunar_location_buttons_scroll_timeout_destroy (gpointer user_data)
{
THUNAR_LOCATION_BUTTONS (user_data)->scroll_timeout_id = 0;
}
static void
thunar_location_buttons_stop_scrolling (ThunarLocationButtons *buttons)
{
if (buttons->scroll_timeout_id != 0)
g_source_remove (buttons->scroll_timeout_id);
}
static void
thunar_location_buttons_update_sliders (ThunarLocationButtons *buttons)
{
GtkWidget *button;
if (G_LIKELY (buttons->list != NULL))
{
button = GTK_WIDGET (buttons->list->data);
if (gtk_widget_get_child_visible (button))
{
gtk_widget_set_sensitive (buttons->right_slider, FALSE);
thunar_location_buttons_stop_scrolling (buttons);
}
else
{
gtk_widget_set_sensitive (buttons->right_slider, TRUE);
}
button = GTK_WIDGET (g_list_last (buttons->list)->data);
if (gtk_widget_get_child_visible (button) && buttons->fake_root_button == NULL)
{
gtk_widget_set_sensitive (buttons->left_slider, FALSE);
thunar_location_buttons_stop_scrolling (buttons);
}
else
{
gtk_widget_set_sensitive (buttons->left_slider, TRUE);
}
}
}
static gboolean
thunar_location_buttons_slider_button_press (GtkWidget *button,
GdkEventButton *event,
ThunarLocationButtons *buttons)
{
if (!gtk_widget_has_focus (button))
gtk_widget_grab_focus (button);
if (event->type != GDK_BUTTON_PRESS || event->button != 1)
return FALSE;
buttons->ignore_click = FALSE;
if (button == buttons->left_slider)
thunar_location_buttons_scroll_left (button, buttons);
else if (button == buttons->right_slider)
thunar_location_buttons_scroll_right (button, buttons);
if (G_LIKELY (buttons->scroll_timeout_id == 0))
{
buttons->scroll_timeout_id = gdk_threads_add_timeout_full (G_PRIORITY_LOW,
THUNAR_LOCATION_BUTTONS_SCROLL_TIMEOUT,
thunar_location_buttons_scroll_timeout, buttons,
thunar_location_buttons_scroll_timeout_destroy);
}
return FALSE;
}
static gboolean
thunar_location_buttons_slider_button_release (GtkWidget *button,
GdkEventButton *event,
ThunarLocationButtons *buttons)
{
if (event->type == GDK_BUTTON_RELEASE)
{
thunar_location_buttons_stop_scrolling (buttons);
buttons->ignore_click = TRUE;
}
return FALSE;
}
static void
thunar_location_buttons_scroll_left (GtkWidget *button,
ThunarLocationButtons *buttons)
{
if (G_UNLIKELY (buttons->ignore_click))
{
buttons->ignore_click = FALSE;
return;
}
gtk_widget_queue_resize (GTK_WIDGET (buttons));
if (buttons->first_visible_button->next != NULL)
{
if (buttons->first_visible_button == buttons->fake_root_button)
buttons->fake_root_button = NULL;
buttons->first_visible_button = buttons->first_visible_button->next;
buttons->last_visible_button = NULL;
}
}
static void
thunar_location_buttons_scroll_right (GtkWidget *button,
ThunarLocationButtons *buttons)
{
if (G_UNLIKELY (buttons->ignore_click))
{
buttons->ignore_click = FALSE;
return;
}
gtk_widget_queue_resize (GTK_WIDGET (buttons));
if (buttons->last_visible_button->prev != NULL)
{
buttons->last_visible_button = buttons->last_visible_button->prev;
buttons->first_visible_button = NULL;
}
}
static void
thunar_location_buttons_clicked (ThunarLocationButton *button,
gboolean open_in_tab,
ThunarLocationButtons *buttons)
{
ThunarFile *directory;
GList *lp;
_thunar_return_if_fail (THUNAR_IS_LOCATION_BUTTON (button));
_thunar_return_if_fail (THUNAR_IS_LOCATION_BUTTONS (buttons));
/* determine the directory associated with the clicked button */
directory = thunar_location_button_get_file (button);
/* open in tab */
if (open_in_tab)
{
thunar_navigator_open_new_tab (THUNAR_NAVIGATOR (buttons), directory);
return;
}
/* disconnect from previous current directory (if any) */
if (G_LIKELY (buttons->current_directory != NULL))
g_object_unref (G_OBJECT (buttons->current_directory));
/* setup the new current directory */
buttons->current_directory = directory;
/* take a reference on the new directory (if any) */
if (G_LIKELY (directory != NULL))
g_object_ref (G_OBJECT (directory));
/* check if the button is visible on the button bar */
if (!gtk_widget_get_child_visible (GTK_WIDGET (button)))
{
for (lp = buttons->list; lp != NULL; lp = lp->next)
{
if (lp->data == button)
buttons->first_visible_button = lp;
}
buttons->last_visible_button = NULL;
}
/* update all buttons */
for (lp = buttons->list; lp != NULL; lp = lp->next)
{
/* determine the directory for this button */
button = THUNAR_LOCATION_BUTTON (lp->data);
directory = thunar_location_button_get_file (button);
/* update the location button state (making sure to not recurse with the "clicked" handler) */
g_signal_handlers_block_by_func (G_OBJECT (button), thunar_location_buttons_clicked, buttons);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), directory == buttons->current_directory);
g_signal_handlers_unblock_by_func (G_OBJECT (button), thunar_location_buttons_clicked, buttons);
}
/* notify the surrounding module that we want to change to a different directory. */
thunar_navigator_change_directory (THUNAR_NAVIGATOR (buttons), buttons->current_directory);
}
static void
thunar_location_buttons_gone (ThunarLocationButton *button,
ThunarLocationButtons *buttons)
{
_thunar_return_if_fail (THUNAR_IS_LOCATION_BUTTON (button));
_thunar_return_if_fail (THUNAR_IS_LOCATION_BUTTONS (buttons));
_thunar_return_if_fail (g_list_find (buttons->list, button) != NULL);
/* drop all buttons up to the button that emitted the "gone" signal */
while (buttons->list->data != button)
gtk_widget_destroy (buttons->list->data);
/* drop the button itself */
gtk_widget_destroy (GTK_WIDGET (button));
/* clear both scroll positions */
buttons->first_visible_button = NULL;
buttons->last_visible_button = NULL;
}
static gboolean
thunar_location_buttons_context_menu (ThunarLocationButton *button,
ThunarLocationButtons *buttons)
{
ThunarFile *file;
GtkWidget *window;
GList *files;
ThunarMenu *context_menu;
gboolean is_current_directory;
_thunar_return_val_if_fail (THUNAR_IS_LOCATION_BUTTONS (buttons), FALSE);
_thunar_return_val_if_fail (THUNAR_IS_LOCATION_BUTTON (button), FALSE);
/* determine the file for the button */
file = thunar_location_button_get_file (button);
if (G_UNLIKELY (file == NULL))
return FALSE;
files = g_list_append (NULL, file);
g_object_set (G_OBJECT (buttons->action_mgr), "current-directory", file, NULL);
g_object_set (G_OBJECT (buttons->action_mgr), "selected-files", files, NULL);
g_list_free (files);
is_current_directory = g_file_equal (thunar_file_get_file (file), thunar_file_get_file (buttons->current_directory));
context_menu = g_object_new (THUNAR_TYPE_MENU, "menu-type", THUNAR_MENU_TYPE_CONTEXT_LOCATION_BUTTONS,
"action_mgr", buttons->action_mgr,
"force-section-open", TRUE,
"change_directory-support-disabled", is_current_directory, NULL);
if (is_current_directory)
{
/* context menu on current directory */
thunar_menu_add_sections (context_menu, THUNAR_MENU_SECTION_OPEN
| THUNAR_MENU_SECTION_SENDTO);
thunar_menu_add_sections (context_menu, THUNAR_MENU_SECTION_CREATE_NEW_FILES
| THUNAR_MENU_SECTION_COPY_PASTE
| THUNAR_MENU_SECTION_EMPTY_TRASH
| THUNAR_MENU_SECTION_CUSTOM_ACTIONS
| THUNAR_MENU_SECTION_PROPERTIES);
}
else
{
/* context menu on some other location button */
thunar_menu_add_sections (context_menu, THUNAR_MENU_SECTION_OPEN
| THUNAR_MENU_SECTION_SENDTO
| THUNAR_MENU_SECTION_COPY_PASTE
| THUNAR_MENU_SECTION_CUSTOM_ACTIONS
| THUNAR_MENU_SECTION_PROPERTIES);
}
thunar_gtk_menu_hide_accel_labels (GTK_MENU (context_menu));
gtk_widget_show_all (GTK_WIDGET (context_menu));
window = gtk_widget_get_toplevel (GTK_WIDGET (buttons));
thunar_window_redirect_menu_tooltips_to_statusbar (THUNAR_WINDOW (window), GTK_MENU (context_menu));
thunar_gtk_menu_run (GTK_MENU (context_menu));
return TRUE;
}