/*
* Nautilus
*
* Copyright (C) 2000 Eazel, Inc.
*
* Nautilus 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.
*
* Nautilus 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; see the file COPYING. If not,
* see .
*
* Author: Maciej Stachowiak
* Ettore Perazzoli
* Michael Meeks
* Andy Hertzfeld
*
*/
/* nautilus-location-bar.c - Location bar for Nautilus
*/
#include
#include "nautilus-location-entry.h"
#include "nautilus-application.h"
#include "nautilus-window.h"
#include
#include
#include
#include
#include "nautilus-file-utilities.h"
#include "nautilus-clipboard.h"
#include
#include
#include
#include
#include
#define NAUTILUS_DND_URI_LIST_TYPE "text/uri-list"
#define NAUTILUS_DND_TEXT_PLAIN_TYPE "text/plain"
enum
{
NAUTILUS_DND_URI_LIST,
NAUTILUS_DND_TEXT_PLAIN,
NAUTILUS_DND_NTARGETS
};
static const GtkTargetEntry drag_types [] =
{
{ NAUTILUS_DND_URI_LIST_TYPE, 0, NAUTILUS_DND_URI_LIST },
{ NAUTILUS_DND_TEXT_PLAIN_TYPE, 0, NAUTILUS_DND_TEXT_PLAIN },
};
static const GtkTargetEntry drop_types [] =
{
{ NAUTILUS_DND_URI_LIST_TYPE, 0, NAUTILUS_DND_URI_LIST },
{ NAUTILUS_DND_TEXT_PLAIN_TYPE, 0, NAUTILUS_DND_TEXT_PLAIN },
};
typedef struct _NautilusLocationEntryPrivate
{
char *current_directory;
GFilenameCompleter *completer;
guint idle_id;
GFile *last_location;
gboolean has_special_text;
gboolean setting_special_text;
gchar *special_text;
NautilusLocationEntryAction secondary_action;
} NautilusLocationEntryPrivate;
enum
{
CANCEL,
LOCATION_CHANGED,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];
G_DEFINE_TYPE_WITH_PRIVATE (NautilusLocationEntry, nautilus_location_entry, GTK_TYPE_ENTRY);
static GFile *
nautilus_location_entry_get_location (NautilusLocationEntry *entry)
{
char *user_location;
GFile *location;
user_location = gtk_editable_get_chars (GTK_EDITABLE (entry), 0, -1);
location = g_file_parse_name (user_location);
g_free (user_location);
return location;
}
static void
emit_location_changed (NautilusLocationEntry *entry)
{
GFile *location;
location = nautilus_location_entry_get_location (entry);
g_signal_emit (entry, signals[LOCATION_CHANGED], 0, location);
g_object_unref (location);
}
static void
nautilus_location_entry_update_action (NautilusLocationEntry *entry)
{
NautilusLocationEntryPrivate *priv;
const char *current_text;
GFile *location;
priv = nautilus_location_entry_get_instance_private (entry);
if (priv->last_location == NULL)
{
nautilus_location_entry_set_secondary_action (entry,
NAUTILUS_LOCATION_ENTRY_ACTION_GOTO);
return;
}
current_text = gtk_entry_get_text (GTK_ENTRY (entry));
location = g_file_parse_name (current_text);
if (g_file_equal (priv->last_location, location))
{
nautilus_location_entry_set_secondary_action (entry,
NAUTILUS_LOCATION_ENTRY_ACTION_CLEAR);
}
else
{
nautilus_location_entry_set_secondary_action (entry,
NAUTILUS_LOCATION_ENTRY_ACTION_GOTO);
}
g_object_unref (location);
}
static int
get_editable_number_of_chars (GtkEditable *editable)
{
char *text;
int length;
text = gtk_editable_get_chars (editable, 0, -1);
length = g_utf8_strlen (text, -1);
g_free (text);
return length;
}
static void
set_position_and_selection_to_end (GtkEditable *editable)
{
int end;
end = get_editable_number_of_chars (editable);
gtk_editable_select_region (editable, end, end);
gtk_editable_set_position (editable, end);
}
static void
nautilus_location_entry_update_current_uri (NautilusLocationEntry *entry,
const char *uri)
{
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
g_free (priv->current_directory);
priv->current_directory = g_strdup (uri);
gtk_entry_set_text (GTK_ENTRY (entry), uri);
set_position_and_selection_to_end (GTK_EDITABLE (entry));
}
void
nautilus_location_entry_set_location (NautilusLocationEntry *entry,
GFile *location)
{
NautilusLocationEntryPrivate *priv;
gchar *uri, *formatted_uri;
g_assert (location != NULL);
priv = nautilus_location_entry_get_instance_private (entry);
/* Note: This is called in reaction to external changes, and
* thus should not emit the LOCATION_CHANGED signal. */
uri = g_file_get_uri (location);
formatted_uri = g_file_get_parse_name (location);
if (eel_uri_is_search (uri))
{
nautilus_location_entry_set_special_text (entry, "");
}
else
{
nautilus_location_entry_update_current_uri (entry, formatted_uri);
}
/* remember the original location for later comparison */
if (!priv->last_location ||
!g_file_equal (priv->last_location, location))
{
g_clear_object (&priv->last_location);
priv->last_location = g_object_ref (location);
}
nautilus_location_entry_update_action (entry);
g_free (uri);
g_free (formatted_uri);
}
static void
drag_data_received_callback (GtkWidget *widget,
GdkDragContext *context,
int x,
int y,
GtkSelectionData *data,
guint info,
guint32 time,
gpointer callback_data)
{
char **names;
int name_count;
GtkWidget *window;
gboolean new_windows_for_extras;
char *prompt;
char *detail;
GFile *location;
NautilusLocationEntry *self = NAUTILUS_LOCATION_ENTRY (widget);
g_assert (data != NULL);
g_assert (callback_data == NULL);
names = g_uri_list_extract_uris ((const gchar *) gtk_selection_data_get_data (data));
if (names == NULL || *names == NULL)
{
g_warning ("No D&D URI's");
gtk_drag_finish (context, FALSE, FALSE, time);
return;
}
window = gtk_widget_get_toplevel (widget);
new_windows_for_extras = FALSE;
/* Ask user if they really want to open multiple windows
* for multiple dropped URIs. This is likely to have been
* a mistake.
*/
name_count = g_strv_length (names);
if (name_count > 1)
{
prompt = g_strdup_printf (ngettext ("Do you want to view %d location?",
"Do you want to view %d locations?",
name_count),
name_count);
detail = g_strdup_printf (ngettext ("This will open %d separate window.",
"This will open %d separate windows.",
name_count),
name_count);
/* eel_run_simple_dialog should really take in pairs
* like gtk_dialog_new_with_buttons() does. */
new_windows_for_extras = eel_run_simple_dialog (GTK_WIDGET (window),
TRUE,
GTK_MESSAGE_QUESTION,
prompt,
detail,
_("_Cancel"), _("_OK"),
NULL) != 0 /* GNOME_OK */;
g_free (prompt);
g_free (detail);
if (!new_windows_for_extras)
{
gtk_drag_finish (context, FALSE, FALSE, time);
return;
}
}
location = g_file_new_for_uri (names[0]);
nautilus_location_entry_set_location (self, location);
emit_location_changed (self);
g_object_unref (location);
if (new_windows_for_extras)
{
int i;
for (i = 1; names[i] != NULL; ++i)
{
location = g_file_new_for_uri (names[i]);
nautilus_application_open_location_full (NAUTILUS_APPLICATION (g_application_get_default ()),
location, NAUTILUS_WINDOW_OPEN_FLAG_NEW_WINDOW, NULL, NULL, NULL);
g_object_unref (location);
}
}
g_strfreev (names);
gtk_drag_finish (context, TRUE, FALSE, time);
}
static void
drag_data_get_callback (GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *selection_data,
guint info,
guint32 time,
gpointer callback_data)
{
NautilusLocationEntry *self;
GFile *location;
gchar *uri;
g_assert (selection_data != NULL);
self = callback_data;
location = nautilus_location_entry_get_location (self);
uri = g_file_get_uri (location);
switch (info)
{
case NAUTILUS_DND_URI_LIST:
case NAUTILUS_DND_TEXT_PLAIN:
{
gtk_selection_data_set (selection_data,
gtk_selection_data_get_target (selection_data),
8, (guchar *) uri,
strlen (uri));
}
break;
default:
g_assert_not_reached ();
}
g_free (uri);
g_object_unref (location);
}
/* routine that performs the tab expansion. Extract the directory name and
* incomplete basename, then iterate through the directory trying to complete it. If we
* find something, add it to the entry */
static gboolean
try_to_expand_path (gpointer callback_data)
{
NautilusLocationEntry *entry;
NautilusLocationEntryPrivate *priv;
GtkEditable *editable;
char *suffix, *user_location, *absolute_location, *uri_scheme;
int user_location_length, pos;
entry = NAUTILUS_LOCATION_ENTRY (callback_data);
priv = nautilus_location_entry_get_instance_private (entry);
editable = GTK_EDITABLE (entry);
user_location = gtk_editable_get_chars (editable, 0, -1);
user_location_length = g_utf8_strlen (user_location, -1);
priv->idle_id = 0;
uri_scheme = g_uri_parse_scheme (user_location);
if (!g_path_is_absolute (user_location) && uri_scheme == NULL && user_location[0] != '~')
{
absolute_location = g_build_filename (priv->current_directory, user_location, NULL);
suffix = g_filename_completer_get_completion_suffix (priv->completer,
absolute_location);
g_free (absolute_location);
}
else
{
suffix = g_filename_completer_get_completion_suffix (priv->completer,
user_location);
}
g_free (user_location);
g_free (uri_scheme);
/* if we've got something, add it to the entry */
if (suffix != NULL)
{
pos = user_location_length;
gtk_editable_insert_text (editable,
suffix, -1, &pos);
pos = user_location_length;
gtk_editable_select_region (editable, pos, -1);
g_free (suffix);
}
return FALSE;
}
/* Until we have a more elegant solution, this is how we figure out if
* the GtkEntry inserted characters, assuming that the return value is
* TRUE indicating that the GtkEntry consumed the key event for some
* reason. This is a clone of code from GtkEntry.
*/
static gboolean
entry_would_have_inserted_characters (const GdkEventKey *event)
{
switch (event->keyval)
{
case GDK_KEY_BackSpace:
case GDK_KEY_Clear:
case GDK_KEY_Insert:
case GDK_KEY_Delete:
case GDK_KEY_Home:
case GDK_KEY_End:
case GDK_KEY_KP_Home:
case GDK_KEY_KP_End:
case GDK_KEY_Left:
case GDK_KEY_Right:
case GDK_KEY_KP_Left:
case GDK_KEY_KP_Right:
case GDK_KEY_Return:
{
return FALSE;
}
default:
if (event->keyval >= 0x20 && event->keyval <= 0xFF)
{
if ((event->state & GDK_CONTROL_MASK) != 0)
{
return FALSE;
}
if ((event->state & GDK_MOD1_MASK) != 0)
{
return FALSE;
}
}
return event->length > 0;
}
}
static gboolean
position_and_selection_are_at_end (GtkEditable *editable)
{
int end;
int start_sel, end_sel;
end = get_editable_number_of_chars (editable);
if (gtk_editable_get_selection_bounds (editable, &start_sel, &end_sel))
{
if (start_sel != end || end_sel != end)
{
return FALSE;
}
}
return gtk_editable_get_position (editable) == end;
}
static void
got_completion_data_callback (GFilenameCompleter *completer,
NautilusLocationEntry *entry)
{
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
if (priv->idle_id)
{
g_source_remove (priv->idle_id);
priv->idle_id = 0;
}
try_to_expand_path (entry);
}
static void
editable_event_after_callback (GtkEntry *entry,
GdkEvent *event,
NautilusLocationEntry *location_entry)
{
NautilusLocationEntryPrivate *priv;
GtkEditable *editable;
GdkEventKey *keyevent;
if (event->type != GDK_KEY_PRESS)
{
return;
}
priv = nautilus_location_entry_get_instance_private (location_entry);
editable = GTK_EDITABLE (entry);
keyevent = (GdkEventKey *) event;
/* After typing the right arrow key we move the selection to
* the end, if we have a valid selection - since this is most
* likely an auto-completion. We ignore shift / control since
* they can validly be used to extend the selection.
*/
if ((keyevent->keyval == GDK_KEY_Right || keyevent->keyval == GDK_KEY_End) &&
!(keyevent->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) &&
gtk_editable_get_selection_bounds (editable, NULL, NULL))
{
set_position_and_selection_to_end (editable);
}
/* Only do expanding when we are typing at the end of the
* text. Do the expand at idle time to avoid slowing down
* typing when the directory is large. Only trigger the expand
* when we type a key that would have inserted characters.
*/
if (position_and_selection_are_at_end (editable))
{
if (entry_would_have_inserted_characters (keyevent))
{
if (priv->idle_id == 0)
{
priv->idle_id = g_idle_add (try_to_expand_path, location_entry);
}
}
}
else
{
/* FIXME: Also might be good to do this when you click
* to change the position or selection.
*/
if (priv->idle_id != 0)
{
g_source_remove (priv->idle_id);
priv->idle_id = 0;
}
}
}
static void
finalize (GObject *object)
{
NautilusLocationEntry *entry;
NautilusLocationEntryPrivate *priv;
entry = NAUTILUS_LOCATION_ENTRY (object);
priv = nautilus_location_entry_get_instance_private (entry);
g_object_unref (priv->completer);
g_free (priv->special_text);
g_clear_object (&priv->last_location);
G_OBJECT_CLASS (nautilus_location_entry_parent_class)->finalize (object);
}
static void
destroy (GtkWidget *object)
{
NautilusLocationEntry *entry;
NautilusLocationEntryPrivate *priv;
entry = NAUTILUS_LOCATION_ENTRY (object);
priv = nautilus_location_entry_get_instance_private (entry);
/* cancel the pending idle call, if any */
if (priv->idle_id != 0)
{
g_source_remove (priv->idle_id);
priv->idle_id = 0;
}
g_free (priv->current_directory);
priv->current_directory = NULL;
GTK_WIDGET_CLASS (nautilus_location_entry_parent_class)->destroy (object);
}
static void
nautilus_location_entry_text_changed (NautilusLocationEntry *entry,
GParamSpec *pspec)
{
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
if (priv->setting_special_text)
{
return;
}
priv->has_special_text = FALSE;
}
static void
nautilus_location_entry_icon_release (GtkEntry *gentry,
GtkEntryIconPosition position,
GdkEvent *event,
gpointer unused)
{
NautilusLocationEntry *entry;
NautilusLocationEntryPrivate *priv;
entry = NAUTILUS_LOCATION_ENTRY (gentry);
priv = nautilus_location_entry_get_instance_private (entry);
switch (priv->secondary_action)
{
case NAUTILUS_LOCATION_ENTRY_ACTION_GOTO:
{
g_signal_emit_by_name (gentry, "activate", gentry);
}
break;
case NAUTILUS_LOCATION_ENTRY_ACTION_CLEAR:
{
gtk_entry_set_text (gentry, "");
}
break;
default:
g_assert_not_reached ();
}
}
static gboolean
nautilus_location_entry_focus_in (GtkWidget *widget,
GdkEventFocus *event)
{
NautilusLocationEntry *entry = NAUTILUS_LOCATION_ENTRY (widget);
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
if (priv->has_special_text)
{
priv->setting_special_text = TRUE;
gtk_entry_set_text (GTK_ENTRY (entry), "");
priv->setting_special_text = FALSE;
}
return GTK_WIDGET_CLASS (nautilus_location_entry_parent_class)->focus_in_event (widget, event);
}
static gboolean
nautilus_location_entry_on_key_press (GtkWidget *widget,
GdkEventKey *event)
{
GtkEditable *editable;
GtkWidgetClass *parent_widget_class;
int position;
gboolean result;
editable = GTK_EDITABLE (widget);
if (!gtk_editable_get_editable (editable))
{
return FALSE;
}
/* The location bar entry wants TAB to work kind of
* like it does in the shell for command completion,
* so if we get a tab and there's a selection, we
* should position the insertion point at the end of
* the selection.
*/
if (event->keyval == GDK_KEY_Tab &&
gtk_editable_get_selection_bounds (editable, NULL, NULL))
{
position = strlen (gtk_entry_get_text (GTK_ENTRY (editable)));
gtk_editable_select_region (editable, position, position);
return TRUE;
}
parent_widget_class = GTK_WIDGET_CLASS (nautilus_location_entry_parent_class);
result = parent_widget_class->key_press_event (widget, event);
return result;
}
static void
nautilus_location_entry_activate (GtkEntry *entry)
{
NautilusLocationEntry *loc_entry;
NautilusLocationEntryPrivate *priv;
const gchar *entry_text;
gchar *full_path, *uri_scheme = NULL;
loc_entry = NAUTILUS_LOCATION_ENTRY (entry);
priv = nautilus_location_entry_get_instance_private (loc_entry);
entry_text = gtk_entry_get_text (entry);
if (entry_text != NULL && *entry_text != '\0')
{
uri_scheme = g_uri_parse_scheme (entry_text);
if (!g_path_is_absolute (entry_text) && uri_scheme == NULL && entry_text[0] != '~')
{
/* Fix non absolute paths */
full_path = g_build_filename (priv->current_directory, entry_text, NULL);
gtk_entry_set_text (entry, full_path);
g_free (full_path);
}
g_free (uri_scheme);
}
GTK_ENTRY_CLASS (nautilus_location_entry_parent_class)->activate (entry);
}
static void
nautilus_location_entry_cancel (NautilusLocationEntry *entry)
{
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
nautilus_location_entry_set_location (entry, priv->last_location);
}
static void
nautilus_location_entry_class_init (NautilusLocationEntryClass *class)
{
GtkWidgetClass *widget_class;
GObjectClass *gobject_class;
GtkEntryClass *entry_class;
GtkBindingSet *binding_set;
widget_class = GTK_WIDGET_CLASS (class);
widget_class->focus_in_event = nautilus_location_entry_focus_in;
widget_class->destroy = destroy;
widget_class->key_press_event = nautilus_location_entry_on_key_press;
gobject_class = G_OBJECT_CLASS (class);
gobject_class->finalize = finalize;
entry_class = GTK_ENTRY_CLASS (class);
entry_class->activate = nautilus_location_entry_activate;
class->cancel = nautilus_location_entry_cancel;
signals[CANCEL] = g_signal_new
("cancel",
G_TYPE_FROM_CLASS (class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (NautilusLocationEntryClass,
cancel),
NULL, NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
signals[LOCATION_CHANGED] = g_signal_new
("location-changed",
G_TYPE_FROM_CLASS (class),
G_SIGNAL_RUN_LAST, 0,
NULL, NULL,
g_cclosure_marshal_generic,
G_TYPE_NONE, 1, G_TYPE_OBJECT);
binding_set = gtk_binding_set_by_class (class);
gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "cancel", 0);
}
void
nautilus_location_entry_set_secondary_action (NautilusLocationEntry *entry,
NautilusLocationEntryAction secondary_action)
{
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
if (priv->secondary_action == secondary_action)
{
return;
}
switch (secondary_action)
{
case NAUTILUS_LOCATION_ENTRY_ACTION_CLEAR:
{
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry),
GTK_ENTRY_ICON_SECONDARY,
"edit-clear-symbolic");
}
break;
case NAUTILUS_LOCATION_ENTRY_ACTION_GOTO:
{
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry),
GTK_ENTRY_ICON_SECONDARY,
"go-next-symbolic");
}
break;
default:
g_assert_not_reached ();
}
priv->secondary_action = secondary_action;
}
static void
editable_activate_callback (GtkEntry *entry,
gpointer user_data)
{
NautilusLocationEntry *self = user_data;
const char *entry_text;
entry_text = gtk_entry_get_text (entry);
if (entry_text != NULL && *entry_text != '\0')
{
emit_location_changed (self);
}
}
static void
editable_changed_callback (GtkEntry *entry,
gpointer user_data)
{
nautilus_location_entry_update_action (NAUTILUS_LOCATION_ENTRY (entry));
}
static void
nautilus_location_entry_init (NautilusLocationEntry *entry)
{
NautilusLocationEntryPrivate *priv;
GtkTargetList *targetlist;
priv = nautilus_location_entry_get_instance_private (entry);
priv->completer = g_filename_completer_new ();
g_filename_completer_set_dirs_only (priv->completer, TRUE);
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, "folder-symbolic");
gtk_entry_set_icon_activatable (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, FALSE);
targetlist = gtk_target_list_new (drag_types, G_N_ELEMENTS (drag_types));
gtk_entry_set_icon_drag_source (GTK_ENTRY (entry), GTK_ENTRY_ICON_PRIMARY, targetlist, GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
gtk_target_list_unref (targetlist);
nautilus_location_entry_set_secondary_action (entry,
NAUTILUS_LOCATION_ENTRY_ACTION_CLEAR);
g_signal_connect (entry, "event-after",
G_CALLBACK (editable_event_after_callback), entry);
g_signal_connect (entry, "notify::text",
G_CALLBACK (nautilus_location_entry_text_changed), NULL);
g_signal_connect (entry, "icon-release",
G_CALLBACK (nautilus_location_entry_icon_release), NULL);
g_signal_connect (priv->completer, "got-completion-data",
G_CALLBACK (got_completion_data_callback), entry);
/* Drag source */
g_signal_connect_object (entry, "drag-data-get",
G_CALLBACK (drag_data_get_callback), entry, 0);
/* Drag dest. */
gtk_drag_dest_set (GTK_WIDGET (entry),
GTK_DEST_DEFAULT_ALL,
drop_types, G_N_ELEMENTS (drop_types),
GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK);
g_signal_connect (entry, "drag-data-received",
G_CALLBACK (drag_data_received_callback), NULL);
g_signal_connect_object (entry, "activate",
G_CALLBACK (editable_activate_callback), entry, G_CONNECT_AFTER);
g_signal_connect_object (entry, "changed",
G_CALLBACK (editable_changed_callback), entry, 0);
}
GtkWidget *
nautilus_location_entry_new (void)
{
GtkWidget *entry;
entry = gtk_widget_new (NAUTILUS_TYPE_LOCATION_ENTRY, "max-width-chars", 350, NULL);
return entry;
}
void
nautilus_location_entry_set_special_text (NautilusLocationEntry *entry,
const char *special_text)
{
NautilusLocationEntryPrivate *priv;
priv = nautilus_location_entry_get_instance_private (entry);
priv->has_special_text = TRUE;
g_free (priv->special_text);
priv->special_text = g_strdup (special_text);
priv->setting_special_text = TRUE;
gtk_entry_set_text (GTK_ENTRY (entry), special_text);
priv->setting_special_text = FALSE;
}