thunar-usagebars/thunar/thunar-path-entry.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

1430 lines
46 KiB
C

/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
* Copyright (c) 2005-2006 Benedikt Meurer <benny@xfce.org>
* Copyright (c) 2009-2010 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., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* The icon code is based on ideas from SexyIconEntry, which was written by
* Christian Hammond <chipx86@chipx86.com>.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_MEMORY_H
#include <memory.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif
#include "thunar/thunar-gobject-extensions.h"
#include "thunar/thunar-icon-factory.h"
#include "thunar/thunar-icon-renderer.h"
#include "thunar/thunar-list-model.h"
#include "thunar/thunar-path-entry.h"
#include "thunar/thunar-preferences.h"
#include "thunar/thunar-private.h"
#include "thunar/thunar-util.h"
#include "thunar/thunar-window.h"
#include <gdk/gdkkeysyms.h>
#define ICON_MARGIN (2)
enum
{
PROP_0,
PROP_CURRENT_FILE,
};
static void
thunar_path_entry_editable_init (GtkEditableInterface *iface);
static void
thunar_path_entry_finalize (GObject *object);
static void
thunar_path_entry_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec);
static void
thunar_path_entry_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec);
static gboolean
thunar_path_entry_focus (GtkWidget *widget,
GtkDirectionType direction);
static gboolean
thunar_path_entry_icon_press_event (GtkEntry *entry,
GtkEntryIconPosition icon_pos,
GdkEventButton *event,
gpointer userdata);
static void
thunar_path_entry_scale_changed (GObject *object,
GParamSpec *pspec,
gpointer user_data);
static gboolean
thunar_path_entry_key_press_event (GtkWidget *widget,
GdkEventKey *event);
static void
thunar_path_entry_drag_data_get (GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *selection_data,
guint info,
guint timestamp);
static void
thunar_path_entry_drag_end (GtkWidget *widget,
GdkDragContext *context);
static void
thunar_path_entry_activate (GtkEntry *entry);
static void
thunar_path_entry_changed (GtkEditable *editable);
static void
thunar_path_entry_update_icon (ThunarPathEntry *path_entry);
static void
thunar_path_entry_do_insert_text (GtkEditable *editable,
const gchar *new_text,
gint new_text_length,
gint *position);
static void
thunar_path_entry_clear_completion (ThunarPathEntry *path_entry);
static void
thunar_path_entry_common_prefix_append (ThunarPathEntry *path_entry,
gboolean highlight);
static void
thunar_path_entry_common_prefix_lookup (ThunarPathEntry *path_entry,
gchar **prefix_return,
ThunarFile **file_return);
static gboolean
thunar_path_entry_match_func (GtkEntryCompletion *completion,
const gchar *key,
GtkTreeIter *iter,
gpointer user_data);
static gboolean
thunar_path_entry_match_selected (GtkEntryCompletion *completion,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer user_data);
static gboolean
thunar_path_entry_parse (ThunarPathEntry *path_entry,
gchar **folder_part,
gchar **file_part,
GError **error);
static void
thunar_path_entry_queue_check_completion (ThunarPathEntry *path_entry);
static gboolean
thunar_path_entry_check_completion_idle (gpointer user_data);
static void
thunar_path_entry_check_completion_idle_destroy (gpointer user_data);
struct _ThunarPathEntryClass
{
GtkEntryClass __parent__;
};
struct _ThunarPathEntry
{
GtkEntry __parent__;
ThunarIconFactory *icon_factory;
ThunarFile *current_folder;
ThunarFile *current_file;
GFile *working_directory;
guint drag_button;
/* auto completion support */
guint in_change : 1;
guint has_completion : 1;
guint check_completion_idle_id;
gboolean search_mode;
};
static const GtkTargetEntry drag_targets[] = {
{
"text/uri-list",
0,
0,
},
};
static GtkEditableInterface *thunar_path_entry_editable_parent_iface;
G_DEFINE_TYPE_WITH_CODE (ThunarPathEntry, thunar_path_entry, GTK_TYPE_ENTRY,
G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, thunar_path_entry_editable_init))
static void
thunar_path_entry_class_init (ThunarPathEntryClass *klass)
{
GtkWidgetClass *gtkwidget_class;
GtkEntryClass *gtkentry_class;
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = thunar_path_entry_finalize;
gobject_class->get_property = thunar_path_entry_get_property;
gobject_class->set_property = thunar_path_entry_set_property;
gtkwidget_class = GTK_WIDGET_CLASS (klass);
gtkwidget_class->focus = thunar_path_entry_focus;
gtkwidget_class->drag_data_get = thunar_path_entry_drag_data_get;
gtkwidget_class->drag_end = thunar_path_entry_drag_end;
gtkentry_class = GTK_ENTRY_CLASS (klass);
gtkentry_class->activate = thunar_path_entry_activate;
/**
* ThunarPathEntry:current-file:
*
* The #ThunarFile currently displayed by the path entry or %NULL.
**/
g_object_class_install_property (gobject_class,
PROP_CURRENT_FILE,
g_param_spec_object ("current-file",
"current-file",
"current-file",
THUNAR_TYPE_FILE,
EXO_PARAM_READWRITE));
/**
* ThunarPathEntry:icon-size:
*
* The preferred size of the icon displayed in the path entry.
**/
gtk_widget_class_install_style_property (gtkwidget_class,
g_param_spec_int ("icon-size",
_("Icon size"),
_("The icon size for the path entry"),
1, G_MAXINT, 16, EXO_PARAM_READABLE));
}
static void
thunar_path_entry_editable_init (GtkEditableInterface *iface)
{
thunar_path_entry_editable_parent_iface = g_type_interface_peek_parent (iface);
iface->changed = thunar_path_entry_changed;
iface->do_insert_text = thunar_path_entry_do_insert_text;
}
static void
thunar_path_entry_init (ThunarPathEntry *path_entry)
{
GtkEntryCompletion *completion;
GtkCellRenderer *renderer;
ThunarStandardViewModel *store;
path_entry->check_completion_idle_id = 0;
path_entry->working_directory = NULL;
/* allocate a new entry completion for the given model */
completion = gtk_entry_completion_new ();
gtk_entry_completion_set_popup_single_match (completion, FALSE);
gtk_entry_completion_set_match_func (completion, thunar_path_entry_match_func, path_entry, NULL);
g_signal_connect (G_OBJECT (completion), "match-selected", G_CALLBACK (thunar_path_entry_match_selected), path_entry);
/* add the icon renderer to the entry completion */
renderer = g_object_new (THUNAR_TYPE_ICON_RENDERER, "size", 16, NULL);
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (completion), renderer, FALSE);
gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (completion), renderer, "file", THUNAR_COLUMN_FILE);
/* add the text renderer to the entry completion */
renderer = gtk_cell_renderer_text_new ();
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (completion), renderer, TRUE);
gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (completion), renderer, "text", THUNAR_COLUMN_NAME);
/* allocate a new list mode for the completion */
store = thunar_list_model_new ();
thunar_list_model_check_file_in_model_before_use (THUNAR_LIST_MODEL (store));
thunar_standard_view_model_set_show_hidden (store, TRUE);
thunar_standard_view_model_set_folders_first (store, TRUE);
gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store), THUNAR_COLUMN_FILE_NAME, GTK_SORT_ASCENDING);
gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
g_object_unref (G_OBJECT (store));
/* need to connect the "key-press-event" before the GtkEntry class connects the completion signals, so
* we get the Tab key before its handled as part of the completion stuff.
*/
g_signal_connect (G_OBJECT (path_entry), "key-press-event", G_CALLBACK (thunar_path_entry_key_press_event), NULL);
/* setup the new completion */
gtk_entry_set_completion (GTK_ENTRY (path_entry), completion);
/* cleanup */
g_object_unref (G_OBJECT (completion));
/* clear the auto completion whenever the cursor is moved manually or the selection is changed manually */
g_signal_connect (G_OBJECT (path_entry), "notify::cursor-position", G_CALLBACK (thunar_path_entry_clear_completion), NULL);
g_signal_connect (G_OBJECT (path_entry), "notify::selection-bound", G_CALLBACK (thunar_path_entry_clear_completion), NULL);
/* connect the icon signals */
g_signal_connect (G_OBJECT (path_entry), "icon-press", G_CALLBACK (thunar_path_entry_icon_press_event), NULL);
g_signal_connect (G_OBJECT (path_entry), "notify::scale-factor", G_CALLBACK (thunar_path_entry_scale_changed), NULL);
/* disabled initially */
path_entry->search_mode = FALSE;
}
static void
thunar_path_entry_finalize (GObject *object)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (object);
/* release factory */
if (path_entry->icon_factory != NULL)
g_object_unref (path_entry->icon_factory);
/* release the current-folder reference */
if (G_LIKELY (path_entry->current_folder != NULL))
g_object_unref (G_OBJECT (path_entry->current_folder));
/* release the current-file reference */
if (G_LIKELY (path_entry->current_file != NULL))
{
g_signal_handlers_disconnect_by_func (G_OBJECT (path_entry->current_file), thunar_path_entry_set_current_file, path_entry);
g_object_unref (G_OBJECT (path_entry->current_file));
}
/* release the working directory */
if (G_LIKELY (path_entry->working_directory != NULL))
g_object_unref (G_OBJECT (path_entry->working_directory));
/* drop the check_completion_idle source */
if (G_UNLIKELY (path_entry->check_completion_idle_id != 0))
g_source_remove (path_entry->check_completion_idle_id);
(*G_OBJECT_CLASS (thunar_path_entry_parent_class)->finalize) (object);
}
static void
thunar_path_entry_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (object);
switch (prop_id)
{
case PROP_CURRENT_FILE:
g_value_set_object (value, thunar_path_entry_get_current_file (path_entry));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
thunar_path_entry_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (object);
switch (prop_id)
{
case PROP_CURRENT_FILE:
thunar_path_entry_set_current_file (path_entry, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static gboolean
thunar_path_entry_focus (GtkWidget *widget,
GtkDirectionType direction)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (widget);
GdkModifierType state;
gboolean control_pressed;
/* determine whether control is pressed */
control_pressed = (gtk_get_current_event_state (&state) && (state & GDK_CONTROL_MASK) != 0);
/* evil hack, but works for GtkFileChooserEntry, so who cares :-) */
if ((direction == GTK_DIR_TAB_FORWARD) && (gtk_widget_has_focus (widget)) && !control_pressed)
{
/* if we don't have a completion and the cursor is at the end of the line, we just insert the common prefix */
if (!path_entry->has_completion && gtk_editable_get_position (GTK_EDITABLE (path_entry)) == gtk_entry_get_text_length (GTK_ENTRY (path_entry)))
thunar_path_entry_common_prefix_append (path_entry, FALSE);
/* place the cursor at the end */
gtk_editable_set_position (GTK_EDITABLE (path_entry), gtk_entry_get_text_length (GTK_ENTRY (path_entry)));
return TRUE;
}
else
return (*GTK_WIDGET_CLASS (thunar_path_entry_parent_class)->focus) (widget, direction);
}
static void
thunar_path_entry_scale_changed (GObject *object,
GParamSpec *pspec,
gpointer user_data)
{
gtk_widget_queue_draw (GTK_WIDGET (object));
}
static gboolean
thunar_path_entry_icon_press_event (GtkEntry *entry,
GtkEntryIconPosition icon_pos,
GdkEventButton *event,
gpointer user_data)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (entry);
GdkDragContext *context;
GtkTargetList *target_list;
GdkPixbuf *icon;
cairo_surface_t *surface;
gint size;
gint scale_factor;
if (path_entry->current_file == NULL)
return FALSE;
if (event->button == 1 && icon_pos == GTK_ENTRY_ICON_PRIMARY)
{
/* save the drag button state */
path_entry->drag_button = event->button;
/* create a drag context */
target_list = gtk_target_list_new (drag_targets, G_N_ELEMENTS (drag_targets));
context = gtk_drag_begin_with_coordinates (GTK_WIDGET (entry), target_list,
GDK_ACTION_COPY | GDK_ACTION_LINK,
event->button,
(GdkEvent *) event, -1, -1);
gtk_target_list_unref (target_list);
/* setup the drag icon (atleast 24px) */
gtk_widget_style_get (GTK_WIDGET (entry), "icon-size", &size, NULL);
scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (entry));
icon = thunar_icon_factory_load_file_icon (path_entry->icon_factory,
path_entry->current_file,
THUNAR_FILE_ICON_STATE_DEFAULT,
MAX (size, 16), scale_factor,
FALSE, NULL);
if (G_LIKELY (icon != NULL))
{
surface = gdk_cairo_surface_create_from_pixbuf (icon, scale_factor, gtk_widget_get_window (GTK_WIDGET (entry)));
g_object_unref (G_OBJECT (icon));
gtk_drag_set_icon_surface (context, surface);
cairo_surface_destroy (surface);
}
return TRUE;
}
return FALSE;
}
static gboolean
thunar_path_entry_key_press_event (GtkWidget *widget,
GdkEventKey *event)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (widget);
/* check if we have a tab key press here and control is not pressed */
if (G_UNLIKELY (event->keyval == GDK_KEY_Tab && (event->state & GDK_CONTROL_MASK) == 0) && path_entry->search_mode == FALSE)
{
/* if we don't have a completion and the cursor is at the end of the line, we just insert the common prefix */
if (!path_entry->has_completion && gtk_editable_get_position (GTK_EDITABLE (path_entry)) == gtk_entry_get_text_length (GTK_ENTRY (path_entry)))
thunar_path_entry_common_prefix_append (path_entry, FALSE);
/* place the cursor at the end */
gtk_editable_set_position (GTK_EDITABLE (path_entry), gtk_entry_get_text_length (GTK_ENTRY (path_entry)));
/* emit "changed", so the completion window is popped up */
g_signal_emit_by_name (G_OBJECT (path_entry), "changed", 0);
/* we handled the event */
return TRUE;
}
/* cancel search with `Escape` */
if (G_UNLIKELY (path_entry->search_mode == TRUE && event->keyval == GDK_KEY_Escape && (event->state & GDK_CONTROL_MASK) == 0))
{
GtkWidget *window = gtk_widget_get_toplevel (widget);
thunar_window_action_cancel_search (THUNAR_WINDOW (window));
return TRUE;
}
return FALSE;
}
static void
thunar_path_entry_drag_data_get (GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *selection_data,
guint info,
guint timestamp)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (widget);
GList file_list;
gchar **uris;
/* check if the icon was clicked and verify that we actually display a path */
if (path_entry->drag_button > 0 && path_entry->current_file != NULL)
{
/* transform the path for the current file into an uri string list */
file_list.next = file_list.prev = NULL;
file_list.data = thunar_file_get_file (path_entry->current_file);
/* setup the uri list for the drag selection */
uris = thunar_g_file_list_to_stringv (&file_list);
gtk_selection_data_set_uris (selection_data, uris);
g_strfreev (uris);
return;
}
(*GTK_WIDGET_CLASS (thunar_path_entry_parent_class)->drag_data_get) (widget, context, selection_data, info, timestamp);
}
static void
thunar_path_entry_drag_end (GtkWidget *widget,
GdkDragContext *context)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (widget);
/* reset the drag button state */
path_entry->drag_button = 0;
(*GTK_WIDGET_CLASS (thunar_path_entry_parent_class)->drag_end) (widget, context);
}
static void
thunar_path_entry_activate (GtkEntry *entry)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (entry);
if (G_LIKELY (path_entry->has_completion))
{
/* place cursor at the end of the text if we have completion set */
gtk_editable_set_position (GTK_EDITABLE (path_entry), -1);
}
/* emit the "activate" signal */
(*GTK_ENTRY_CLASS (thunar_path_entry_parent_class)->activate) (entry);
}
static gboolean
thunar_path_entry_file_exists (GFile *file)
{
gchar *path;
gboolean result = FALSE;
_thunar_return_val_if_fail (G_IS_FILE (file), FALSE);
/* support for locations like 'admin://' and 'network://' */
if (!g_file_is_native (file))
return TRUE;
/* check if native file exists */
path = g_file_get_path (file);
if (path != NULL)
{
if (g_file_test (path, G_FILE_TEST_EXISTS))
result = TRUE;
g_free (path);
}
return result;
}
static void
thunar_path_entry_changed (GtkEditable *editable)
{
GtkEntryCompletion *completion;
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (editable);
ThunarFolder *folder;
GtkTreeModel *model;
const gchar *text;
gchar *scheme;
ThunarFile *current_folder = NULL;
ThunarFile *current_file = NULL;
GFile *folder_path = NULL;
GFile *file_path = NULL;
gchar *folder_part = NULL;
gchar *file_part = NULL;
gboolean update_icon = FALSE;
/* check if we should ignore this event */
if (G_UNLIKELY (path_entry->in_change))
return;
/* parse the entered string (handling URIs properly) */
text = gtk_entry_get_text (GTK_ENTRY (path_entry));
if (G_UNLIKELY (text != NULL && thunar_util_is_a_search_query (text)) == TRUE)
{
GtkWidget *window = gtk_widget_get_toplevel (GTK_WIDGET (editable));
path_entry->search_mode = TRUE;
update_icon = TRUE;
if (GTK_IS_WINDOW (window))
thunar_window_update_search (THUNAR_WINDOW (window));
}
else
{
/* cancel search when part of 'Search: ' is deleted */
if (path_entry->search_mode == TRUE)
{
GtkWidget *window = gtk_widget_get_toplevel (GTK_WIDGET (editable));
thunar_window_action_cancel_search (THUNAR_WINDOW (window));
}
/* location/folder-path code */
scheme = g_uri_parse_scheme (text);
if (G_UNLIKELY (scheme != NULL))
{
/* Text is URI */
g_free (scheme);
file_path = g_file_new_for_uri (text);
/* use the same file if the text assumes we're in a directory */
if (g_str_has_suffix (text, "/"))
folder_path = G_FILE (g_object_ref (G_OBJECT (file_path)));
else
folder_path = g_file_get_parent (file_path);
}
else if (thunar_path_entry_parse (path_entry, &folder_part, &file_part, NULL))
{
/* determine the folder path */
folder_path = g_file_new_for_path (folder_part);
/* determine the relative file path */
if (G_LIKELY (*file_part != '\0'))
file_path = g_file_resolve_relative_path (folder_path, file_part);
else
file_path = g_object_ref (folder_path);
/* cleanup the part strings */
g_free (folder_part);
g_free (file_part);
}
}
/* determine new current file/folder from the paths */
if (folder_path != NULL && thunar_path_entry_file_exists (folder_path))
current_folder = thunar_file_get (folder_path, NULL);
if (file_path != NULL && thunar_path_entry_file_exists (file_path))
current_file = thunar_file_get (file_path, NULL);
/* determine the entry completion */
completion = gtk_entry_get_completion (GTK_ENTRY (path_entry));
/* update the current folder if required */
if (current_folder != path_entry->current_folder)
{
/* take a reference on the current folder */
if (G_LIKELY (path_entry->current_folder != NULL))
g_object_unref (G_OBJECT (path_entry->current_folder));
path_entry->current_folder = current_folder;
if (G_LIKELY (current_folder != NULL))
g_object_ref (G_OBJECT (current_folder));
/* try to open the current-folder file as folder */
if (current_folder != NULL && thunar_file_is_directory (current_folder))
folder = thunar_folder_get_for_file (current_folder);
else
folder = NULL;
/* set the new folder for the completion model, but disconnect the model from the
* completion first, because GtkEntryCompletion has become very slow recently when
* updating the model being used (https://bugzilla.xfce.org/show_bug.cgi?id=1681).
*/
model = gtk_entry_completion_get_model (completion);
g_object_ref (G_OBJECT (model));
gtk_entry_completion_set_model (completion, NULL);
thunar_standard_view_model_set_folder (THUNAR_STANDARD_VIEW_MODEL (model), folder, NULL);
gtk_entry_completion_set_model (completion, model);
g_object_unref (G_OBJECT (model));
/* cleanup */
if (G_LIKELY (folder != NULL))
g_object_unref (G_OBJECT (folder));
/* we most likely need a new icon */
update_icon = TRUE;
}
/* update the current file if required */
if (current_file != path_entry->current_file)
{
if (G_UNLIKELY (path_entry->current_file != NULL))
{
g_signal_handlers_disconnect_by_func (G_OBJECT (path_entry->current_file), thunar_path_entry_set_current_file, path_entry);
g_object_unref (G_OBJECT (path_entry->current_file));
}
path_entry->current_file = current_file;
if (G_UNLIKELY (current_file != NULL))
{
g_object_ref (G_OBJECT (current_file));
g_signal_connect_swapped (G_OBJECT (current_file), "changed", G_CALLBACK (thunar_path_entry_set_current_file), path_entry);
}
g_object_notify (G_OBJECT (path_entry), "current-file");
/* we most likely need a new icon */
update_icon = TRUE;
}
if (update_icon)
thunar_path_entry_update_icon (path_entry);
/* cleanup */
if (G_LIKELY (current_folder != NULL))
g_object_unref (G_OBJECT (current_folder));
if (G_LIKELY (current_file != NULL))
g_object_unref (G_OBJECT (current_file));
if (G_LIKELY (folder_path != NULL))
g_object_unref (folder_path);
if (G_LIKELY (file_path != NULL))
g_object_unref (file_path);
}
static void
thunar_path_entry_update_icon (ThunarPathEntry *path_entry)
{
ThunarPreferences *preferences;
GdkPixbuf *icon = NULL;
GtkIconTheme *icon_theme;
gint icon_size;
gint scale_factor;
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));
if (path_entry->search_mode == TRUE)
{
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (path_entry), GTK_ENTRY_ICON_PRIMARY,
use_symbolic_icons ? "system-search-symbolic" : "system-search");
return;
}
if (path_entry->icon_factory == NULL)
{
icon_theme = gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (path_entry)));
path_entry->icon_factory = thunar_icon_factory_get_for_icon_theme (icon_theme);
}
gtk_widget_style_get (GTK_WIDGET (path_entry), "icon-size", &icon_size, NULL);
scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (path_entry));
if (G_UNLIKELY (path_entry->current_file != NULL))
{
icon = thunar_icon_factory_load_file_icon (path_entry->icon_factory,
path_entry->current_file,
THUNAR_FILE_ICON_STATE_DEFAULT,
icon_size, scale_factor,
FALSE, NULL);
}
else if (G_LIKELY (path_entry->current_folder != NULL))
{
icon = thunar_icon_factory_load_file_icon (path_entry->icon_factory,
path_entry->current_folder,
THUNAR_FILE_ICON_STATE_DEFAULT,
icon_size, scale_factor,
FALSE, NULL);
}
if (icon != NULL)
{
gtk_entry_set_icon_from_pixbuf (GTK_ENTRY (path_entry),
GTK_ENTRY_ICON_PRIMARY,
icon);
g_object_unref (icon);
}
else
{
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (path_entry), GTK_ENTRY_ICON_PRIMARY,
use_symbolic_icons ? "dialog-error-symbolic" : "dialog-error");
}
}
static void
thunar_path_entry_do_insert_text (GtkEditable *editable,
const gchar *new_text,
gint new_text_length,
gint *position)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (editable);
/* let the GtkEntry class handle the insert */
(*thunar_path_entry_editable_parent_iface->do_insert_text) (editable, new_text, new_text_length, position);
/* queue a completion check if this insert operation was triggered by the user */
if (G_LIKELY (!path_entry->in_change))
thunar_path_entry_queue_check_completion (path_entry);
}
static void
thunar_path_entry_clear_completion (ThunarPathEntry *path_entry)
{
/* reset the completion and apply the new text */
if (G_UNLIKELY (path_entry->has_completion))
{
path_entry->has_completion = FALSE;
thunar_path_entry_changed (GTK_EDITABLE (path_entry));
}
}
static void
thunar_path_entry_common_prefix_append (ThunarPathEntry *path_entry,
gboolean highlight)
{
const gchar *last_slash;
const gchar *text;
ThunarFile *file;
gchar *prefix;
gchar *tmp;
gint prefix_length;
gint text_length;
gint offset;
gint base;
/* determine the common prefix */
thunar_path_entry_common_prefix_lookup (path_entry, &prefix, &file);
/* check if we should append a slash to the prefix */
if (G_LIKELY (file != NULL))
{
/* we only append slashes for directories */
if (thunar_file_is_directory (file) && file != path_entry->current_file)
{
tmp = g_strconcat (prefix, G_DIR_SEPARATOR_S, NULL);
g_free (prefix);
prefix = tmp;
}
/* release the file */
g_object_unref (G_OBJECT (file));
}
/* check if we have a common prefix */
if (G_LIKELY (prefix != NULL))
{
/* determine the UTF-8 length of the entry text */
text = gtk_entry_get_text (GTK_ENTRY (path_entry));
last_slash = g_utf8_strrchr (text, -1, G_DIR_SEPARATOR);
if (G_LIKELY (last_slash != NULL))
offset = g_utf8_strlen (text, last_slash - text) + 1;
else
offset = 0;
text_length = g_utf8_strlen (text, -1) - offset;
/* determine the UTF-8 length of the prefix */
prefix_length = g_utf8_strlen (prefix, -1);
/* append only if the prefix is longer than the already entered text */
if (G_LIKELY (prefix_length > text_length))
{
/* remember the base offset */
base = offset;
/* insert the prefix */
path_entry->in_change = TRUE;
gtk_editable_delete_text (GTK_EDITABLE (path_entry), offset, -1);
gtk_editable_insert_text (GTK_EDITABLE (path_entry), prefix, -1, &offset);
path_entry->in_change = FALSE;
/* highlight the prefix if requested */
if (G_LIKELY (highlight))
{
gtk_editable_select_region (GTK_EDITABLE (path_entry), base + text_length, base + prefix_length);
path_entry->has_completion = TRUE;
}
}
/* cleanup */
g_free (prefix);
}
}
static gboolean
thunar_path_entry_has_prefix_casefolded (const gchar *string,
const gchar *prefix)
{
gchar *string_casefolded;
gchar *prefix_casefolded;
gboolean has_prefix;
if (string == NULL || prefix == NULL)
return FALSE;
string_casefolded = g_utf8_casefold (string, -1);
prefix_casefolded = g_utf8_casefold (prefix, -1);
has_prefix = g_str_has_prefix (string_casefolded, prefix_casefolded);
g_free (string_casefolded);
g_free (prefix_casefolded);
return has_prefix;
}
static void
thunar_path_entry_common_prefix_lookup (ThunarPathEntry *path_entry,
gchar **prefix_return,
ThunarFile **file_return)
{
GtkTreeModel *model;
GtkTreeIter iter;
const gchar *text;
const gchar *s;
gchar *name;
gchar *t;
*prefix_return = NULL;
*file_return = NULL;
/* lookup the last slash character in the entry text */
text = gtk_entry_get_text (GTK_ENTRY (path_entry));
s = strrchr (text, G_DIR_SEPARATOR);
if (G_UNLIKELY (s != NULL && s[1] == '\0'))
return;
else if (G_LIKELY (s != NULL))
text = s + 1;
/* check all items in the model */
model = gtk_entry_completion_get_model (gtk_entry_get_completion (GTK_ENTRY (path_entry)));
if (gtk_tree_model_get_iter_first (model, &iter))
{
do
{
/* determine the real file name for the iter */
gtk_tree_model_get (model, &iter, THUNAR_COLUMN_FILE_NAME, &name, -1);
/* check if we have a valid prefix here */
if (thunar_path_entry_has_prefix_casefolded (name, text))
{
/* check if we're the first to match */
if (*prefix_return == NULL)
{
/* remember the prefix */
*prefix_return = g_strdup (name);
/* determine the file for the iter */
gtk_tree_model_get (model, &iter, THUNAR_COLUMN_FILE, file_return, -1);
}
else
{
/* we already have another prefix, so determine the common part */
for (s = name, t = *prefix_return; *s != '\0' && *s == *t; ++s, ++t)
;
*t = '\0';
/* release the file, since it's not a unique match */
if (G_LIKELY (*file_return != NULL))
{
g_object_unref (G_OBJECT (*file_return));
*file_return = NULL;
}
}
}
/* cleanup */
g_free (name);
}
while (gtk_tree_model_iter_next (model, &iter));
}
}
static gboolean
thunar_path_entry_match_func (GtkEntryCompletion *completion,
const gchar *key,
GtkTreeIter *iter,
gpointer user_data)
{
GtkTreeModel *model;
ThunarPathEntry *path_entry;
const gchar *last_slash;
ThunarFile *file;
gboolean matched;
gchar *text_normalized;
gchar *name_normalized;
gchar *name;
/* determine the model from the completion */
model = gtk_entry_completion_get_model (completion);
/* leave if the model is null, we do this in thunar_path_entry_changed() to speed
* things up, but that causes https://bugzilla.xfce.org/show_bug.cgi?id=4847. */
if (G_UNLIKELY (model == NULL))
return FALSE;
/* leave if the auto completion highlight was not cleared yet, to prevent
* https://bugzilla.xfce.org/show_bug.cgi?id=16267. */
path_entry = THUNAR_PATH_ENTRY (user_data);
if (G_UNLIKELY (path_entry->has_completion))
return FALSE;
/* determine the current text (UTF-8 normalized) */
text_normalized = g_utf8_normalize (gtk_entry_get_text (GTK_ENTRY (user_data)), -1, G_NORMALIZE_ALL);
/* lookup the last slash character in the key */
last_slash = strrchr (text_normalized, G_DIR_SEPARATOR);
if (G_UNLIKELY (last_slash != NULL && last_slash[1] == '\0'))
{
/* check if the file is hidden */
gtk_tree_model_get (model, iter, THUNAR_COLUMN_FILE, &file, -1);
matched = !thunar_file_is_hidden (file);
g_object_unref (G_OBJECT (file));
}
else
{
if (G_UNLIKELY (last_slash == NULL))
last_slash = text_normalized;
else
last_slash += 1;
/* determine the real file name for the iter */
gtk_tree_model_get (model, iter, THUNAR_COLUMN_FILE_NAME, &name, -1);
name_normalized = g_utf8_normalize (name, -1, G_NORMALIZE_ALL);
if (G_LIKELY (name_normalized != NULL))
g_free (name);
else
name_normalized = name;
/* check if we have a match here */
if (name_normalized != NULL)
matched = thunar_path_entry_has_prefix_casefolded (name_normalized, last_slash);
else
matched = FALSE;
/* cleanup */
g_free (name_normalized);
}
/* cleanup */
g_free (text_normalized);
return matched;
}
static gboolean
thunar_path_entry_match_selected (GtkEntryCompletion *completion,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer user_data)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (user_data);
const gchar *last_slash;
const gchar *text;
ThunarFile *file;
gchar *real_name;
gchar *tmp;
gint offset;
/* determine the file for the iterator */
gtk_tree_model_get (model, iter, THUNAR_COLUMN_FILE, &file, -1);
/* determine the real name for the file */
gtk_tree_model_get (model, iter, THUNAR_COLUMN_FILE_NAME, &real_name, -1);
/* append a slash if we have a folder here */
if (G_LIKELY (thunar_file_is_directory (file)))
{
tmp = g_strconcat (real_name, G_DIR_SEPARATOR_S, NULL);
g_free (real_name);
real_name = tmp;
}
/* determine the UTF-8 offset of the last slash on the entry text */
text = gtk_entry_get_text (GTK_ENTRY (path_entry));
last_slash = g_utf8_strrchr (text, -1, G_DIR_SEPARATOR);
if (G_LIKELY (last_slash != NULL))
offset = g_utf8_strlen (text, last_slash - text) + 1;
else
offset = 0;
/* delete the previous text at the specified offset */
gtk_editable_delete_text (GTK_EDITABLE (path_entry), offset, -1);
/* insert the new file/folder name */
gtk_editable_insert_text (GTK_EDITABLE (path_entry), real_name, -1, &offset);
/* move the cursor to the end of the text entry */
gtk_editable_set_position (GTK_EDITABLE (path_entry), -1);
/* cleanup */
g_object_unref (G_OBJECT (file));
g_free (real_name);
return TRUE;
}
static gboolean
thunar_path_entry_parse (ThunarPathEntry *path_entry,
gchar **folder_part,
gchar **file_part,
GError **error)
{
const gchar *last_slash;
gchar *filename;
gchar *path;
_thunar_return_val_if_fail (THUNAR_IS_PATH_ENTRY (path_entry), FALSE);
_thunar_return_val_if_fail (error == NULL || *error == NULL, FALSE);
_thunar_return_val_if_fail (folder_part != NULL, FALSE);
_thunar_return_val_if_fail (file_part != NULL, FALSE);
/* expand the filename */
filename = thunar_util_expand_filename (gtk_entry_get_text (GTK_ENTRY (path_entry)),
path_entry->working_directory,
error);
if (G_UNLIKELY (filename == NULL))
return FALSE;
/* lookup the last slash character in the filename */
last_slash = strrchr (filename, G_DIR_SEPARATOR);
if (G_UNLIKELY (last_slash == NULL))
{
/* no slash character, it's relative to the home dir */
*file_part = g_filename_from_utf8 (filename, -1, NULL, NULL, error);
if (G_LIKELY (*file_part != NULL))
*folder_part = g_strdup (xfce_get_homedir ());
}
else
{
if (G_LIKELY (last_slash != filename))
*folder_part = g_filename_from_utf8 (filename, last_slash - filename, NULL, NULL, error);
else
*folder_part = g_strdup ("/");
if (G_LIKELY (*folder_part != NULL))
{
/* if folder_part doesn't start with '/', it's relative to the home dir */
if (G_UNLIKELY (**folder_part != G_DIR_SEPARATOR))
{
path = xfce_get_homefile (*folder_part, NULL);
g_free (*folder_part);
*folder_part = path;
}
/* determine the file part */
*file_part = g_filename_from_utf8 (last_slash + 1, -1, NULL, NULL, error);
if (G_UNLIKELY (*file_part == NULL))
{
g_free (*folder_part);
*folder_part = NULL;
}
}
else
{
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "%s", g_strerror (EINVAL));
}
}
/* release the filename */
g_free (filename);
return (*folder_part != NULL);
}
static void
thunar_path_entry_queue_check_completion (ThunarPathEntry *path_entry)
{
if (G_LIKELY (path_entry->check_completion_idle_id == 0))
{
path_entry->check_completion_idle_id = g_idle_add_full (G_PRIORITY_HIGH, thunar_path_entry_check_completion_idle,
path_entry, thunar_path_entry_check_completion_idle_destroy);
}
}
static gboolean
thunar_path_entry_check_completion_idle (gpointer user_data)
{
ThunarPathEntry *path_entry = THUNAR_PATH_ENTRY (user_data);
const gchar *text;
/* check if the user entered at least part of a filename */
text = gtk_entry_get_text (GTK_ENTRY (path_entry));
if (*text != '\0' && text[strlen (text) - 1] != '/')
{
/* automatically insert the common prefix */
thunar_path_entry_common_prefix_append (path_entry, TRUE);
}
return FALSE;
}
static void
thunar_path_entry_check_completion_idle_destroy (gpointer user_data)
{
THUNAR_PATH_ENTRY (user_data)->check_completion_idle_id = 0;
}
/**
* thunar_path_entry_new:
*
* Allocates a new #ThunarPathEntry instance.
*
* Return value: the newly allocated #ThunarPathEntry.
**/
GtkWidget *
thunar_path_entry_new (void)
{
return g_object_new (THUNAR_TYPE_PATH_ENTRY, NULL);
}
/**
* thunar_path_entry_get_current_file:
* @path_entry : a #ThunarPathEntry.
*
* Returns the #ThunarFile currently being displayed by
* @path_entry or %NULL if @path_entry doesn't contain
* a valid #ThunarFile.
*
* Return value: the #ThunarFile for @path_entry or %NULL.
**/
ThunarFile *
thunar_path_entry_get_current_file (ThunarPathEntry *path_entry)
{
_thunar_return_val_if_fail (THUNAR_IS_PATH_ENTRY (path_entry), NULL);
return path_entry->current_file;
}
/**
* thunar_path_entry_set_current_file:
* @path_entry : a #ThunarPathEntry.
* @current_file : a #ThunarFile or %NULL.
*
* Sets the #ThunarFile that should be displayed by
* @path_entry to @current_file.
**/
void
thunar_path_entry_set_current_file (ThunarPathEntry *path_entry,
ThunarFile *current_file)
{
GFile *file;
gchar *text;
gchar *unescaped;
gchar *tmp;
gboolean is_uri = FALSE;
_thunar_return_if_fail (THUNAR_IS_PATH_ENTRY (path_entry));
_thunar_return_if_fail (current_file == NULL || THUNAR_IS_FILE (current_file));
file = (current_file != NULL) ? thunar_file_get_file (current_file) : NULL;
if (G_UNLIKELY (file == NULL))
{
/* invalid file */
text = g_strdup ("");
}
else
{
/* check if the file is native to the platform */
if (g_file_is_native (file))
{
/* it is, try the local path first */
text = g_file_get_path (file);
/* if there is no local path, use the URI (which always works) */
if (text == NULL)
{
text = g_file_get_uri (file);
is_uri = TRUE;
}
}
else
{
/* not a native file, use the URI */
text = g_file_get_uri (file);
is_uri = TRUE;
}
/* if the file is a directory, end with a / to avoid loading the parent
* directory which is probably not something the user wants */
if (thunar_file_is_directory (current_file)
&& !g_str_has_suffix (text, "/"))
{
tmp = g_strconcat (text, "/", NULL);
g_free (text);
text = tmp;
}
// convert filename into valid UTF-8 string for display
tmp = text;
text = g_filename_display_name (tmp);
g_free (tmp);
}
if (is_uri)
unescaped = g_uri_unescape_string (text, NULL);
else
unescaped = g_strdup (text);
g_free (text);
/* setup the entry text */
gtk_entry_set_text (GTK_ENTRY (path_entry), unescaped);
g_free (unescaped);
/* update the icon */
thunar_path_entry_update_icon (path_entry);
gtk_editable_set_position (GTK_EDITABLE (path_entry), -1);
gtk_widget_queue_draw (GTK_WIDGET (path_entry));
}
/**
* thunar_path_entry_set_working_directory:
* @path_entry : a #ThunarPathEntry.
* @working_directory : a #ThunarFile or %NULL.
*
* Sets the #ThunarFile that should be used as the
* working directory for @path_entry.
**/
void
thunar_path_entry_set_working_directory (ThunarPathEntry *path_entry,
ThunarFile *working_directory)
{
_thunar_return_if_fail (THUNAR_IS_PATH_ENTRY (path_entry));
_thunar_return_if_fail (working_directory == NULL || THUNAR_IS_FILE (working_directory));
if (G_LIKELY (path_entry->working_directory != NULL))
g_object_unref (path_entry->working_directory);
path_entry->working_directory = NULL;
if (THUNAR_IS_FILE (working_directory))
path_entry->working_directory = g_object_ref (thunar_file_get_file (working_directory));
}
/**
* thunar_path_entry_get_search_query:
* @path_entry : a #ThunarPathEntry.
*
* Returns a copy of the search query in the text field of the @path_entry or "" if the path_entry doesn't contain
* a search query.
*
* It's the responsibility of the caller to free the returned string using `g_free`.
**/
gchar *
thunar_path_entry_get_search_query (ThunarPathEntry *path_entry)
{
unsigned long search_prefix_length;
const gchar *text = gtk_entry_get_text (GTK_ENTRY (path_entry));
search_prefix_length = strlen (thunar_util_get_search_prefix ());
_thunar_return_val_if_fail (THUNAR_IS_PATH_ENTRY (path_entry), NULL);
_thunar_return_val_if_fail (strncmp (text, thunar_util_get_search_prefix (), search_prefix_length) == 0, NULL);
return strlen (text) > search_prefix_length ? g_strdup (&text[search_prefix_length]) : g_strdup ("");
}
void
thunar_path_entry_cancel_search (ThunarPathEntry *path_entry)
{
_thunar_return_if_fail (THUNAR_IS_PATH_ENTRY (path_entry));
path_entry->search_mode = FALSE;
gtk_widget_grab_focus (gtk_widget_get_parent (GTK_WIDGET (path_entry)));
}