diff options
author | Georges Basile Stavracas Neto <georges.stavracas@gmail.com> | 2015-08-18 15:23:02 -0300 |
---|---|---|
committer | Georges Basile Stavracas Neto <georges.stavracas@gmail.com> | 2015-08-20 17:38:35 -0300 |
commit | 404f1492dbe03b407abd4ca0c2b743f7810f4b3e (patch) | |
tree | babcbe37fa37648d8731acd20a19e0967ebe8cc9 /src | |
parent | abef8cac2f1df4b808fcb66b35877b75b54774e4 (diff) | |
download | nautilus-404f1492dbe03b407abd4ca0c2b743f7810f4b3e.tar.gz |
places-view: implement a view for Other Locations
GtkFileChooser received a Other Locations view that lists
persistent devices, as well as networks and the root location
for the computer's hard drive.
Since Nautilus is a file management tool too, it should keep
consistency between Gtk+ file chooser, something that doesn't
happen since it doesn't display Other Locations.
To fix that, add NautilusPlacesView, a NautilusView implementation
that displays the GtkPlacesView widget. In order to implement that,
update window-slot to correctly display the places-view whenever
Other Locations is clicked.
https://bugzilla.gnome.org/show_bug.cgi?id=753871
Diffstat (limited to 'src')
-rw-r--r-- | src/Makefile.am | 6 | ||||
-rw-r--r-- | src/gtk/gtkplacesview.c | 2436 | ||||
-rw-r--r-- | src/gtk/gtkplacesview.ui | 332 | ||||
-rw-r--r-- | src/gtk/gtkplacesviewprivate.h | 91 | ||||
-rw-r--r-- | src/gtk/gtkplacesviewrow.c | 326 | ||||
-rw-r--r-- | src/gtk/gtkplacesviewrow.ui | 109 | ||||
-rw-r--r-- | src/gtk/gtkplacesviewrowprivate.h | 53 | ||||
-rw-r--r-- | src/nautilus-application.c | 2 | ||||
-rw-r--r-- | src/nautilus-pathbar.c | 23 | ||||
-rw-r--r-- | src/nautilus-places-view.c | 359 | ||||
-rw-r--r-- | src/nautilus-places-view.h | 37 | ||||
-rw-r--r-- | src/nautilus-window-slot-dnd.c | 8 | ||||
-rw-r--r-- | src/nautilus-window-slot.c | 222 | ||||
-rw-r--r-- | src/nautilus-window.c | 15 | ||||
-rw-r--r-- | src/nautilus-window.ui | 2 | ||||
-rw-r--r-- | src/nautilus.gresource.xml | 2 |
16 files changed, 3917 insertions, 106 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 2b01996db..56972248b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -139,6 +139,10 @@ nautilus_built_sources = \ $(NULL) nautilus_SOURCES = \ + gtk/gtkplacesview.c \ + gtk/gtkplacesviewprivate.h \ + gtk/gtkplacesviewrow.c \ + gtk/gtkplacesviewrowprivate.h \ nautilus-application.c \ nautilus-application.h \ nautilus-application-actions.c \ @@ -189,6 +193,8 @@ nautilus_SOURCES = \ nautilus-notebook.h \ nautilus-pathbar.c \ nautilus-pathbar.h \ + nautilus-places-view.c \ + nautilus-places-view.h \ nautilus-previewer.c \ nautilus-previewer.h \ nautilus-progress-info-widget.c \ diff --git a/src/gtk/gtkplacesview.c b/src/gtk/gtkplacesview.c new file mode 100644 index 000000000..ca58d3afc --- /dev/null +++ b/src/gtk/gtkplacesview.c @@ -0,0 +1,2436 @@ +/* gtkplacesview.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gtk/gtk.h> + +#include "gtkplacesviewprivate.h" +#include "gtkplacesviewrowprivate.h" + +/** + * SECTION:gtkplacesview + * @Short_description: Widget that displays persistent drives and manages mounted networks + * @Title: GtkPlacesView + * @See_also: #GtkFileChooser + * + * #GtkPlacesView is a stock widget that displays a list of persistent drives + * such as harddisk partitions and networks. #GtkPlacesView does not monitor + * removable devices. + * + * The places view displays drives and networks, and will automatically mount + * them when the user activates. Network addresses are stored even if they fail + * to connect. When the connection is successful, the connected network is + * shown at the network list. + * + * To make use of the places view, an application at least needs to connect + * to the #GtkPlacesView::open-location signal. This is emitted when the user + * selects a location to open in the view. + */ + +struct _GtkPlacesViewPrivate +{ + GVolumeMonitor *volume_monitor; + GtkPlacesOpenFlags open_flags; + GtkPlacesOpenFlags current_open_flags; + + GFile *server_list_file; + GFileMonitor *server_list_monitor; + + GCancellable *cancellable; + + gchar *search_query; + + GtkWidget *actionbar; + GtkWidget *address_entry; + GtkWidget *connect_button; + GtkWidget *listbox; + GtkWidget *popup_menu; + GtkWidget *recent_servers_listbox; + GtkWidget *recent_servers_popover; + GtkWidget *recent_servers_stack; + GtkWidget *stack; + GtkWidget *network_header_spinner; + GtkWidget *network_placeholder; + GtkWidget *network_placeholder_label; + + GtkEntryCompletion *address_entry_completion; + GtkListStore *completion_store; + + GList *detected_networks; + GCancellable *networks_fetching_cancellable; + + guint local_only : 1; + guint should_open_location : 1; + guint should_pulse_entry : 1; + guint entry_pulse_timeout_id; + guint connecting_to_server : 1; + guint fetching_networks : 1; + guint loading : 1; +}; + +static void mount_volume (GtkPlacesView *view, + GVolume *volume); + +static gboolean on_button_press_event (GtkPlacesViewRow *row, + GdkEventButton *event); + +static void on_eject_button_clicked (GtkWidget *widget, + GtkPlacesViewRow *row); + +static gboolean on_row_popup_menu (GtkPlacesViewRow *row); + +static void populate_servers (GtkPlacesView *view); + +G_DEFINE_TYPE_WITH_PRIVATE (GtkPlacesView, gtk_places_view, GTK_TYPE_BOX) + +/* GtkPlacesView properties & signals */ +enum { + PROP_0, + PROP_LOCAL_ONLY, + PROP_OPEN_FLAGS, + PROP_LOADING, + LAST_PROP +}; + +enum { + OPEN_LOCATION, + SHOW_ERROR_MESSAGE, + LAST_SIGNAL +}; + +const gchar *unsupported_protocols [] = +{ + "file", "afc", "obex", "http", + "trash", "burn", "computer", + "archive", "recent", "localtest", + NULL +}; + +static guint places_view_signals [LAST_SIGNAL] = { 0 }; +static GParamSpec *properties [LAST_PROP]; + +static void +emit_open_location (GtkPlacesView *view, + GFile *location, + GtkPlacesOpenFlags open_flags) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (view); + + if ((open_flags & priv->open_flags) == 0) + open_flags = GTK_PLACES_OPEN_NORMAL; + + g_signal_emit (view, places_view_signals[OPEN_LOCATION], 0, location, open_flags); +} + +static void +emit_show_error_message (GtkPlacesView *view, + gchar *primary_message, + gchar *secondary_message) +{ + g_signal_emit (view, places_view_signals[SHOW_ERROR_MESSAGE], + 0, primary_message, secondary_message); +} + +static void +server_file_changed_cb (GtkPlacesView *view) +{ + populate_servers (view); +} + +static GBookmarkFile * +server_list_load (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + GBookmarkFile *bookmarks; + GError *error = NULL; + gchar *datadir; + gchar *filename; + + priv = gtk_places_view_get_instance_private (view); + bookmarks = g_bookmark_file_new (); + datadir = g_build_filename (g_get_user_config_dir (), "gtk-3.0", NULL); + filename = g_build_filename (datadir, "servers", NULL); + + g_mkdir_with_parents (datadir, 0700); + g_bookmark_file_load_from_file (bookmarks, filename, &error); + + if (error) + { + if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + { + /* only warn if the file exists */ + g_warning ("Unable to open server bookmarks: %s", error->message); + g_clear_pointer (&bookmarks, g_bookmark_file_free); + } + + g_clear_error (&error); + } + + /* Monitor the file in case it's modified outside this code */ + if (!priv->server_list_monitor) + { + priv->server_list_file = g_file_new_for_path (filename); + + if (priv->server_list_file) + { + priv->server_list_monitor = g_file_monitor_file (priv->server_list_file, + G_FILE_MONITOR_NONE, + NULL, + &error); + + if (error) + { + g_warning ("Cannot monitor server file: %s", error->message); + g_clear_error (&error); + } + else + { + g_signal_connect_swapped (priv->server_list_monitor, + "changed", + G_CALLBACK (server_file_changed_cb), + view); + } + } + + g_clear_object (&priv->server_list_file); + } + + g_free (datadir); + g_free (filename); + + return bookmarks; +} + +static void +server_list_save (GBookmarkFile *bookmarks) +{ + gchar *filename; + + filename = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "servers", NULL); + g_bookmark_file_to_file (bookmarks, filename, NULL); + g_free (filename); +} + +static void +server_list_add_server (GtkPlacesView *view, + GFile *file) +{ + GBookmarkFile *bookmarks; + GFileInfo *info; + GError *error; + gchar *title; + gchar *uri; + + error = NULL; + bookmarks = server_list_load (view); + + if (!bookmarks) + return; + + uri = g_file_get_uri (file); + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + G_FILE_QUERY_INFO_NONE, + NULL, + &error); + title = g_file_info_get_attribute_as_string (info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME); + + g_bookmark_file_set_title (bookmarks, uri, title); + g_bookmark_file_set_visited (bookmarks, uri, -1); + g_bookmark_file_add_application (bookmarks, uri, NULL, NULL); + + server_list_save (bookmarks); + + g_bookmark_file_free (bookmarks); + g_clear_object (&info); + g_free (title); + g_free (uri); +} + +static void +server_list_remove_server (GtkPlacesView *view, + const gchar *uri) +{ + GBookmarkFile *bookmarks; + + bookmarks = server_list_load (view); + + if (!bookmarks) + return; + + g_bookmark_file_remove_item (bookmarks, uri, NULL); + server_list_save (bookmarks); + + g_bookmark_file_free (bookmarks); +} + +/* Returns a toplevel GtkWindow, or NULL if none */ +static GtkWindow * +get_toplevel (GtkWidget *widget) +{ + GtkWidget *toplevel; + + toplevel = gtk_widget_get_toplevel (widget); + if (!gtk_widget_is_toplevel (toplevel)) + return NULL; + else + return GTK_WINDOW (toplevel); +} + +static void +set_busy_cursor (GtkPlacesView *view, + gboolean busy) +{ + GtkWidget *widget; + GtkWindow *toplevel; + GdkDisplay *display; + GdkCursor *cursor; + + toplevel = get_toplevel (GTK_WIDGET (view)); + widget = GTK_WIDGET (toplevel); + if (!toplevel || !gtk_widget_get_realized (widget)) + return; + + display = gtk_widget_get_display (widget); + + if (busy) + { + cursor = gdk_cursor_new_from_name (display, "left_ptr_watch"); + if (cursor == NULL) + cursor = gdk_cursor_new_for_display (display, GDK_WATCH); + } + else + cursor = NULL; + + gdk_window_set_cursor (gtk_widget_get_window (widget), cursor); + gdk_display_flush (display); + + if (cursor) + g_object_unref (cursor); +} + +/* Activates the given row, with the given flags as parameter */ +static void +activate_row (GtkPlacesView *view, + GtkPlacesViewRow *row, + GtkPlacesOpenFlags flags) +{ + GtkPlacesViewPrivate *priv; + GVolume *volume; + GMount *mount; + GFile *file; + + priv = gtk_places_view_get_instance_private (view); + mount = gtk_places_view_row_get_mount (row); + volume = gtk_places_view_row_get_volume (row); + file = gtk_places_view_row_get_file (row); + + if (file) + { + emit_open_location (view, file, flags); + } + else if (mount) + { + GFile *location = g_mount_get_default_location (mount); + + emit_open_location (view, location, flags); + + g_object_unref (location); + } + else if (volume && g_volume_can_mount (volume)) + { + /* + * When the row is activated, the unmounted volume shall + * be mounted and opened right after. + */ + priv->should_open_location = TRUE; + + gtk_places_view_row_set_busy (row, TRUE); + mount_volume (view, volume); + } +} + +static void update_places (GtkPlacesView *view); + +static void +gtk_places_view_finalize (GObject *object) +{ + GtkPlacesView *self = (GtkPlacesView *)object; + GtkPlacesViewPrivate *priv = gtk_places_view_get_instance_private (self); + + g_signal_handlers_disconnect_by_func (priv->volume_monitor, update_places, object); + + if (priv->entry_pulse_timeout_id > 0) + g_source_remove (priv->entry_pulse_timeout_id); + + g_cancellable_cancel (priv->cancellable); + g_cancellable_cancel (priv->networks_fetching_cancellable); + + g_clear_pointer (&priv->search_query, g_free); + g_clear_object (&priv->server_list_file); + g_clear_object (&priv->server_list_monitor); + g_clear_object (&priv->volume_monitor); + g_clear_object (&priv->cancellable); + g_clear_object (&priv->networks_fetching_cancellable); + + G_OBJECT_CLASS (gtk_places_view_parent_class)->finalize (object); +} + +static void +gtk_places_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkPlacesView *self = GTK_PLACES_VIEW (object); + + switch (prop_id) + { + case PROP_LOCAL_ONLY: + g_value_set_boolean (value, gtk_places_view_get_local_only (self)); + break; + + case PROP_LOADING: + g_value_set_boolean (value, gtk_places_view_get_loading (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtk_places_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkPlacesView *self = GTK_PLACES_VIEW (object); + + switch (prop_id) + { + case PROP_LOCAL_ONLY: + gtk_places_view_set_local_only (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +is_removable_volume (GVolume *volume) +{ + gboolean is_removable; + GDrive *drive; + GMount *mount; + gchar *id; + + drive = g_volume_get_drive (volume); + mount = g_volume_get_mount (volume); + id = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS); + + is_removable = g_volume_can_eject (volume); + + /* NULL volume identifier only happens on removable devices */ + is_removable |= !id; + + if (drive) + is_removable |= g_drive_can_eject (drive); + + if (mount) + is_removable |= (g_mount_can_eject (mount) && !g_mount_can_unmount (mount)); + + g_clear_object (&drive); + g_clear_object (&mount); + g_free (id); + + return is_removable; +} + +typedef struct +{ + gchar *uri; + GtkPlacesView *view; +} RemoveServerData; + +static void +on_remove_server_button_clicked (RemoveServerData *data) +{ + server_list_remove_server (data->view, data->uri); + + populate_servers (data->view); +} + +static void +populate_servers (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + GBookmarkFile *server_list; + GList *children; + gchar **uris; + gsize num_uris; + gint i; + + priv = gtk_places_view_get_instance_private (view); + server_list = server_list_load (view); + + if (!server_list) + return; + + uris = g_bookmark_file_get_uris (server_list, &num_uris); + + gtk_stack_set_visible_child_name (GTK_STACK (priv->recent_servers_stack), + num_uris > 0 ? "list" : "empty"); + + if (!uris) + { + g_bookmark_file_free (server_list); + return; + } + + /* clear previous items */ + children = gtk_container_get_children (GTK_CONTAINER (priv->recent_servers_listbox)); + g_list_free_full (children, (GDestroyNotify) gtk_widget_destroy); + + gtk_list_store_clear (priv->completion_store); + + for (i = 0; i < num_uris; i++) + { + RemoveServerData *data; + GtkTreeIter iter; + GtkWidget *row; + GtkWidget *grid; + GtkWidget *button; + GtkWidget *label; + gchar *name; + gchar *dup_uri; + + name = g_bookmark_file_get_title (server_list, uris[i], NULL); + dup_uri = g_strdup (uris[i]); + + /* add to the completion list */ + gtk_list_store_append (priv->completion_store, &iter); + gtk_list_store_set (priv->completion_store, + &iter, + 0, name, + 1, uris[i], + -1); + + /* add to the recent servers listbox */ + row = gtk_list_box_row_new (); + + grid = g_object_new (GTK_TYPE_GRID, + "orientation", GTK_ORIENTATION_VERTICAL, + "border-width", 6, + NULL); + + /* name of the connected uri, if any */ + label = gtk_label_new (name); + gtk_widget_set_hexpand (label, TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END); + gtk_container_add (GTK_CONTAINER (grid), label); + + /* the uri itself */ + label = gtk_label_new (uris[i]); + gtk_widget_set_hexpand (label, TRUE); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_container_add (GTK_CONTAINER (grid), label); + + /* remove button */ + button = gtk_button_new (); + gtk_widget_set_halign (button, GTK_ALIGN_END); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "image-button"); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "sidebar-button"); + gtk_grid_attach (GTK_GRID (grid), button, 1, 0, 1, 2); + gtk_container_add (GTK_CONTAINER (button), + gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_BUTTON)); + + gtk_container_add (GTK_CONTAINER (row), grid); + gtk_container_add (GTK_CONTAINER (priv->recent_servers_listbox), row); + + /* custom data */ + data = g_new0 (RemoveServerData, 1); + data->view = view; + data->uri = dup_uri; + + g_object_set_data_full (G_OBJECT (row), "uri", dup_uri, g_free); + g_object_set_data_full (G_OBJECT (row), "remove-server-data", data, g_free); + + g_signal_connect_swapped (button, + "clicked", + G_CALLBACK (on_remove_server_button_clicked), + data); + + gtk_widget_show_all (row); + + g_free (name); + } + + g_strfreev (uris); + g_bookmark_file_free (server_list); +} + +static void +update_view_mode (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + GList *children; + GList *l; + gboolean show_listbox; + + priv = gtk_places_view_get_instance_private (view); + show_listbox = FALSE; + + /* drives */ + children = gtk_container_get_children (GTK_CONTAINER (priv->listbox)); + + for (l = children; l; l = l->next) + { + /* GtkListBox filter rows by changing their GtkWidget::child-visible property */ + if (gtk_widget_get_child_visible (l->data)) + { + show_listbox = TRUE; + break; + } + } + + g_list_free (children); + + if (!show_listbox && + priv->search_query && + priv->search_query[0] != '\0') + { + gtk_stack_set_visible_child_name (GTK_STACK (priv->stack), "empty-search"); + } + else + { + gtk_stack_set_visible_child_name (GTK_STACK (priv->stack), "browse"); + } +} + +static void +insert_row (GtkPlacesView *view, + GtkWidget *row, + gboolean is_network) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (view); + + g_object_set_data (G_OBJECT (row), "is-network", GINT_TO_POINTER (is_network)); + + g_signal_connect_swapped (gtk_places_view_row_get_event_box (GTK_PLACES_VIEW_ROW (row)), + "button-press-event", + G_CALLBACK (on_button_press_event), + row); + + g_signal_connect (row, + "popup-menu", + G_CALLBACK (on_row_popup_menu), + row); + + g_signal_connect (gtk_places_view_row_get_eject_button (GTK_PLACES_VIEW_ROW (row)), + "clicked", + G_CALLBACK (on_eject_button_clicked), + row); + + gtk_container_add (GTK_CONTAINER (priv->listbox), row); +} + +static void +add_volume (GtkPlacesView *view, + GVolume *volume) +{ + gboolean is_network; + GDrive *drive; + GMount *mount; + GFile *root; + GIcon *icon; + gchar *identifier; + gchar *name; + gchar *path; + + if (is_removable_volume (volume)) + return; + + drive = g_volume_get_drive (volume); + + if (drive) + { + gboolean is_removable; + + is_removable = g_drive_is_media_removable (drive) || + g_volume_can_eject (volume); + g_object_unref (drive); + + if (is_removable) + return; + } + + identifier = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS); + is_network = g_strcmp0 (identifier, "network") == 0; + + mount = g_volume_get_mount (volume); + root = mount ? g_mount_get_default_location (mount) : NULL; + icon = g_volume_get_icon (volume); + name = g_volume_get_name (volume); + path = !is_network ? g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE) : NULL; + + if (!mount || + (mount && !g_mount_is_shadowed (mount))) + { + GtkWidget *row; + + row = g_object_new (GTK_TYPE_PLACES_VIEW_ROW, + "icon", icon, + "name", name, + "path", path ? path : "", + "volume", volume, + "mount", mount, + "file", NULL, + "is-network", is_network, + NULL); + + insert_row (view, row, is_network); + } + + g_clear_object (&root); + g_clear_object (&icon); + g_clear_object (&mount); + g_free (identifier); + g_free (name); + g_free (path); +} + +static void +add_mount (GtkPlacesView *view, + GMount *mount) +{ + gboolean is_network; + GFile *root; + GIcon *icon; + gchar *name; + gchar *path; + gchar *uri; + gchar *schema; + + icon = g_mount_get_icon (mount); + name = g_mount_get_name (mount); + root = g_mount_get_default_location (mount); + path = root ? g_file_get_parse_name (root) : NULL; + uri = g_file_get_uri (root); + schema = g_uri_parse_scheme (uri); + is_network = g_strcmp0 (schema, "file") != 0; + + if (is_network) + g_clear_pointer (&path, g_free); + + if (!g_mount_is_shadowed (mount)) + { + GtkWidget *row; + + row = g_object_new (GTK_TYPE_PLACES_VIEW_ROW, + "icon", icon, + "name", name, + "path", path ? path : "", + "volume", NULL, + "mount", mount, + "file", NULL, + "is-network", is_network, + NULL); + + insert_row (view, row, is_network); + } + + g_clear_object (&root); + g_clear_object (&icon); + g_free (name); + g_free (path); + g_free (uri); + g_free (schema); +} + +static void +add_drive (GtkPlacesView *view, + GDrive *drive) +{ + GList *volumes; + GList *l; + + /* Removable devices won't appear here */ + if (g_drive_can_eject (drive)) + return; + + volumes = g_drive_get_volumes (drive); + + for (l = volumes; l != NULL; l = l->next) + add_volume (view, l->data); + + g_list_free_full (volumes, g_object_unref); +} + +static void +add_file (GtkPlacesView *view, + GFile *file, + GIcon *icon, + const gchar *display_name, + const gchar *path, + gboolean is_network) +{ + GtkWidget *row; + row = g_object_new (GTK_TYPE_PLACES_VIEW_ROW, + "icon", icon, + "name", display_name, + "path", path, + "volume", NULL, + "mount", NULL, + "file", file, + "is_network", is_network, + NULL); + + insert_row (view, row, is_network); +} + +static gboolean +has_networks (GtkPlacesView *view) +{ + GList *l; + GtkPlacesViewPrivate *priv; + GList *children; + gboolean has_network = FALSE; + + priv = gtk_places_view_get_instance_private (view); + + children = gtk_container_get_children (GTK_CONTAINER (priv->listbox)); + for (l = children; l != NULL; l = l->next) + { + if (GPOINTER_TO_INT (g_object_get_data (l->data, "is-network")) == TRUE && + g_object_get_data (l->data, "is-placeholder") == NULL) + { + has_network = TRUE; + break; + } + } + + g_list_free (children); + + return has_network; +} + +static void +update_network_state (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (view); + + if (priv->network_placeholder == NULL) + { + priv->network_placeholder = gtk_list_box_row_new (); + priv->network_placeholder_label = gtk_label_new (""); + gtk_label_set_xalign (GTK_LABEL (priv->network_placeholder_label), 0.0); + gtk_widget_set_margin_start (priv->network_placeholder_label, 12); + gtk_widget_set_margin_end (priv->network_placeholder_label, 12); + gtk_widget_set_margin_top (priv->network_placeholder_label, 6); + gtk_widget_set_margin_bottom (priv->network_placeholder_label, 6); + gtk_widget_set_hexpand (priv->network_placeholder_label, TRUE); + gtk_widget_set_sensitive (priv->network_placeholder, FALSE); + gtk_container_add (GTK_CONTAINER (priv->network_placeholder), + priv->network_placeholder_label); + g_object_set_data (G_OBJECT (priv->network_placeholder), + "is-network", GINT_TO_POINTER (TRUE)); + /* mark the row as placeholder, so it always goes first */ + g_object_set_data (G_OBJECT (priv->network_placeholder), + "is-placeholder", GINT_TO_POINTER (TRUE)); + gtk_container_add (GTK_CONTAINER (priv->listbox), priv->network_placeholder); + + gtk_widget_show_all (GTK_WIDGET (priv->network_placeholder)); + gtk_list_box_invalidate_headers (GTK_LIST_BOX (priv->listbox)); + } + + if (priv->fetching_networks) + { + gtk_spinner_start (GTK_SPINNER (priv->network_header_spinner)); + /* only show a placeholder with a message if the list is empty. + * otherwise just show the spinner in the header */ + if (!has_networks (view)) + { + gtk_widget_show_all (priv->network_placeholder); + gtk_label_set_text (GTK_LABEL (priv->network_placeholder_label), + _("Searching for network locations")); + } + } + else if (!has_networks (view)) + { + gtk_spinner_stop (GTK_SPINNER (priv->network_header_spinner)); + gtk_widget_show_all (priv->network_placeholder); + gtk_label_set_text (GTK_LABEL (priv->network_placeholder_label), + _("No networks locations found")); + } + else + { + gtk_spinner_stop (GTK_SPINNER (priv->network_header_spinner)); + gtk_widget_hide (priv->network_placeholder); + } +} + +static void +populate_networks (GtkPlacesView *view, + GFileEnumerator *enumerator) +{ + GList *l; + GtkPlacesViewPrivate *priv; + GFile *file; + GFile *activatable_file; + gchar *uri; + GFileType type; + GIcon *icon; + gchar *display_name; + + + priv = gtk_places_view_get_instance_private (view); + + for (l = priv->detected_networks; l != NULL; l = l->next) + { + file = g_file_enumerator_get_child (enumerator, l->data); + type = g_file_info_get_file_type (l->data); + if (type == G_FILE_TYPE_SHORTCUT || type == G_FILE_TYPE_MOUNTABLE) + uri = g_file_info_get_attribute_as_string (l->data, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + else + uri = g_file_get_uri (file); + activatable_file = g_file_new_for_uri (uri); + display_name = g_file_info_get_attribute_as_string (l->data, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME); + icon = g_file_info_get_icon (l->data); + + add_file (view, activatable_file, icon, display_name, NULL, TRUE); + + g_free (uri); + g_free (display_name); + g_clear_object (&file); + g_clear_object (&activatable_file); + } + + g_clear_object (&enumerator); +} + +static void +network_enumeration_next_files_finished (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + GtkPlacesView *view; + GError *error; + + view = GTK_PLACES_VIEW (user_data); + priv = gtk_places_view_get_instance_private (view); + error = NULL; + + /* clean previous fetched networks */ + g_list_free_full (priv->detected_networks, g_object_unref); + priv->fetching_networks = FALSE; + + priv->detected_networks = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (source_object), + res, &error); + if (error) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to fetch network locations: %s", error->message); + + g_clear_error (&error); + g_clear_object (&source_object); + } + else + { + populate_networks (view, G_FILE_ENUMERATOR (source_object)); + } + + /* avoid to update widgets if the operation was cancelled in finalize */ + if (priv->listbox != NULL) + update_network_state (view); + + priv->loading = FALSE; + g_object_notify_by_pspec (G_OBJECT (view), properties[PROP_LOADING]); +} + +static void +network_enumeration_finished (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + GFileEnumerator *enumerator; + GtkPlacesView *view; + GError *error; + + view = GTK_PLACES_VIEW (user_data); + priv = gtk_places_view_get_instance_private (view); + error = NULL; + enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, &error); + + if (error) + { + if (error->code != G_IO_ERROR_CANCELLED) + g_warning ("Failed to fetch network locations: %s", error->message); + + g_clear_error (&error); + } + else + { + g_file_enumerator_next_files_async (enumerator, + G_MAXINT32, + G_PRIORITY_DEFAULT, + priv->networks_fetching_cancellable, + network_enumeration_next_files_finished, + user_data); + } +} + +static void +fetch_networks (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + GFile *network_file; + + priv = gtk_places_view_get_instance_private (view); + network_file = g_file_new_for_uri ("network:///"); + + g_cancellable_cancel (priv->networks_fetching_cancellable); + g_clear_object (&priv->networks_fetching_cancellable); + priv->networks_fetching_cancellable = g_cancellable_new (); + priv->fetching_networks = TRUE; + + update_network_state (view); + + g_file_enumerate_children_async (network_file, + "standard::type,standard::target-uri,standard::name,standard::display-name,standard::icon", + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + priv->networks_fetching_cancellable, + network_enumeration_finished, + view); + + g_clear_object (&network_file); +} + +static void +update_places (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + GList *children; + GList *mounts; + GList *volumes; + GList *drives; + GList *l; + GIcon *icon; + GFile *file; + + priv = gtk_places_view_get_instance_private (view); + + /* Clear all previously added items */ + children = gtk_container_get_children (GTK_CONTAINER (priv->listbox)); + g_list_free_full (children, (GDestroyNotify) gtk_widget_destroy); + priv->network_placeholder = NULL; + priv->network_header_spinner = NULL; + + priv->loading = TRUE; + g_object_notify_by_pspec (G_OBJECT (view), properties[PROP_LOADING]); + + /* Add "Computer" row */ + file = g_file_new_for_path ("/"); + icon = g_themed_icon_new_with_default_fallbacks ("drive-harddisk"); + + add_file (view, file, icon, _("Computer"), "/", FALSE); + + g_clear_object (&file); + g_clear_object (&icon); + + /* Add currently connected drives */ + drives = g_volume_monitor_get_connected_drives (priv->volume_monitor); + + for (l = drives; l != NULL; l = l->next) + add_drive (view, l->data); + + g_list_free_full (drives, g_object_unref); + + /* + * Since all volumes with an associated GDrive were already added with + * add_drive before, add all volumes that aren't associated with a + * drive. + */ + volumes = g_volume_monitor_get_volumes (priv->volume_monitor); + + for (l = volumes; l != NULL; l = l->next) + { + GVolume *volume; + GDrive *drive; + + volume = l->data; + drive = g_volume_get_drive (volume); + + if (drive) + { + g_object_unref (drive); + continue; + } + + add_volume (view, volume); + } + + g_list_free_full (volumes, g_object_unref); + + /* + * Now that all necessary drives and volumes were already added, add mounts + * that have no volume, such as /etc/mtab mounts, ftp, sftp, etc. + */ + mounts = g_volume_monitor_get_mounts (priv->volume_monitor); + + for (l = mounts; l != NULL; l = l->next) + { + GMount *mount; + GVolume *volume; + + mount = l->data; + volume = g_mount_get_volume (mount); + + if (volume) + { + g_object_unref (volume); + continue; + } + + add_mount (view, mount); + } + + g_list_free_full (mounts, g_object_unref); + + /* load saved servers */ + populate_servers (view); + + /* fetch networks and add them asynchronously */ + fetch_networks (view); + + update_view_mode (view); +} + +static void +server_mount_ready_cb (GObject *source_file, + GAsyncResult *res, + gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + GtkPlacesView *view; + gboolean should_show; + GError *error; + GFile *location; + + view = GTK_PLACES_VIEW (user_data); + priv = gtk_places_view_get_instance_private (view); + location = G_FILE (source_file); + should_show = TRUE; + error = NULL; + + priv->should_pulse_entry = FALSE; + set_busy_cursor (view, FALSE); + + g_file_mount_enclosing_volume_finish (location, res, &error); + /* Restore from Cancel to Connect */ + gtk_button_set_label (GTK_BUTTON (priv->connect_button), _("Con_nect")); + gtk_widget_set_sensitive (priv->address_entry, TRUE); + priv->connecting_to_server = FALSE; + + if (error) + { + should_show = FALSE; + + if (error->code == G_IO_ERROR_ALREADY_MOUNTED) + { + /* + * Already mounted volume is not a critical error + * and we can still continue with the operation. + */ + should_show = TRUE; + } + else if (error->domain != G_IO_ERROR || + (error->code != G_IO_ERROR_CANCELLED && + error->code != G_IO_ERROR_FAILED_HANDLED)) + { + /* if it wasn't cancelled show a dialog */ + emit_show_error_message (view, _("Unable to access location"), error->message); + should_show = FALSE; + } + + g_clear_error (&error); + } + + if (should_show) + { + server_list_add_server (view, location); + + /* + * Only clear the entry if it successfully connects to the server. + * Otherwise, the user would lost the typed address even if it fails + * to connect. + */ + gtk_entry_set_text (GTK_ENTRY (priv->address_entry), ""); + + if (priv->should_open_location) + { + GMount *mount_point; + GError *error; + GFile *enclosing_location; + + error = NULL; + mount_point = g_file_find_enclosing_mount (location, NULL, &error); + + if (error) + { + emit_show_error_message (view, _("Unable to access location"), error->message); + g_clear_error (&error); + goto out; + } + + enclosing_location = g_mount_get_default_location (mount_point); + + emit_open_location (view, enclosing_location, priv->open_flags); + + g_object_unref (enclosing_location); + } + } + +out: + update_places (view); +} + +static void +volume_mount_ready_cb (GObject *source_volume, + GAsyncResult *res, + gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + GtkPlacesView *view; + gboolean should_show; + GVolume *volume; + GError *error; + + view = GTK_PLACES_VIEW (user_data); + priv = gtk_places_view_get_instance_private (view); + volume = G_VOLUME (source_volume); + should_show = TRUE; + error = NULL; + + set_busy_cursor (view, FALSE); + + g_volume_mount_finish (volume, res, &error); + + if (error) + { + should_show = FALSE; + + if (error->code == G_IO_ERROR_ALREADY_MOUNTED) + { + /* + * If the volume was already mounted, it's not a hard error + * and we can still continue with the operation. + */ + should_show = TRUE; + } + else if (error->domain != G_IO_ERROR || + (error->code != G_IO_ERROR_CANCELLED && + error->code != G_IO_ERROR_FAILED_HANDLED)) + { + /* if it wasn't cancelled show a dialog */ + emit_show_error_message (GTK_PLACES_VIEW (user_data), _("Unable to access location"), error->message); + should_show = FALSE; + } + + g_clear_error (&error); + } + + if (should_show) + { + GMount *mount; + GFile *root; + + mount = g_volume_get_mount (volume); + root = g_mount_get_default_location (mount); + + if (priv->should_open_location) + emit_open_location (GTK_PLACES_VIEW (user_data), root, priv->open_flags); + + g_object_unref (mount); + g_object_unref (root); + } + + update_places (view); +} + +static void +unmount_ready_cb (GObject *source_mount, + GAsyncResult *res, + gpointer user_data) +{ + GtkPlacesView *view; + GMount *mount; + GError *error; + + view = GTK_PLACES_VIEW (user_data); + mount = G_MOUNT (source_mount); + error = NULL; + + set_busy_cursor (view, FALSE); + + g_mount_unmount_with_operation_finish (mount, res, &error); + + if (error) + { + if (error->domain != G_IO_ERROR || + (error->code != G_IO_ERROR_CANCELLED && + error->code != G_IO_ERROR_FAILED_HANDLED)) + { + /* if it wasn't cancelled show a dialog */ + emit_show_error_message (view, _("Unable to unmount volume"), error->message); + } + + g_clear_error (&error); + } +} + +static gboolean +pulse_entry_cb (gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (user_data)); + + if (priv->should_pulse_entry) + { + gtk_entry_progress_pulse (GTK_ENTRY (priv->address_entry)); + + return G_SOURCE_CONTINUE; + } + else + { + gtk_entry_set_progress_pulse_step (GTK_ENTRY (priv->address_entry), 0.0); + gtk_entry_set_progress_fraction (GTK_ENTRY (priv->address_entry), 0.0); + priv->entry_pulse_timeout_id = 0; + + return G_SOURCE_REMOVE; + } +} + +static void +unmount_mount (GtkPlacesView *view, + GMount *mount) +{ + GtkPlacesViewPrivate *priv; + GMountOperation *operation; + GtkWidget *toplevel; + + priv = gtk_places_view_get_instance_private (view); + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + + set_busy_cursor (GTK_PLACES_VIEW (view), TRUE); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + priv->cancellable = g_cancellable_new (); + + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + g_mount_unmount_with_operation (mount, + 0, + operation, + priv->cancellable, + unmount_ready_cb, + view); + g_object_unref (operation); +} + +static void +mount_server (GtkPlacesView *view, + GFile *location) +{ + GtkPlacesViewPrivate *priv; + GMountOperation *operation; + GtkWidget *toplevel; + + priv = gtk_places_view_get_instance_private (view); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + /* User cliked when the operation was ongoing, so wanted to cancel it */ + if (priv->connecting_to_server) + return; + + priv->cancellable = g_cancellable_new (); + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + + set_busy_cursor (view, TRUE); + priv->should_pulse_entry = TRUE; + gtk_entry_set_progress_pulse_step (GTK_ENTRY (priv->address_entry), 0.1); + /* Allow to cancel the operation */ + gtk_button_set_label (GTK_BUTTON (priv->connect_button), _("Cance_l")); + gtk_widget_set_sensitive (priv->address_entry, FALSE); + priv->connecting_to_server = TRUE; + + if (priv->entry_pulse_timeout_id == 0) + priv->entry_pulse_timeout_id = g_timeout_add (100, (GSourceFunc) pulse_entry_cb, view); + + g_mount_operation_set_password_save (operation, G_PASSWORD_SAVE_FOR_SESSION); + + g_file_mount_enclosing_volume (location, + 0, + operation, + priv->cancellable, + server_mount_ready_cb, + view); + + /* unref operation here - g_file_mount_enclosing_volume() does ref for itself */ + g_object_unref (operation); +} + +static void +mount_volume (GtkPlacesView *view, + GVolume *volume) +{ + GtkPlacesViewPrivate *priv; + GMountOperation *operation; + GtkWidget *toplevel; + + priv = gtk_places_view_get_instance_private (view); + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view)); + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + + set_busy_cursor (view, TRUE); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + priv->cancellable = g_cancellable_new (); + + g_mount_operation_set_password_save (operation, G_PASSWORD_SAVE_FOR_SESSION); + + g_volume_mount (volume, + 0, + operation, + priv->cancellable, + volume_mount_ready_cb, + view); + + /* unref operation here - g_file_mount_enclosing_volume() does ref for itself */ + g_object_unref (operation); +} + +/* Callback used when the file list's popup menu is detached */ +static void +popup_menu_detach_cb (GtkWidget *attach_widget, + GtkMenu *menu) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (attach_widget)); + priv->popup_menu = NULL; +} + +static void +get_view_and_file (GtkPlacesViewRow *row, + GtkWidget **view, + GFile **file) +{ + if (view) + *view = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW); + + if (file) + { + GVolume *volume; + GMount *mount; + + volume = gtk_places_view_row_get_volume (row); + mount = gtk_places_view_row_get_mount (row); + + if (mount) + *file = g_mount_get_default_location (mount); + else if (volume) + *file = g_volume_get_activation_root (volume); + else + *file = NULL; + } +} + +static void +open_cb (GtkMenuItem *item, + GtkPlacesViewRow *row) +{ + GtkWidget *view; + GFile *file; + + get_view_and_file (row, &view, &file); + + if (file) + emit_open_location (GTK_PLACES_VIEW (view), file, GTK_PLACES_OPEN_NORMAL); + + g_clear_object (&file); +} + +static void +open_in_new_tab_cb (GtkMenuItem *item, + GtkPlacesViewRow *row) +{ + GtkWidget *view; + GFile *file; + + get_view_and_file (row, &view, &file); + + if (file) + emit_open_location (GTK_PLACES_VIEW (view), file, GTK_PLACES_OPEN_NEW_TAB); + + g_clear_object (&file); +} + +static void +open_in_new_window_cb (GtkMenuItem *item, + GtkPlacesViewRow *row) +{ + GtkWidget *view; + GFile *file; + + get_view_and_file (row, &view, &file); + + if (file) + emit_open_location (GTK_PLACES_VIEW (view), file, GTK_PLACES_OPEN_NEW_WINDOW); + + g_clear_object (&file); +} + +static void +mount_cb (GtkMenuItem *item, + GtkPlacesViewRow *row) +{ + GtkPlacesViewPrivate *priv; + GtkWidget *view; + GVolume *volume; + + view = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW); + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (view)); + volume = gtk_places_view_row_get_volume (row); + + /* + * When the mount item is activated, it's expected that + * the volume only gets mounted, without opening it after + * the operation is complete. + */ + priv->should_open_location = FALSE; + + gtk_places_view_row_set_busy (row, TRUE); + mount_volume (GTK_PLACES_VIEW (view), volume); +} + +static void +unmount_cb (GtkMenuItem *item, + GtkPlacesViewRow *row) +{ + GtkWidget *view; + GMount *mount; + + view = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW); + mount = gtk_places_view_row_get_mount (row); + + gtk_places_view_row_set_busy (row, TRUE); + + unmount_mount (GTK_PLACES_VIEW (view), mount); +} + +/* Constructs the popup menu if needed */ +static void +build_popup_menu (GtkPlacesView *view, + GtkPlacesViewRow *row) +{ + GtkPlacesViewPrivate *priv; + GtkWidget *item; + GMount *mount; + GFile *file; + gboolean is_network; + + priv = gtk_places_view_get_instance_private (view); + mount = gtk_places_view_row_get_mount (row); + file = gtk_places_view_row_get_file (row); + is_network = gtk_places_view_row_get_is_network (row); + + priv->popup_menu = gtk_menu_new (); + gtk_style_context_add_class (gtk_widget_get_style_context (priv->popup_menu), + GTK_STYLE_CLASS_CONTEXT_MENU); + + gtk_menu_attach_to_widget (GTK_MENU (priv->popup_menu), + GTK_WIDGET (view), + popup_menu_detach_cb); + + /* Open item is always present */ + item = gtk_menu_item_new_with_mnemonic (_("_Open")); + g_signal_connect (item, + "activate", + G_CALLBACK (open_cb), + row); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (priv->popup_menu), item); + + if (priv->open_flags & GTK_PLACES_OPEN_NEW_TAB) + { + item = gtk_menu_item_new_with_mnemonic (_("Open in New _Tab")); + g_signal_connect (item, + "activate", + G_CALLBACK (open_in_new_tab_cb), + row); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (priv->popup_menu), item); + } + + if (priv->open_flags & GTK_PLACES_OPEN_NEW_WINDOW) + { + item = gtk_menu_item_new_with_mnemonic (_("Open in New _Window")); + g_signal_connect (item, + "activate", + G_CALLBACK (open_in_new_window_cb), + row); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (priv->popup_menu), item); + } + + /* + * The only item that contains a file up to now is the Computer + * item, which cannot be mounted or unmounted. + */ + if (file) + return; + + /* Separator */ + item = gtk_separator_menu_item_new (); + gtk_widget_show (item); + gtk_menu_shell_insert (GTK_MENU_SHELL (priv->popup_menu), item, -1); + + /* Mount/Unmount items */ + if (mount) + { + item = gtk_menu_item_new_with_mnemonic (is_network ? _("_Disconnect") : _("_Unmount")); + g_signal_connect (item, + "activate", + G_CALLBACK (unmount_cb), + row); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (priv->popup_menu), item); + } + else + { + item = gtk_menu_item_new_with_mnemonic (is_network ? _("_Connect") : _("_Mount")); + g_signal_connect (item, + "activate", + G_CALLBACK (mount_cb), + row); + gtk_widget_show (item); + gtk_menu_shell_append (GTK_MENU_SHELL (priv->popup_menu), item); + } +} + +static void +popup_menu (GtkPlacesViewRow *row, + GdkEventButton *event) +{ + GtkPlacesViewPrivate *priv; + GtkWidget *view; + gint button; + + view = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW); + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (view)); + + g_clear_pointer (&priv->popup_menu, gtk_widget_destroy); + + build_popup_menu (GTK_PLACES_VIEW (view), row); + + /* The event button needs to be 0 if we're popping up this menu from + * a button release, else a 2nd click outside the menu with any button + * other than the one that invoked the menu will be ignored (instead + * of dismissing the menu). This is a subtle fragility of the GTK menu code. + */ + if (event) + { + if (event->type == GDK_BUTTON_PRESS) + button = 0; + else + button = event->button; + } + else + { + button = 0; + } + + gtk_menu_popup (GTK_MENU (priv->popup_menu), + NULL, + NULL, + NULL, + NULL, + button, + event ? event->time : gtk_get_current_event_time ()); +} + +static gboolean +on_row_popup_menu (GtkPlacesViewRow *row) +{ + popup_menu (row, NULL); + return TRUE; +} + +static gboolean +on_button_press_event (GtkPlacesViewRow *row, + GdkEventButton *event) +{ + if (row && + gdk_event_triggers_context_menu ((GdkEvent*) event) && + event->type == GDK_BUTTON_PRESS) + { + popup_menu (row, event); + + return TRUE; + } + + return FALSE; +} + +static gboolean +on_key_press_event (GtkWidget *widget, + GdkEventKey *event, + GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (view); + + if (event) + { + guint modifiers; + + modifiers = gtk_accelerator_get_default_mod_mask (); + + if (event->keyval == GDK_KEY_Return || + event->keyval == GDK_KEY_KP_Enter || + event->keyval == GDK_KEY_ISO_Enter || + event->keyval == GDK_KEY_space) + { + GtkWidget *focus_widget; + GtkWindow *toplevel; + + priv->current_open_flags = GTK_PLACES_OPEN_NORMAL; + toplevel = get_toplevel (GTK_WIDGET (view)); + + if (!toplevel) + return FALSE; + + focus_widget = gtk_window_get_focus (toplevel); + + if (!GTK_IS_PLACES_VIEW_ROW (focus_widget)) + return FALSE; + + if ((event->state & modifiers) == GDK_SHIFT_MASK) + priv->current_open_flags = GTK_PLACES_OPEN_NEW_TAB; + else if ((event->state & modifiers) == GDK_CONTROL_MASK) + priv->current_open_flags = GTK_PLACES_OPEN_NEW_WINDOW; + + activate_row (view, GTK_PLACES_VIEW_ROW (focus_widget), priv->current_open_flags); + + return TRUE; + } + } + + return FALSE; +} + +static void +on_eject_button_clicked (GtkWidget *widget, + GtkPlacesViewRow *row) +{ + if (row) + { + GtkWidget *view = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_PLACES_VIEW); + + unmount_mount (GTK_PLACES_VIEW (view), gtk_places_view_row_get_mount (row)); + } +} + +static void +on_connect_button_clicked (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + const gchar *uri; + GFile *file; + + priv = gtk_places_view_get_instance_private (view); + file = NULL; + + /* + * Since the 'Connect' button is updated whenever the typed + * address changes, it is sufficient to check if it's sensitive + * or not, in order to determine if the given address is valid. + */ + if (!gtk_widget_get_sensitive (priv->connect_button)) + return; + + uri = gtk_entry_get_text (GTK_ENTRY (priv->address_entry)); + + if (uri != NULL && uri[0] != '\0') + file = g_file_new_for_commandline_arg (uri); + + if (file) + { + priv->should_open_location = TRUE; + + mount_server (view, file); + } + else + { + emit_show_error_message (view, _("Unable to get remote server location"), NULL); + } +} + +static void +on_address_entry_text_changed (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + const gchar* const *supported_protocols; + gchar *address, *scheme; + gboolean supported; + + priv = gtk_places_view_get_instance_private (view); + supported = FALSE; + supported_protocols = g_vfs_get_supported_uri_schemes (g_vfs_get_default ()); + + if (!supported_protocols) + return; + + address = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->address_entry))); + scheme = g_uri_parse_scheme (address); + + if (!scheme) + goto out; + + supported = g_strv_contains (supported_protocols, scheme) && + !g_strv_contains (unsupported_protocols, scheme); + +out: + gtk_widget_set_sensitive (priv->connect_button, supported); + g_free (address); +} + +static void +on_recent_servers_listbox_row_activated (GtkPlacesView *view, + GtkPlacesViewRow *row, + GtkWidget *listbox) +{ + GtkPlacesViewPrivate *priv; + gchar *uri; + + priv = gtk_places_view_get_instance_private (view); + uri = g_object_get_data (G_OBJECT (row), "uri"); + + gtk_entry_set_text (GTK_ENTRY (priv->address_entry), uri); + + gtk_widget_hide (priv->recent_servers_popover); +} + +static void +on_listbox_row_activated (GtkPlacesView *view, + GtkPlacesViewRow *row, + GtkWidget *listbox) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (view); + + activate_row (view, row, priv->current_open_flags); +} + +static gboolean +listbox_filter_func (GtkListBoxRow *row, + gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + gboolean is_network; + gboolean is_placeholder; + gboolean retval; + gchar *name; + gchar *path; + + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (user_data)); + retval = FALSE; + + is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-network")); + is_placeholder = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-placeholder")); + + if (is_network && priv->local_only) + return FALSE; + + if (is_placeholder) + return FALSE; + + if (!priv->search_query || priv->search_query[0] == '\0') + return TRUE; + + g_object_get (row, + "name", &name, + "path", &path, + NULL); + + if (name) + retval |= strstr (name, priv->search_query) != NULL; + + if (path) + retval |= strstr (path, priv->search_query) != NULL; + + g_free (name); + g_free (path); + + return retval; +} + +static void +listbox_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkPlacesViewPrivate *priv; + gboolean row_is_network; + gchar *text; + + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (user_data)); + text = NULL; + row_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-network")); + + if (!before) + { + text = g_strdup_printf ("<b>%s</b>", row_is_network ? _("Networks") : _("On This Computer")); + } + else + { + gboolean before_is_network; + + before_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (before), "is-network")); + + if (before_is_network != row_is_network) + text = g_strdup_printf ("<b>%s</b>", row_is_network ? _("Networks") : _("On This Computer")); + } + + if (text) + { + GtkWidget *header; + GtkWidget *label; + GtkWidget *separator; + + header = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + + label = g_object_new (GTK_TYPE_LABEL, + "use_markup", TRUE, + "margin-top", 6, + "margin-start", 12, + "label", text, + "xalign", 0.0f, + NULL); + if (row_is_network) + { + GtkWidget *header_name; + + g_object_set (label, + "margin-end", 6, + NULL); + + header_name = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + priv->network_header_spinner = gtk_spinner_new (); + g_object_set (priv->network_header_spinner, + "margin-end", 12, + NULL); + gtk_container_add (GTK_CONTAINER (header_name), label); + gtk_container_add (GTK_CONTAINER (header_name), priv->network_header_spinner); + gtk_container_add (GTK_CONTAINER (header), header_name); + } + else + { + g_object_set (label, + "hexpand", TRUE, + "margin-end", 12, + NULL); + gtk_container_add (GTK_CONTAINER (header), label); + } + + gtk_container_add (GTK_CONTAINER (header), separator); + gtk_widget_show_all (header); + + gtk_list_box_row_set_header (row, header); + + g_free (text); + } + else + { + gtk_list_box_row_set_header (row, NULL); + } +} + +static gint +listbox_sort_func (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + gboolean row1_is_network; + gboolean row2_is_network; + gchar *location1; + gchar *location2; + gboolean *is_placeholder1; + gboolean *is_placeholder2; + gint retval; + + row1_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row1), "is-network")); + row2_is_network = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row2), "is-network")); + + retval = row1_is_network - row2_is_network; + + if (retval != 0) + return retval; + + is_placeholder1 = g_object_get_data (G_OBJECT (row1), "is-placeholder"); + is_placeholder2 = g_object_get_data (G_OBJECT (row2), "is-placeholder"); + + /* we can't have two placeholders for the same section */ + g_assert (!(is_placeholder1 != NULL && is_placeholder2 != NULL)); + + if (is_placeholder1) + return -1; + if (is_placeholder2) + return 1; + + g_object_get (row1, "path", &location1, NULL); + g_object_get (row2, "path", &location2, NULL); + + retval = g_strcmp0 (location1, location2); + + g_free (location1); + g_free (location2); + + return retval; +} + +static void +gtk_places_view_constructed (GObject *object) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (object)); + + G_OBJECT_CLASS (gtk_places_view_parent_class)->constructed (object); + + gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->listbox), + listbox_sort_func, + object, + NULL); + gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->listbox), + listbox_filter_func, + object, + NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (priv->listbox), + listbox_header_func, + object, + NULL); + + /* load drives */ + update_places (GTK_PLACES_VIEW (object)); + + g_signal_connect_swapped (priv->volume_monitor, + "mount-added", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (priv->volume_monitor, + "mount-changed", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (priv->volume_monitor, + "mount-removed", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (priv->volume_monitor, + "volume-added", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (priv->volume_monitor, + "volume-changed", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (priv->volume_monitor, + "volume-removed", + G_CALLBACK (update_places), + object); +} + +static void +gtk_places_view_map (GtkWidget *widget) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (GTK_PLACES_VIEW (widget)); + + gtk_entry_set_text (GTK_ENTRY (priv->address_entry), ""); + + GTK_WIDGET_CLASS (gtk_places_view_parent_class)->map (widget); +} + +static void +gtk_places_view_class_init (GtkPlacesViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtk_places_view_finalize; + object_class->constructed = gtk_places_view_constructed; + object_class->get_property = gtk_places_view_get_property; + object_class->set_property = gtk_places_view_set_property; + + widget_class->map = gtk_places_view_map; + + /** + * GtkPlacesView::open-location: + * @view: the object which received the signal. + * @location: (type Gio.File): #GFile to which the caller should switch. + * @open_flags: a single value from #GtkPlacesOpenFlags specifying how the @location + * should be opened. + * + * The places view emits this signal when the user selects a location + * in it. The calling application should display the contents of that + * location; for example, a file manager should show a list of files in + * the specified location. + * + * Since: 3.18 + */ + places_view_signals [OPEN_LOCATION] = + g_signal_new (_("open-location"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (GtkPlacesViewClass, open_location), + NULL, NULL, NULL, + G_TYPE_NONE, 2, + G_TYPE_OBJECT, + GTK_TYPE_PLACES_OPEN_FLAGS); + + /** + * GtkPlacesView::show-error-message: + * @view: the object which received the signal. + * @primary: primary message with a summary of the error to show. + * @secondary: secondary message with details of the error to show. + * + * The places view emits this signal when it needs the calling + * application to present an error message. Most of these messages + * refer to mounting or unmounting media, for example, when a drive + * cannot be started for some reason. + * + * Since: 3.18 + */ + places_view_signals [SHOW_ERROR_MESSAGE] = + g_signal_new (_("show-error-message"), + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (GtkPlacesViewClass, show_error_message), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_STRING); + + properties[PROP_LOCAL_ONLY] = + g_param_spec_boolean ("local-only", + _("Local Only"), + _("Whether the sidebar only includes local files"), + FALSE, + G_PARAM_READWRITE); + + properties[PROP_LOADING] = + g_param_spec_boolean ("loading", + _("Loading"), + _("Whether the view is loading locations"), + FALSE, + G_PARAM_READABLE); + + properties[PROP_OPEN_FLAGS] = + g_param_spec_flags ("open-flags", + _("Open Flags"), + _("Modes in which the calling application can open locations selected in the sidebar"), + GTK_TYPE_PLACES_OPEN_FLAGS, + GTK_PLACES_OPEN_NORMAL, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + /* Bind class to template */ + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/nautilus/gtk/gtkplacesview.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, actionbar); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, address_entry); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, address_entry_completion); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, completion_store); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, connect_button); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, listbox); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, recent_servers_listbox); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, recent_servers_popover); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, recent_servers_stack); + gtk_widget_class_bind_template_child_private (widget_class, GtkPlacesView, stack); + + gtk_widget_class_bind_template_callback (widget_class, on_address_entry_text_changed); + gtk_widget_class_bind_template_callback (widget_class, on_connect_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_key_press_event); + gtk_widget_class_bind_template_callback (widget_class, on_listbox_row_activated); + gtk_widget_class_bind_template_callback (widget_class, on_recent_servers_listbox_row_activated); +} + +static void +gtk_places_view_init (GtkPlacesView *self) +{ + GtkPlacesViewPrivate *priv; + + priv = gtk_places_view_get_instance_private (self); + + priv->volume_monitor = g_volume_monitor_get (); + priv->open_flags = GTK_PLACES_OPEN_NORMAL; + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +/** + * gtk_places_view_new: + * + * Creates a new #GtkPlacesView widget. + * + * The application should connect to at least the + * #GtkPlacesView::open-location signal to be notified + * when the user makes a selection in the view. + * + * Returns: a newly created #GtkPlacesView + * + * Since: 3.18 + */ +GtkWidget * +gtk_places_view_new (void) +{ + return g_object_new (GTK_TYPE_PLACES_VIEW, NULL); +} + +/** + * gtk_places_view_set_open_flags: + * @view: a #GtkPlacesView + * @flags: Bitmask of modes in which the calling application can open locations + * + * Sets the way in which the calling application can open new locations from + * the places view. For example, some applications only open locations + * “directly” into their main view, while others may support opening locations + * in a new notebook tab or a new window. + * + * This function is used to tell the places @view about the ways in which the + * application can open new locations, so that the view can display (or not) + * the “Open in new tab” and “Open in new window” menu items as appropriate. + * + * When the #GtkPlacesView::open-location signal is emitted, its flags + * argument will be set to one of the @flags that was passed in + * gtk_places_view_set_open_flags(). + * + * Passing 0 for @flags will cause #GTK_PLACES_OPEN_NORMAL to always be sent + * to callbacks for the “open-location” signal. + * + * Since: 3.18 + */ +void +gtk_places_view_set_open_flags (GtkPlacesView *view, + GtkPlacesOpenFlags flags) +{ + GtkPlacesViewPrivate *priv; + + g_return_if_fail (GTK_IS_PLACES_VIEW (view)); + + priv = gtk_places_view_get_instance_private (view); + + if (priv->open_flags != flags) + { + priv->open_flags = flags; + g_object_notify_by_pspec (G_OBJECT (view), properties[PROP_OPEN_FLAGS]); + } +} + +/** + * gtk_places_view_get_open_flags: + * @view: a #GtkPlacesSidebar + * + * Gets the open flags. + * + * Returns: the #GtkPlacesOpenFlags of @view + * + * Since: 3.18 + */ +GtkPlacesOpenFlags +gtk_places_view_get_open_flags (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + + g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), 0); + + priv = gtk_places_view_get_instance_private (view); + + return priv->open_flags; +} + +/** + * gtk_places_view_get_search_query: + * @view: a #GtkPlacesView + * + * Retrieves the current search query from @view. + * + * Returns: (transfer none): the current search query. + */ +const gchar* +gtk_places_view_get_search_query (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + + g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), NULL); + + priv = gtk_places_view_get_instance_private (view); + + return priv->search_query; +} + +/** + * gtk_places_view_set_search_query: + * @view: a #GtkPlacesView + * @query_text: the query, or NULL. + * + * Sets the search query of @view. The search is immediately performed + * once the query is set. + * + * Returns: + */ +void +gtk_places_view_set_search_query (GtkPlacesView *view, + const gchar *query_text) +{ + GtkPlacesViewPrivate *priv; + + g_return_if_fail (GTK_IS_PLACES_VIEW (view)); + + priv = gtk_places_view_get_instance_private (view); + + if (g_strcmp0 (priv->search_query, query_text) != 0) + { + g_clear_pointer (&priv->search_query, g_free); + priv->search_query = g_strdup (query_text); + + gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->listbox)); + gtk_list_box_invalidate_headers (GTK_LIST_BOX (priv->listbox)); + + update_view_mode (view); + } +} + +/** + * gtk_places_view_get_loading: + * @view: a #GtkPlacesView + * + * Returns %TRUE if the view is loading locations. + * + * Since: 3.18 + */ +gboolean +gtk_places_view_get_loading (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + + g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), FALSE); + + priv = gtk_places_view_get_instance_private (view); + + return priv->loading; +} + +/** + * gtk_places_view_get_local_only: + * @view: a #GtkPlacesView + * + * Returns %TRUE if only local volumes are shown, i.e. no networks + * are displayed. + * + * Returns: %TRUE if only local volumes are shown, %FALSE otherwise. + * + * Since: 3.18 + */ +gboolean +gtk_places_view_get_local_only (GtkPlacesView *view) +{ + GtkPlacesViewPrivate *priv; + + g_return_val_if_fail (GTK_IS_PLACES_VIEW (view), FALSE); + + priv = gtk_places_view_get_instance_private (view); + + return priv->local_only; +} + +/** + * gtk_places_view_set_local_only: + * @view: a #GtkPlacesView + * @local_only: %TRUE to hide remote locations, %FALSE to show. + * + * Sets the #GtkPlacesView::local-only property to @local_only. + * + * Returns: + * + * Since: 3.18 + */ +void +gtk_places_view_set_local_only (GtkPlacesView *view, + gboolean local_only) +{ + GtkPlacesViewPrivate *priv; + + g_return_if_fail (GTK_IS_PLACES_VIEW (view)); + + priv = gtk_places_view_get_instance_private (view); + + if (priv->local_only != local_only) + { + priv->local_only = local_only; + + gtk_widget_set_visible (priv->actionbar, !local_only); + gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->listbox)); + + update_view_mode (view); + + g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_LOCAL_ONLY]); + } +} diff --git a/src/gtk/gtkplacesview.ui b/src/gtk/gtkplacesview.ui new file mode 100644 index 000000000..5ee523e92 --- /dev/null +++ b/src/gtk/gtkplacesview.ui @@ -0,0 +1,332 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="gtk30"> + <requires lib="gtk+" version="3.16"/> + <object class="GtkListStore" id="completion_store"> + <columns> + <!-- column-name name --> + <column type="gchararray"/> + <!-- column-name uri --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkEntryCompletion" id="address_entry_completion"> + <property name="model">completion_store</property> + <property name="minimum_key_length">1</property> + <property name="text_column">1</property> + <property name="inline_completion">True</property> + <property name="popup_completion">False</property> + </object> + <object class="GtkPopover" id="recent_servers_popover"> + <child> + <object class="GtkStack" id="recent_servers_stack"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vexpand">True</property> + <property name="valign">center</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pixel-size">48</property> + <property name="icon_name">network-server-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" comments="Translators: Server as any successfully connected network address">No recent servers found</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">empty</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Recent Servers</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="vexpand">True</property> + <property name="shadow_type">in</property> + <property name="min_content_width">250</property> + <property name="min_content_height">150</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkListBox" id="recent_servers_listbox"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + <signal name="row-activated" handler="on_recent_servers_listbox_row_activated" object="GtkPlacesView" swapped="yes" /> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="name">list</property> + </packing> + </child> + </object> + </child> + </object> + <template class="GtkPlacesView" parent="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <signal name="key-press-event" handler="on_key_press_event" object="GtkPlacesView" swapped="no" /> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="vhomogeneous">False</property> + <property name="transition_type">crossfade</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkListBox" id="listbox"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection_mode">none</property> + <signal name="row-activated" handler="on_listbox_row_activated" object="GtkPlacesView" swapped="yes" /> + </object> + </child> + <style> + <class name="view"/> + </style> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="name">browse</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pixel_size">72</property> + <property name="icon_name">edit-find-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">No results found</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.44"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Try a different search</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="name">empty-search</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkActionBar" id="actionbar"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Connect to _Server</property> + <property name="mnemonic_widget">address_entry</property> + <property name="use_underline">True</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child> + <object class="GtkButton" id="connect_button"> + <property name="label" translatable="yes">Con_nect</property> + <property name="use_underline">True</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="sensitive">False</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="on_connect_button_clicked" object="GtkPlacesView" swapped="yes" /> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="address_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="width_chars">20</property> + <property name="placeholder_text" translatable="yes">Enter server address…</property> + <property name="completion">address_entry_completion</property> + <signal name="notify::text" handler="on_address_entry_text_changed" object="GtkPlacesView" swapped="yes" /> + <signal name="activate" handler="on_connect_button_clicked" object="GtkPlacesView" swapped="yes" /> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <child> + <object class="GtkMenuButton" id="server_list_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="direction">up</property> + <property name="popover">recent_servers_popover</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">pan-up-symbolic</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + </packing> + </child> + <style> + <class name="linked"/> + </style> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </template> +</interface> diff --git a/src/gtk/gtkplacesviewprivate.h b/src/gtk/gtkplacesviewprivate.h new file mode 100644 index 000000000..706481652 --- /dev/null +++ b/src/gtk/gtkplacesviewprivate.h @@ -0,0 +1,91 @@ +/* gtkplacesview.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + + +#ifndef GTK_PLACES_VIEW_H +#define GTK_PLACES_VIEW_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_PLACES_VIEW (gtk_places_view_get_type ()) +#define GTK_PLACES_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_PLACES_VIEW, GtkPlacesView)) +#define GTK_PLACES_VIEW_CLASS(klass)(G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_PLACES_VIEW, GtkPlacesViewClass)) +#define GTK_IS_PLACES_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_PLACES_VIEW)) +#define GTK_IS_PLACES_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_PLACES_VIEW)) +#define GTK_PLACES_VIEW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_PLACES_VIEW, GtkPlacesViewClass)) + +typedef struct _GtkPlacesView GtkPlacesView; +typedef struct _GtkPlacesViewClass GtkPlacesViewClass; +typedef struct _GtkPlacesViewPrivate GtkPlacesViewPrivate; + +struct _GtkPlacesViewClass +{ + GtkBoxClass parent_class; + + void (* open_location) (GtkPlacesView *view, + GFile *location, + GtkPlacesOpenFlags open_flags); + + void (* show_error_message) (GtkPlacesSidebar *sidebar, + const gchar *primary, + const gchar *secondary); + + /*< private >*/ + + /* Padding for future expansion */ + gpointer reserved[10]; +}; + +struct _GtkPlacesView +{ + GtkBox parent_instance; +}; + +GDK_AVAILABLE_IN_3_18 +GType gtk_places_view_get_type (void) G_GNUC_CONST; + +GDK_AVAILABLE_IN_3_18 +GtkPlacesOpenFlags gtk_places_view_get_open_flags (GtkPlacesView *view); +GDK_AVAILABLE_IN_3_18 +void gtk_places_view_set_open_flags (GtkPlacesView *view, + GtkPlacesOpenFlags flags); + +GDK_AVAILABLE_IN_3_18 +const gchar* gtk_places_view_get_search_query (GtkPlacesView *view); +GDK_AVAILABLE_IN_3_18 +void gtk_places_view_set_search_query (GtkPlacesView *view, + const gchar *query_text); + +GDK_AVAILABLE_IN_3_18 +gboolean gtk_places_view_get_local_only (GtkPlacesView *view); + +GDK_AVAILABLE_IN_3_18 +void gtk_places_view_set_local_only (GtkPlacesView *view, + gboolean local_only); + +GDK_AVAILABLE_IN_3_18 +gboolean gtk_places_view_get_loading (GtkPlacesView *view); + +GDK_AVAILABLE_IN_3_18 +GtkWidget * gtk_places_view_new (void); + +G_END_DECLS + +#endif /* GTK_PLACES_VIEW_H */ diff --git a/src/gtk/gtkplacesviewrow.c b/src/gtk/gtkplacesviewrow.c new file mode 100644 index 000000000..dea648817 --- /dev/null +++ b/src/gtk/gtkplacesviewrow.c @@ -0,0 +1,326 @@ +/* gtkplacesviewrow.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> +#include <gtk/gtk.h> + +#include "gtkplacesviewrowprivate.h" + +struct _GtkPlacesViewRow +{ + GtkListBoxRow parent_instance; + + GtkSpinner *busy_spinner; + GtkButton *eject_button; + GtkImage *eject_icon; + GtkEventBox *event_box; + GtkImage *icon_image; + GtkLabel *name_label; + GtkLabel *path_label; + + GVolume *volume; + GMount *mount; + GFile *file; + + gint is_network : 1; +}; + +G_DEFINE_TYPE (GtkPlacesViewRow, gtk_places_view_row, GTK_TYPE_LIST_BOX_ROW) + +enum { + PROP_0, + PROP_ICON, + PROP_NAME, + PROP_PATH, + PROP_VOLUME, + PROP_MOUNT, + PROP_FILE, + PROP_IS_NETWORK, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static void +gtk_places_view_row_finalize (GObject *object) +{ + GtkPlacesViewRow *self = GTK_PLACES_VIEW_ROW (object); + + g_clear_object (&self->volume); + g_clear_object (&self->mount); + g_clear_object (&self->file); + + G_OBJECT_CLASS (gtk_places_view_row_parent_class)->finalize (object); +} + +static void +gtk_places_view_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkPlacesViewRow *self; + GIcon *icon; + + self = GTK_PLACES_VIEW_ROW (object); + icon = NULL; + + switch (prop_id) + { + case PROP_ICON: + gtk_image_get_gicon (self->icon_image, &icon, NULL); + g_value_set_object (value, icon); + break; + + case PROP_NAME: + g_value_set_string (value, gtk_label_get_label (self->name_label)); + break; + + case PROP_PATH: + g_value_set_string (value, gtk_label_get_label (self->path_label)); + break; + + case PROP_VOLUME: + g_value_set_object (value, self->volume); + break; + + case PROP_MOUNT: + g_value_set_object (value, self->mount); + break; + + case PROP_FILE: + g_value_set_object (value, self->file); + break; + + case PROP_IS_NETWORK: + g_value_set_boolean (value, self->is_network); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtk_places_view_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkPlacesViewRow *self = GTK_PLACES_VIEW_ROW (object); + + switch (prop_id) + { + case PROP_ICON: + gtk_image_set_from_gicon (self->icon_image, + g_value_get_object (value), + GTK_ICON_SIZE_LARGE_TOOLBAR); + break; + + case PROP_NAME: + gtk_label_set_label (self->name_label, g_value_get_string (value)); + break; + + case PROP_PATH: + gtk_label_set_label (self->path_label, g_value_get_string (value)); + break; + + case PROP_VOLUME: + g_set_object (&self->volume, g_value_get_object (value)); + break; + + case PROP_MOUNT: + g_set_object (&self->mount, g_value_get_object (value)); + gtk_widget_set_visible (GTK_WIDGET (self->eject_button), self->mount != NULL); + break; + + case PROP_FILE: + g_set_object (&self->file, g_value_get_object (value)); + break; + + case PROP_IS_NETWORK: + gtk_places_view_row_set_is_network (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gtk_places_view_row_class_init (GtkPlacesViewRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = gtk_places_view_row_finalize; + object_class->get_property = gtk_places_view_row_get_property; + object_class->set_property = gtk_places_view_row_set_property; + + properties[PROP_ICON] = + g_param_spec_object ("icon", + _("Icon of the row"), + _("The icon representing the volume"), + G_TYPE_ICON, + G_PARAM_READWRITE); + + properties[PROP_NAME] = + g_param_spec_string ("name", + _("Name of the volume"), + _("The name of the volume"), + "", + G_PARAM_READWRITE); + + properties[PROP_PATH] = + g_param_spec_string ("path", + _("Path of the volume"), + _("The path of the volume"), + "", + G_PARAM_READWRITE); + + properties[PROP_VOLUME] = + g_param_spec_object ("volume", + _("Volume represented by the row"), + _("The volume represented by the row"), + G_TYPE_VOLUME, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + properties[PROP_MOUNT] = + g_param_spec_object ("mount", + _("Mount represented by the row"), + _("The mount point represented by the row, if any"), + G_TYPE_MOUNT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + properties[PROP_FILE] = + g_param_spec_object ("file", + _("File represented by the row"), + _("The file represented by the row, if any"), + G_TYPE_FILE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + properties[PROP_IS_NETWORK] = + g_param_spec_boolean ("is-network", + _("Whether the row represents a network location"), + _("Whether the row represents a network location"), + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/nautilus/gtk/gtkplacesviewrow.ui"); + + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, busy_spinner); + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, eject_button); + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, eject_icon); + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, event_box); + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, icon_image); + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, name_label); + gtk_widget_class_bind_template_child (widget_class, GtkPlacesViewRow, path_label); +} + +static void +gtk_places_view_row_init (GtkPlacesViewRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +gtk_places_view_row_new (GVolume *volume, + GMount *mount) +{ + return g_object_new (GTK_TYPE_PLACES_VIEW_ROW, + "volume", volume, + "mount", mount, + NULL); +} + +GMount* +gtk_places_view_row_get_mount (GtkPlacesViewRow *row) +{ + g_return_val_if_fail (GTK_IS_PLACES_VIEW_ROW (row), NULL); + + return row->mount; +} + +GVolume* +gtk_places_view_row_get_volume (GtkPlacesViewRow *row) +{ + g_return_val_if_fail (GTK_IS_PLACES_VIEW_ROW (row), NULL); + + return row->volume; +} + +GFile* +gtk_places_view_row_get_file (GtkPlacesViewRow *row) +{ + g_return_val_if_fail (GTK_IS_PLACES_VIEW_ROW (row), NULL); + + return row->file; +} + +GtkWidget* +gtk_places_view_row_get_eject_button (GtkPlacesViewRow *row) +{ + g_return_val_if_fail (GTK_IS_PLACES_VIEW_ROW (row), NULL); + + return GTK_WIDGET (row->eject_button); +} + +GtkWidget* +gtk_places_view_row_get_event_box (GtkPlacesViewRow *row) +{ + g_return_val_if_fail (GTK_IS_PLACES_VIEW_ROW (row), NULL); + + return GTK_WIDGET (row->event_box); +} + +void +gtk_places_view_row_set_busy (GtkPlacesViewRow *row, + gboolean is_busy) +{ + g_return_if_fail (GTK_IS_PLACES_VIEW_ROW (row)); + + gtk_widget_set_visible (GTK_WIDGET (row->busy_spinner), is_busy); +} + +gboolean +gtk_places_view_row_get_is_network (GtkPlacesViewRow *row) +{ + g_return_val_if_fail (GTK_IS_PLACES_VIEW_ROW (row), FALSE); + + return row->is_network; +} + +void +gtk_places_view_row_set_is_network (GtkPlacesViewRow *row, + gboolean is_network) +{ + if (row->is_network != is_network) + { + row->is_network = is_network; + + gtk_image_set_from_icon_name (row->eject_icon, + is_network ? "network-offline-symbolic" : "media-eject-symbolic", + GTK_ICON_SIZE_BUTTON); + gtk_widget_set_tooltip_text (GTK_WIDGET (row->eject_button), is_network ? _("Disconnect") : _("Unmount")); + } +} diff --git a/src/gtk/gtkplacesviewrow.ui b/src/gtk/gtkplacesviewrow.ui new file mode 100644 index 000000000..d1e923b19 --- /dev/null +++ b/src/gtk/gtkplacesviewrow.ui @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="gtk30"> + <requires lib="gtk+" version="3.16"/> + <template class="GtkPlacesViewRow" parent="GtkListBoxRow"> + <property name="width_request">100</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <style> + <class name="volume-row" /> + </style> + <child> + <object class="GtkEventBox" id="event_box"> + <property name="visible">True</property> + <child> + <object class="GtkBox" id="box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <property name="margin_top">6</property> + <property name="margin_bottom">6</property> + <property name="border_width">0</property> + <property name="spacing">18</property> + <child> + <object class="GtkImage" id="icon_image"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pixel_size">32</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="path_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="justify">right</property> + <property name="ellipsize">middle</property> + <property name="xalign">1</property> + <property name="width_chars">15</property> + <property name="max_width_chars">15</property> + <style> + <class name="dim-label"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="eject_button"> + <property name="visible">False</property> + <property name="halign">end</property> + <property name="valign">center</property> + <property name="tooltip-text" translatable="yes">Unmount</property> + <child> + <object class="GtkImage" id="eject_icon"> + <property name="visible">True</property> + <property name="icon_name">media-eject-symbolic</property> + <property name="icon_size">1</property> + </object> + </child> + <style> + <class name="image-button"/> + <class name="sidebar-button"/> + </style> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkSpinner" id="busy_spinner"> + <property name="visible">False</property> + <property name="can_focus">False</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gtk/gtkplacesviewrowprivate.h b/src/gtk/gtkplacesviewrowprivate.h new file mode 100644 index 000000000..59294dec6 --- /dev/null +++ b/src/gtk/gtkplacesviewrowprivate.h @@ -0,0 +1,53 @@ +/* gtkplacesviewrow.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#ifndef GTK_PLACES_VIEW_ROW_H +#define GTK_PLACES_VIEW_ROW_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_PLACES_VIEW_ROW (gtk_places_view_row_get_type()) + +G_DECLARE_FINAL_TYPE (GtkPlacesViewRow, gtk_places_view_row, GTK, PLACES_VIEW_ROW, GtkListBoxRow) + +GtkWidget* gtk_places_view_row_new (GVolume *volume, + GMount *mount); + +GtkWidget* gtk_places_view_row_get_eject_button (GtkPlacesViewRow *row); + +GtkWidget* gtk_places_view_row_get_event_box (GtkPlacesViewRow *row); + +GMount* gtk_places_view_row_get_mount (GtkPlacesViewRow *row); + +GVolume* gtk_places_view_row_get_volume (GtkPlacesViewRow *row); + +GFile* gtk_places_view_row_get_file (GtkPlacesViewRow *row); + +void gtk_places_view_row_set_busy (GtkPlacesViewRow *row, + gboolean is_busy); + +gboolean gtk_places_view_row_get_is_network (GtkPlacesViewRow *row); + +void gtk_places_view_row_set_is_network (GtkPlacesViewRow *row, + gboolean is_network); + +G_END_DECLS + +#endif /* GTK_PLACES_VIEW_ROW_H */ diff --git a/src/nautilus-application.c b/src/nautilus-application.c index bc6da1739..dbe78bed0 100644 --- a/src/nautilus-application.c +++ b/src/nautilus-application.c @@ -389,7 +389,7 @@ get_window_slot_for_location (NautilusApplication *application, GFile *location) slot = NULL; file = nautilus_file_get (location); - if (!nautilus_file_is_directory (file)) { + if (!nautilus_file_is_directory (file) && !nautilus_file_is_other_locations (file)) { location = g_file_get_parent (location); } else { g_object_ref (location); diff --git a/src/nautilus-pathbar.c b/src/nautilus-pathbar.c index 19ff073e8..8e8319af8 100644 --- a/src/nautilus-pathbar.c +++ b/src/nautilus-pathbar.c @@ -43,6 +43,7 @@ enum { typedef enum { NORMAL_BUTTON, + OTHER_LOCATIONS_BUTTON, ROOT_BUTTON, HOME_BUTTON, MOUNT_BUTTON @@ -400,11 +401,14 @@ nautilus_path_bar_dispose (GObject *object) static const char * get_dir_name (ButtonData *button_data) { - if (button_data->type == HOME_BUTTON) { - return _("Home"); - } else { - return button_data->dir_name; - } + switch (button_data->type) { + case HOME_BUTTON: + return _("Home"); + case OTHER_LOCATIONS_BUTTON: + return _("Other Locations"); + default: + return button_data->dir_name; + } } /* We always want to request the same size for the label, whether @@ -1629,8 +1633,13 @@ setup_button_type (ButtonData *button_data, GFile *location) { GMount *mount; + gchar *uri; + + uri = g_file_get_uri (location); - if (nautilus_is_root_directory (location)) { + if (g_strcmp0 (uri, "other-locations:///") == 0) { + button_data->type = OTHER_LOCATIONS_BUTTON; + } else if (nautilus_is_root_directory (location)) { button_data->type = ROOT_BUTTON; } else if (nautilus_is_home_directory (location)) { button_data->type = HOME_BUTTON; @@ -1644,6 +1653,8 @@ setup_button_type (ButtonData *button_data, } else { button_data->type = NORMAL_BUTTON; } + + g_free (uri); } static void diff --git a/src/nautilus-places-view.c b/src/nautilus-places-view.c new file mode 100644 index 000000000..7a4d15fc3 --- /dev/null +++ b/src/nautilus-places-view.c @@ -0,0 +1,359 @@ +/* nautilus-places-view.c + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#include "nautilus-mime-actions.h" +#include "nautilus-places-view.h" +#include "nautilus-window-slot.h" +#include "gtk/gtkplacesviewprivate.h" + +typedef struct +{ + GFile *location; + GIcon *icon; + NautilusQuery *search_query; + + GtkWidget *places_view; +} NautilusPlacesViewPrivate; + +struct _NautilusPlacesView +{ + GtkFrameClass parent; +}; + +static void nautilus_places_view_iface_init (NautilusViewInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (NautilusPlacesView, nautilus_places_view, GTK_TYPE_BOX, + G_ADD_PRIVATE (NautilusPlacesView) + G_IMPLEMENT_INTERFACE (NAUTILUS_TYPE_VIEW, nautilus_places_view_iface_init)); + +enum { + PROP_0, + PROP_ICON, + PROP_LOCATION, + PROP_SEARCH_QUERY, + PROP_VIEW_WIDGET, + PROP_IS_LOADING, + PROP_IS_SEARCHING, + LAST_PROP +}; + +static void +open_location_cb (NautilusPlacesView *view, + GFile *location, + GtkPlacesOpenFlags open_flags) +{ + NautilusWindowOpenFlags flags; + GtkWidget *slot; + + slot = gtk_widget_get_ancestor (GTK_WIDGET (view), NAUTILUS_TYPE_WINDOW_SLOT); + + switch (open_flags) { + case GTK_PLACES_OPEN_NEW_TAB: + flags = NAUTILUS_WINDOW_OPEN_FLAG_NEW_TAB; + break; + + case GTK_PLACES_OPEN_NEW_WINDOW: + flags = NAUTILUS_WINDOW_OPEN_FLAG_NEW_WINDOW; + break; + + case GTK_PLACES_OPEN_NORMAL: /* fall-through */ + default: + flags = 0; + break; + } + + if (slot) { + NautilusFile *file; + GtkWidget *window; + char *path; + + path = "other-locations:///"; + file = nautilus_file_get (location); + window = gtk_widget_get_toplevel (GTK_WIDGET (view)); + + nautilus_mime_activate_file (GTK_WINDOW (window), + NAUTILUS_WINDOW_SLOT (slot), + file, + path, + flags); + + } +} + +static void +loading_cb (NautilusView *view) +{ + g_object_notify (G_OBJECT (view), "is-loading"); +} + +static void +nautilus_places_view_finalize (GObject *object) +{ + NautilusPlacesView *self = (NautilusPlacesView *)object; + NautilusPlacesViewPrivate *priv = nautilus_places_view_get_instance_private (self); + + g_clear_object (&priv->icon); + g_clear_object (&priv->location); + g_clear_object (&priv->search_query); + + G_OBJECT_CLASS (nautilus_places_view_parent_class)->finalize (object); +} + +static void +nautilus_places_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + NautilusView *view = NAUTILUS_VIEW (object); + + switch (prop_id) { + case PROP_ICON: + g_value_set_object (value, nautilus_view_get_icon (view)); + break; + + case PROP_LOCATION: + g_value_set_object (value, nautilus_view_get_location (view)); + break; + + case PROP_SEARCH_QUERY: + g_value_set_object (value, nautilus_view_get_search_query (view)); + break; + + case PROP_VIEW_WIDGET: + g_value_set_object (value, nautilus_view_get_view_widget (view)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +nautilus_places_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + NautilusView *view = NAUTILUS_VIEW (object); + + switch (prop_id) { + case PROP_LOCATION: + nautilus_view_set_location (view, g_value_get_object (value)); + break; + + case PROP_SEARCH_QUERY: + nautilus_view_set_search_query (view, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static GIcon* +nautilus_places_view_get_icon (NautilusView *view) +{ + NautilusPlacesViewPrivate *priv; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + + return priv->icon; +} + +static GFile* +nautilus_places_view_get_location (NautilusView *view) +{ + NautilusPlacesViewPrivate *priv; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + + return priv->location; +} + +static void +nautilus_places_view_set_location (NautilusView *view, + GFile *location) +{ + if (location) { + NautilusPlacesViewPrivate *priv; + gchar *uri; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + uri = g_file_get_uri (location); + + /* + * If it's not trying to open the places view itself, simply + * delegates the location to window-slot, which takes care of + * selecting the appropriate view. + */ + if (!g_strcmp0 (uri, "other-locations:///") == 0) { + GtkWidget *slot; + + slot = gtk_widget_get_ancestor (GTK_WIDGET (view), NAUTILUS_TYPE_WINDOW_SLOT); + g_assert (slot != NULL); + + nautilus_window_slot_open_location (NAUTILUS_WINDOW_SLOT (slot), location, 0); + } else { + g_set_object (&priv->location, location); + } + + g_free (uri); + } +} + +static GList* +nautilus_places_view_get_selection (NautilusView *view) +{ + /* STUB */ + return NULL; +} + +static void +nautilus_places_view_set_selection (NautilusView *view, + GList *selection) +{ + /* STUB */ +} + +static NautilusQuery* +nautilus_places_view_get_search_query (NautilusView *view) +{ + NautilusPlacesViewPrivate *priv; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + + return priv->search_query; +} + +static void +nautilus_places_view_set_search_query (NautilusView *view, + NautilusQuery *query) +{ + NautilusPlacesViewPrivate *priv; + gchar *text; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + + g_set_object (&priv->search_query, query); + + text = query ? nautilus_query_get_text (query) : NULL; + + gtk_places_view_set_search_query (GTK_PLACES_VIEW (priv->places_view), text); + + g_free (text); +} + +static GtkWidget* +nautilus_places_view_get_view_widget (NautilusView *view) +{ + /* By returning NULL, the view menu button turns insensitive */ + return NULL; +} + +static gboolean +nautilus_places_view_is_loading (NautilusView *view) +{ + NautilusPlacesViewPrivate *priv; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + + return gtk_places_view_get_loading (GTK_PLACES_VIEW (priv->places_view)); +} + +static gboolean +nautilus_places_view_is_searching (NautilusView *view) +{ + NautilusPlacesViewPrivate *priv; + + priv = nautilus_places_view_get_instance_private (NAUTILUS_PLACES_VIEW (view)); + + return priv->search_query != NULL; +} + +static void +nautilus_places_view_iface_init (NautilusViewInterface *iface) +{ + iface->get_icon = nautilus_places_view_get_icon; + iface->get_location = nautilus_places_view_get_location; + iface->set_location = nautilus_places_view_set_location; + iface->get_selection = nautilus_places_view_get_selection; + iface->set_selection = nautilus_places_view_set_selection; + iface->get_search_query = nautilus_places_view_get_search_query; + iface->set_search_query = nautilus_places_view_set_search_query; + iface->get_view_widget = nautilus_places_view_get_view_widget; + iface->is_loading = nautilus_places_view_is_loading; + iface->is_searching = nautilus_places_view_is_searching; +} + +static void +nautilus_places_view_class_init (NautilusPlacesViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = nautilus_places_view_finalize; + object_class->get_property = nautilus_places_view_get_property; + object_class->set_property = nautilus_places_view_set_property; + + g_object_class_override_property (object_class, PROP_ICON, "icon"); + g_object_class_override_property (object_class, PROP_IS_LOADING, "is-loading"); + g_object_class_override_property (object_class, PROP_IS_SEARCHING, "is-searching"); + g_object_class_override_property (object_class, PROP_LOCATION, "location"); + g_object_class_override_property (object_class, PROP_SEARCH_QUERY, "search-query"); + g_object_class_override_property (object_class, PROP_VIEW_WIDGET, "view-widget"); +} + +static void +nautilus_places_view_init (NautilusPlacesView *self) +{ + NautilusPlacesViewPrivate *priv; + + priv = nautilus_places_view_get_instance_private (self); + + /* Icon */ + priv->icon = g_themed_icon_new_with_default_fallbacks ("view-list-symbolic"); + + /* Location */ + priv->location = g_file_new_for_uri ("other-locations:///"); + + /* Places view */ + priv->places_view = gtk_places_view_new (); + gtk_places_view_set_open_flags (GTK_PLACES_VIEW (priv->places_view), + GTK_PLACES_OPEN_NEW_TAB | GTK_PLACES_OPEN_NEW_WINDOW | GTK_PLACES_OPEN_NORMAL); + gtk_widget_set_hexpand (priv->places_view, TRUE); + gtk_widget_set_vexpand (priv->places_view, TRUE); + gtk_widget_show (priv->places_view); + gtk_container_add (GTK_CONTAINER (self), priv->places_view); + + g_signal_connect_swapped (priv->places_view, + "notify::loading", + G_CALLBACK (loading_cb), + self); + + g_signal_connect_swapped (priv->places_view, + "open-location", + G_CALLBACK (open_location_cb), + self); + +} + +NautilusPlacesView * +nautilus_places_view_new (void) +{ + return g_object_new (NAUTILUS_TYPE_PLACES_VIEW, NULL); +} diff --git a/src/nautilus-places-view.h b/src/nautilus-places-view.h new file mode 100644 index 000000000..51939103e --- /dev/null +++ b/src/nautilus-places-view.h @@ -0,0 +1,37 @@ +/* nautilus-places-view.h + * + * Copyright (C) 2015 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#ifndef NAUTILUS_PLACES_VIEW_H +#define NAUTILUS_PLACES_VIEW_H + +#include <glib.h> +#include <gtk/gtk.h> + +#include "nautilus-view.h" + +G_BEGIN_DECLS + +#define NAUTILUS_TYPE_PLACES_VIEW (nautilus_places_view_get_type()) + +G_DECLARE_FINAL_TYPE (NautilusPlacesView, nautilus_places_view, NAUTILUS, PLACES_VIEW, GtkBox) + +NautilusPlacesView* nautilus_places_view_new (void); + +G_END_DECLS + +#endif /* NAUTILUS_PLACES_VIEW_H */ diff --git a/src/nautilus-window-slot-dnd.c b/src/nautilus-window-slot-dnd.c index 71386b140..af15c18bc 100644 --- a/src/nautilus-window-slot-dnd.c +++ b/src/nautilus-window-slot-dnd.c @@ -360,7 +360,13 @@ slot_proxy_handle_drop (GtkWidget *widget, target_view = NULL; if (target_slot != NULL) { - target_view = nautilus_window_slot_get_current_view (target_slot); + NautilusView *view; + + view = nautilus_window_slot_get_current_view (target_slot); + + if (view && NAUTILUS_IS_FILES_VIEW (view)) { + target_view = NAUTILUS_FILES_VIEW (view); + } } if (target_slot != NULL && target_view != NULL) { diff --git a/src/nautilus-window-slot.c b/src/nautilus-window-slot.c index 70ab1bfd5..128730865 100644 --- a/src/nautilus-window-slot.c +++ b/src/nautilus-window-slot.c @@ -29,6 +29,7 @@ #include "nautilus-desktop-window.h" #include "nautilus-list-view.h" #include "nautilus-mime-actions.h" +#include "nautilus-places-view.h" #include "nautilus-special-location-bar.h" #include "nautilus-trash-bar.h" #include "nautilus-view.h" @@ -121,6 +122,7 @@ struct NautilusWindowSlotDetails { NautilusWindowGoToCallback open_callback; gpointer open_callback_user_data; gchar *view_mode_before_search; + gchar *view_mode_before_places; }; static guint signals[LAST_SIGNAL] = { 0 }; @@ -133,15 +135,108 @@ static void nautilus_window_slot_sync_actions (NautilusWindowSlot *slot); static void nautilus_window_slot_connect_new_content_view (NautilusWindowSlot *slot); static void nautilus_window_slot_disconnect_content_view (NautilusWindowSlot *slot); static void nautilus_window_slot_emit_location_change (NautilusWindowSlot *slot, GFile *from, GFile *to); +static gboolean nautilus_window_slot_content_view_matches (NautilusWindowSlot *slot, const char *iid); +static NautilusView* nautilus_window_slot_get_view_for_location (NautilusWindowSlot *slot, GFile *location); + +static NautilusView* +nautilus_window_slot_get_view_for_location (NautilusWindowSlot *slot, + GFile *location) +{ + NautilusWindow *window; + NautilusView *view; + NautilusFile *file; + + window = nautilus_window_slot_get_window (slot); + file = nautilus_file_get (location); + view = NULL; + + /* FIXME bugzilla.gnome.org 41243: + * We should use inheritance instead of these special cases + * for the desktop window. + */ + if (NAUTILUS_IS_DESKTOP_WINDOW (window)) { + view = NAUTILUS_VIEW (nautilus_files_view_new (NAUTILUS_DESKTOP_ICON_VIEW_IID, slot)); + } else if (nautilus_file_is_other_locations (file)) { + view = NAUTILUS_VIEW (nautilus_places_view_new ()); + + /* Save the current view, so we can go back after places view */ + if (slot->details->content_view && NAUTILUS_IS_FILES_VIEW (slot->details->content_view)) { + g_clear_pointer (&slot->details->view_mode_before_places, g_free); + slot->details->view_mode_before_places = g_strdup (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view))); + } + } else { + gchar *view_id; + + view_id = NULL; + + /* If we are in search, try to use by default list view. This will be deactivated + * if the user manually switch to a diferent view mode */ + if (nautilus_file_is_in_search (file)) { + if (g_settings_get_boolean (nautilus_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_ON_SEARCH)) { + /* If it's already set, is because we already made the change to search mode, + * so the view mode of the current view will be the one search is using, + * which is not the one we are interested in */ + if (slot->details->view_mode_before_search == NULL) { + slot->details->view_mode_before_search = g_strdup (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view))); + } + view_id = g_strdup (NAUTILUS_LIST_VIEW_IID); + } else { + g_free (slot->details->view_mode_before_search); + slot->details->view_mode_before_search = NULL; + } + } + + /* If there is already a view, just use the view mode that it's currently using, or + * if we were on search before, use what we were using before entering + * search mode */ + if (slot->details->content_view != NULL && view_id == NULL) { + if (slot->details->view_mode_before_search != NULL) { + view_id = slot->details->view_mode_before_search; + slot->details->view_mode_before_search = NULL; + } else if (NAUTILUS_IS_PLACES_VIEW (slot->details->content_view)) { + view_id = slot->details->view_mode_before_places; + slot->details->view_mode_before_places = NULL; + } else { + view_id = g_strdup (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view))); + } + } + + /* If there is not previous view in this slot, use the default view mode + * from preferences */ + if (view_id == NULL) { + view_id = nautilus_global_preferences_get_default_folder_viewer_preference_as_iid (); + } + + /* Try to reuse the current view */ + if (nautilus_window_slot_content_view_matches (slot, view_id)) { + view = slot->details->content_view; + } else { + view = NAUTILUS_VIEW (nautilus_files_view_new (view_id, slot)); + } + + g_free (view_id); + } + + nautilus_file_unref (file); + + return g_object_ref (view); +} static gboolean -nautilus_window_slot_content_view_matches_iid (NautilusWindowSlot *slot, - const char *iid) +nautilus_window_slot_content_view_matches (NautilusWindowSlot *slot, + const char *iid) { if (slot->details->content_view == NULL) { return FALSE; } - return g_strcmp0 (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view)), iid) == 0; + + if (!iid && NAUTILUS_IS_PLACES_VIEW (slot->details->content_view)) { + return TRUE; + } else if (iid && NAUTILUS_IS_FILES_VIEW (slot->details->content_view)){ + return g_strcmp0 (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view)), iid) == 0; + } else { + return FALSE; + } } static void @@ -170,10 +265,11 @@ nautilus_window_slot_sync_actions (NautilusWindowSlot *slot) /* View mode */ action = g_action_map_lookup_action (G_ACTION_MAP (slot->details->window), "view-mode"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE); - if (nautilus_window_slot_content_view_matches_iid (slot, NAUTILUS_LIST_VIEW_ID)) { + if (nautilus_window_slot_content_view_matches (slot, NAUTILUS_LIST_VIEW_ID)) { g_action_change_state (action, g_variant_new_string ("list")); - } else if (nautilus_window_slot_content_view_matches_iid (slot, NAUTILUS_CANVAS_VIEW_ID)) { + } else if (nautilus_window_slot_content_view_matches (slot, NAUTILUS_CANVAS_VIEW_ID)) { g_action_change_state (action, g_variant_new_string ("grid")); } else { g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE); @@ -521,8 +617,8 @@ static void end_location_change (NautilusWindowSlot static void cancel_location_change (NautilusWindowSlot *slot); static void got_file_info_for_view_selection_callback (NautilusFile *file, gpointer callback_data); -static gboolean create_content_view (NautilusWindowSlot *slot, - const char *view_id, +static gboolean setup_view (NautilusWindowSlot *slot, + NautilusView *view, GError **error); static void load_new_location (NautilusWindowSlot *slot, GFile *location, @@ -742,13 +838,11 @@ begin_location_change (NautilusWindowSlot *slot, nautilus_profile_start (NULL); - directory = nautilus_directory_get (location); - /* Avoid to update status from the current view in our async calls */ nautilus_window_slot_disconnect_content_view (slot); /* We are going to change the location, so make sure we stop any loading * or searching of the previous view, so we avoid to be slow */ - if (slot->details->content_view) { + if (slot->details->content_view && NAUTILUS_IS_FILES_VIEW (slot->details->content_view)) { nautilus_files_view_stop_loading (NAUTILUS_FILES_VIEW (slot->details->content_view)); } @@ -796,6 +890,8 @@ begin_location_change (NautilusWindowSlot *slot, * after determining an initial view (in the components), then * we end up fetching things twice. */ + directory = nautilus_directory_get (location); + if (type == NAUTILUS_LOCATION_CHANGE_RELOAD) { force_reload = TRUE; } else if (!nautilus_monitor_active ()) { @@ -819,7 +915,8 @@ begin_location_change (NautilusWindowSlot *slot, /* Set current_bookmark scroll pos */ if (slot->details->current_location_bookmark != NULL && - slot->details->content_view != NULL) { + slot->details->content_view != NULL && + NAUTILUS_IS_FILES_VIEW (slot->details->content_view)) { current_pos = nautilus_files_view_get_first_visible_file (NAUTILUS_FILES_VIEW (slot->details->content_view)); nautilus_bookmark_set_scroll_pos (slot->details->current_location_bookmark, current_pos); g_free (current_pos); @@ -1016,10 +1113,10 @@ got_file_info_for_view_selection_callback (NautilusFile *file, gpointer callback_data) { GError *error = NULL; - char *view_id; NautilusWindow *window; NautilusWindowSlot *slot; NautilusFile *viewed_file, *parent_file; + NautilusView *view; GFile *location, *default_location; GMountOperation *mount_op; MountNotMountedData *data; @@ -1116,54 +1213,16 @@ got_file_info_for_view_selection_callback (NautilusFile *file, nautilus_file_unref (parent_file); location = slot->details->pending_location; - view_id = NULL; - - if (error == NULL || - (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_NOT_SUPPORTED)) { - /* We got the information we need, now pick what view to use: */ + view = NULL; - /* If we are in search, try to use by default list view. This will be deactivated - * if the user manually switch to a diferent view mode */ - if (nautilus_file_is_in_search (nautilus_file_get (location))) { - if (g_settings_get_boolean (nautilus_preferences, NAUTILUS_PREFERENCES_LIST_VIEW_ON_SEARCH)) { - /* If it's already set, is because we already made the change to search mode, - * so the view mode of the current view will be the one search is using, - * which is not the one we are interested in */ - if (slot->details->view_mode_before_search == NULL) { - slot->details->view_mode_before_search = g_strdup (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view))); - } - view_id = g_strdup (NAUTILUS_LIST_VIEW_IID); - } else { - g_free (slot->details->view_mode_before_search); - slot->details->view_mode_before_search = NULL; - } - } - - /* If there is already a view, just use the view mode that it's currently using, or - * if we were on search before, use what we were using before entering - * search mode */ - if (slot->details->content_view != NULL && view_id == NULL) { - if (slot->details->view_mode_before_search != NULL) { - view_id = g_strdup (slot->details->view_mode_before_search); - g_free (slot->details->view_mode_before_search); - slot->details->view_mode_before_search = NULL; - } else { - view_id = g_strdup (nautilus_files_view_get_view_id (NAUTILUS_FILES_VIEW (slot->details->content_view))); - } - } - - /* If there is not previous view in this slot, use the default view mode - * from preferences */ - if (view_id == NULL) { - view_id = nautilus_global_preferences_get_default_folder_viewer_preference_as_iid (); - } + if (!error || (error && error->domain == G_IO_ERROR && error->code == G_IO_ERROR_NOT_SUPPORTED)) { + view = nautilus_window_slot_get_view_for_location (slot, location); } - if (view_id != NULL) { + if (view != NULL) { GError *err = NULL; - create_content_view (slot, view_id, &err); - g_free (view_id); + setup_view (slot, view, &err); report_callback (slot, err); g_clear_error (&err); @@ -1240,7 +1299,6 @@ got_file_info_for_view_selection_callback (NautilusFile *file, g_clear_error (&error); nautilus_file_unref (file); - nautilus_profile_end (NULL); } @@ -1252,52 +1310,26 @@ got_file_info_for_view_selection_callback (NautilusFile *file, * view, and the current location will be used. */ static gboolean -create_content_view (NautilusWindowSlot *slot, - const char *view_id, - GError **error_out) +setup_view (NautilusWindowSlot *slot, + NautilusView *view, + GError **error_out) { - NautilusWindow *window; - NautilusView *view; - GList *selection; gboolean ret = TRUE; GError *error = NULL; GFile *old_location; - window = nautilus_window_slot_get_window (slot); - nautilus_profile_start (NULL); - /* FIXME bugzilla.gnome.org 41243: - * We should use inheritance instead of these special cases - * for the desktop window. - */ - if (NAUTILUS_IS_DESKTOP_WINDOW (window)) { - /* We force the desktop to use a desktop_icon_view. It's simpler - * to fix it here than trying to make it pick the right view in - * the first place. - */ - view_id = NAUTILUS_DESKTOP_ICON_VIEW_IID; - } - nautilus_window_slot_disconnect_content_view (slot); - if (nautilus_window_slot_content_view_matches_iid (slot, view_id)) { - /* reuse existing content view */ - view = slot->details->content_view; - slot->details->new_content_view = view; - g_object_ref (view); - } else { - /* create a new content view */ - view = NAUTILUS_VIEW (nautilus_files_view_new (view_id, slot)); - slot->details->new_content_view = view; - } + slot->details->new_content_view = view; + nautilus_window_slot_connect_new_content_view (slot); /* Forward search selection and state before loading the new model */ old_location = slot->details->content_view ? nautilus_view_get_location (slot->details->content_view) : NULL; /* Actually load the pending location and selection: */ - if (slot->details->pending_location != NULL) { load_new_location (slot, slot->details->pending_location, @@ -1345,13 +1377,11 @@ load_new_location (NautilusWindowSlot *slot, gboolean tell_current_content_view, gboolean tell_new_content_view) { - GList *selection_copy; NautilusView *view; g_assert (slot != NULL); g_assert (location != NULL); - selection_copy = g_list_copy_deep (selection, (GCopyFunc) g_object_ref, NULL); view = NULL; nautilus_profile_start (NULL); @@ -1373,8 +1403,6 @@ load_new_location (NautilusWindowSlot *slot, nautilus_view_set_selection (view, selection); } - g_list_free_full (selection_copy, g_object_unref); - nautilus_profile_end (NULL); } @@ -1447,7 +1475,8 @@ cancel_location_change (NautilusWindowSlot *slot) if (slot->details->pending_location != NULL && location != NULL - && slot->details->content_view != NULL) { + && slot->details->content_view != NULL + && NAUTILUS_IS_FILES_VIEW (slot->details->content_view)) { /* No need to tell the new view - either it is the * same as the old view, in which case it will already @@ -1548,7 +1577,7 @@ nautilus_window_slot_set_content_view (NautilusWindowSlot *slot, DEBUG ("Change view of window %s to %s", uri, id); g_free (uri); - if (nautilus_window_slot_content_view_matches_iid (slot, id)) { + if (nautilus_window_slot_content_view_matches (slot, id)) { return; } @@ -1564,14 +1593,13 @@ nautilus_window_slot_set_content_view (NautilusWindowSlot *slot, * is currently visible */ slot->details->pending_scroll_to = nautilus_files_view_get_first_visible_file (NAUTILUS_FILES_VIEW (slot->details->content_view)); } + slot->details->location_change_type = NAUTILUS_LOCATION_CHANGE_RELOAD; - if (!create_content_view (slot, id, NULL)) { + if (!setup_view (slot, NAUTILUS_VIEW (view), NULL)) { /* Just load the homedir. */ nautilus_window_slot_go_home (slot, FALSE); } - - nautilus_file_list_free (selection); } void @@ -2152,7 +2180,7 @@ nautilus_window_slot_connect_new_content_view (NautilusWindowSlot *slot) static void nautilus_window_slot_disconnect_content_view (NautilusWindowSlot *slot) { - if (slot->details->content_view != NULL) { + if (slot->details->new_content_view && NAUTILUS_IS_FILES_VIEW (slot->details->new_content_view)) { /* disconnect old view */ g_signal_handlers_disconnect_by_func (slot->details->content_view, G_CALLBACK (view_is_loading_changed_cb), diff --git a/src/nautilus-window.c b/src/nautilus-window.c index 296ba533c..4353ee3ca 100644 --- a/src/nautilus-window.c +++ b/src/nautilus-window.c @@ -986,6 +986,18 @@ places_sidebar_show_connect_to_server_cb (GtkPlacesSidebar *sidebar, gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); } +static void +places_sidebar_show_other_locations (NautilusWindow *window) +{ + GFile *location; + + location = g_file_new_for_uri ("other-locations:///"); + + open_location_cb (window, location, GTK_PLACES_OPEN_NORMAL); + + g_object_unref (location); +} + static GList * build_selection_list_from_gfile_list (GList *gfile_list) { @@ -1271,7 +1283,6 @@ nautilus_window_set_up_sidebar (NautilusWindow *window) (GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_TAB | GTK_PLACES_OPEN_NEW_WINDOW)); - gtk_places_sidebar_set_show_connect_to_server (GTK_PLACES_SIDEBAR (window->priv->places_sidebar), TRUE); g_signal_connect_swapped (window->priv->places_sidebar, "open-location", G_CALLBACK (open_location_cb), window); @@ -2534,6 +2545,8 @@ nautilus_window_class_init (NautilusWindowClass *class) gtk_widget_class_bind_template_child_private (wclass, NautilusWindow, notification_operation_open); gtk_widget_class_bind_template_child_private (wclass, NautilusWindow, notification_operation_close); + gtk_widget_class_bind_template_callback (wclass, places_sidebar_show_other_locations); + properties[PROP_DISABLE_CHROME] = g_param_spec_boolean ("disable-chrome", "Disable chrome", diff --git a/src/nautilus-window.ui b/src/nautilus-window.ui index 7e7a7f28a..b23e3a5a4 100644 --- a/src/nautilus-window.ui +++ b/src/nautilus-window.ui @@ -33,6 +33,8 @@ <object class="GtkPlacesSidebar" id="places_sidebar"> <property name="visible">True</property> <property name="populate-all">True</property> + <property name="show-other-locations">True</property> + <signal name="show-other-locations" handler="places_sidebar_show_other_locations" object="NautilusWindow" swapped="yes" /> </object> <packing> <property name="pack_type">start</property> diff --git a/src/nautilus.gresource.xml b/src/nautilus.gresource.xml index 5a3e324e9..c663e73a8 100644 --- a/src/nautilus.gresource.xml +++ b/src/nautilus.gresource.xml @@ -16,6 +16,8 @@ <file>nautilus-window.ui</file> <file>nautilus-no-search-results.ui</file> <file>nautilus-folder-is-empty.ui</file> + <file>gtk/gtkplacesview.ui</file> + <file>gtk/gtkplacesviewrow.ui</file> <file alias="icons/thumbnail_frame.png">../icons/thumbnail_frame.png</file> <file alias="icons/filmholes.png">../icons/filmholes.png</file> <file alias="icons/knob.png">../icons/knob.png</file> |