diff options
Diffstat (limited to '')
-rw-r--r-- | src/gtk/.editorconfig | 30 | ||||
-rw-r--r-- | src/gtk/nautilusgtkbookmarksmanager.c | 643 | ||||
-rw-r--r-- | src/gtk/nautilusgtkbookmarksmanagerprivate.h | 89 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacessidebar.c | 5142 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacessidebarprivate.h | 148 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacesview.c | 2635 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacesview.ui | 261 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacesviewprivate.h | 55 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacesviewrow.c | 508 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacesviewrow.ui | 83 | ||||
-rw-r--r-- | src/gtk/nautilusgtkplacesviewrowprivate.h | 59 | ||||
-rw-r--r-- | src/gtk/nautilusgtksidebarrow.c | 692 | ||||
-rw-r--r-- | src/gtk/nautilusgtksidebarrow.ui | 69 | ||||
-rw-r--r-- | src/gtk/nautilusgtksidebarrowprivate.h | 60 |
14 files changed, 10474 insertions, 0 deletions
diff --git a/src/gtk/.editorconfig b/src/gtk/.editorconfig new file mode 100644 index 0000000..e1ee505 --- /dev/null +++ b/src/gtk/.editorconfig @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2021 The GTK Authors +# SPDX-License-Identifier: CC0-1.0 + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true + +[*.[ch]] +indent_size = 2 +indent_style = space +insert_final_newline = true +# For the legacy tabs which still exist in the code: +tab_width = 8 + +[*.ui] +indent_size = 2 +indent_style = space +insert_final_newline = true + +[*.xml] +indent_size = 2 +indent_style = space + +[meson.build] +indent_size = 2 +indent_style = space + +[*.md] +max_line_length = 80 diff --git a/src/gtk/nautilusgtkbookmarksmanager.c b/src/gtk/nautilusgtkbookmarksmanager.c new file mode 100644 index 0000000..86f3bb4 --- /dev/null +++ b/src/gtk/nautilusgtkbookmarksmanager.c @@ -0,0 +1,643 @@ +/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */ +/* GTK - The GIMP Toolkit + * nautilusgtkbookmarksmanager.c: Utilities to manage and monitor ~/.gtk-bookmarks + * Copyright (C) 2003, Red Hat, Inc. + * Copyright (C) 2007-2008 Carlos Garnacho + * Copyright (C) 2011 Suse + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Federico Mena Quintero <federico@gnome.org> + */ + +#include "config.h" +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include "nautilus-enum-types.h" + +#include <string.h> + +#include "nautilusgtkbookmarksmanagerprivate.h" + +static void +_gtk_bookmark_free (gpointer data) +{ + GtkBookmark *bookmark = data; + + g_object_unref (bookmark->file); + g_free (bookmark->label); + g_slice_free (GtkBookmark, bookmark); +} + +static void +set_error_bookmark_doesnt_exist (GFile *file, GError **error) +{ + char *uri = g_file_get_uri (file); + + g_set_error (error, + GTK_FILE_CHOOSER_ERROR, + GTK_FILE_CHOOSER_ERROR_NONEXISTENT, + _("%s does not exist in the bookmarks list"), + uri); + + g_free (uri); +} + +static GFile * +get_legacy_bookmarks_file (void) +{ + GFile *file; + char *filename; + + filename = g_build_filename (g_get_home_dir (), ".gtk-bookmarks", NULL); + file = g_file_new_for_path (filename); + g_free (filename); + + return file; +} + +static GFile * +get_bookmarks_file (void) +{ + GFile *file; + char *filename; + + /* Use gtk-3.0's bookmarks file as the format didn't change. + * Add the 3.0 file format to get_legacy_bookmarks_file() when + * the format does change. + */ + filename = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "bookmarks", NULL); + file = g_file_new_for_path (filename); + g_free (filename); + + return file; +} + +static GSList * +parse_bookmarks (const char *contents) +{ + char **lines, *space; + GSList *bookmarks = NULL; + int i; + + lines = g_strsplit (contents, "\n", -1); + + for (i = 0; lines[i]; i++) + { + GtkBookmark *bookmark; + + if (!*lines[i]) + continue; + + if (!g_utf8_validate (lines[i], -1, NULL)) + continue; + + bookmark = g_slice_new0 (GtkBookmark); + + if ((space = strchr (lines[i], ' ')) != NULL) + { + space[0] = '\0'; + bookmark->label = g_strdup (space + 1); + } + + bookmark->file = g_file_new_for_uri (lines[i]); + bookmarks = g_slist_prepend (bookmarks, bookmark); + } + + bookmarks = g_slist_reverse (bookmarks); + g_strfreev (lines); + + return bookmarks; +} + +static GSList * +read_bookmarks (GFile *file) +{ + char *contents; + GSList *bookmarks = NULL; + + if (!g_file_load_contents (file, NULL, &contents, + NULL, NULL, NULL)) + return NULL; + + bookmarks = parse_bookmarks (contents); + + g_free (contents); + + return bookmarks; +} + +static void +notify_changed (NautilusGtkBookmarksManager *manager) +{ + if (manager->changed_func) + manager->changed_func (manager->changed_func_data); +} + +static void +read_bookmarks_finish (GObject *source, + GAsyncResult *result, + gpointer data) +{ + GFile *file = G_FILE (source); + NautilusGtkBookmarksManager *manager = data; + char *contents = NULL; + GError *error = NULL; + + if (!g_file_load_contents_finish (file, result, &contents, NULL, NULL, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Failed to load '%s': %s", g_file_peek_path (file), error->message); + g_error_free (error); + return; + } + + g_slist_free_full (manager->bookmarks, _gtk_bookmark_free); + manager->bookmarks = parse_bookmarks (contents); + + g_free (contents); + + notify_changed (manager); +} + +static void +save_bookmarks (GFile *bookmarks_file, + GSList *bookmarks) +{ + GError *error = NULL; + GString *contents; + GSList *l; + GFile *parent = NULL; + + contents = g_string_new (""); + + for (l = bookmarks; l; l = l->next) + { + GtkBookmark *bookmark = l->data; + char *uri; + + uri = g_file_get_uri (bookmark->file); + if (!uri) + continue; + + g_string_append (contents, uri); + + if (bookmark->label && g_utf8_validate (bookmark->label, -1, NULL)) + g_string_append_printf (contents, " %s", bookmark->label); + + g_string_append_c (contents, '\n'); + g_free (uri); + } + + parent = g_file_get_parent (bookmarks_file); + if (!g_file_make_directory_with_parents (parent, NULL, &error)) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + g_clear_error (&error); + else + goto out; + } + if (!g_file_replace_contents (bookmarks_file, + contents->str, + contents->len, + NULL, FALSE, 0, NULL, + NULL, &error)) + goto out; + + out: + if (error) + { + g_critical ("%s", error->message); + g_error_free (error); + } + g_clear_object (&parent); + g_string_free (contents, TRUE); +} + +static void +bookmarks_file_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event, + gpointer data) +{ + NautilusGtkBookmarksManager *manager = data; + + switch (event) + { + case G_FILE_MONITOR_EVENT_CHANGED: + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: + case G_FILE_MONITOR_EVENT_CREATED: + g_file_load_contents_async (file, manager->cancellable, read_bookmarks_finish, manager); + break; + + case G_FILE_MONITOR_EVENT_DELETED: + case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: + case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: + case G_FILE_MONITOR_EVENT_UNMOUNTED: + case G_FILE_MONITOR_EVENT_MOVED: + case G_FILE_MONITOR_EVENT_RENAMED: + case G_FILE_MONITOR_EVENT_MOVED_IN: + case G_FILE_MONITOR_EVENT_MOVED_OUT: + default: + /* ignore at the moment */ + break; + } +} + +NautilusGtkBookmarksManager * +_nautilus_gtk_bookmarks_manager_new (GtkBookmarksChangedFunc changed_func, gpointer changed_func_data) +{ + NautilusGtkBookmarksManager *manager; + GFile *bookmarks_file; + GError *error; + + manager = g_new0 (NautilusGtkBookmarksManager, 1); + + manager->changed_func = changed_func; + manager->changed_func_data = changed_func_data; + + manager->cancellable = g_cancellable_new (); + + bookmarks_file = get_bookmarks_file (); + if (!g_file_query_exists (bookmarks_file, NULL)) + { + GFile *legacy_bookmarks_file; + + /* Read the legacy one and write it to the new one */ + legacy_bookmarks_file = get_legacy_bookmarks_file (); + manager->bookmarks = read_bookmarks (legacy_bookmarks_file); + if (manager->bookmarks) + save_bookmarks (bookmarks_file, manager->bookmarks); + + g_object_unref (legacy_bookmarks_file); + } + else + g_file_load_contents_async (bookmarks_file, manager->cancellable, read_bookmarks_finish, manager); + + error = NULL; + manager->bookmarks_monitor = g_file_monitor_file (bookmarks_file, + G_FILE_MONITOR_NONE, + NULL, &error); + if (error) + { + g_warning ("%s", error->message); + g_error_free (error); + } + else + manager->bookmarks_monitor_changed_id = g_signal_connect (manager->bookmarks_monitor, "changed", + G_CALLBACK (bookmarks_file_changed), manager); + + + g_object_unref (bookmarks_file); + + return manager; +} + +void +_nautilus_gtk_bookmarks_manager_free (NautilusGtkBookmarksManager *manager) +{ + g_return_if_fail (manager != NULL); + + g_cancellable_cancel (manager->cancellable); + g_object_unref (manager->cancellable); + + if (manager->bookmarks_monitor) + { + g_file_monitor_cancel (manager->bookmarks_monitor); + g_signal_handler_disconnect (manager->bookmarks_monitor, manager->bookmarks_monitor_changed_id); + manager->bookmarks_monitor_changed_id = 0; + g_object_unref (manager->bookmarks_monitor); + } + + g_slist_free_full (manager->bookmarks, _gtk_bookmark_free); + + g_free (manager); +} + +GSList * +_nautilus_gtk_bookmarks_manager_list_bookmarks (NautilusGtkBookmarksManager *manager) +{ + GSList *bookmarks, *files = NULL; + + g_return_val_if_fail (manager != NULL, NULL); + + bookmarks = manager->bookmarks; + + while (bookmarks) + { + GtkBookmark *bookmark; + + bookmark = bookmarks->data; + bookmarks = bookmarks->next; + + files = g_slist_prepend (files, g_object_ref (bookmark->file)); + } + + return g_slist_reverse (files); +} + +static GSList * +find_bookmark_link_for_file (GSList *bookmarks, GFile *file, int *position_ret) +{ + int pos; + + pos = 0; + for (; bookmarks; bookmarks = bookmarks->next) + { + GtkBookmark *bookmark = bookmarks->data; + + if (g_file_equal (file, bookmark->file)) + { + if (position_ret) + *position_ret = pos; + + return bookmarks; + } + + pos++; + } + + if (position_ret) + *position_ret = -1; + + return NULL; +} + +gboolean +_nautilus_gtk_bookmarks_manager_has_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file) +{ + GSList *link; + + link = find_bookmark_link_for_file (manager->bookmarks, file, NULL); + return (link != NULL); +} + +gboolean +_nautilus_gtk_bookmarks_manager_insert_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file, + int position, + GError **error) +{ + GSList *link; + GtkBookmark *bookmark; + GFile *bookmarks_file; + + g_return_val_if_fail (manager != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + link = find_bookmark_link_for_file (manager->bookmarks, file, NULL); + + if (link) + { + char *uri; + bookmark = link->data; + uri = g_file_get_uri (bookmark->file); + + g_set_error (error, + GTK_FILE_CHOOSER_ERROR, + GTK_FILE_CHOOSER_ERROR_ALREADY_EXISTS, + _("%s already exists in the bookmarks list"), + uri); + + g_free (uri); + + return FALSE; + } + + bookmark = g_slice_new0 (GtkBookmark); + bookmark->file = g_object_ref (file); + + manager->bookmarks = g_slist_insert (manager->bookmarks, bookmark, position); + + bookmarks_file = get_bookmarks_file (); + save_bookmarks (bookmarks_file, manager->bookmarks); + g_object_unref (bookmarks_file); + + notify_changed (manager); + + return TRUE; +} + +gboolean +_nautilus_gtk_bookmarks_manager_remove_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file, + GError **error) +{ + GSList *link; + GFile *bookmarks_file; + + g_return_val_if_fail (manager != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + if (!manager->bookmarks) + return FALSE; + + link = find_bookmark_link_for_file (manager->bookmarks, file, NULL); + if (link) + { + GtkBookmark *bookmark = link->data; + + manager->bookmarks = g_slist_remove_link (manager->bookmarks, link); + _gtk_bookmark_free (bookmark); + g_slist_free_1 (link); + } + else + { + set_error_bookmark_doesnt_exist (file, error); + return FALSE; + } + + bookmarks_file = get_bookmarks_file (); + save_bookmarks (bookmarks_file, manager->bookmarks); + g_object_unref (bookmarks_file); + + notify_changed (manager); + + return TRUE; +} + +gboolean +_nautilus_gtk_bookmarks_manager_reorder_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file, + int new_position, + GError **error) +{ + GSList *link; + GFile *bookmarks_file; + int old_position; + + g_return_val_if_fail (manager != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + g_return_val_if_fail (new_position >= 0, FALSE); + + if (!manager->bookmarks) + return FALSE; + + link = find_bookmark_link_for_file (manager->bookmarks, file, &old_position); + if (new_position == old_position) + return TRUE; + + if (link) + { + GtkBookmark *bookmark = link->data; + + manager->bookmarks = g_slist_remove_link (manager->bookmarks, link); + g_slist_free_1 (link); + + if (new_position > old_position) + new_position--; + + manager->bookmarks = g_slist_insert (manager->bookmarks, bookmark, new_position); + } + else + { + set_error_bookmark_doesnt_exist (file, error); + return FALSE; + } + + bookmarks_file = get_bookmarks_file (); + save_bookmarks (bookmarks_file, manager->bookmarks); + g_object_unref (bookmarks_file); + + notify_changed (manager); + + return TRUE; +} + +char * +_nautilus_gtk_bookmarks_manager_get_bookmark_label (NautilusGtkBookmarksManager *manager, + GFile *file) +{ + GSList *bookmarks; + char *label = NULL; + + g_return_val_if_fail (manager != NULL, NULL); + g_return_val_if_fail (file != NULL, NULL); + + bookmarks = manager->bookmarks; + + while (bookmarks) + { + GtkBookmark *bookmark; + + bookmark = bookmarks->data; + bookmarks = bookmarks->next; + + if (g_file_equal (file, bookmark->file)) + { + label = g_strdup (bookmark->label); + break; + } + } + + return label; +} + +gboolean +_nautilus_gtk_bookmarks_manager_set_bookmark_label (NautilusGtkBookmarksManager *manager, + GFile *file, + const char *label, + GError **error) +{ + GFile *bookmarks_file; + GSList *link; + + g_return_val_if_fail (manager != NULL, FALSE); + g_return_val_if_fail (file != NULL, FALSE); + + link = find_bookmark_link_for_file (manager->bookmarks, file, NULL); + if (link) + { + GtkBookmark *bookmark = link->data; + + g_free (bookmark->label); + bookmark->label = g_strdup (label); + } + else + { + set_error_bookmark_doesnt_exist (file, error); + return FALSE; + } + + bookmarks_file = get_bookmarks_file (); + save_bookmarks (bookmarks_file, manager->bookmarks); + g_object_unref (bookmarks_file); + + notify_changed (manager); + + return TRUE; +} + +static gboolean +_nautilus_gtk_bookmarks_manager_get_xdg_type (NautilusGtkBookmarksManager *manager, + GFile *file, + GUserDirectory *directory) +{ + GSList *link; + gboolean match; + GFile *location; + const char *path; + GUserDirectory dir; + GtkBookmark *bookmark; + + link = find_bookmark_link_for_file (manager->bookmarks, file, NULL); + if (!link) + return FALSE; + + match = FALSE; + bookmark = link->data; + + for (dir = 0; dir < G_USER_N_DIRECTORIES; dir++) + { + path = g_get_user_special_dir (dir); + if (!path) + continue; + + location = g_file_new_for_path (path); + match = g_file_equal (location, bookmark->file); + g_object_unref (location); + + if (match) + break; + } + + if (match && directory != NULL) + *directory = dir; + + return match; +} + +gboolean +_nautilus_gtk_bookmarks_manager_get_is_builtin (NautilusGtkBookmarksManager *manager, + GFile *file) +{ + GUserDirectory xdg_type; + + /* if this is not an XDG dir, it's never builtin */ + if (!_nautilus_gtk_bookmarks_manager_get_xdg_type (manager, file, &xdg_type)) + return FALSE; + + /* exclude XDG locations we don't display by default */ + return _nautilus_gtk_bookmarks_manager_get_is_xdg_dir_builtin (xdg_type); +} + +gboolean +_nautilus_gtk_bookmarks_manager_get_is_xdg_dir_builtin (GUserDirectory xdg_type) +{ + return (xdg_type != G_USER_DIRECTORY_DESKTOP) && + (xdg_type != G_USER_DIRECTORY_TEMPLATES) && + (xdg_type != G_USER_DIRECTORY_PUBLIC_SHARE); +} diff --git a/src/gtk/nautilusgtkbookmarksmanagerprivate.h b/src/gtk/nautilusgtkbookmarksmanagerprivate.h new file mode 100644 index 0000000..99890a3 --- /dev/null +++ b/src/gtk/nautilusgtkbookmarksmanagerprivate.h @@ -0,0 +1,89 @@ +/* -*- Mode: C; c-file-style: "gnu"; tab-width: 8 -*- */ +/* GTK - The GIMP Toolkit + * nautilusgtkbookmarksmanager.h: Utilities to manage and monitor ~/.gtk-bookmarks + * Copyright (C) 2003, Red Hat, Inc. + * Copyright (C) 2007-2008 Carlos Garnacho + * Copyright (C) 2011 Suse + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Federico Mena Quintero <federico@gnome.org> + */ + +#ifndef __NAUTILUS_GTK_BOOKMARKS_MANAGER_H__ +#define __NAUTILUS_GTK_BOOKMARKS_MANAGER_H__ + +#include <gio/gio.h> + +typedef void (* GtkBookmarksChangedFunc) (gpointer data); + +typedef struct +{ + /* This list contains GtkBookmark structs */ + GSList *bookmarks; + + GFileMonitor *bookmarks_monitor; + gulong bookmarks_monitor_changed_id; + + gpointer changed_func_data; + GtkBookmarksChangedFunc changed_func; + + GCancellable *cancellable; +} NautilusGtkBookmarksManager; + +typedef struct +{ + GFile *file; + char *label; +} GtkBookmark; + +NautilusGtkBookmarksManager *_nautilus_gtk_bookmarks_manager_new (GtkBookmarksChangedFunc changed_func, + gpointer changed_func_data); + + +void _nautilus_gtk_bookmarks_manager_free (NautilusGtkBookmarksManager *manager); + +GSList *_nautilus_gtk_bookmarks_manager_list_bookmarks (NautilusGtkBookmarksManager *manager); + +gboolean _nautilus_gtk_bookmarks_manager_insert_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file, + int position, + GError **error); + +gboolean _nautilus_gtk_bookmarks_manager_remove_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file, + GError **error); + +gboolean _nautilus_gtk_bookmarks_manager_reorder_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file, + int new_position, + GError **error); + +gboolean _nautilus_gtk_bookmarks_manager_has_bookmark (NautilusGtkBookmarksManager *manager, + GFile *file); + +char * _nautilus_gtk_bookmarks_manager_get_bookmark_label (NautilusGtkBookmarksManager *manager, + GFile *file); + +gboolean _nautilus_gtk_bookmarks_manager_set_bookmark_label (NautilusGtkBookmarksManager *manager, + GFile *file, + const char *label, + GError **error); + +gboolean _nautilus_gtk_bookmarks_manager_get_is_builtin (NautilusGtkBookmarksManager *manager, + GFile *file); + +gboolean _nautilus_gtk_bookmarks_manager_get_is_xdg_dir_builtin (GUserDirectory xdg_type); + +#endif /* __NAUTILUS_GTK_BOOKMARKS_MANAGER_H__ */ diff --git a/src/gtk/nautilusgtkplacessidebar.c b/src/gtk/nautilusgtkplacessidebar.c new file mode 100644 index 0000000..c536e89 --- /dev/null +++ b/src/gtk/nautilusgtkplacessidebar.c @@ -0,0 +1,5142 @@ +/* NautilusGtkPlacesSidebar - sidebar widget for places in the filesystem + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * This code is originally from Nautilus. + * + * Authors : Mr Jamie McCracken (jamiemcc at blueyonder dot co dot uk) + * Cosimo Cecchi <cosimoc@gnome.org> + * Federico Mena Quintero <federico@gnome.org> + * Carlos Soriano <csoriano@gnome.org> + */ + +#include "config.h" +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include "nautilus-enum-types.h" + +#include <gio/gio.h> +#include <cloudproviders.h> + +#include "nautilusgtkplacessidebarprivate.h" +#include "nautilusgtksidebarrowprivate.h" +#include "gdk/gdkkeysyms.h" +#include "nautilusgtkbookmarksmanagerprivate.h" +#include "nautilus-dnd.h" +#include "nautilus-dbus-launcher.h" +#include "nautilus-file.h" +#include "nautilus-file-operations.h" +#include "nautilus-global-preferences.h" +#include "nautilus-properties-window.h" +#include "nautilus-trash-monitor.h" + +#ifdef GDK_WINDOWING_X11 +#include <gdk/x11/gdkx.h> +#endif + +#pragma GCC diagnostic ignored "-Wshadow" + +/*< private > + * NautilusGtkPlacesSidebar: + * + * NautilusGtkPlacesSidebar is a widget that displays a list of frequently-used places in the + * file system: the user’s home directory, the user’s bookmarks, and volumes and drives. + * This widget is used as a sidebar in GtkFileChooser and may be used by file managers + * and similar programs. + * + * The places sidebar displays drives and volumes, and will automatically mount + * or unmount them when the user selects them. + * + * Applications can hook to various signals in the places sidebar to customize + * its behavior. For example, they can add extra commands to the context menu + * of the sidebar. + * + * While bookmarks are completely in control of the user, the places sidebar also + * allows individual applications to provide extra shortcut folders that are unique + * to each application. For example, a Paint program may want to add a shortcut + * for a Clipart folder. You can do this with nautilus_gtk_places_sidebar_add_shortcut(). + * + * To make use of the places sidebar, an application at least needs to connect + * to the NautilusGtkPlacesSidebar::open-location signal. This is emitted when the + * user selects in the sidebar a location to open. The application should also + * call nautilus_gtk_places_sidebar_set_location() when it changes the currently-viewed + * location. + * + * # CSS nodes + * + * NautilusGtkPlacesSidebar uses a single CSS node with name placessidebar and style + * class .sidebar. + * + * Among the children of the places sidebar, the following style classes can + * be used: + * - .sidebar-new-bookmark-row for the 'Add new bookmark' row + * - .sidebar-placeholder-row for a row that is a placeholder + * - .has-open-popup when a popup is open for a row + */ + +/* These are used when a destination-side DND operation is taking place. + * Normally, when a common drag action is taking place, the state will be + * DROP_STATE_NEW_BOOKMARK_ARMED, however, if the client of NautilusGtkPlacesSidebar + * wants to show hints about the valid targets, we sill set it as + * DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT, so the sidebar will show drop hints + * until the client says otherwise + */ +typedef enum { + DROP_STATE_NORMAL, + DROP_STATE_NEW_BOOKMARK_ARMED, + DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT, +} DropState; + +struct _NautilusGtkPlacesSidebar { + GtkWidget parent; + + GtkWidget *swin; + GtkWidget *list_box; + GtkWidget *new_bookmark_row; + + NautilusGtkBookmarksManager *bookmarks_manager; + + GActionGroup *row_actions; + + CloudProvidersCollector *cloud_manager; + GList *unready_accounts; + + GVolumeMonitor *volume_monitor; + GtkSettings *gtk_settings; + GFile *current_location; + + GtkWidget *rename_popover; + GtkWidget *rename_entry; + GtkWidget *rename_button; + GtkWidget *rename_error; + char *rename_uri; + + GtkWidget *trash_row; + + /* DND */ + gboolean dragging_over; + GtkWidget *drag_row; + int drag_row_height; + int drag_row_x; + int drag_row_y; + GtkWidget *row_placeholder; + DropState drop_state; + guint hover_timer_id; + graphene_point_t hover_start_point; + GtkListBoxRow *hover_row; + + /* volume mounting - delayed open process */ + NautilusGtkPlacesOpenFlags go_to_after_mount_open_flags; + GCancellable *cancellable; + + GtkWidget *popover; + NautilusGtkSidebarRow *context_row; + GListStore *shortcuts; + + GDBusProxy *hostnamed_proxy; + GCancellable *hostnamed_cancellable; + char *hostname; + + NautilusGtkPlacesOpenFlags open_flags; + + guint mounting : 1; + guint show_recent_set : 1; + guint show_recent : 1; + guint show_desktop_set : 1; + guint show_desktop : 1; + guint show_enter_location : 1; + guint show_other_locations : 1; + guint show_trash : 1; + guint show_starred_location : 1; +}; + +struct _NautilusGtkPlacesSidebarClass { + GtkWidgetClass parent_class; + + void (* open_location) (NautilusGtkPlacesSidebar *sidebar, + GFile *location, + NautilusGtkPlacesOpenFlags open_flags); + void (* show_error_message) (NautilusGtkPlacesSidebar *sidebar, + const char *primary, + const char *secondary); + GdkDragAction (* drag_action_requested) (NautilusGtkPlacesSidebar *sidebar, + GFile *dest_file, + GSList *source_file_list); + GdkDragAction (* drag_action_ask) (NautilusGtkPlacesSidebar *sidebar, + GdkDragAction actions); + void (* drag_perform_drop) (NautilusGtkPlacesSidebar *sidebar, + GFile *dest_file, + GList *source_file_list, + GdkDragAction action); + void (* show_enter_location) (NautilusGtkPlacesSidebar *sidebar); + + void (* show_other_locations_with_flags) (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkPlacesOpenFlags open_flags); + + void (* show_starred_location) (NautilusGtkPlacesSidebar *sidebar); + + void (* mount) (NautilusGtkPlacesSidebar *sidebar, + GMountOperation *mount_operation); + void (* unmount) (NautilusGtkPlacesSidebar *sidebar, + GMountOperation *unmount_operation); +}; + +enum { + OPEN_LOCATION, + SHOW_ERROR_MESSAGE, + SHOW_ENTER_LOCATION, + DRAG_ACTION_REQUESTED, + DRAG_ACTION_ASK, + DRAG_PERFORM_DROP, + SHOW_OTHER_LOCATIONS_WITH_FLAGS, + SHOW_STARRED_LOCATION, + MOUNT, + UNMOUNT, + LAST_SIGNAL +}; + +enum { + PROP_LOCATION = 1, + PROP_OPEN_FLAGS, + PROP_SHOW_RECENT, + PROP_SHOW_DESKTOP, + PROP_SHOW_ENTER_LOCATION, + PROP_SHOW_TRASH, + PROP_SHOW_STARRED_LOCATION, + PROP_SHOW_OTHER_LOCATIONS, + NUM_PROPERTIES +}; + +/* Names for themed icons */ +#define ICON_NAME_HOME "user-home-symbolic" +#define ICON_NAME_DESKTOP "user-desktop-symbolic" +#define ICON_NAME_FILESYSTEM "drive-harddisk-symbolic" +#define ICON_NAME_EJECT "media-eject-symbolic" +#define ICON_NAME_NETWORK "network-workgroup-symbolic" +#define ICON_NAME_NETWORK_SERVER "network-server-symbolic" +#define ICON_NAME_FOLDER_NETWORK "folder-remote-symbolic" +#define ICON_NAME_OTHER_LOCATIONS "list-add-symbolic" + +#define ICON_NAME_FOLDER "folder-symbolic" +#define ICON_NAME_FOLDER_DESKTOP "user-desktop-symbolic" +#define ICON_NAME_FOLDER_DOCUMENTS "folder-documents-symbolic" +#define ICON_NAME_FOLDER_DOWNLOAD "folder-download-symbolic" +#define ICON_NAME_FOLDER_MUSIC "folder-music-symbolic" +#define ICON_NAME_FOLDER_PICTURES "folder-pictures-symbolic" +#define ICON_NAME_FOLDER_PUBLIC_SHARE "folder-publicshare-symbolic" +#define ICON_NAME_FOLDER_TEMPLATES "folder-templates-symbolic" +#define ICON_NAME_FOLDER_VIDEOS "folder-videos-symbolic" +#define ICON_NAME_FOLDER_SAVED_SEARCH "folder-saved-search-symbolic" + +static guint places_sidebar_signals [LAST_SIGNAL] = { 0 }; +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +static gboolean eject_or_unmount_bookmark (NautilusGtkSidebarRow *row); +static gboolean eject_or_unmount_selection (NautilusGtkPlacesSidebar *sidebar); +static void check_unmount_and_eject (GMount *mount, + GVolume *volume, + GDrive *drive, + gboolean *show_unmount, + gboolean *show_eject); +static void on_row_pressed (GtkGestureClick *gesture, + int n_press, + double x, + double y, + NautilusGtkSidebarRow *row); +static void on_row_released (GtkGestureClick *gesture, + int n_press, + double x, + double y, + NautilusGtkSidebarRow *row); +static void on_row_dragged (GtkGestureDrag *gesture, + double x, + double y, + NautilusGtkSidebarRow *row); + +static void popup_menu_cb (NautilusGtkSidebarRow *row); +static void long_press_cb (GtkGesture *gesture, + double x, + double y, + NautilusGtkPlacesSidebar *sidebar); +static void stop_drop_feedback (NautilusGtkPlacesSidebar *sidebar); +static GMountOperation * get_mount_operation (NautilusGtkPlacesSidebar *sidebar); +static GMountOperation * get_unmount_operation (NautilusGtkPlacesSidebar *sidebar); + + +G_DEFINE_TYPE (NautilusGtkPlacesSidebar, nautilus_gtk_places_sidebar, GTK_TYPE_WIDGET); + +static void +emit_open_location (NautilusGtkPlacesSidebar *sidebar, + GFile *location, + NautilusGtkPlacesOpenFlags open_flags) +{ + if ((open_flags & sidebar->open_flags) == 0) + open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + + g_signal_emit (sidebar, places_sidebar_signals[OPEN_LOCATION], 0, + location, open_flags); +} + +static void +emit_show_error_message (NautilusGtkPlacesSidebar *sidebar, + const char *primary, + const char *secondary) +{ + g_signal_emit (sidebar, places_sidebar_signals[SHOW_ERROR_MESSAGE], 0, + primary, secondary); +} + +static void +emit_show_enter_location (NautilusGtkPlacesSidebar *sidebar) +{ + g_signal_emit (sidebar, places_sidebar_signals[SHOW_ENTER_LOCATION], 0); +} + +static void +emit_show_other_locations_with_flags (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkPlacesOpenFlags open_flags) +{ + g_signal_emit (sidebar, places_sidebar_signals[SHOW_OTHER_LOCATIONS_WITH_FLAGS], + 0, open_flags); +} + +static void +emit_show_starred_location (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkPlacesOpenFlags open_flags) +{ + g_signal_emit (sidebar, places_sidebar_signals[SHOW_STARRED_LOCATION], 0, + open_flags); +} + + +static void +emit_mount_operation (NautilusGtkPlacesSidebar *sidebar, + GMountOperation *mount_op) +{ + g_signal_emit (sidebar, places_sidebar_signals[MOUNT], 0, mount_op); +} + +static void +emit_unmount_operation (NautilusGtkPlacesSidebar *sidebar, + GMountOperation *mount_op) +{ + g_signal_emit (sidebar, places_sidebar_signals[UNMOUNT], 0, mount_op); +} + +static GdkDragAction +emit_drag_action_requested (NautilusGtkPlacesSidebar *sidebar, + NautilusFile *dest_file, + GSList *source_file_list) +{ + GdkDragAction ret_action = 0; + + g_signal_emit (sidebar, places_sidebar_signals[DRAG_ACTION_REQUESTED], 0, + dest_file, source_file_list, &ret_action); + + return ret_action; +} + +static void +emit_drag_perform_drop (NautilusGtkPlacesSidebar *sidebar, + GFile *dest_file, + GSList *source_file_list, + GdkDragAction action) +{ + g_signal_emit (sidebar, places_sidebar_signals[DRAG_PERFORM_DROP], 0, + dest_file, source_file_list, action); +} +static void +list_box_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + NautilusGtkPlacesSectionType row_section_type; + NautilusGtkPlacesSectionType before_section_type; + GtkWidget *separator; + + gtk_list_box_row_set_header (row, NULL); + + g_object_get (row, "section-type", &row_section_type, NULL); + if (before) + { + g_object_get (before, "section-type", &before_section_type, NULL); + } + else + { + before_section_type = NAUTILUS_GTK_PLACES_SECTION_INVALID; + } + + if (before && before_section_type != row_section_type) + { + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_list_box_row_set_header (row, separator); + } +} + +static GtkWidget* +add_place (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkPlacesPlaceType place_type, + NautilusGtkPlacesSectionType section_type, + const char *name, + GIcon *start_icon, + GIcon *end_icon, + const char *uri, + GDrive *drive, + GVolume *volume, + GMount *mount, + CloudProvidersAccount *cloud_provider_account, + const int index, + const char *tooltip) +{ + gboolean show_eject, show_unmount; + gboolean show_eject_button; + GtkWidget *row; + GtkWidget *eject_button; + GtkGesture *gesture; + char *eject_tooltip; + + check_unmount_and_eject (mount, volume, drive, + &show_unmount, &show_eject); + + if (show_unmount || show_eject) + g_assert (place_type != NAUTILUS_GTK_PLACES_BOOKMARK); + + show_eject_button = (show_unmount || show_eject); + if (mount != NULL && volume == NULL && drive == NULL) + eject_tooltip = _("Disconnect"); + else if (show_eject) + eject_tooltip = _("Eject"); + else + eject_tooltip = _("Unmount"); + + row = g_object_new (NAUTILUS_TYPE_GTK_SIDEBAR_ROW, + "sidebar", sidebar, + "start-icon", start_icon, + "end-icon", end_icon, + "label", name, + "tooltip", tooltip, + "ejectable", show_eject_button, + "eject-tooltip", eject_tooltip, + "order-index", index, + "section-type", section_type, + "place-type", place_type, + "uri", uri, + "drive", drive, + "volume", volume, + "mount", mount, + "cloud-provider-account", cloud_provider_account, + NULL); + + eject_button = nautilus_gtk_sidebar_row_get_eject_button (NAUTILUS_GTK_SIDEBAR_ROW (row)); + + g_signal_connect_swapped (eject_button, "clicked", + G_CALLBACK (eject_or_unmount_bookmark), row); + + gesture = gtk_gesture_click_new (); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 0); + g_signal_connect (gesture, "pressed", + G_CALLBACK (on_row_pressed), row); + g_signal_connect (gesture, "released", + G_CALLBACK (on_row_released), row); + gtk_widget_add_controller (row, GTK_EVENT_CONTROLLER (gesture)); + + gesture = gtk_gesture_drag_new (); + g_signal_connect (gesture, "drag-update", + G_CALLBACK (on_row_dragged), row); + gtk_widget_add_controller (row, GTK_EVENT_CONTROLLER (gesture)); + + gtk_list_box_insert (GTK_LIST_BOX (sidebar->list_box), GTK_WIDGET (row), -1); + + return row; +} + +static GIcon * +special_directory_get_gicon (GUserDirectory directory) +{ +#define ICON_CASE(x) \ + case G_USER_DIRECTORY_ ## x: \ + return g_themed_icon_new_with_default_fallbacks (ICON_NAME_FOLDER_ ## x); + + switch (directory) + { + + ICON_CASE (DESKTOP); + ICON_CASE (DOCUMENTS); + ICON_CASE (DOWNLOAD); + ICON_CASE (MUSIC); + ICON_CASE (PICTURES); + ICON_CASE (PUBLIC_SHARE); + ICON_CASE (TEMPLATES); + ICON_CASE (VIDEOS); + + case G_USER_N_DIRECTORIES: + default: + return g_themed_icon_new_with_default_fallbacks (ICON_NAME_FOLDER); + } + +#undef ICON_CASE +} + +static gboolean +recent_files_setting_is_enabled (NautilusGtkPlacesSidebar *sidebar) +{ + GtkSettings *settings; + gboolean enabled; + + settings = gtk_widget_get_settings (GTK_WIDGET (sidebar)); + g_object_get (settings, "gtk-recent-files-enabled", &enabled, NULL); + + return enabled; +} + +static gboolean +recent_scheme_is_supported (void) +{ + const char * const *supported; + + supported = g_vfs_get_supported_uri_schemes (g_vfs_get_default ()); + if (supported != NULL) + return g_strv_contains (supported, "recent"); + + return FALSE; +} + +static gboolean +should_show_recent (NautilusGtkPlacesSidebar *sidebar) +{ + return recent_files_setting_is_enabled (sidebar) && + ((sidebar->show_recent_set && sidebar->show_recent) || + (!sidebar->show_recent_set && recent_scheme_is_supported ())); +} + +static gboolean +path_is_home_dir (const char *path) +{ + GFile *home_dir; + GFile *location; + const char *home_path; + gboolean res; + + home_path = g_get_home_dir (); + if (!home_path) + return FALSE; + + home_dir = g_file_new_for_path (home_path); + location = g_file_new_for_path (path); + res = g_file_equal (home_dir, location); + + g_object_unref (home_dir); + g_object_unref (location); + + return res; +} + +static void +open_home (NautilusGtkPlacesSidebar *sidebar) +{ + const char *home_path; + GFile *home_dir; + + home_path = g_get_home_dir (); + if (!home_path) + return; + + home_dir = g_file_new_for_path (home_path); + emit_open_location (sidebar, home_dir, 0); + + g_object_unref (home_dir); +} + +static void +add_special_dirs (NautilusGtkPlacesSidebar *sidebar) +{ + GList *dirs; + int index; + + dirs = NULL; + for (index = 0; index < G_USER_N_DIRECTORIES; index++) + { + const char *path; + GFile *root; + GIcon *start_icon; + char *name; + char *mount_uri; + char *tooltip; + + if (!_nautilus_gtk_bookmarks_manager_get_is_xdg_dir_builtin (index)) + continue; + + path = g_get_user_special_dir (index); + + /* XDG resets special dirs to the home directory in case + * it's not finiding what it expects. We don't want the home + * to be added multiple times in that weird configuration. + */ + if (path == NULL || + path_is_home_dir (path) || + g_list_find_custom (dirs, path, (GCompareFunc) g_strcmp0) != NULL) + continue; + + root = g_file_new_for_path (path); + + name = _nautilus_gtk_bookmarks_manager_get_bookmark_label (sidebar->bookmarks_manager, root); + if (!name) + name = g_file_get_basename (root); + + start_icon = special_directory_get_gicon (index); + mount_uri = g_file_get_uri (root); + tooltip = g_file_get_parse_name (root); + + add_place (sidebar, NAUTILUS_GTK_PLACES_XDG_DIR, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + name, start_icon, NULL, mount_uri, + NULL, NULL, NULL, NULL, 0, + tooltip); + g_free (name); + g_object_unref (root); + g_object_unref (start_icon); + g_free (mount_uri); + g_free (tooltip); + + dirs = g_list_prepend (dirs, (char *)path); + } + + g_list_free (dirs); +} + +static char * +get_home_directory_uri (void) +{ + const char *home; + + home = g_get_home_dir (); + if (!home) + return NULL; + + return g_filename_to_uri (home, NULL, NULL); +} + +static char * +get_desktop_directory_uri (void) +{ + const char *name; + + name = g_get_user_special_dir (G_USER_DIRECTORY_DESKTOP); + + /* "To disable a directory, point it to the homedir." + * See http://freedesktop.org/wiki/Software/xdg-user-dirs + */ + if (path_is_home_dir (name)) + return NULL; + + return g_filename_to_uri (name, NULL, NULL); +} + +static gboolean +file_is_shown (NautilusGtkPlacesSidebar *sidebar, + GFile *file) +{ + char *uri; + GtkWidget *row; + gboolean found = FALSE; + + for (row = gtk_widget_get_first_child (GTK_WIDGET (sidebar->list_box)); + row != NULL && !found; + row = gtk_widget_get_next_sibling (row)) + { + if (!GTK_IS_LIST_BOX_ROW (row)) + continue; + + g_object_get (row, "uri", &uri, NULL); + if (uri) + { + GFile *other; + other = g_file_new_for_uri (uri); + found = g_file_equal (file, other); + g_object_unref (other); + g_free (uri); + } + } + + return found; +} + +typedef struct +{ + NautilusGtkPlacesSidebar *sidebar; + guint position; +} ShortcutData; + +static void +on_app_shortcuts_query_complete (GObject *source, + GAsyncResult *result, + gpointer data) +{ + ShortcutData *sdata = data; + NautilusGtkPlacesSidebar *sidebar = sdata->sidebar; + guint pos = sdata->position; + GFile *file = G_FILE (source); + GFileInfo *info; + + g_free (sdata); + + info = g_file_query_info_finish (file, result, NULL); + + if (info) + { + char *uri; + char *tooltip; + const char *name; + GIcon *start_icon; + + name = g_file_info_get_display_name (info); + start_icon = g_file_info_get_symbolic_icon (info); + uri = g_file_get_uri (file); + tooltip = g_file_get_parse_name (file); + + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + name, start_icon, NULL, uri, + NULL, NULL, NULL, NULL, + pos, + tooltip); + + g_free (uri); + g_free (tooltip); + + g_object_unref (info); + } +} + +static void +add_application_shortcuts (NautilusGtkPlacesSidebar *sidebar) +{ + guint i, n; + + n = g_list_model_get_n_items (G_LIST_MODEL (sidebar->shortcuts)); + for (i = 0; i < n; i++) + { + GFile *file = g_list_model_get_item (G_LIST_MODEL (sidebar->shortcuts), i); + ShortcutData *data; + + g_object_unref (file); + + if (file_is_shown (sidebar, file)) + continue; + + data = g_new (ShortcutData, 1); + data->sidebar = sidebar; + data->position = i; + g_file_query_info_async (file, + "standard::display-name,standard::symbolic-icon", + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + sidebar->cancellable, + on_app_shortcuts_query_complete, + data); + } +} + +typedef struct { + NautilusGtkPlacesSidebar *sidebar; + int index; + gboolean is_native; +} BookmarkQueryClosure; + +static void +on_bookmark_query_info_complete (GObject *source, + GAsyncResult *result, + gpointer data) +{ + BookmarkQueryClosure *clos = data; + NautilusGtkPlacesSidebar *sidebar = clos->sidebar; + GFile *root = G_FILE (source); + GError *error = NULL; + GFileInfo *info; + char *bookmark_name; + char *mount_uri; + char *tooltip; + GIcon *start_icon; + + info = g_file_query_info_finish (root, result, &error); + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + goto out; + + bookmark_name = _nautilus_gtk_bookmarks_manager_get_bookmark_label (sidebar->bookmarks_manager, root); + if (bookmark_name == NULL && info != NULL) + bookmark_name = g_strdup (g_file_info_get_display_name (info)); + else if (bookmark_name == NULL) + { + /* Don't add non-UTF-8 bookmarks */ + bookmark_name = g_file_get_basename (root); + if (bookmark_name == NULL) + goto out; + + if (!g_utf8_validate (bookmark_name, -1, NULL)) + { + g_free (bookmark_name); + goto out; + } + } + + if (info) + start_icon = g_object_ref (g_file_info_get_symbolic_icon (info)); + else + start_icon = g_themed_icon_new_with_default_fallbacks (clos->is_native ? ICON_NAME_FOLDER : ICON_NAME_FOLDER_NETWORK); + + mount_uri = g_file_get_uri (root); + tooltip = clos->is_native ? g_file_get_path (root) : g_uri_unescape_string (mount_uri, NULL); + + add_place (sidebar, NAUTILUS_GTK_PLACES_BOOKMARK, + NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS, + bookmark_name, start_icon, NULL, mount_uri, + NULL, NULL, NULL, NULL, clos->index, + tooltip); + + g_free (mount_uri); + g_free (tooltip); + g_free (bookmark_name); + g_object_unref (start_icon); + +out: + g_clear_object (&info); + g_clear_error (&error); + g_slice_free (BookmarkQueryClosure, clos); +} + +static gboolean +is_external_volume (GVolume *volume) +{ + gboolean is_external; + GDrive *drive; + char *id; + + drive = g_volume_get_drive (volume); + id = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS); + + is_external = g_volume_can_eject (volume); + + /* NULL volume identifier only happens on removable devices */ + is_external |= !id; + + if (drive) + is_external |= g_drive_is_removable (drive); + + g_clear_object (&drive); + g_free (id); + + return is_external; +} + +static void +update_trash_icon (NautilusGtkPlacesSidebar *sidebar) +{ + if (sidebar->trash_row) + { + GIcon *icon; + + icon = nautilus_trash_monitor_get_symbolic_icon (); + nautilus_gtk_sidebar_row_set_start_icon (NAUTILUS_GTK_SIDEBAR_ROW (sidebar->trash_row), icon); + g_object_unref (icon); + } +} + +static gboolean +create_cloud_provider_account_row (NautilusGtkPlacesSidebar *sidebar, + CloudProvidersAccount *account) +{ + GIcon *end_icon; + GIcon *start_icon; + const char *mount_path; + const char *name; + char *mount_uri; + char *tooltip; + guint provider_account_status; + + start_icon = cloud_providers_account_get_icon (account); + name = cloud_providers_account_get_name (account); + provider_account_status = cloud_providers_account_get_status (account); + mount_path = cloud_providers_account_get_path (account); + if (start_icon != NULL + && name != NULL + && provider_account_status != CLOUD_PROVIDERS_ACCOUNT_STATUS_INVALID + && mount_path != NULL) + { + switch (provider_account_status) + { + case CLOUD_PROVIDERS_ACCOUNT_STATUS_IDLE: + end_icon = NULL; + break; + + case CLOUD_PROVIDERS_ACCOUNT_STATUS_SYNCING: + end_icon = g_themed_icon_new ("emblem-synchronizing-symbolic"); + break; + + case CLOUD_PROVIDERS_ACCOUNT_STATUS_ERROR: + end_icon = g_themed_icon_new ("dialog-warning-symbolic"); + break; + + default: + return FALSE; + } + + mount_uri = g_strconcat ("file://", mount_path, NULL); + + /* translators: %s is the name of a cloud provider for files */ + tooltip = g_strdup_printf (_("Open %s"), name); + + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_CLOUD, + name, start_icon, end_icon, mount_uri, + NULL, NULL, NULL, account, 0, + tooltip); + + g_free (tooltip); + g_free (mount_uri); + g_clear_object (&end_icon); + return TRUE; + } + else + { + return FALSE; + } +} + +static void +on_account_updated (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + CloudProvidersAccount *account = CLOUD_PROVIDERS_ACCOUNT (object); + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (user_data); + + if (create_cloud_provider_account_row (sidebar, account)) + { + g_signal_handlers_disconnect_by_data (account, sidebar); + sidebar->unready_accounts = g_list_remove (sidebar->unready_accounts, account); + g_object_unref (account); + } +} + +static void +update_places (NautilusGtkPlacesSidebar *sidebar) +{ + GList *mounts, *l, *ll; + GMount *mount; + GList *drives; + GDrive *drive; + GList *volumes; + GVolume *volume; + GSList *bookmarks, *sl; + int index; + char *original_uri, *name, *identifier; + GtkListBoxRow *selected; + char *home_uri; + GIcon *start_icon; + GFile *root; + char *tooltip; + GList *network_mounts, *network_volumes; + GIcon *new_bookmark_icon; + GtkWidget *child; + GList *cloud_providers; + GList *cloud_providers_accounts; + CloudProvidersAccount *cloud_provider_account; + CloudProvidersProvider *cloud_provider; + + /* save original selection */ + selected = gtk_list_box_get_selected_row (GTK_LIST_BOX (sidebar->list_box)); + if (selected) + g_object_get (selected, "uri", &original_uri, NULL); + else + original_uri = NULL; + + g_cancellable_cancel (sidebar->cancellable); + + g_object_unref (sidebar->cancellable); + sidebar->cancellable = g_cancellable_new (); + + /* Reset drag state, just in case we update the places while dragging or + * ending a drag */ + stop_drop_feedback (sidebar); + while ((child = gtk_widget_get_first_child (GTK_WIDGET (sidebar->list_box)))) + gtk_list_box_remove (GTK_LIST_BOX (sidebar->list_box), child); + + network_mounts = network_volumes = NULL; + + /* add built-in places */ + if (should_show_recent (sidebar)) + { + start_icon = g_themed_icon_new_with_default_fallbacks ("document-open-recent-symbolic"); + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + _("Recent"), start_icon, NULL, "recent:///", + NULL, NULL, NULL, NULL, 0, + _("Recent files")); + g_object_unref (start_icon); + } + + if (sidebar->show_starred_location) + { + start_icon = g_themed_icon_new_with_default_fallbacks ("starred-symbolic"); + add_place (sidebar, NAUTILUS_GTK_PLACES_STARRED_LOCATION, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + _("Starred"), start_icon, NULL, "starred:///", + NULL, NULL, NULL, NULL, 0, + _("Starred files")); + g_object_unref (start_icon); + } + + /* home folder */ + home_uri = get_home_directory_uri (); + start_icon = g_themed_icon_new_with_default_fallbacks (ICON_NAME_HOME); + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + _("Home"), start_icon, NULL, home_uri, + NULL, NULL, NULL, NULL, 0, + _("Open your personal folder")); + g_object_unref (start_icon); + g_free (home_uri); + + /* desktop */ + if (sidebar->show_desktop) + { + char *mount_uri = get_desktop_directory_uri (); + if (mount_uri) + { + start_icon = g_themed_icon_new_with_default_fallbacks (ICON_NAME_DESKTOP); + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + _("Desktop"), start_icon, NULL, mount_uri, + NULL, NULL, NULL, NULL, 0, + _("Open the contents of your desktop in a folder")); + g_object_unref (start_icon); + g_free (mount_uri); + } + } + + /* XDG directories */ + add_special_dirs (sidebar); + + if (sidebar->show_enter_location) + { + start_icon = g_themed_icon_new_with_default_fallbacks (ICON_NAME_NETWORK_SERVER); + add_place (sidebar, NAUTILUS_GTK_PLACES_ENTER_LOCATION, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + _("Enter Location"), start_icon, NULL, NULL, + NULL, NULL, NULL, NULL, 0, + _("Manually enter a location")); + g_object_unref (start_icon); + } + + /* Trash */ + if (sidebar->show_trash) + { + start_icon = nautilus_trash_monitor_get_symbolic_icon (); + sidebar->trash_row = add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + _("Trash"), start_icon, NULL, "trash:///", + NULL, NULL, NULL, NULL, 0, + _("Open the trash")); + g_object_add_weak_pointer (G_OBJECT (sidebar->trash_row), + (gpointer *) &sidebar->trash_row); + g_object_unref (start_icon); + } + + /* Application-side shortcuts */ + add_application_shortcuts (sidebar); + + /* Cloud providers */ + cloud_providers = cloud_providers_collector_get_providers (sidebar->cloud_manager); + for (l = sidebar->unready_accounts; l != NULL; l = l->next) + { + g_signal_handlers_disconnect_by_data (l->data, sidebar); + } + g_list_free_full (sidebar->unready_accounts, g_object_unref); + sidebar->unready_accounts = NULL; + for (l = cloud_providers; l != NULL; l = l->next) + { + cloud_provider = CLOUD_PROVIDERS_PROVIDER (l->data); + g_signal_connect_swapped (cloud_provider, "accounts-changed", + G_CALLBACK (update_places), sidebar); + cloud_providers_accounts = cloud_providers_provider_get_accounts (cloud_provider); + for (ll = cloud_providers_accounts; ll != NULL; ll = ll->next) + { + cloud_provider_account = CLOUD_PROVIDERS_ACCOUNT (ll->data); + if (!create_cloud_provider_account_row (sidebar, cloud_provider_account)) + { + + g_signal_connect (cloud_provider_account, "notify::name", + G_CALLBACK (on_account_updated), sidebar); + g_signal_connect (cloud_provider_account, "notify::status", + G_CALLBACK (on_account_updated), sidebar); + g_signal_connect (cloud_provider_account, "notify::status-details", + G_CALLBACK (on_account_updated), sidebar); + g_signal_connect (cloud_provider_account, "notify::path", + G_CALLBACK (on_account_updated), sidebar); + sidebar->unready_accounts = g_list_append (sidebar->unready_accounts, + g_object_ref (cloud_provider_account)); + continue; + } + + } + } + + /* go through all connected drives */ + drives = g_volume_monitor_get_connected_drives (sidebar->volume_monitor); + + for (l = drives; l != NULL; l = l->next) + { + drive = l->data; + + volumes = g_drive_get_volumes (drive); + if (volumes != NULL) + { + for (ll = volumes; ll != NULL; ll = ll->next) + { + volume = ll->data; + identifier = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS); + + if (g_strcmp0 (identifier, "network") == 0) + { + g_free (identifier); + network_volumes = g_list_prepend (network_volumes, volume); + continue; + } + g_free (identifier); + + if (sidebar->show_other_locations && !is_external_volume (volume)) + { + g_object_unref (volume); + continue; + } + + mount = g_volume_get_mount (volume); + if (mount != NULL) + { + char *mount_uri; + + /* Show mounted volume in the sidebar */ + start_icon = g_mount_get_symbolic_icon (mount); + root = g_mount_get_default_location (mount); + mount_uri = g_file_get_uri (root); + name = g_mount_get_name (mount); + tooltip = g_file_get_parse_name (root); + + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, mount_uri, + drive, volume, mount, NULL, 0, tooltip); + g_object_unref (root); + g_object_unref (mount); + g_object_unref (start_icon); + g_free (tooltip); + g_free (name); + g_free (mount_uri); + } + else + { + /* Do show the unmounted volumes in the sidebar; + * this is so the user can mount it (in case automounting + * is off). + * + * Also, even if automounting is enabled, this gives a visual + * cue that the user should remember to yank out the media if + * he just unmounted it. + */ + start_icon = g_volume_get_symbolic_icon (volume); + name = g_volume_get_name (volume); + tooltip = g_strdup_printf (_("Mount and open “%s”"), name); + + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, NULL, + drive, volume, NULL, NULL, 0, tooltip); + g_object_unref (start_icon); + g_free (name); + g_free (tooltip); + } + g_object_unref (volume); + } + g_list_free (volumes); + } + else + { + if (g_drive_is_media_removable (drive) && !g_drive_is_media_check_automatic (drive)) + { + /* If the drive has no mountable volumes and we cannot detect media change.. we + * display the drive in the sidebar so the user can manually poll the drive by + * right clicking and selecting "Rescan..." + * + * This is mainly for drives like floppies where media detection doesn't + * work.. but it's also for human beings who like to turn off media detection + * in the OS to save battery juice. + */ + start_icon = g_drive_get_symbolic_icon (drive); + name = g_drive_get_name (drive); + tooltip = g_strdup_printf (_("Mount and open “%s”"), name); + + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, NULL, + drive, NULL, NULL, NULL, 0, tooltip); + g_object_unref (start_icon); + g_free (tooltip); + g_free (name); + } + } + } + g_list_free_full (drives, g_object_unref); + + /* add all network volumes that are not associated with a drive, and + * loop devices + */ + volumes = g_volume_monitor_get_volumes (sidebar->volume_monitor); + for (l = volumes; l != NULL; l = l->next) + { + gboolean is_loop = FALSE; + volume = l->data; + drive = g_volume_get_drive (volume); + if (drive != NULL) + { + g_object_unref (volume); + g_object_unref (drive); + continue; + } + + identifier = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS); + + if (g_strcmp0 (identifier, "network") == 0) + { + g_free (identifier); + network_volumes = g_list_prepend (network_volumes, volume); + continue; + } + else if (g_strcmp0 (identifier, "loop") == 0) + is_loop = TRUE; + g_free (identifier); + + if (sidebar->show_other_locations && + !is_external_volume (volume) && + !is_loop) + { + g_object_unref (volume); + continue; + } + + mount = g_volume_get_mount (volume); + if (mount != NULL) + { + char *mount_uri; + + start_icon = g_mount_get_symbolic_icon (mount); + root = g_mount_get_default_location (mount); + mount_uri = g_file_get_uri (root); + tooltip = g_file_get_parse_name (root); + name = g_mount_get_name (mount); + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, mount_uri, + NULL, volume, mount, NULL, 0, tooltip); + g_object_unref (mount); + g_object_unref (root); + g_object_unref (start_icon); + g_free (name); + g_free (tooltip); + g_free (mount_uri); + } + else + { + /* see comment above in why we add an icon for an unmounted mountable volume */ + start_icon = g_volume_get_symbolic_icon (volume); + name = g_volume_get_name (volume); + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, NULL, + NULL, volume, NULL, NULL, 0, name); + g_object_unref (start_icon); + g_free (name); + } + g_object_unref (volume); + } + g_list_free (volumes); + + /* file system root */ + if (!sidebar->show_other_locations) + { + start_icon = g_themed_icon_new_with_default_fallbacks (ICON_NAME_FILESYSTEM); + add_place (sidebar, NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + sidebar->hostname, start_icon, NULL, "file:///", + NULL, NULL, NULL, NULL, 0, + _("Open the contents of the file system")); + g_object_unref (start_icon); + } + + /* add mounts that has no volume (/etc/mtab mounts, ftp, sftp,...) */ + mounts = g_volume_monitor_get_mounts (sidebar->volume_monitor); + + for (l = mounts; l != NULL; l = l->next) + { + char *mount_uri; + + mount = l->data; + if (g_mount_is_shadowed (mount)) + { + g_object_unref (mount); + continue; + } + volume = g_mount_get_volume (mount); + if (volume != NULL) + { + g_object_unref (volume); + g_object_unref (mount); + continue; + } + root = g_mount_get_default_location (mount); + + if (!g_file_is_native (root)) + { + network_mounts = g_list_prepend (network_mounts, mount); + g_object_unref (root); + continue; + } + + start_icon = g_mount_get_symbolic_icon (mount); + mount_uri = g_file_get_uri (root); + name = g_mount_get_name (mount); + tooltip = g_file_get_parse_name (root); + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + name, start_icon, NULL, mount_uri, + NULL, NULL, mount, NULL, 0, tooltip); + g_object_unref (root); + g_object_unref (mount); + g_object_unref (start_icon); + g_free (name); + g_free (mount_uri); + g_free (tooltip); + } + g_list_free (mounts); + + /* add bookmarks */ + bookmarks = _nautilus_gtk_bookmarks_manager_list_bookmarks (sidebar->bookmarks_manager); + + for (sl = bookmarks, index = 0; sl; sl = sl->next, index++) + { + gboolean is_native; + BookmarkQueryClosure *clos; + + root = sl->data; + is_native = g_file_is_native (root); + + if (_nautilus_gtk_bookmarks_manager_get_is_builtin (sidebar->bookmarks_manager, root)) + continue; + + clos = g_slice_new (BookmarkQueryClosure); + clos->sidebar = sidebar; + clos->index = index; + clos->is_native = is_native; + g_file_query_info_async (root, + "standard::display-name,standard::symbolic-icon", + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + sidebar->cancellable, + on_bookmark_query_info_complete, + clos); + } + + g_slist_free_full (bookmarks, g_object_unref); + + /* Add new bookmark row */ + new_bookmark_icon = g_themed_icon_new ("bookmark-new-symbolic"); + sidebar->new_bookmark_row = add_place (sidebar, NAUTILUS_GTK_PLACES_DROP_FEEDBACK, + NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS, + _("New bookmark"), new_bookmark_icon, NULL, NULL, + NULL, NULL, NULL, NULL, 0, + _("Add a new bookmark")); + gtk_widget_add_css_class (sidebar->new_bookmark_row, "sidebar-new-bookmark-row"); + g_object_unref (new_bookmark_icon); + + /* network */ + network_volumes = g_list_reverse (network_volumes); + for (l = network_volumes; l != NULL; l = l->next) + { + volume = l->data; + mount = g_volume_get_mount (volume); + + if (mount != NULL) + { + network_mounts = g_list_prepend (network_mounts, mount); + continue; + } + else + { + start_icon = g_volume_get_symbolic_icon (volume); + name = g_volume_get_name (volume); + tooltip = g_strdup_printf (_("Mount and open “%s”"), name); + + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, NULL, + NULL, volume, NULL, NULL, 0, tooltip); + g_object_unref (start_icon); + g_free (name); + g_free (tooltip); + } + } + + network_mounts = g_list_reverse (network_mounts); + for (l = network_mounts; l != NULL; l = l->next) + { + char *mount_uri; + + mount = l->data; + root = g_mount_get_default_location (mount); + start_icon = g_mount_get_symbolic_icon (mount); + mount_uri = g_file_get_uri (root); + name = g_mount_get_name (mount); + tooltip = g_file_get_parse_name (root); + add_place (sidebar, NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + name, start_icon, NULL, mount_uri, + NULL, NULL, mount, NULL, 0, tooltip); + g_object_unref (root); + g_object_unref (start_icon); + g_free (name); + g_free (mount_uri); + g_free (tooltip); + } + + + g_list_free_full (network_volumes, g_object_unref); + g_list_free_full (network_mounts, g_object_unref); + + /* Other locations */ + if (sidebar->show_other_locations) + { + start_icon = g_themed_icon_new_with_default_fallbacks (ICON_NAME_OTHER_LOCATIONS); + + add_place (sidebar, NAUTILUS_GTK_PLACES_OTHER_LOCATIONS, + NAUTILUS_GTK_PLACES_SECTION_OTHER_LOCATIONS, + _("Other Locations"), start_icon, NULL, "other-locations:///", + NULL, NULL, NULL, NULL, 0, _("Show other locations")); + + g_object_unref (start_icon); + } + + gtk_widget_show (GTK_WIDGET (sidebar)); + /* We want this hidden by default, but need to do it after the show_all call */ + nautilus_gtk_sidebar_row_hide (NAUTILUS_GTK_SIDEBAR_ROW (sidebar->new_bookmark_row), TRUE); + + /* restore original selection */ + if (original_uri) + { + GFile *restore; + + restore = g_file_new_for_uri (original_uri); + nautilus_gtk_places_sidebar_set_location (sidebar, restore); + g_object_unref (restore); + g_free (original_uri); + } +} + +static gboolean +hover_timer (gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar = user_data; + gboolean open_folder_on_hover; + g_autofree gchar *uri = NULL; + g_autoptr (GFile) location = NULL; + + open_folder_on_hover = g_settings_get_boolean (nautilus_preferences, + NAUTILUS_PREFERENCES_OPEN_FOLDER_ON_DND_HOVER); + sidebar->hover_timer_id = 0; + + if (open_folder_on_hover && sidebar->hover_row != NULL) + { + g_object_get (sidebar->hover_row, "uri", &uri, NULL); + if (uri != NULL) + { + location = g_file_new_for_uri (uri); + emit_open_location (sidebar, location, 0); + } + } + + return G_SOURCE_REMOVE; +} + +static gboolean +check_valid_drop_target (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkSidebarRow *row, + const GValue *value) +{ + NautilusGtkPlacesPlaceType place_type; + NautilusGtkPlacesSectionType section_type; + g_autoptr (NautilusFile) dest_file = NULL; + gboolean valid = FALSE; + char *uri; + int drag_action; + + g_return_val_if_fail (value != NULL, TRUE); + + if (row == NULL) + return FALSE; + + g_object_get (row, + "place-type", &place_type, + "section_type", §ion_type, + "uri", &uri, + "file", &dest_file, + NULL); + + if (place_type == NAUTILUS_GTK_PLACES_CONNECT_TO_SERVER) + { + g_free (uri); + return FALSE; + } + + if (place_type == NAUTILUS_GTK_PLACES_OTHER_LOCATIONS) + { + g_free (uri); + return FALSE; + } + + if (place_type == NAUTILUS_GTK_PLACES_DROP_FEEDBACK) + { + g_free (uri); + return TRUE; + } + + /* Disallow drops on recent:/// */ + if (place_type == NAUTILUS_GTK_PLACES_BUILT_IN) + { + if (g_strcmp0 (uri, "recent:///") == 0) + { + g_free (uri); + return FALSE; + } + } + + /* Dragging a bookmark? */ + if (G_VALUE_HOLDS (value, NAUTILUS_TYPE_GTK_SIDEBAR_ROW)) + { + /* Don't allow reordering bookmarks into non-bookmark areas */ + valid = section_type == NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS; + } + else if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) + { + /* Dragging a file */ + if (uri != NULL) + { + drag_action = emit_drag_action_requested (sidebar, dest_file, g_value_get_boxed (value)); + valid = drag_action > 0; + } + else + { + valid = FALSE; + } + } + else + { + g_assert_not_reached (); + valid = TRUE; + } + + g_free (uri); + return valid; +} + +static void +update_possible_drop_targets (NautilusGtkPlacesSidebar *sidebar, + const GValue *value) +{ + GtkWidget *row; + + for (row = gtk_widget_get_first_child (GTK_WIDGET (sidebar->list_box)); + row != NULL; + row = gtk_widget_get_next_sibling (row)) + { + gboolean sensitive; + + if (!GTK_IS_LIST_BOX_ROW (row)) + continue; + + sensitive = value == NULL || + check_valid_drop_target (sidebar, NAUTILUS_GTK_SIDEBAR_ROW (row), value); + gtk_widget_set_sensitive (row, sensitive); + } +} + +static void +start_drop_feedback (NautilusGtkPlacesSidebar *sidebar, + const GValue *value) +{ + if (value != NULL && G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) + { + GSList *source_list = g_value_get_boxed (value); + if (g_slist_length (source_list) == 1) + { + g_autoptr (NautilusFile) file = NULL; + file = nautilus_file_get (source_list->data); + if (nautilus_file_is_directory (file)) + nautilus_gtk_sidebar_row_reveal (NAUTILUS_GTK_SIDEBAR_ROW (sidebar->new_bookmark_row)); + } + } + if (value && !G_VALUE_HOLDS (value, NAUTILUS_TYPE_GTK_SIDEBAR_ROW)) + { + /* If the state is permanent, don't change it. The application controls it. */ + if (sidebar->drop_state != DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT) + sidebar->drop_state = DROP_STATE_NEW_BOOKMARK_ARMED; + } + + update_possible_drop_targets (sidebar, value); +} + +static void +stop_drop_feedback (NautilusGtkPlacesSidebar *sidebar) +{ + update_possible_drop_targets (sidebar, NULL); + + if (sidebar->drop_state != DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT && + sidebar->new_bookmark_row != NULL) + { + nautilus_gtk_sidebar_row_hide (NAUTILUS_GTK_SIDEBAR_ROW (sidebar->new_bookmark_row), FALSE); + sidebar->drop_state = DROP_STATE_NORMAL; + } + + if (sidebar->drag_row != NULL) + { + gtk_widget_show (sidebar->drag_row); + sidebar->drag_row = NULL; + } + + if (sidebar->row_placeholder != NULL) + { + if (gtk_widget_get_parent (sidebar->row_placeholder) != NULL) + gtk_list_box_remove (GTK_LIST_BOX (sidebar->list_box), sidebar->row_placeholder); + sidebar->row_placeholder = NULL; + } + + sidebar->dragging_over = FALSE; +} + +static GtkWidget * +create_placeholder_row (NautilusGtkPlacesSidebar *sidebar) +{ + return g_object_new (NAUTILUS_TYPE_GTK_SIDEBAR_ROW, "placeholder", TRUE, NULL); +} + +static GdkDragAction +drag_motion_callback (GtkDropTarget *target, + double x, + double y, + NautilusGtkPlacesSidebar *sidebar) +{ + GdkDragAction action; + GtkListBoxRow *row; + NautilusGtkPlacesPlaceType place_type; + char *drop_target_uri = NULL; + int row_index; + int row_placeholder_index; + const GValue *value; + graphene_point_t start; + + sidebar->dragging_over = TRUE; + action = 0; + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (sidebar->list_box), y); + + start = sidebar->hover_start_point; + if (row != sidebar->hover_row || + gtk_drag_check_threshold (GTK_WIDGET (sidebar), start.x, start.y, x, y)) + { + g_clear_handle_id (&sidebar->hover_timer_id, g_source_remove); + sidebar->hover_row = row; + sidebar->hover_timer_id = g_timeout_add (HOVER_TIMEOUT, hover_timer, sidebar); + sidebar->hover_start_point.x = x; + sidebar->hover_start_point.y = y; + } + + /* Workaround https://gitlab.gnome.org/GNOME/gtk/-/issues/5023 */ + gtk_list_box_drag_unhighlight_row (GTK_LIST_BOX (sidebar->list_box)); + + /* Nothing to do if no value yet */ + value = gtk_drop_target_get_value (target); + if (value == NULL) + goto out; + + /* Nothing to do if the target is not valid drop destination */ + if (!check_valid_drop_target (sidebar, NAUTILUS_GTK_SIDEBAR_ROW (row), value)) + goto out; + + if (G_VALUE_HOLDS (value, NAUTILUS_TYPE_GTK_SIDEBAR_ROW)) + { + /* Dragging bookmarks always moves them to another position in the bookmarks list */ + action = GDK_ACTION_MOVE; + if (sidebar->row_placeholder == NULL) + { + sidebar->row_placeholder = create_placeholder_row (sidebar); + g_object_ref_sink (sidebar->row_placeholder); + } + else if (GTK_WIDGET (row) == sidebar->row_placeholder) + { + goto out; + } + + if (gtk_widget_get_parent (sidebar->row_placeholder) != NULL) + gtk_list_box_remove (GTK_LIST_BOX (sidebar->list_box), sidebar->row_placeholder); + + if (row != NULL) + { + g_object_get (row, "order-index", &row_index, NULL); + g_object_get (sidebar->row_placeholder, "order-index", &row_placeholder_index, NULL); + /* We order the bookmarks sections based on the bookmark index that we + * set on the row as order-index property, but we have to deal with + * the placeholder row wanting to be between two consecutive bookmarks, + * with two consecutive order-index values which is the usual case. + * For that, in the list box sort func we give priority to the placeholder row, + * that means that if the index-order is the same as another bookmark + * the placeholder row goes before. However if we want to show it after + * the current row, for instance when the cursor is in the lower half + * of the row, we need to increase the order-index. + */ + row_placeholder_index = row_index; + gtk_widget_translate_coordinates (GTK_WIDGET (sidebar), GTK_WIDGET (row), + x, y, + &x, &y); + + if (y > sidebar->drag_row_height / 2 && row_index > 0) + row_placeholder_index++; + } + else + { + /* If the user is dragging over an area that has no row, place the row + * placeholder in the last position + */ + row_placeholder_index = G_MAXINT32; + } + + g_object_set (sidebar->row_placeholder, "order-index", row_placeholder_index, NULL); + + gtk_list_box_prepend (GTK_LIST_BOX (sidebar->list_box), + sidebar->row_placeholder); + } + else if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) + { + NautilusFile *file; + gtk_list_box_drag_highlight_row (GTK_LIST_BOX (sidebar->list_box), row); + + g_object_get (row, + "place-type", &place_type, + "uri", &drop_target_uri, + "file", &file, + NULL); + /* URIs are being dragged. See if the caller wants to handle a + * file move/copy operation itself, or if we should only try to + * create bookmarks out of the dragged URIs. + */ + if (place_type == NAUTILUS_GTK_PLACES_DROP_FEEDBACK) + { + action = GDK_ACTION_COPY; + } + else + { + /* uri may be NULL for unmounted volumes, for example, so we don't allow drops there */ + if (drop_target_uri != NULL) + { + GFile *dest_file = g_file_new_for_uri (drop_target_uri); + + action = emit_drag_action_requested (sidebar, file, g_value_get_boxed (value)); + + g_object_unref (dest_file); + } + } + + nautilus_file_unref (file); + g_free (drop_target_uri); + } + else + { + g_assert_not_reached (); + } + + out: + start_drop_feedback (sidebar, value); + return action; +} + +/* Reorders the bookmark to the specified position */ +static void +reorder_bookmarks (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkSidebarRow *row, + int new_position) +{ + char *uri; + GFile *file; + + g_object_get (row, "uri", &uri, NULL); + file = g_file_new_for_uri (uri); + _nautilus_gtk_bookmarks_manager_reorder_bookmark (sidebar->bookmarks_manager, file, new_position, NULL); + + g_object_unref (file); + g_free (uri); +} + +/* Creates bookmarks for the specified files at the given position in the bookmarks list */ +static void +drop_files_as_bookmarks (NautilusGtkPlacesSidebar *sidebar, + GSList *files, + int position) +{ + GSList *l; + + for (l = files; l; l = l->next) + { + GFile *f = G_FILE (l->data); + GFileInfo *info = g_file_query_info (f, + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, + NULL); + + if (info) + { + if ((g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY || + g_file_info_get_file_type (info) == G_FILE_TYPE_MOUNTABLE || + g_file_info_get_file_type (info) == G_FILE_TYPE_SHORTCUT || + g_file_info_get_file_type (info) == G_FILE_TYPE_SYMBOLIC_LINK)) + _nautilus_gtk_bookmarks_manager_insert_bookmark (sidebar->bookmarks_manager, f, position++, NULL); + + g_object_unref (info); + } + } +} + +static gboolean +drag_drop_callback (GtkDropTarget *target, + const GValue *value, + double x, + double y, + NautilusGtkPlacesSidebar *sidebar) +{ + int target_order_index; + NautilusGtkPlacesPlaceType target_place_type; + NautilusGtkPlacesSectionType target_section_type; + char *target_uri; + GtkListBoxRow *target_row; + gboolean result; + + target_row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (sidebar->list_box), y); + if (target_row == NULL) + return FALSE; + + if (!check_valid_drop_target (sidebar, NAUTILUS_GTK_SIDEBAR_ROW (target_row), value)) + return FALSE; + + g_object_get (target_row, + "place-type", &target_place_type, + "section-type", &target_section_type, + "order-index", &target_order_index, + "uri", &target_uri, + NULL); + result = FALSE; + + if (G_VALUE_HOLDS (value, NAUTILUS_TYPE_GTK_SIDEBAR_ROW)) + { + GtkWidget *source_row; + /* A bookmark got reordered */ + if (target_section_type != NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS) + goto out; + + source_row = g_value_get_object (value); + + if (sidebar->row_placeholder != NULL) + g_object_get (sidebar->row_placeholder, "order-index", &target_order_index, NULL); + + reorder_bookmarks (sidebar, NAUTILUS_GTK_SIDEBAR_ROW (source_row), target_order_index); + result = TRUE; + } + else if (G_VALUE_HOLDS (value, GDK_TYPE_FILE_LIST)) + { + /* Dropping URIs! */ + if (target_place_type == NAUTILUS_GTK_PLACES_DROP_FEEDBACK) + { + drop_files_as_bookmarks (sidebar, g_value_get_boxed (value), target_order_index); + } + else + { + GFile *dest_file = g_file_new_for_uri (target_uri); + GdkDragAction actions; + + actions = gdk_drop_get_actions (gtk_drop_target_get_current_drop (target)); + + #ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gtk_widget_get_display (GTK_WIDGET (sidebar)))) + { + /* Temporary workaround until the below GTK MR (or equivalend fix) + * is merged. Without this fix, the preferred action isn't set correctly. + * https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4982 */ + GdkDrag *drag = gdk_drop_get_drag (gtk_drop_target_get_current_drop (target)); + actions = gdk_drag_get_selected_action (drag); + } + #endif + + emit_drag_perform_drop (sidebar, + dest_file, + g_value_get_boxed (value), + actions); + + g_object_unref (dest_file); + } + result = TRUE; + } + else + { + g_assert_not_reached (); + } + +out: + stop_drop_feedback (sidebar); + g_free (target_uri); + return result; +} + +static void +dnd_finished_cb (GdkDrag *drag, + NautilusGtkPlacesSidebar *sidebar) +{ + stop_drop_feedback (sidebar); +} + +static void +dnd_cancel_cb (GdkDrag *drag, + GdkDragCancelReason reason, + NautilusGtkPlacesSidebar *sidebar) +{ + stop_drop_feedback (sidebar); +} + +/* This functions is called every time the drag source leaves + * the sidebar widget. + * The problem is that, we start showing hints for drop when the source + * start being above the sidebar or when the application request so show + * drop hints, but at some moment we need to restore to normal + * state. + * One could think that here we could simply call stop_drop_feedback, + * but that's not true, because this function is called also before drag_drop, + * which needs the data from the drag so we cannot free the drag data here. + * So now one could think we could just do nothing here, and wait for + * drag-end or drag-cancel signals and just stop_drop_feedback there. But that + * is also not true, since when the drag comes from a different widget than the + * sidebar, when the drag stops the last drag signal we receive is drag-leave. + * So here what we will do is restore the state of the sidebar as if no drag + * is being done (and if the application didn't request for permanent hints with + * nautilus_gtk_places_sidebar_show_drop_hints) and we will free the drag data next time + * we build new drag data in drag_data_received. + */ +static void +drag_leave_callback (GtkDropTarget *dest, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (user_data); + + gtk_list_box_drag_unhighlight_row (GTK_LIST_BOX (sidebar->list_box)); + + if (sidebar->drop_state != DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT) + { + update_possible_drop_targets (sidebar, FALSE); + nautilus_gtk_sidebar_row_hide (NAUTILUS_GTK_SIDEBAR_ROW (sidebar->new_bookmark_row), FALSE); + sidebar->drop_state = DROP_STATE_NORMAL; + } + + g_clear_handle_id (&sidebar->hover_timer_id, g_source_remove); + sidebar->dragging_over = FALSE; +} + +static void +check_unmount_and_eject (GMount *mount, + GVolume *volume, + GDrive *drive, + gboolean *show_unmount, + gboolean *show_eject) +{ + *show_unmount = FALSE; + *show_eject = FALSE; + + if (drive != NULL) + *show_eject = g_drive_can_eject (drive); + + if (volume != NULL) + *show_eject |= g_volume_can_eject (volume); + + if (mount != NULL) + { + *show_eject |= g_mount_can_eject (mount); + *show_unmount = g_mount_can_unmount (mount) && !*show_eject; + } +} + +static void +drive_start_from_bookmark_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar; + GError *error; + char *primary; + char *name; + + sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (user_data); + + error = NULL; + if (!g_drive_poll_for_media_finish (G_DRIVE (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED) + { + name = g_drive_get_name (G_DRIVE (source_object)); + primary = g_strdup_printf (_("Unable to start “%s”"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } +} + +static void +volume_mount_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + NautilusGtkSidebarRow *row = NAUTILUS_GTK_SIDEBAR_ROW (user_data); + NautilusGtkPlacesSidebar *sidebar; + GVolume *volume; + GError *error; + char *primary; + char *name; + GMount *mount; + + volume = G_VOLUME (source_object); + g_object_get (row, "sidebar", &sidebar, NULL); + + error = NULL; + if (!g_volume_mount_finish (volume, result, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED && + error->code != G_IO_ERROR_ALREADY_MOUNTED) + { + name = g_volume_get_name (G_VOLUME (source_object)); + if (g_str_has_prefix (error->message, "Error unlocking")) + /* Translators: This means that unlocking an encrypted storage + * device failed. %s is the name of the device. + */ + primary = g_strdup_printf (_("Error unlocking “%s”"), name); + else + primary = g_strdup_printf (_("Unable to access “%s”"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } + + sidebar->mounting = FALSE; + nautilus_gtk_sidebar_row_set_busy (row, FALSE); + + mount = g_volume_get_mount (volume); + if (mount != NULL) + { + GFile *location; + + location = g_mount_get_default_location (mount); + emit_open_location (sidebar, location, sidebar->go_to_after_mount_open_flags); + + g_object_unref (G_OBJECT (location)); + g_object_unref (G_OBJECT (mount)); + } + + g_object_unref (row); + g_object_unref (sidebar); +} + +static void +mount_volume (NautilusGtkSidebarRow *row, + GVolume *volume) +{ + NautilusGtkPlacesSidebar *sidebar; + GMountOperation *mount_op; + + g_object_get (row, "sidebar", &sidebar, NULL); + + mount_op = get_mount_operation (sidebar); + g_mount_operation_set_password_save (mount_op, G_PASSWORD_SAVE_FOR_SESSION); + + g_object_ref (row); + g_object_ref (sidebar); + g_volume_mount (volume, 0, mount_op, NULL, volume_mount_cb, row); +} + +static void +open_drive (NautilusGtkSidebarRow *row, + GDrive *drive, + NautilusGtkPlacesOpenFlags open_flags) +{ + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, "sidebar", &sidebar, NULL); + + if (drive != NULL && + (g_drive_can_start (drive) || g_drive_can_start_degraded (drive))) + { + GMountOperation *mount_op; + + nautilus_gtk_sidebar_row_set_busy (row, TRUE); + mount_op = get_mount_operation (sidebar); + g_drive_start (drive, G_DRIVE_START_NONE, mount_op, NULL, drive_start_from_bookmark_cb, NULL); + g_object_unref (mount_op); + } +} + +static void +open_volume (NautilusGtkSidebarRow *row, + GVolume *volume, + NautilusGtkPlacesOpenFlags open_flags) +{ + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, "sidebar", &sidebar, NULL); + + if (volume != NULL && !sidebar->mounting) + { + sidebar->mounting = TRUE; + sidebar->go_to_after_mount_open_flags = open_flags; + nautilus_gtk_sidebar_row_set_busy (row, TRUE); + mount_volume (row, volume); + } +} + +static void +open_uri (NautilusGtkPlacesSidebar *sidebar, + const char *uri, + NautilusGtkPlacesOpenFlags open_flags) +{ + GFile *location; + + location = g_file_new_for_uri (uri); + emit_open_location (sidebar, location, open_flags); + g_object_unref (location); +} + +static void +open_row (NautilusGtkSidebarRow *row, + NautilusGtkPlacesOpenFlags open_flags) +{ + char *uri; + GDrive *drive; + GVolume *volume; + NautilusGtkPlacesPlaceType place_type; + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, + "sidebar", &sidebar, + "uri", &uri, + "place-type", &place_type, + "drive", &drive, + "volume", &volume, + NULL); + + if (place_type == NAUTILUS_GTK_PLACES_OTHER_LOCATIONS) + { + emit_show_other_locations_with_flags (sidebar, open_flags); + } + else if (place_type == NAUTILUS_GTK_PLACES_STARRED_LOCATION) + { + emit_show_starred_location (sidebar, open_flags); + } + else if (uri != NULL) + { + open_uri (sidebar, uri, open_flags); + } + else if (place_type == NAUTILUS_GTK_PLACES_ENTER_LOCATION) + { + emit_show_enter_location (sidebar); + } + else if (volume != NULL) + { + open_volume (row, volume, open_flags); + } + else if (drive != NULL) + { + open_drive (row, drive, open_flags); + } + + g_object_unref (sidebar); + if (drive) + g_object_unref (drive); + if (volume) + g_object_unref (volume); + g_free (uri); +} + +/* Callback used for the "Open" menu items in the context menu */ +static void +open_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + NautilusGtkPlacesOpenFlags flags; + + flags = (NautilusGtkPlacesOpenFlags)g_variant_get_int32 (parameter); + open_row (sidebar->context_row, flags); +} + +/* Add bookmark for the selected item - just used from mount points */ +static void +add_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + char *uri; + char *name; + GFile *location; + + g_object_get (sidebar->context_row, + "uri", &uri, + "label", &name, + NULL); + + if (uri != NULL) + { + location = g_file_new_for_uri (uri); + if (_nautilus_gtk_bookmarks_manager_insert_bookmark (sidebar->bookmarks_manager, location, -1, NULL)) + _nautilus_gtk_bookmarks_manager_set_bookmark_label (sidebar->bookmarks_manager, location, name, NULL); + g_object_unref (location); + } + + g_free (uri); + g_free (name); +} + +static void +rename_entry_changed (GtkEntry *entry, + NautilusGtkPlacesSidebar *sidebar) +{ + NautilusGtkPlacesPlaceType type; + char *name; + char *uri; + const char *new_name; + gboolean found = FALSE; + GtkWidget *row; + + new_name = gtk_editable_get_text (GTK_EDITABLE (sidebar->rename_entry)); + + if (strcmp (new_name, "") == 0) + { + gtk_widget_set_sensitive (sidebar->rename_button, FALSE); + gtk_label_set_label (GTK_LABEL (sidebar->rename_error), ""); + return; + } + + for (row = gtk_widget_get_first_child (GTK_WIDGET (sidebar->list_box)); + row != NULL && !found; + row = gtk_widget_get_next_sibling (row)) + { + if (!GTK_IS_LIST_BOX_ROW (row)) + continue; + + g_object_get (row, + "place-type", &type, + "uri", &uri, + "label", &name, + NULL); + + if ((type == NAUTILUS_GTK_PLACES_XDG_DIR || type == NAUTILUS_GTK_PLACES_BOOKMARK) && + strcmp (uri, sidebar->rename_uri) != 0 && + strcmp (new_name, name) == 0) + found = TRUE; + + g_free (uri); + g_free (name); + } + + gtk_widget_set_sensitive (sidebar->rename_button, !found); + gtk_label_set_label (GTK_LABEL (sidebar->rename_error), + found ? _("This name is already taken") : ""); +} + +static void +do_rename (GtkButton *button, + NautilusGtkPlacesSidebar *sidebar) +{ + char *new_text; + GFile *file; + + new_text = g_strdup (gtk_editable_get_text (GTK_EDITABLE (sidebar->rename_entry))); + + file = g_file_new_for_uri (sidebar->rename_uri); + if (!_nautilus_gtk_bookmarks_manager_has_bookmark (sidebar->bookmarks_manager, file)) + _nautilus_gtk_bookmarks_manager_insert_bookmark (sidebar->bookmarks_manager, file, -1, NULL); + + if (sidebar->rename_popover) + { + gtk_popover_popdown (GTK_POPOVER (sidebar->rename_popover)); + } + + _nautilus_gtk_bookmarks_manager_set_bookmark_label (sidebar->bookmarks_manager, file, new_text, NULL); + + g_object_unref (file); + g_free (new_text); + + g_clear_pointer (&sidebar->rename_uri, g_free); + +} + +static void +on_rename_popover_destroy (GtkWidget *rename_popover, + NautilusGtkPlacesSidebar *sidebar) +{ + if (sidebar) + { + sidebar->rename_popover = NULL; + sidebar->rename_entry = NULL; + sidebar->rename_button = NULL; + sidebar->rename_error = NULL; + } +} + +static void +create_rename_popover (NautilusGtkPlacesSidebar *sidebar) +{ + GtkWidget *popover; + GtkWidget *grid; + GtkWidget *label; + GtkWidget *entry; + GtkWidget *button; + GtkWidget *error; + char *str; + + if (sidebar->rename_popover) + return; + + popover = gtk_popover_new (); + gtk_widget_set_parent (popover, GTK_WIDGET (sidebar)); + /* Clean sidebar pointer when its destroyed, most of the times due to its + * relative_to associated row being destroyed */ + g_signal_connect (popover, "destroy", G_CALLBACK (on_rename_popover_destroy), sidebar); + gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_RIGHT); + grid = gtk_grid_new (); + gtk_popover_set_child (GTK_POPOVER (popover), grid); + g_object_set (grid, + "margin-start", 10, + "margin-end", 10, + "margin-top", 10, + "margin-bottom", 10, + "row-spacing", 6, + "column-spacing", 6, + NULL); + entry = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); + g_signal_connect (entry, "changed", G_CALLBACK (rename_entry_changed), sidebar); + str = g_strdup_printf ("<b>%s</b>", _("Name")); + label = gtk_label_new (str); + gtk_widget_set_halign (label, GTK_ALIGN_START); + gtk_label_set_use_markup (GTK_LABEL (label), TRUE); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry); + g_free (str); + button = gtk_button_new_with_mnemonic (_("_Rename")); + gtk_widget_add_css_class (button, "suggested-action"); + g_signal_connect (button, "clicked", G_CALLBACK (do_rename), sidebar); + error = gtk_label_new (""); + gtk_widget_set_halign (error, GTK_ALIGN_START); + gtk_grid_attach (GTK_GRID (grid), label, 0, 0, 2, 1); + gtk_grid_attach (GTK_GRID (grid), entry, 0, 1, 1, 1); + gtk_grid_attach (GTK_GRID (grid), button,1, 1, 1, 1); + gtk_grid_attach (GTK_GRID (grid), error, 0, 2, 2, 1); + gtk_popover_set_default_widget (GTK_POPOVER (popover), button); + + sidebar->rename_popover = popover; + sidebar->rename_entry = entry; + sidebar->rename_button = button; + sidebar->rename_error = error; +} + +/* Style the row differently while we show a popover for it. + * Otherwise, the popover is 'pointing to nothing'. Since the + * main popover and the rename popover interleave their hiding + * and showing, we have to count to ensure that we don't loose + * the state before the last popover is gone. + * + * This would be nicer as a state, but reusing hover for this + * interferes with the normal handling of this state, so just + * use a style class. + */ +static void +update_popover_shadowing (GtkWidget *row, + gboolean shown) +{ + int count; + + count = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "popover-count")); + count = shown ? count + 1 : count - 1; + g_object_set_data (G_OBJECT (row), "popover-count", GINT_TO_POINTER (count)); + + if (count > 0) + gtk_widget_add_css_class (row, "has-open-popup"); + else + gtk_widget_remove_css_class (row, "has-open-popup"); +} + +static void +set_prelight (NautilusGtkPlacesSidebar *sidebar) +{ + update_popover_shadowing (GTK_WIDGET (sidebar->context_row), TRUE); +} + +static void +unset_prelight (NautilusGtkPlacesSidebar *sidebar) +{ + update_popover_shadowing (GTK_WIDGET (sidebar->context_row), FALSE); +} + +static void +setup_popover_shadowing (GtkWidget *popover, + NautilusGtkPlacesSidebar *sidebar) +{ + g_signal_connect_swapped (popover, "map", G_CALLBACK (set_prelight), sidebar); + g_signal_connect_swapped (popover, "unmap", G_CALLBACK (unset_prelight), sidebar); +} + +static void +_popover_set_pointing_to_widget (GtkPopover *popover, + GtkWidget *target) +{ + GtkWidget *parent; + double x, y, w, h; + + parent = gtk_widget_get_parent (GTK_WIDGET (popover)); + + if (!gtk_widget_translate_coordinates (target, parent, 0, 0, &x, &y)) + return; + + w = gtk_widget_get_allocated_width (GTK_WIDGET (target)); + h = gtk_widget_get_allocated_height (GTK_WIDGET (target)); + + gtk_popover_set_pointing_to (popover, &(GdkRectangle){x, y, w, h}); +} + +static void +show_rename_popover (NautilusGtkSidebarRow *row) +{ + char *name; + char *uri; + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, + "sidebar", &sidebar, + "label", &name, + "uri", &uri, + NULL); + + create_rename_popover (sidebar); + + if (sidebar->rename_uri) + g_free (sidebar->rename_uri); + sidebar->rename_uri = g_strdup (uri); + + gtk_editable_set_text (GTK_EDITABLE (sidebar->rename_entry), name); + + _popover_set_pointing_to_widget (GTK_POPOVER (sidebar->rename_popover), + GTK_WIDGET (row)); + + setup_popover_shadowing (sidebar->rename_popover, sidebar); + + gtk_popover_popup (GTK_POPOVER (sidebar->rename_popover)); + gtk_widget_grab_focus (sidebar->rename_entry); + + g_free (name); + g_free (uri); + g_object_unref (sidebar); +} + +static void +rename_bookmark (NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesPlaceType type; + + g_object_get (row, "place-type", &type, NULL); + + if (type != NAUTILUS_GTK_PLACES_BOOKMARK && type != NAUTILUS_GTK_PLACES_XDG_DIR) + return; + + show_rename_popover (row); +} + +static void +rename_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + + rename_bookmark (sidebar->context_row); +} + +static void +properties_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GList *list; + NautilusFile *file; + + g_object_get (sidebar->context_row, "file", &file, NULL); + + list = g_list_append (NULL, file); + nautilus_properties_window_present (list, GTK_WIDGET (sidebar), NULL, NULL, NULL); + + nautilus_file_list_free (list); +} + +static void +empty_trash_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + nautilus_file_operations_empty_trash (GTK_WIDGET (sidebar), TRUE, NULL); +} + +static void +remove_bookmark (NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesPlaceType type; + char *uri; + GFile *file; + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, + "sidebar", &sidebar, + "place-type", &type, + "uri", &uri, + NULL); + + if (type == NAUTILUS_GTK_PLACES_BOOKMARK) + { + file = g_file_new_for_uri (uri); + _nautilus_gtk_bookmarks_manager_remove_bookmark (sidebar->bookmarks_manager, file, NULL); + g_object_unref (file); + } + + g_free (uri); + g_object_unref (sidebar); +} + +static void +remove_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + + remove_bookmark (sidebar->context_row); +} + +static void +mount_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GVolume *volume; + + g_object_get (sidebar->context_row, + "volume", &volume, + NULL); + + if (volume != NULL) + mount_volume (sidebar->context_row, volume); + + g_object_unref (volume); +} + +static GMountOperation * +get_mount_operation (NautilusGtkPlacesSidebar *sidebar) +{ + GMountOperation *mount_op; + + mount_op = gtk_mount_operation_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (sidebar)))); + + emit_mount_operation (sidebar, mount_op); + + return mount_op; +} + +static GMountOperation * +get_unmount_operation (NautilusGtkPlacesSidebar *sidebar) +{ + GMountOperation *mount_op; + + mount_op = gtk_mount_operation_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (sidebar)))); + + emit_unmount_operation (sidebar, mount_op); + + return mount_op; +} + +/* Returns TRUE if file1 is prefix of file2 or if both files have the + * same path + */ +static gboolean +file_prefix_or_same (GFile *file1, + GFile *file2) +{ + return g_file_has_prefix (file1, file2) || + g_file_equal (file1, file2); +} + +static gboolean +is_current_location_on_volume (NautilusGtkPlacesSidebar *sidebar, + GMount *mount, + GVolume *volume, + GDrive *drive) +{ + gboolean current_location_on_volume; + GFile *mount_default_location; + GMount *mount_for_volume; + GList *volumes_for_drive; + GList *volume_for_drive; + + current_location_on_volume = FALSE; + + if (sidebar->current_location != NULL) + { + if (mount != NULL) + { + mount_default_location = g_mount_get_default_location (mount); + current_location_on_volume = file_prefix_or_same (sidebar->current_location, + mount_default_location); + + g_object_unref (mount_default_location); + } + /* This code path is probably never reached since mount always exists, + * and if it doesn't exists we don't offer a way to eject a volume or + * drive in the UI. Do it for defensive programming + */ + else if (volume != NULL) + { + mount_for_volume = g_volume_get_mount (volume); + if (mount_for_volume != NULL) + { + mount_default_location = g_mount_get_default_location (mount_for_volume); + current_location_on_volume = file_prefix_or_same (sidebar->current_location, + mount_default_location); + + g_object_unref (mount_default_location); + g_object_unref (mount_for_volume); + } + } + /* This code path is probably never reached since mount always exists, + * and if it doesn't exists we don't offer a way to eject a volume or + * drive in the UI. Do it for defensive programming + */ + else if (drive != NULL) + { + volumes_for_drive = g_drive_get_volumes (drive); + for (volume_for_drive = volumes_for_drive; volume_for_drive != NULL; volume_for_drive = volume_for_drive->next) + { + mount_for_volume = g_volume_get_mount (volume_for_drive->data); + if (mount_for_volume != NULL) + { + mount_default_location = g_mount_get_default_location (mount_for_volume); + current_location_on_volume = file_prefix_or_same (sidebar->current_location, + mount_default_location); + + g_object_unref (mount_default_location); + g_object_unref (mount_for_volume); + + if (current_location_on_volume) + break; + } + } + g_list_free_full (volumes_for_drive, g_object_unref); + } + } + + return current_location_on_volume; +} + +static void +do_unmount (GMount *mount, + NautilusGtkPlacesSidebar *sidebar) +{ + if (mount != NULL) + { + GMountOperation *mount_op; + GtkWindow *parent; + + if (is_current_location_on_volume (sidebar, mount, NULL, NULL)) + open_home (sidebar); + + mount_op = get_unmount_operation (sidebar); + parent = gtk_mount_operation_get_parent (GTK_MOUNT_OPERATION (mount_op)); + nautilus_file_operations_unmount_mount_full (parent, mount, mount_op, + FALSE, TRUE, NULL, NULL); + g_object_unref (mount_op); + } +} + +static void +unmount_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GMount *mount; + + g_object_get (sidebar->context_row, + "mount", &mount, + NULL); + + do_unmount (mount, sidebar); + + if (mount) + g_object_unref (mount); +} + +static void +drive_stop_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar; + GError *error; + char *primary; + char *name; + + sidebar = user_data; + + error = NULL; + if (!g_drive_stop_finish (G_DRIVE (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED) + { + name = g_drive_get_name (G_DRIVE (source_object)); + primary = g_strdup_printf (_("Unable to stop “%s”"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } + + g_object_unref (sidebar); +} + +static void +drive_eject_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar; + GError *error; + char *primary; + char *name; + + sidebar = user_data; + + error = NULL; + if (!g_drive_eject_with_operation_finish (G_DRIVE (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED) + { + name = g_drive_get_name (G_DRIVE (source_object)); + primary = g_strdup_printf (_("Unable to eject “%s”"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } + + g_object_unref (sidebar); +} + +static void +volume_eject_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar; + GError *error; + char *primary; + char *name; + + sidebar = user_data; + + error = NULL; + if (!g_volume_eject_with_operation_finish (G_VOLUME (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED) + { + name = g_volume_get_name (G_VOLUME (source_object)); + primary = g_strdup_printf (_("Unable to eject %s"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } + + g_object_unref (sidebar); +} + +static void +do_eject (GMount *mount, + GVolume *volume, + GDrive *drive, + NautilusGtkPlacesSidebar *sidebar) +{ + GMountOperation *mount_op; + GtkWindow *parent; + + mount_op = get_unmount_operation (sidebar); + + if (is_current_location_on_volume (sidebar, mount, volume, drive)) + open_home (sidebar); + + if (mount != NULL) + { + parent = gtk_mount_operation_get_parent (GTK_MOUNT_OPERATION (mount_op)); + nautilus_file_operations_unmount_mount_full (parent, mount, mount_op, + TRUE, TRUE, NULL, NULL); + } + /* This code path is probably never reached since mount always exists, + * and if it doesn't exists we don't offer a way to eject a volume or + * drive in the UI. Do it for defensive programming + */ + else if (volume != NULL) + g_volume_eject_with_operation (volume, 0, mount_op, NULL, volume_eject_cb, + g_object_ref (sidebar)); + /* This code path is probably never reached since mount always exists, + * and if it doesn't exists we don't offer a way to eject a volume or + * drive in the UI. Do it for defensive programming + */ + else if (drive != NULL) + { + if (g_drive_can_stop (drive)) + g_drive_stop (drive, 0, mount_op, NULL, drive_stop_cb, + g_object_ref (sidebar)); + else + g_drive_eject_with_operation (drive, 0, mount_op, NULL, drive_eject_cb, + g_object_ref (sidebar)); + } + g_object_unref (mount_op); +} + +static void +eject_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GMount *mount; + GVolume *volume; + GDrive *drive; + + g_object_get (sidebar->context_row, + "mount", &mount, + "volume", &volume, + "drive", &drive, + NULL); + + do_eject (mount, volume, drive, sidebar); + + if (mount) + g_object_unref (mount); + if (volume) + g_object_unref (volume); + if (drive) + g_object_unref (drive); +} + +static gboolean +eject_or_unmount_bookmark (NautilusGtkSidebarRow *row) +{ + gboolean can_unmount, can_eject; + GMount *mount; + GVolume *volume; + GDrive *drive; + gboolean ret; + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, + "sidebar", &sidebar, + "mount", &mount, + "volume", &volume, + "drive", &drive, + NULL); + ret = FALSE; + + check_unmount_and_eject (mount, volume, drive, &can_unmount, &can_eject); + /* if we can eject, it has priority over unmount */ + if (can_eject) + { + do_eject (mount, volume, drive, sidebar); + ret = TRUE; + } + else if (can_unmount) + { + do_unmount (mount, sidebar); + ret = TRUE; + } + + g_object_unref (sidebar); + if (mount) + g_object_unref (mount); + if (volume) + g_object_unref (volume); + if (drive) + g_object_unref (drive); + + return ret; +} + +static gboolean +eject_or_unmount_selection (NautilusGtkPlacesSidebar *sidebar) +{ + gboolean ret; + GtkListBoxRow *row; + + row = gtk_list_box_get_selected_row (GTK_LIST_BOX (sidebar->list_box)); + ret = eject_or_unmount_bookmark (NAUTILUS_GTK_SIDEBAR_ROW (row)); + + return ret; +} + +static void +drive_poll_for_media_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar; + GError *error; + char *primary; + char *name; + + sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (user_data); + + error = NULL; + if (!g_drive_poll_for_media_finish (G_DRIVE (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED) + { + name = g_drive_get_name (G_DRIVE (source_object)); + primary = g_strdup_printf (_("Unable to poll “%s” for media changes"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } + + g_object_unref (sidebar); +} + +static void +rescan_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GDrive *drive; + + g_object_get (sidebar->context_row, + "drive", &drive, + NULL); + + if (drive != NULL) + { + g_drive_poll_for_media (drive, NULL, drive_poll_for_media_cb, g_object_ref (sidebar)); + g_object_unref (drive); + } +} + +static void +drive_start_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar; + GError *error; + char *primary; + char *name; + + sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (user_data); + + error = NULL; + if (!g_drive_start_finish (G_DRIVE (source_object), res, &error)) + { + if (error->code != G_IO_ERROR_FAILED_HANDLED) + { + name = g_drive_get_name (G_DRIVE (source_object)); + primary = g_strdup_printf (_("Unable to start “%s”"), name); + g_free (name); + emit_show_error_message (sidebar, primary, error->message); + g_free (primary); + } + g_error_free (error); + } + + g_object_unref (sidebar); +} + +static void +start_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GDrive *drive; + + g_object_get (sidebar->context_row, + "drive", &drive, + NULL); + + if (drive != NULL) + { + GMountOperation *mount_op; + + mount_op = get_mount_operation (sidebar); + + g_drive_start (drive, G_DRIVE_START_NONE, mount_op, NULL, drive_start_cb, g_object_ref (sidebar)); + + g_object_unref (mount_op); + g_object_unref (drive); + } +} + +static void +stop_shortcut_cb (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + GDrive *drive; + + g_object_get (sidebar->context_row, + "drive", &drive, + NULL); + + if (drive != NULL) + { + GMountOperation *mount_op; + + mount_op = get_unmount_operation (sidebar); + g_drive_stop (drive, G_MOUNT_UNMOUNT_NONE, mount_op, NULL, drive_stop_cb, + g_object_ref (sidebar)); + + g_object_unref (mount_op); + g_object_unref (drive); + } +} + +static gboolean +on_key_pressed (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + NautilusGtkPlacesSidebar *sidebar) +{ + guint modifiers; + GtkListBoxRow *row; + + row = gtk_list_box_get_selected_row (GTK_LIST_BOX (sidebar->list_box)); + if (row) + { + modifiers = gtk_accelerator_get_default_mod_mask (); + + if (keyval == GDK_KEY_Return || + keyval == GDK_KEY_KP_Enter || + keyval == GDK_KEY_ISO_Enter || + keyval == GDK_KEY_space) + { + NautilusGtkPlacesOpenFlags open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + + if ((state & modifiers) == GDK_SHIFT_MASK) + open_flags = NAUTILUS_GTK_PLACES_OPEN_NEW_TAB; + else if ((state & modifiers) == GDK_CONTROL_MASK) + open_flags = NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW; + + open_row (NAUTILUS_GTK_SIDEBAR_ROW (row), open_flags); + + return TRUE; + } + + if (keyval == GDK_KEY_Down && + (state & modifiers) == GDK_ALT_MASK) + return eject_or_unmount_selection (sidebar); + + if ((keyval == GDK_KEY_Delete || + keyval == GDK_KEY_KP_Delete) && + (state & modifiers) == 0) + { + remove_bookmark (NAUTILUS_GTK_SIDEBAR_ROW (row)); + return TRUE; + } + + if ((keyval == GDK_KEY_F2) && + (state & modifiers) == 0) + { + rename_bookmark (NAUTILUS_GTK_SIDEBAR_ROW (row)); + return TRUE; + } + + if ((keyval == GDK_KEY_Menu) || + ((keyval == GDK_KEY_F10) && + (state & modifiers) == GDK_SHIFT_MASK)) + { + popup_menu_cb (NAUTILUS_GTK_SIDEBAR_ROW (row)); + return TRUE; + } + } + + return FALSE; +} + +static void +format_cb (GSimpleAction *action, + GVariant *variant, + gpointer data) +{ + NautilusGtkPlacesSidebar *sidebar = data; + g_autoptr (GVolume) volume = NULL; + g_autofree gchar *device_identifier = NULL; + GVariant *parameters; + + g_object_get (sidebar->context_row, "volume", &volume, NULL); + device_identifier = g_volume_get_identifier (volume, + G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE); + + parameters = g_variant_new_parsed ("(objectpath '/org/gnome/DiskUtility', @aay [], " + "{'options': <{'block-device': <%s>, " + "'format-device': <true> }> })", device_identifier); + + nautilus_dbus_launcher_call (nautilus_dbus_launcher_get(), + NAUTILUS_DBUS_LAUNCHER_DISKS, + "CommandLine", + parameters, + GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (sidebar)))); + +} + +static GActionEntry entries[] = { + { "open", open_shortcut_cb, "i", NULL, NULL }, + { "open-other", open_shortcut_cb, "i", NULL, NULL }, + { "bookmark", add_shortcut_cb, NULL, NULL, NULL }, + { "remove", remove_shortcut_cb, NULL, NULL, NULL }, + { "rename", rename_shortcut_cb, NULL, NULL, NULL }, + { "mount", mount_shortcut_cb, NULL, NULL, NULL }, + { "unmount", unmount_shortcut_cb, NULL, NULL, NULL }, + { "eject", eject_shortcut_cb, NULL, NULL, NULL }, + { "rescan", rescan_shortcut_cb, NULL, NULL, NULL }, + { "start", start_shortcut_cb, NULL, NULL, NULL }, + { "stop", stop_shortcut_cb, NULL, NULL, NULL }, + { "properties", properties_cb, NULL, NULL, NULL }, + { "empty-trash", empty_trash_cb, NULL, NULL, NULL }, + { "format", format_cb, NULL, NULL, NULL }, +}; + +static gboolean +should_show_format_command (GVolume *volume, + gchar *uri) +{ + g_autofree gchar *unix_device_id = NULL; + gboolean disks_available; + + if (volume == NULL || !G_IS_VOLUME (volume) || g_str_has_prefix (uri, "mtp://")) + return FALSE; + + unix_device_id = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE); + disks_available = nautilus_dbus_launcher_is_available (nautilus_dbus_launcher_get(), + NAUTILUS_DBUS_LAUNCHER_DISKS); + + return unix_device_id != NULL && disks_available; +} + +static void +on_row_popover_destroy (GtkWidget *row_popover, + NautilusGtkPlacesSidebar *sidebar) +{ + if (sidebar) + sidebar->popover = NULL; +} + +static void +build_popup_menu_using_gmenu (NautilusGtkSidebarRow *row) +{ + CloudProvidersAccount *cloud_provider_account; + NautilusGtkPlacesSidebar *sidebar; + GMenuModel *cloud_provider_menu; + GActionGroup *cloud_provider_action_group; + + g_object_get (row, + "sidebar", &sidebar, + "cloud-provider-account", &cloud_provider_account, + NULL); + + /* Cloud provider account */ + if (cloud_provider_account) + { + GMenu *menu = g_menu_new (); + GMenuItem *item; + item = g_menu_item_new (_("_Open"), "row.open"); + g_menu_item_set_action_and_target_value (item, "row.open", + g_variant_new_int32 (NAUTILUS_GTK_PLACES_OPEN_NORMAL)); + g_menu_append_item (menu, item); + g_object_unref (item); + + if (sidebar->open_flags & NAUTILUS_GTK_PLACES_OPEN_NEW_TAB) + { + item = g_menu_item_new (_("Open in New _Tab"), "row.open-other"); + g_menu_item_set_action_and_target_value (item, "row.open-other", g_variant_new_int32(NAUTILUS_GTK_PLACES_OPEN_NEW_TAB)); + g_menu_append_item (menu, item); + g_object_unref (item); + } + if (sidebar->open_flags & NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW) + { + item = g_menu_item_new (_("Open in New _Window"), "row.open-other"); + g_menu_item_set_action_and_target_value (item, "row.open-other", g_variant_new_int32(NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW)); + g_menu_append_item (menu, item); + g_object_unref (item); + } + cloud_provider_menu = cloud_providers_account_get_menu_model (cloud_provider_account); + cloud_provider_action_group = cloud_providers_account_get_action_group (cloud_provider_account); + if (cloud_provider_menu != NULL && cloud_provider_action_group != NULL) + { + g_menu_append_section (menu, NULL, cloud_provider_menu); + gtk_widget_insert_action_group (GTK_WIDGET (sidebar), + "cloudprovider", + G_ACTION_GROUP (cloud_provider_action_group)); + } + if (sidebar->popover) + gtk_widget_unparent (sidebar->popover); + + sidebar->popover = gtk_popover_menu_new_from_model_full (G_MENU_MODEL (menu), + GTK_POPOVER_MENU_NESTED); + g_object_unref (menu); + gtk_widget_set_parent (sidebar->popover, GTK_WIDGET (sidebar)); + gtk_widget_set_halign (sidebar->popover, GTK_ALIGN_START); + gtk_popover_set_has_arrow (GTK_POPOVER (sidebar->popover), FALSE); + g_signal_connect (sidebar->popover, "destroy", + G_CALLBACK (on_row_popover_destroy), sidebar); + + setup_popover_shadowing (sidebar->popover, sidebar); + + g_object_unref (sidebar); + g_object_unref (cloud_provider_account); + } +} + +/* Constructs the popover for the sidebar row if needed */ +static void +create_row_popover (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesPlaceType type; + GMenu *menu, *section; + GMenuItem *item; + GMount *mount; + GVolume *volume; + GDrive *drive; + GAction *action; + gboolean show_unmount, show_eject; + gboolean show_stop; + g_autofree gchar *uri = NULL; + g_autoptr (GFile) file = NULL; + gboolean show_properties; + g_autoptr (GFile) trash = NULL; + gboolean is_trash; + CloudProvidersAccount *cloud_provider_account; + + g_object_get (row, + "place-type", &type, + "drive", &drive, + "volume", &volume, + "mount", &mount, + "uri", &uri, + NULL); + + check_unmount_and_eject (mount, volume, drive, &show_unmount, &show_eject); + if (uri != NULL) + { + file = g_file_new_for_uri (uri); + trash = g_file_new_for_uri("trash:///"); + is_trash = g_file_equal (trash, file); + show_properties = (g_file_is_native (file) || is_trash || mount != NULL); + } + else + { + show_properties = FALSE; + is_trash = FALSE; + } + + g_object_get (row, "cloud-provider-account", &cloud_provider_account, NULL); + + if (cloud_provider_account) + { + build_popup_menu_using_gmenu (row); + return; + } + + action = g_action_map_lookup_action (G_ACTION_MAP (sidebar->row_actions), "remove"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), (type == NAUTILUS_GTK_PLACES_BOOKMARK)); + action = g_action_map_lookup_action (G_ACTION_MAP (sidebar->row_actions), "rename"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), (type == NAUTILUS_GTK_PLACES_BOOKMARK || + type == NAUTILUS_GTK_PLACES_XDG_DIR)); + action = g_action_map_lookup_action (G_ACTION_MAP (sidebar->row_actions), "bookmark"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), (type == NAUTILUS_GTK_PLACES_MOUNTED_VOLUME)); + action = g_action_map_lookup_action (G_ACTION_MAP (sidebar->row_actions), "open"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), !gtk_list_box_row_is_selected (GTK_LIST_BOX_ROW (row))); + action = g_action_map_lookup_action (G_ACTION_MAP (sidebar->row_actions), "empty-trash"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), !nautilus_trash_monitor_is_empty()); + + menu = g_menu_new (); + section = g_menu_new (); + + item = g_menu_item_new (_("_Open"), "row.open"); + g_menu_item_set_action_and_target_value (item, "row.open", + g_variant_new_int32 (NAUTILUS_GTK_PLACES_OPEN_NORMAL)); + g_menu_append_item (section, item); + g_object_unref (item); + + if (sidebar->open_flags & NAUTILUS_GTK_PLACES_OPEN_NEW_TAB) + { + item = g_menu_item_new (_("Open in New _Tab"), "row.open-other"); + g_menu_item_set_action_and_target_value (item, "row.open-other", + g_variant_new_int32 (NAUTILUS_GTK_PLACES_OPEN_NEW_TAB)); + g_menu_append_item (section, item); + g_object_unref (item); + } + + if (sidebar->open_flags & NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW) + { + item = g_menu_item_new (_("Open in New _Window"), "row.open-other"); + g_menu_item_set_action_and_target_value (item, "row.open-other", + g_variant_new_int32 (NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW)); + g_menu_append_item (section, item); + g_object_unref (item); + } + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + section = g_menu_new (); + item = g_menu_item_new (_("Add to _Bookmarks"), "row.bookmark"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("_Remove from Bookmarks"), "row.remove"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("_Rename"), "row.rename"); + g_menu_append_item (section, item); + g_object_unref (item); + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + if (is_trash) { + section = g_menu_new (); + item = g_menu_item_new (_("Empty Trash"), "row.empty-trash"); + g_menu_append_item (section, item); + g_object_unref (item); + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + } + + section = g_menu_new (); + + if (volume != NULL && mount == NULL && + g_volume_can_mount (volume)) + { + item = g_menu_item_new (_("_Mount"), "row.mount"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + show_stop = (drive != NULL && g_drive_can_stop (drive)); + + if (show_unmount && !show_stop) + { + item = g_menu_item_new (_("_Unmount"), "row.unmount"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + if (show_eject) + { + item = g_menu_item_new (_("_Eject"), "row.eject"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + if (drive != NULL && + g_drive_is_media_removable (drive) && + !g_drive_is_media_check_automatic (drive) && + g_drive_can_poll_for_media (drive)) + { + item = g_menu_item_new (_("_Detect Media"), "row.rescan"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + if (drive != NULL && + (g_drive_can_start (drive) || g_drive_can_start_degraded (drive))) + { + const guint ss_type = g_drive_get_start_stop_type (drive); + const char *start_label = _("_Start"); + + if (ss_type == G_DRIVE_START_STOP_TYPE_SHUTDOWN) start_label = _("_Power On"); + else if (ss_type == G_DRIVE_START_STOP_TYPE_NETWORK) start_label = _("_Connect Drive"); + else if (ss_type == G_DRIVE_START_STOP_TYPE_MULTIDISK) start_label = _("_Start Multi-disk Device"); + else if (ss_type == G_DRIVE_START_STOP_TYPE_PASSWORD) start_label = _("_Unlock Device"); + + item = g_menu_item_new (start_label, "row.start"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + if (show_stop && !show_unmount) + { + const guint ss_type = g_drive_get_start_stop_type (drive); + const char *stop_label = _("_Stop"); + + if (ss_type == G_DRIVE_START_STOP_TYPE_SHUTDOWN) stop_label = _("_Safely Remove Drive"); + else if (ss_type == G_DRIVE_START_STOP_TYPE_NETWORK) stop_label = _("_Disconnect Drive"); + else if (ss_type == G_DRIVE_START_STOP_TYPE_MULTIDISK) stop_label = _("_Stop Multi-disk Device"); + else if (ss_type == G_DRIVE_START_STOP_TYPE_PASSWORD) stop_label = _("_Lock Device"); + + item = g_menu_item_new (stop_label, "row.stop"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + if (should_show_format_command (volume, uri)) + { + item = g_menu_item_new (_("Format…"), "row.format"); + g_menu_append_item (section, item); + g_object_unref (item); + } + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + if (show_properties) { + section = g_menu_new (); + item = g_menu_item_new (_("Properties"), "row.properties"); + g_menu_append_item (section, item); + g_object_unref (item); + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + } + + sidebar->popover = gtk_popover_menu_new_from_model (G_MENU_MODEL (menu)); + g_object_unref (menu); + gtk_widget_set_parent (sidebar->popover, GTK_WIDGET (sidebar)); + gtk_widget_set_halign (sidebar->popover, GTK_ALIGN_START); + gtk_popover_set_has_arrow (GTK_POPOVER (sidebar->popover), FALSE); + g_signal_connect (sidebar->popover, "destroy", G_CALLBACK (on_row_popover_destroy), sidebar); + + setup_popover_shadowing (sidebar->popover, sidebar); +} + +static void +show_row_popover (NautilusGtkSidebarRow *row, + double x, + double y) +{ + NautilusGtkPlacesSidebar *sidebar; + double x_in_sidebar, y_in_sidebar; + + g_object_get (row, "sidebar", &sidebar, NULL); + + g_clear_pointer (&sidebar->popover, gtk_widget_unparent); + + create_row_popover (sidebar, row); + + if (x == -1 && y == -1) + _popover_set_pointing_to_widget (GTK_POPOVER (sidebar->popover), GTK_WIDGET (row)); + else + { + gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (sidebar), + x, y, &x_in_sidebar, &y_in_sidebar); + gtk_popover_set_pointing_to (GTK_POPOVER (sidebar->popover), + &(GdkRectangle){x_in_sidebar, y_in_sidebar, 0, 0}); + } + + sidebar->context_row = row; + gtk_popover_popup (GTK_POPOVER (sidebar->popover)); + + g_object_unref (sidebar); +} + +static void +on_row_activated (GtkListBox *list_box, + GtkListBoxRow *row, + gpointer user_data) +{ + NautilusGtkSidebarRow *selected_row; + + /* Avoid to open a location if the user is dragging. Changing the location + * while dragging usually makes clients changing the view of the files, which + * is confusing while the user has the attention on the drag + */ + if (NAUTILUS_GTK_PLACES_SIDEBAR (user_data)->dragging_over) + return; + + selected_row = NAUTILUS_GTK_SIDEBAR_ROW (gtk_list_box_get_selected_row (list_box)); + open_row (selected_row, 0); +} + +static void +on_row_pressed (GtkGestureClick *gesture, + int n_press, + double x, + double y, + NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesSidebar *sidebar; + NautilusGtkPlacesSectionType section_type; + NautilusGtkPlacesPlaceType row_type; + + g_object_get (row, + "sidebar", &sidebar, + "section_type", §ion_type, + "place-type", &row_type, + NULL); + + if (section_type == NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS) + { + sidebar->drag_row = GTK_WIDGET (row); + sidebar->drag_row_x = (int)x; + sidebar->drag_row_y = (int)y; + } + + g_object_unref (sidebar); +} + +static void +on_row_released (GtkGestureClick *gesture, + int n_press, + double x, + double y, + NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesSidebar *sidebar; + NautilusGtkPlacesSectionType section_type; + NautilusGtkPlacesPlaceType row_type; + guint button, state; + + g_object_get (row, + "sidebar", &sidebar, + "section_type", §ion_type, + "place-type", &row_type, + NULL); + + button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)); + state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture)); + + if (row) + { + if (button == 2) + { + NautilusGtkPlacesOpenFlags open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + + open_flags = (state & GDK_CONTROL_MASK) ? + NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW : + NAUTILUS_GTK_PLACES_OPEN_NEW_TAB; + + open_row (NAUTILUS_GTK_SIDEBAR_ROW (row), open_flags); + gtk_gesture_set_state (GTK_GESTURE (gesture), + GTK_EVENT_SEQUENCE_CLAIMED); + } + else if (button == 3) + { + if (row_type != NAUTILUS_GTK_PLACES_CONNECT_TO_SERVER) + show_row_popover (NAUTILUS_GTK_SIDEBAR_ROW (row), x, y); + } + } +} + +static void +on_row_dragged (GtkGestureDrag *gesture, + double x, + double y, + NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesSidebar *sidebar; + + g_object_get (row, "sidebar", &sidebar, NULL); + + if (sidebar->drag_row == NULL || sidebar->dragging_over) + { + g_object_unref (sidebar); + return; + } + + if (gtk_drag_check_threshold (GTK_WIDGET (row), 0, 0, x, y)) + { + double start_x, start_y; + double drag_x, drag_y; + GdkContentProvider *content; + GdkSurface *surface; + GdkDevice *device; + GtkAllocation allocation; + GtkWidget *drag_widget; + GdkDrag *drag; + + gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y); + gtk_widget_translate_coordinates (GTK_WIDGET (row), + GTK_WIDGET (sidebar), + start_x, start_y, + &drag_x, &drag_y); + + sidebar->dragging_over = TRUE; + + content = gdk_content_provider_new_typed (NAUTILUS_TYPE_GTK_SIDEBAR_ROW, sidebar->drag_row); + + surface = gtk_native_get_surface (gtk_widget_get_native (GTK_WIDGET (sidebar))); + device = gtk_gesture_get_device (GTK_GESTURE (gesture)); + + drag = gdk_drag_begin (surface, device, content, GDK_ACTION_MOVE, drag_x, drag_y); + + g_object_unref (content); + + g_signal_connect (drag, "dnd-finished", G_CALLBACK (dnd_finished_cb), sidebar); + g_signal_connect (drag, "cancel", G_CALLBACK (dnd_cancel_cb), sidebar); + + gtk_widget_get_allocation (sidebar->drag_row, &allocation); + gtk_widget_hide (sidebar->drag_row); + + drag_widget = GTK_WIDGET (nautilus_gtk_sidebar_row_clone (NAUTILUS_GTK_SIDEBAR_ROW (sidebar->drag_row))); + sidebar->drag_row_height = allocation.height; + gtk_widget_set_size_request (drag_widget, allocation.width, allocation.height); + gtk_widget_set_opacity (drag_widget, 0.8); + + gtk_drag_icon_set_child (GTK_DRAG_ICON (gtk_drag_icon_get_for_drag (drag)), drag_widget); + + g_object_unref (drag); + } + + g_object_unref (sidebar); +} + +static void +popup_menu_cb (NautilusGtkSidebarRow *row) +{ + NautilusGtkPlacesPlaceType row_type; + + g_object_get (row, "place-type", &row_type, NULL); + + if (row_type != NAUTILUS_GTK_PLACES_CONNECT_TO_SERVER) + show_row_popover (row, -1, -1); +} + +static void +long_press_cb (GtkGesture *gesture, + double x, + double y, + NautilusGtkPlacesSidebar *sidebar) +{ + GtkWidget *row; + + row = GTK_WIDGET (gtk_list_box_get_row_at_y (GTK_LIST_BOX (sidebar->list_box), y)); + if (NAUTILUS_IS_GTK_SIDEBAR_ROW (row)) + popup_menu_cb (NAUTILUS_GTK_SIDEBAR_ROW (row)); +} + +static int +list_box_sort_func (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + NautilusGtkPlacesSectionType section_type_1, section_type_2; + NautilusGtkPlacesPlaceType place_type_1, place_type_2; + char *label_1, *label_2; + int index_1, index_2; + int retval = 0; + + g_object_get (row1, + "label", &label_1, + "place-type", &place_type_1, + "section-type", §ion_type_1, + "order-index", &index_1, + NULL); + g_object_get (row2, + "label", &label_2, + "place-type", &place_type_2, + "section-type", §ion_type_2, + "order-index", &index_2, + NULL); + + /* Always last position for "connect to server" */ + if (place_type_1 == NAUTILUS_GTK_PLACES_CONNECT_TO_SERVER) + { + retval = 1; + } + else if (place_type_2 == NAUTILUS_GTK_PLACES_CONNECT_TO_SERVER) + { + retval = -1; + } + else + { + if (section_type_1 == section_type_2) + { + if ((section_type_1 == NAUTILUS_GTK_PLACES_SECTION_COMPUTER && + place_type_1 == place_type_2 && + place_type_1 == NAUTILUS_GTK_PLACES_XDG_DIR) || + section_type_1 == NAUTILUS_GTK_PLACES_SECTION_MOUNTS) + { + retval = g_utf8_collate (label_1, label_2); + } + else if ((place_type_1 == NAUTILUS_GTK_PLACES_BOOKMARK || place_type_2 == NAUTILUS_GTK_PLACES_DROP_FEEDBACK) && + (place_type_1 == NAUTILUS_GTK_PLACES_DROP_FEEDBACK || place_type_2 == NAUTILUS_GTK_PLACES_BOOKMARK)) + { + retval = index_1 - index_2; + } + /* We order the bookmarks sections based on the bookmark index that we + * set on the row as an order-index property, but we have to deal with + * the placeholder row wanted to be between two consecutive bookmarks, + * with two consecutive order-index values which is the usual case. + * For that, in the list box sort func we give priority to the placeholder row, + * that means that if the index-order is the same as another bookmark + * the placeholder row goes before. However if we want to show it after + * the current row, for instance when the cursor is in the lower half + * of the row, we need to increase the order-index. + */ + else if (place_type_1 == NAUTILUS_GTK_PLACES_BOOKMARK_PLACEHOLDER && place_type_2 == NAUTILUS_GTK_PLACES_BOOKMARK) + { + if (index_1 == index_2) + retval = index_1 - index_2 - 1; + else + retval = index_1 - index_2; + } + else if (place_type_1 == NAUTILUS_GTK_PLACES_BOOKMARK && place_type_2 == NAUTILUS_GTK_PLACES_BOOKMARK_PLACEHOLDER) + { + if (index_1 == index_2) + retval = index_1 - index_2 + 1; + else + retval = index_1 - index_2; + } + } + else + { + /* Order by section. That means the order in the enum of section types + * define the actual order of them in the list */ + retval = section_type_1 - section_type_2; + } + } + + g_free (label_1); + g_free (label_2); + + return retval; +} + +static void +update_hostname (NautilusGtkPlacesSidebar *sidebar) +{ + GVariant *variant; + gsize len; + const char *hostname; + + if (sidebar->hostnamed_proxy == NULL) + return; + + variant = g_dbus_proxy_get_cached_property (sidebar->hostnamed_proxy, + "PrettyHostname"); + if (variant == NULL) + return; + + hostname = g_variant_get_string (variant, &len); + if (len > 0 && + g_strcmp0 (sidebar->hostname, hostname) != 0) + { + g_free (sidebar->hostname); + sidebar->hostname = g_strdup (hostname); + update_places (sidebar); + } + + g_variant_unref (variant); +} + +static void +hostname_proxy_new_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar = user_data; + GError *error = NULL; + GDBusProxy *proxy; + + proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + sidebar->hostnamed_proxy = proxy; + g_clear_object (&sidebar->hostnamed_cancellable); + + if (error != NULL) + { + g_debug ("Failed to create D-Bus proxy: %s", error->message); + g_error_free (error); + return; + } + + g_signal_connect_swapped (sidebar->hostnamed_proxy, + "g-properties-changed", + G_CALLBACK (update_hostname), + sidebar); + update_hostname (sidebar); +} + +static void +create_volume_monitor (NautilusGtkPlacesSidebar *sidebar) +{ + g_assert (sidebar->volume_monitor == NULL); + + sidebar->volume_monitor = g_volume_monitor_get (); + + g_signal_connect_object (sidebar->volume_monitor, "volume_added", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "volume_removed", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "volume_changed", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "mount_added", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "mount_removed", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "mount_changed", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "drive_disconnected", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "drive_connected", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); + g_signal_connect_object (sidebar->volume_monitor, "drive_changed", + G_CALLBACK (update_places), sidebar, G_CONNECT_SWAPPED); +} + +static void +shell_shows_desktop_changed (GtkSettings *settings, + GParamSpec *pspec, + gpointer user_data) +{ + NautilusGtkPlacesSidebar *sidebar = user_data; + gboolean show_desktop; + + g_assert (settings == sidebar->gtk_settings); + + /* Check if the user explicitly set this and, if so, don't change it. */ + if (sidebar->show_desktop_set) + return; + + g_object_get (settings, "gtk-shell-shows-desktop", &show_desktop, NULL); + + if (show_desktop != sidebar->show_desktop) + { + sidebar->show_desktop = show_desktop; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_DESKTOP]); + } +} + +static void +nautilus_gtk_places_sidebar_init (NautilusGtkPlacesSidebar *sidebar) +{ + GtkDropTarget *target; + gboolean show_desktop; + GtkEventController *controller; + GtkGesture *gesture; + + sidebar->cancellable = g_cancellable_new (); + + sidebar->show_trash = TRUE; + sidebar->show_other_locations = TRUE; + sidebar->show_recent = TRUE; + sidebar->show_desktop = TRUE; + + sidebar->shortcuts = g_list_store_new (G_TYPE_FILE); + + create_volume_monitor (sidebar); + + sidebar->open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + + sidebar->bookmarks_manager = _nautilus_gtk_bookmarks_manager_new ((GtkBookmarksChangedFunc)update_places, sidebar); + + g_signal_connect_object (nautilus_trash_monitor_get (), "trash-state-changed", + G_CALLBACK (update_trash_icon), sidebar, + G_CONNECT_SWAPPED); + + sidebar->swin = gtk_scrolled_window_new (); + gtk_widget_set_parent (sidebar->swin, GTK_WIDGET (sidebar)); + gtk_widget_set_size_request (sidebar->swin, 140, 280); + + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sidebar->swin), + GTK_POLICY_NEVER, + GTK_POLICY_AUTOMATIC); + + /* list box */ + sidebar->list_box = gtk_list_box_new (); + gtk_widget_add_css_class (sidebar->list_box, "navigation-sidebar"); + + gtk_list_box_set_header_func (GTK_LIST_BOX (sidebar->list_box), + list_box_header_func, sidebar, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (sidebar->list_box), + list_box_sort_func, NULL, NULL); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (sidebar->list_box), GTK_SELECTION_SINGLE); + gtk_list_box_set_activate_on_single_click (GTK_LIST_BOX (sidebar->list_box), TRUE); + + g_signal_connect (sidebar->list_box, "row-activated", + G_CALLBACK (on_row_activated), sidebar); + + controller = gtk_event_controller_key_new (); + g_signal_connect (controller, "key-pressed", + G_CALLBACK (on_key_pressed), sidebar); + gtk_widget_add_controller (sidebar->list_box, controller); + + gesture = gtk_gesture_long_press_new (); + gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), TRUE); + g_signal_connect (gesture, "pressed", + G_CALLBACK (long_press_cb), sidebar); + gtk_widget_add_controller (GTK_WIDGET (sidebar), GTK_EVENT_CONTROLLER (gesture)); + + /* DND support */ + target = gtk_drop_target_new (G_TYPE_INVALID, GDK_ACTION_ALL); + gtk_drop_target_set_preload (target, TRUE); + gtk_drop_target_set_gtypes (target, (GType[2]) { NAUTILUS_TYPE_GTK_SIDEBAR_ROW, GDK_TYPE_FILE_LIST }, 2); + g_signal_connect (target, "enter", G_CALLBACK (drag_motion_callback), sidebar); + g_signal_connect (target, "motion", G_CALLBACK (drag_motion_callback), sidebar); + g_signal_connect (target, "drop", G_CALLBACK (drag_drop_callback), sidebar); + g_signal_connect (target, "leave", G_CALLBACK (drag_leave_callback), sidebar); + gtk_widget_add_controller (sidebar->list_box, GTK_EVENT_CONTROLLER (target)); + + sidebar->drag_row = NULL; + sidebar->row_placeholder = NULL; + sidebar->dragging_over = FALSE; + + gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sidebar->swin), sidebar->list_box); + + sidebar->hostname = g_strdup (_("Computer")); + sidebar->hostnamed_cancellable = g_cancellable_new (); + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES, + NULL, + "org.freedesktop.hostname1", + "/org/freedesktop/hostname1", + "org.freedesktop.hostname1", + sidebar->hostnamed_cancellable, + hostname_proxy_new_cb, + sidebar); + + sidebar->drop_state = DROP_STATE_NORMAL; + + /* Don't bother trying to trace this across hierarchy changes... */ + sidebar->gtk_settings = gtk_settings_get_default (); + g_signal_connect (sidebar->gtk_settings, "notify::gtk-shell-shows-desktop", + G_CALLBACK (shell_shows_desktop_changed), sidebar); + g_object_get (sidebar->gtk_settings, "gtk-shell-shows-desktop", &show_desktop, NULL); + sidebar->show_desktop = show_desktop; + + /* Cloud providers */ + sidebar->cloud_manager = cloud_providers_collector_dup_singleton (); + g_signal_connect_swapped (sidebar->cloud_manager, + "providers-changed", + G_CALLBACK (update_places), + sidebar); + + /* populate the sidebar */ + update_places (sidebar); + + sidebar->row_actions = G_ACTION_GROUP (g_simple_action_group_new ()); + g_action_map_add_action_entries (G_ACTION_MAP (sidebar->row_actions), + entries, G_N_ELEMENTS (entries), + sidebar); + gtk_widget_insert_action_group (GTK_WIDGET (sidebar), "row", sidebar->row_actions); + + gtk_accessible_update_property (GTK_ACCESSIBLE (sidebar), + GTK_ACCESSIBLE_PROPERTY_LABEL, _("Sidebar"), + GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, + _("List of common shortcuts, mountpoints, and bookmarks."), -1); +} + +static void +nautilus_gtk_places_sidebar_set_property (GObject *obj, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (obj); + + switch (property_id) + { + case PROP_LOCATION: + nautilus_gtk_places_sidebar_set_location (sidebar, g_value_get_object (value)); + break; + + case PROP_OPEN_FLAGS: + nautilus_gtk_places_sidebar_set_open_flags (sidebar, g_value_get_flags (value)); + break; + + case PROP_SHOW_RECENT: + nautilus_gtk_places_sidebar_set_show_recent (sidebar, g_value_get_boolean (value)); + break; + + case PROP_SHOW_DESKTOP: + nautilus_gtk_places_sidebar_set_show_desktop (sidebar, g_value_get_boolean (value)); + break; + + case PROP_SHOW_ENTER_LOCATION: + nautilus_gtk_places_sidebar_set_show_enter_location (sidebar, g_value_get_boolean (value)); + break; + + case PROP_SHOW_OTHER_LOCATIONS: + nautilus_gtk_places_sidebar_set_show_other_locations (sidebar, g_value_get_boolean (value)); + break; + + case PROP_SHOW_TRASH: + nautilus_gtk_places_sidebar_set_show_trash (sidebar, g_value_get_boolean (value)); + break; + + case PROP_SHOW_STARRED_LOCATION: + nautilus_gtk_places_sidebar_set_show_starred_location (sidebar, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); + break; + } +} + +static void +nautilus_gtk_places_sidebar_get_property (GObject *obj, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (obj); + + switch (property_id) + { + case PROP_LOCATION: + g_value_take_object (value, nautilus_gtk_places_sidebar_get_location (sidebar)); + break; + + case PROP_OPEN_FLAGS: + g_value_set_flags (value, nautilus_gtk_places_sidebar_get_open_flags (sidebar)); + break; + + case PROP_SHOW_RECENT: + g_value_set_boolean (value, nautilus_gtk_places_sidebar_get_show_recent (sidebar)); + break; + + case PROP_SHOW_DESKTOP: + g_value_set_boolean (value, nautilus_gtk_places_sidebar_get_show_desktop (sidebar)); + break; + + case PROP_SHOW_ENTER_LOCATION: + g_value_set_boolean (value, nautilus_gtk_places_sidebar_get_show_enter_location (sidebar)); + break; + + case PROP_SHOW_OTHER_LOCATIONS: + g_value_set_boolean (value, nautilus_gtk_places_sidebar_get_show_other_locations (sidebar)); + break; + + case PROP_SHOW_TRASH: + g_value_set_boolean (value, nautilus_gtk_places_sidebar_get_show_trash (sidebar)); + break; + + case PROP_SHOW_STARRED_LOCATION: + g_value_set_boolean (value, nautilus_gtk_places_sidebar_get_show_starred_location (sidebar)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec); + break; + } +} + +static void +nautilus_gtk_places_sidebar_dispose (GObject *object) +{ + NautilusGtkPlacesSidebar *sidebar; + GList *l; + + sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (object); + + if (sidebar->cancellable) + { + g_cancellable_cancel (sidebar->cancellable); + g_object_unref (sidebar->cancellable); + sidebar->cancellable = NULL; + } + + if (sidebar->bookmarks_manager != NULL) + { + _nautilus_gtk_bookmarks_manager_free (sidebar->bookmarks_manager); + sidebar->bookmarks_manager = NULL; + } + + g_clear_pointer (&sidebar->popover, gtk_widget_unparent); + + if (sidebar->rename_popover) + { + gtk_widget_unparent (sidebar->rename_popover); + sidebar->rename_popover = NULL; + sidebar->rename_entry = NULL; + sidebar->rename_button = NULL; + sidebar->rename_error = NULL; + } + + if (sidebar->trash_row) + { + g_object_remove_weak_pointer (G_OBJECT (sidebar->trash_row), + (gpointer *) &sidebar->trash_row); + sidebar->trash_row = NULL; + } + + if (sidebar->volume_monitor != NULL) + { + g_signal_handlers_disconnect_by_func (sidebar->volume_monitor, + update_places, sidebar); + g_clear_object (&sidebar->volume_monitor); + } + + if (sidebar->hostnamed_cancellable != NULL) + { + g_cancellable_cancel (sidebar->hostnamed_cancellable); + g_clear_object (&sidebar->hostnamed_cancellable); + } + + g_clear_object (&sidebar->hostnamed_proxy); + g_free (sidebar->hostname); + sidebar->hostname = NULL; + + if (sidebar->gtk_settings) + { + g_signal_handlers_disconnect_by_func (sidebar->gtk_settings, shell_shows_desktop_changed, sidebar); + sidebar->gtk_settings = NULL; + } + + g_clear_object (&sidebar->current_location); + g_clear_pointer (&sidebar->rename_uri, g_free); + g_clear_object (&sidebar->shortcuts); + + g_clear_handle_id (&sidebar->hover_timer_id, g_source_remove); + + for (l = sidebar->unready_accounts; l != NULL; l = l->next) + { + g_signal_handlers_disconnect_by_data (l->data, sidebar); + } + g_list_free_full (sidebar->unready_accounts, g_object_unref); + sidebar->unready_accounts = NULL; + if (sidebar->cloud_manager) + { + g_signal_handlers_disconnect_by_data (sidebar->cloud_manager, sidebar); + for (l = cloud_providers_collector_get_providers (sidebar->cloud_manager); + l != NULL; l = l->next) + { + g_signal_handlers_disconnect_by_data (l->data, sidebar); + } + g_object_unref (sidebar->cloud_manager); + sidebar->cloud_manager = NULL; + } + + G_OBJECT_CLASS (nautilus_gtk_places_sidebar_parent_class)->dispose (object); +} + +static void +nautilus_gtk_places_sidebar_finalize (GObject *object) +{ + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (object); + + g_clear_object (&sidebar->row_actions); + + g_clear_pointer (&sidebar->swin, gtk_widget_unparent); + + G_OBJECT_CLASS (nautilus_gtk_places_sidebar_parent_class)->finalize (object); +} + +static void +nautilus_gtk_places_sidebar_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (widget); + + gtk_widget_measure (sidebar->swin, + orientation, + for_size, + minimum, natural, + minimum_baseline, natural_baseline); +} + +static void +nautilus_gtk_places_sidebar_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + NautilusGtkPlacesSidebar *sidebar = NAUTILUS_GTK_PLACES_SIDEBAR (widget); + + gtk_widget_size_allocate (sidebar->swin, + &(GtkAllocation) { 0, 0, width, height }, + baseline); + + if (sidebar->popover) + gtk_popover_present (GTK_POPOVER (sidebar->popover)); + + if (sidebar->rename_popover) + gtk_popover_present (GTK_POPOVER (sidebar->rename_popover)); +} + +static void +nautilus_gtk_places_sidebar_class_init (NautilusGtkPlacesSidebarClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + + gobject_class->dispose = nautilus_gtk_places_sidebar_dispose; + gobject_class->finalize = nautilus_gtk_places_sidebar_finalize; + gobject_class->set_property = nautilus_gtk_places_sidebar_set_property; + gobject_class->get_property = nautilus_gtk_places_sidebar_get_property; + + widget_class->measure = nautilus_gtk_places_sidebar_measure; + widget_class->size_allocate = nautilus_gtk_places_sidebar_size_allocate; + + /* + * NautilusGtkPlacesSidebar::open-location: + * @sidebar: the object which received the signal. + * @location: (type Gio.File): GFile to which the caller should switch. + * @open_flags: a single value from NautilusGtkPlacesOpenFlags specifying how the @location should be opened. + * + * The places sidebar 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. + */ + places_sidebar_signals [OPEN_LOCATION] = + g_signal_new ("open-location", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, open_location), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_OBJECT, + NAUTILUS_TYPE_OPEN_FLAGS); + + /* + * NautilusGtkPlacesSidebar::show-error-message: + * @sidebar: 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 sidebar 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. + */ + places_sidebar_signals [SHOW_ERROR_MESSAGE] = + g_signal_new ("show-error-message", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, show_error_message), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_STRING); + + /* + * NautilusGtkPlacesSidebar::show-enter-location: + * @sidebar: the object which received the signal. + * + * The places sidebar emits this signal when it needs the calling + * application to present a way to directly enter a location. + * For example, the application may bring up a dialog box asking for + * a URL like "http://http.example.com". + */ + places_sidebar_signals [SHOW_ENTER_LOCATION] = + g_signal_new ("show-enter-location", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, show_enter_location), + NULL, NULL, + NULL, + G_TYPE_NONE, 0); + + /* + * NautilusGtkPlacesSidebar::drag-action-requested: + * @sidebar: the object which received the signal. + * @drop: (type Gdk.Drop): GdkDrop with information about the drag operation + * @dest_file: (type Gio.File): GFile with the tentative location that is being hovered for a drop + * @source_file_list: (type GLib.SList) (element-type GFile) (transfer none): + * List of GFile that are being dragged + * + * When the user starts a drag-and-drop operation and the sidebar needs + * to ask the application for which drag action to perform, then the + * sidebar will emit this signal. + * + * The application can evaluate the @context for customary actions, or + * it can check the type of the files indicated by @source_file_list against the + * possible actions for the destination @dest_file. + * + * The drag action to use must be the return value of the signal handler. + * + * Returns: The drag action to use, for example, GDK_ACTION_COPY + * or GDK_ACTION_MOVE, or 0 if no action is allowed here (i.e. drops + * are not allowed in the specified @dest_file). + */ + places_sidebar_signals [DRAG_ACTION_REQUESTED] = + g_signal_new ("drag-action-requested", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, drag_action_requested), + NULL, NULL, + NULL, + GDK_TYPE_DRAG_ACTION, 2, + G_TYPE_OBJECT, + GDK_TYPE_FILE_LIST); + + /* + * NautilusGtkPlacesSidebar::drag-action-ask: + * @sidebar: the object which received the signal. + * @actions: Possible drag actions that need to be asked for. + * + * The places sidebar emits this signal when it needs to ask the application + * to pop up a menu to ask the user for which drag action to perform. + * + * Returns: the final drag action that the sidebar should pass to the drag side + * of the drag-and-drop operation. + */ + places_sidebar_signals [DRAG_ACTION_ASK] = + g_signal_new ("drag-action-ask", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, drag_action_ask), + NULL, NULL, + NULL, + GDK_TYPE_DRAG_ACTION, 1, + GDK_TYPE_DRAG_ACTION); + + /* + * NautilusGtkPlacesSidebar::drag-perform-drop: + * @sidebar: the object which received the signal. + * @dest_file: (type Gio.File): Destination GFile. + * @source_file_list: (type GLib.SList) (element-type GFile) (transfer none): + * GSList of GFile that got dropped. + * @action: Drop action to perform. + * + * The places sidebar emits this signal when the user completes a + * drag-and-drop operation and one of the sidebar's items is the + * destination. This item is in the @dest_file, and the + * @source_file_list has the list of files that are dropped into it and + * which should be copied/moved/etc. based on the specified @action. + */ + places_sidebar_signals [DRAG_PERFORM_DROP] = + g_signal_new ("drag-perform-drop", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, drag_perform_drop), + NULL, NULL, + NULL, + G_TYPE_NONE, 3, + G_TYPE_OBJECT, + GDK_TYPE_FILE_LIST, + GDK_TYPE_DRAG_ACTION); + + /* + * NautilusGtkPlacesSidebar::show-other-locations-with-flags: + * @sidebar: the object which received the signal. + * @open_flags: a single value from NautilusGtkPlacesOpenFlags specifying how it should be opened. + * + * The places sidebar emits this signal when it needs the calling + * application to present a way to show other locations e.g. drives + * and network access points. + * For example, the application may bring up a page showing persistent + * volumes and discovered network addresses. + */ + places_sidebar_signals [SHOW_OTHER_LOCATIONS_WITH_FLAGS] = + g_signal_new ("show-other-locations-with-flags", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, show_other_locations_with_flags), + NULL, NULL, + NULL, + G_TYPE_NONE, 1, + NAUTILUS_TYPE_OPEN_FLAGS); + + /* + * NautilusGtkPlacesSidebar::mount: + * @sidebar: the object which received the signal. + * @mount_operation: the GMountOperation that is going to start. + * + * The places sidebar emits this signal when it starts a new operation + * because the user clicked on some location that needs mounting. + * In this way the application using the NautilusGtkPlacesSidebar can track the + * progress of the operation and, for example, show a notification. + */ + places_sidebar_signals [MOUNT] = + g_signal_new ("mount", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, mount), + NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_MOUNT_OPERATION); + /* + * NautilusGtkPlacesSidebar::unmount: + * @sidebar: the object which received the signal. + * @mount_operation: the GMountOperation that is going to start. + * + * The places sidebar emits this signal when it starts a new operation + * because the user for example ejected some drive or unmounted a mount. + * In this way the application using the NautilusGtkPlacesSidebar can track the + * progress of the operation and, for example, show a notification. + */ + places_sidebar_signals [UNMOUNT] = + g_signal_new ("unmount", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, unmount), + NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_MOUNT_OPERATION); + + /* + * NautilusGtkPlacesSidebar::show-starred-location: + * @sidebar: the object which received the signal + * @flags: the flags for the operation + * + * The places sidebar emits this signal when it needs the calling + * application to present a way to show the starred files. In GNOME, + * starred files are implemented by setting the nao:predefined-tag-favorite + * tag in the tracker database. + */ + places_sidebar_signals [SHOW_STARRED_LOCATION] = + g_signal_new ("show-starred-location", + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesSidebarClass, show_starred_location), + NULL, NULL, + NULL, + G_TYPE_NONE, 1, + NAUTILUS_TYPE_OPEN_FLAGS); + + properties[PROP_LOCATION] = + g_param_spec_object ("location", + "Location to Select", + "The location to highlight in the sidebar", + G_TYPE_FILE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + 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", + NAUTILUS_TYPE_OPEN_FLAGS, + NAUTILUS_GTK_PLACES_OPEN_NORMAL, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + properties[PROP_SHOW_RECENT] = + g_param_spec_boolean ("show-recent", + "Show recent files", + "Whether the sidebar includes a builtin shortcut for recent files", + TRUE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + properties[PROP_SHOW_DESKTOP] = + g_param_spec_boolean ("show-desktop", + "Show “Desktop”", + "Whether the sidebar includes a builtin shortcut to the Desktop folder", + TRUE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + properties[PROP_SHOW_ENTER_LOCATION] = + g_param_spec_boolean ("show-enter-location", + "Show “Enter Location”", + "Whether the sidebar includes a builtin shortcut to manually enter a location", + FALSE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + properties[PROP_SHOW_TRASH] = + g_param_spec_boolean ("show-trash", + "Show “Trash”", + "Whether the sidebar includes a builtin shortcut to the Trash location", + TRUE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + properties[PROP_SHOW_OTHER_LOCATIONS] = + g_param_spec_boolean ("show-other-locations", + "Show “Other locations”", + "Whether the sidebar includes an item to show external locations", + TRUE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + properties[PROP_SHOW_STARRED_LOCATION] = + g_param_spec_boolean ("show-starred-location", + "Show “Starred Location”", + "Whether the sidebar includes an item to show starred files", + FALSE, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + + g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties); + + gtk_widget_class_set_css_name (widget_class, "placessidebar"); + gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_LIST); +} + +/* + * nautilus_gtk_places_sidebar_new: + * + * Creates a new NautilusGtkPlacesSidebar widget. + * + * The application should connect to at least the + * NautilusGtkPlacesSidebar::open-location signal to be notified + * when the user makes a selection in the sidebar. + * + * Returns: a newly created NautilusGtkPlacesSidebar + */ +GtkWidget * +nautilus_gtk_places_sidebar_new (void) +{ + return GTK_WIDGET (g_object_new (nautilus_gtk_places_sidebar_get_type (), NULL)); +} + +/* + * nautilus_gtk_places_sidebar_set_open_flags: + * @sidebar: a places sidebar + * @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 sidebar. 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 @sidebar about the ways in which the + * application can open new locations, so that the sidebar can display (or not) + * the “Open in new tab” and “Open in new window” menu items as appropriate. + * + * When the NautilusGtkPlacesSidebar::open-location signal is emitted, its flags + * argument will be set to one of the @flags that was passed in + * nautilus_gtk_places_sidebar_set_open_flags(). + * + * Passing 0 for @flags will cause NAUTILUS_GTK_PLACES_OPEN_NORMAL to always be sent + * to callbacks for the “open-location” signal. + */ +void +nautilus_gtk_places_sidebar_set_open_flags (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkPlacesOpenFlags flags) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + if (sidebar->open_flags != flags) + { + sidebar->open_flags = flags; + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_OPEN_FLAGS]); + } +} + +/* + * nautilus_gtk_places_sidebar_get_open_flags: + * @sidebar: a NautilusGtkPlacesSidebar + * + * Gets the open flags. + * + * Returns: the NautilusGtkPlacesOpenFlags of @sidebar + */ +NautilusGtkPlacesOpenFlags +nautilus_gtk_places_sidebar_get_open_flags (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), 0); + + return sidebar->open_flags; +} + +/* + * nautilus_gtk_places_sidebar_set_location: + * @sidebar: a places sidebar + * @location: (nullable): location to select, or %NULL for no current path + * + * Sets the location that is being shown in the widgets surrounding the + * @sidebar, for example, in a folder view in a file manager. In turn, the + * @sidebar will highlight that location if it is being shown in the list of + * places, or it will unhighlight everything if the @location is not among the + * places in the list. + */ +void +nautilus_gtk_places_sidebar_set_location (NautilusGtkPlacesSidebar *sidebar, + GFile *location) +{ + GtkWidget *row; + char *row_uri; + char *uri; + gboolean found = FALSE; + + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + gtk_list_box_unselect_all (GTK_LIST_BOX (sidebar->list_box)); + + if (sidebar->current_location != NULL) + g_object_unref (sidebar->current_location); + sidebar->current_location = location; + if (sidebar->current_location != NULL) + g_object_ref (sidebar->current_location); + + if (location == NULL) + goto out; + + uri = g_file_get_uri (location); + + for (row = gtk_widget_get_first_child (GTK_WIDGET (sidebar->list_box)); + row != NULL && !found; + row = gtk_widget_get_next_sibling (row)) + { + if (!GTK_IS_LIST_BOX_ROW (row)) + continue; + + g_object_get (row, "uri", &row_uri, NULL); + if (row_uri != NULL && g_strcmp0 (row_uri, uri) == 0) + { + gtk_list_box_select_row (GTK_LIST_BOX (sidebar->list_box), + GTK_LIST_BOX_ROW (row)); + found = TRUE; + } + + g_free (row_uri); + } + + g_free (uri); + + out: + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_LOCATION]); +} + +/* + * nautilus_gtk_places_sidebar_get_location: + * @sidebar: a places sidebar + * + * Gets the currently selected location in the @sidebar. This can be %NULL when + * nothing is selected, for example, when nautilus_gtk_places_sidebar_set_location() has + * been called with a location that is not among the sidebar’s list of places to + * show. + * + * You can use this function to get the selection in the @sidebar. + * + * Returns: (nullable) (transfer full): a GFile with the selected location, or + * %NULL if nothing is visually selected. + */ +GFile * +nautilus_gtk_places_sidebar_get_location (NautilusGtkPlacesSidebar *sidebar) +{ + GtkListBoxRow *selected; + GFile *file; + + g_return_val_if_fail (sidebar != NULL, NULL); + + file = NULL; + selected = gtk_list_box_get_selected_row (GTK_LIST_BOX (sidebar->list_box)); + + if (selected) + { + char *uri; + + g_object_get (selected, "uri", &uri, NULL); + file = g_file_new_for_uri (uri); + g_free (uri); + } + + return file; +} + +char * +nautilus_gtk_places_sidebar_get_location_title (NautilusGtkPlacesSidebar *sidebar) +{ + GtkListBoxRow *selected; + char *title; + + g_return_val_if_fail (sidebar != NULL, NULL); + + title = NULL; + selected = gtk_list_box_get_selected_row (GTK_LIST_BOX (sidebar->list_box)); + + if (selected) + g_object_get (selected, "label", &title, NULL); + + return title; +} + +/* + * nautilus_gtk_places_sidebar_set_show_recent: + * @sidebar: a places sidebar + * @show_recent: whether to show an item for recent files + * + * Sets whether the @sidebar should show an item for recent files. + * The default value for this option is determined by the desktop + * environment, but this function can be used to override it on a + * per-application basis. + */ +void +nautilus_gtk_places_sidebar_set_show_recent (NautilusGtkPlacesSidebar *sidebar, + gboolean show_recent) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + sidebar->show_recent_set = TRUE; + + show_recent = !!show_recent; + if (sidebar->show_recent != show_recent) + { + sidebar->show_recent = show_recent; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_RECENT]); + } +} + +/* + * nautilus_gtk_places_sidebar_get_show_recent: + * @sidebar: a places sidebar + * + * Returns the value previously set with nautilus_gtk_places_sidebar_set_show_recent() + * + * Returns: %TRUE if the sidebar will display a builtin shortcut for recent files + */ +gboolean +nautilus_gtk_places_sidebar_get_show_recent (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), FALSE); + + return sidebar->show_recent; +} + +/* + * nautilus_gtk_places_sidebar_set_show_desktop: + * @sidebar: a places sidebar + * @show_desktop: whether to show an item for the Desktop folder + * + * Sets whether the @sidebar should show an item for the Desktop folder. + * The default value for this option is determined by the desktop + * environment and the user’s configuration, but this function can be + * used to override it on a per-application basis. + */ +void +nautilus_gtk_places_sidebar_set_show_desktop (NautilusGtkPlacesSidebar *sidebar, + gboolean show_desktop) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + /* Don't bother disconnecting from the GtkSettings -- it will just + * complicate things. Besides, it's highly unlikely that this will + * change while we're running, but we can ignore it if it does. + */ + sidebar->show_desktop_set = TRUE; + + show_desktop = !!show_desktop; + if (sidebar->show_desktop != show_desktop) + { + sidebar->show_desktop = show_desktop; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_DESKTOP]); + } +} + +/* + * nautilus_gtk_places_sidebar_get_show_desktop: + * @sidebar: a places sidebar + * + * Returns the value previously set with nautilus_gtk_places_sidebar_set_show_desktop() + * + * Returns: %TRUE if the sidebar will display a builtin shortcut to the desktop folder. + */ +gboolean +nautilus_gtk_places_sidebar_get_show_desktop (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), FALSE); + + return sidebar->show_desktop; +} + +/* + * nautilus_gtk_places_sidebar_set_show_enter_location: + * @sidebar: a places sidebar + * @show_enter_location: whether to show an item to enter a location + * + * Sets whether the @sidebar should show an item for entering a location; + * this is off by default. An application may want to turn this on if manually + * entering URLs is an expected user action. + * + * If you enable this, you should connect to the + * NautilusGtkPlacesSidebar::show-enter-location signal. + */ +void +nautilus_gtk_places_sidebar_set_show_enter_location (NautilusGtkPlacesSidebar *sidebar, + gboolean show_enter_location) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + show_enter_location = !!show_enter_location; + if (sidebar->show_enter_location != show_enter_location) + { + sidebar->show_enter_location = show_enter_location; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_ENTER_LOCATION]); + } +} + +/* + * nautilus_gtk_places_sidebar_get_show_enter_location: + * @sidebar: a places sidebar + * + * Returns the value previously set with nautilus_gtk_places_sidebar_set_show_enter_location() + * + * Returns: %TRUE if the sidebar will display an “Enter Location” item. + */ +gboolean +nautilus_gtk_places_sidebar_get_show_enter_location (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), FALSE); + + return sidebar->show_enter_location; +} + +/* + * nautilus_gtk_places_sidebar_set_show_other_locations: + * @sidebar: a places sidebar + * @show_other_locations: whether to show an item for the Other Locations view + * + * Sets whether the @sidebar should show an item for the application to show + * an Other Locations view; this is off by default. When set to %TRUE, persistent + * devices such as hard drives are hidden, otherwise they are shown in the sidebar. + * An application may want to turn this on if it implements a way for the user to + * see and interact with drives and network servers directly. + * + * If you enable this, you should connect to the + * NautilusGtkPlacesSidebar::show-other-locations-with-flags signal. + */ +void +nautilus_gtk_places_sidebar_set_show_other_locations (NautilusGtkPlacesSidebar *sidebar, + gboolean show_other_locations) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + show_other_locations = !!show_other_locations; + if (sidebar->show_other_locations != show_other_locations) + { + sidebar->show_other_locations = show_other_locations; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_OTHER_LOCATIONS]); + } + } + +/* + * nautilus_gtk_places_sidebar_get_show_other_locations: + * @sidebar: a places sidebar + * + * Returns the value previously set with nautilus_gtk_places_sidebar_set_show_other_locations() + * + * Returns: %TRUE if the sidebar will display an “Other Locations” item. + */ +gboolean +nautilus_gtk_places_sidebar_get_show_other_locations (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), FALSE); + + return sidebar->show_other_locations; +} + +/* + * nautilus_gtk_places_sidebar_set_show_trash: + * @sidebar: a places sidebar + * @show_trash: whether to show an item for the Trash location + * + * Sets whether the @sidebar should show an item for the Trash location. + */ +void +nautilus_gtk_places_sidebar_set_show_trash (NautilusGtkPlacesSidebar *sidebar, + gboolean show_trash) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + show_trash = !!show_trash; + if (sidebar->show_trash != show_trash) + { + sidebar->show_trash = show_trash; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_TRASH]); + } +} + +/* + * nautilus_gtk_places_sidebar_get_show_trash: + * @sidebar: a places sidebar + * + * Returns the value previously set with nautilus_gtk_places_sidebar_set_show_trash() + * + * Returns: %TRUE if the sidebar will display a “Trash” item. + */ +gboolean +nautilus_gtk_places_sidebar_get_show_trash (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), TRUE); + + return sidebar->show_trash; +} + +/* + * nautilus_gtk_places_sidebar_add_shortcut: + * @sidebar: a places sidebar + * @location: location to add as an application-specific shortcut + * + * Applications may want to present some folders in the places sidebar if + * they could be immediately useful to users. For example, a drawing + * program could add a “/usr/share/clipart” location when the sidebar is + * being used in an “Insert Clipart” dialog box. + * + * This function adds the specified @location to a special place for immutable + * shortcuts. The shortcuts are application-specific; they are not shared + * across applications, and they are not persistent. If this function + * is called multiple times with different locations, then they are added + * to the sidebar’s list in the same order as the function is called. + */ +void +nautilus_gtk_places_sidebar_add_shortcut (NautilusGtkPlacesSidebar *sidebar, + GFile *location) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + g_return_if_fail (G_IS_FILE (location)); + + g_list_store_append (sidebar->shortcuts, location); + + update_places (sidebar); +} + +/* + * nautilus_gtk_places_sidebar_remove_shortcut: + * @sidebar: a places sidebar + * @location: location to remove + * + * Removes an application-specific shortcut that has been previously been + * inserted with nautilus_gtk_places_sidebar_add_shortcut(). If the @location is not a + * shortcut in the sidebar, then nothing is done. + */ +void +nautilus_gtk_places_sidebar_remove_shortcut (NautilusGtkPlacesSidebar *sidebar, + GFile *location) +{ + guint i, n; + + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + g_return_if_fail (G_IS_FILE (location)); + + n = g_list_model_get_n_items (G_LIST_MODEL (sidebar->shortcuts)); + for (i = 0; i < n; i++) + { + GFile *shortcut = g_list_model_get_item (G_LIST_MODEL (sidebar->shortcuts), i); + + if (shortcut == location) + { + g_list_store_remove (sidebar->shortcuts, i); + g_object_unref (shortcut); + update_places (sidebar); + return; + } + + g_object_unref (shortcut); + } +} + +/* + * nautilus_gtk_places_sidebar_list_shortcuts: + * @sidebar: a places sidebar + * + * Gets the list of shortcuts, as a list model containing GFile objects. + * + * You should not modify the returned list model. Future changes to + * @sidebar may or may not affect the returned model. + * + * Returns: (transfer full): a list model of GFiles that have been added as + * application-specific shortcuts with nautilus_gtk_places_sidebar_add_shortcut() + */ +GListModel * +nautilus_gtk_places_sidebar_get_shortcuts (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), NULL); + + return G_LIST_MODEL (g_object_ref (sidebar->shortcuts)); +} + +/* + * nautilus_gtk_places_sidebar_get_nth_bookmark: + * @sidebar: a places sidebar + * @n: index of the bookmark to query + * + * This function queries the bookmarks added by the user to the places sidebar, + * and returns one of them. This function is used by GtkFileChooser to implement + * the “Alt-1”, “Alt-2”, etc. shortcuts, which activate the corresponding bookmark. + * + * Returns: (nullable) (transfer full): The bookmark specified by the index @n, or + * %NULL if no such index exist. Note that the indices start at 0, even though + * the file chooser starts them with the keyboard shortcut "Alt-1". + */ +GFile * +nautilus_gtk_places_sidebar_get_nth_bookmark (NautilusGtkPlacesSidebar *sidebar, + int n) +{ + GtkWidget *row; + int k; + GFile *file; + + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), NULL); + + file = NULL; + k = 0; + for (row = gtk_widget_get_first_child (GTK_WIDGET (sidebar->list_box)); + row != NULL; + row = gtk_widget_get_next_sibling (row)) + { + NautilusGtkPlacesPlaceType place_type; + char *uri; + + if (!GTK_IS_LIST_BOX_ROW (row)) + continue; + + g_object_get (row, + "place-type", &place_type, + "uri", &uri, + NULL); + if (place_type == NAUTILUS_GTK_PLACES_BOOKMARK) + { + if (k == n) + { + file = g_file_new_for_uri (uri); + g_free (uri); + break; + } + k++; + } + g_free (uri); + } + + return file; +} + +/* + * nautilus_gtk_places_sidebar_set_drop_targets_visible: + * @sidebar: a places sidebar. + * @visible: whether to show the valid targets or not. + * + * Make the NautilusGtkPlacesSidebar show drop targets, so it can show the available + * drop targets and a "new bookmark" row. This improves the Drag-and-Drop + * experience of the user and allows applications to show all available + * drop targets at once. + * + * This needs to be called when the application is aware of an ongoing drag + * that might target the sidebar. The drop-targets-visible state will be unset + * automatically if the drag finishes in the NautilusGtkPlacesSidebar. You only need + * to unset the state when the drag ends on some other widget on your application. + */ +void +nautilus_gtk_places_sidebar_set_drop_targets_visible (NautilusGtkPlacesSidebar *sidebar, + gboolean visible) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + if (visible) + { + sidebar->drop_state = DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT; + start_drop_feedback (sidebar, NULL); + } + else + { + if (sidebar->drop_state == DROP_STATE_NEW_BOOKMARK_ARMED_PERMANENT || + sidebar->drop_state == DROP_STATE_NEW_BOOKMARK_ARMED) + { + if (!sidebar->dragging_over) + { + sidebar->drop_state = DROP_STATE_NORMAL; + stop_drop_feedback (sidebar); + } + else + { + /* In case this is called while we are dragging we need to mark the + * drop state as no permanent so the leave timeout can do its job. + * This will only happen in applications that call this in a wrong + * time */ + sidebar->drop_state = DROP_STATE_NEW_BOOKMARK_ARMED; + } + } + } +} + +/* + * nautilus_gtk_places_sidebar_set_show_starred_location: + * @sidebar: a places sidebar + * @show_starred_location: whether to show an item for Starred files + * + * If you enable this, you should connect to the + * NautilusGtkPlacesSidebar::show-starred-location signal. + */ +void +nautilus_gtk_places_sidebar_set_show_starred_location (NautilusGtkPlacesSidebar *sidebar, + gboolean show_starred_location) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar)); + + show_starred_location = !!show_starred_location; + if (sidebar->show_starred_location != show_starred_location) + { + sidebar->show_starred_location = show_starred_location; + update_places (sidebar); + g_object_notify_by_pspec (G_OBJECT (sidebar), properties[PROP_SHOW_STARRED_LOCATION]); + } +} + +/* + * nautilus_gtk_places_sidebar_get_show_starred_location: + * @sidebar: a places sidebar + * + * Returns the value previously set with nautilus_gtk_places_sidebar_set_show_starred_location() + * + * Returns: %TRUE if the sidebar will display a Starred item. + */ +gboolean +nautilus_gtk_places_sidebar_get_show_starred_location (NautilusGtkPlacesSidebar *sidebar) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_SIDEBAR (sidebar), FALSE); + + return sidebar->show_starred_location; +} diff --git a/src/gtk/nautilusgtkplacessidebarprivate.h b/src/gtk/nautilusgtkplacessidebarprivate.h new file mode 100644 index 0000000..c1503ad --- /dev/null +++ b/src/gtk/nautilusgtkplacessidebarprivate.h @@ -0,0 +1,148 @@ +/* nautilusgtkplacessidebarprivate.h + * + * Copyright (C) 2015 Red Hat + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Carlos Soriano <csoriano@gnome.org> + */ + +#ifndef __NAUTILUS_GTK_PLACES_SIDEBAR_PRIVATE_H__ +#define __NAUTILUS_GTK_PLACES_SIDEBAR_PRIVATE_H__ + +#include <glib.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define NAUTILUS_TYPE_GTK_PLACES_SIDEBAR (nautilus_gtk_places_sidebar_get_type ()) +#define NAUTILUS_GTK_PLACES_SIDEBAR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NAUTILUS_TYPE_GTK_PLACES_SIDEBAR, NautilusGtkPlacesSidebar)) +#define NAUTILUS_GTK_PLACES_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NAUTILUS_TYPE_GTK_PLACES_SIDEBAR, NautilusGtkPlacesSidebarClass)) +#define NAUTILUS_IS_GTK_PLACES_SIDEBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NAUTILUS_TYPE_GTK_PLACES_SIDEBAR)) +#define NAUTILUS_IS_GTK_PLACES_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NAUTILUS_TYPE_GTK_PLACES_SIDEBAR)) +#define NAUTILUS_GTK_PLACES_SIDEBAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NAUTILUS_TYPE_GTK_PLACES_SIDEBAR, NautilusGtkPlacesSidebarClass)) + +typedef struct _NautilusGtkPlacesSidebar NautilusGtkPlacesSidebar; +typedef struct _NautilusGtkPlacesSidebarClass NautilusGtkPlacesSidebarClass; + +/* + * NautilusGtkPlacesOpenFlags: + * @NAUTILUS_GTK_PLACES_OPEN_NORMAL: This is the default mode that NautilusGtkPlacesSidebar uses if no other flags + * are specified. It indicates that the calling application should open the selected location + * in the normal way, for example, in the folder view beside the sidebar. + * @NAUTILUS_GTK_PLACES_OPEN_NEW_TAB: When passed to nautilus_gtk_places_sidebar_set_open_flags(), this indicates + * that the application can open folders selected from the sidebar in new tabs. This value + * will be passed to the NautilusGtkPlacesSidebar::open-location signal when the user selects + * that a location be opened in a new tab instead of in the standard fashion. + * @NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW: Similar to @NAUTILUS_GTK_PLACES_OPEN_NEW_TAB, but indicates that the application + * can open folders in new windows. + * + * These flags serve two purposes. First, the application can call nautilus_gtk_places_sidebar_set_open_flags() + * using these flags as a bitmask. This tells the sidebar that the application is able to open + * folders selected from the sidebar in various ways, for example, in new tabs or in new windows in + * addition to the normal mode. + * + * Second, when one of these values gets passed back to the application in the + * NautilusGtkPlacesSidebar::open-location signal, it means that the application should + * open the selected location in the normal way, in a new tab, or in a new + * window. The sidebar takes care of determining the desired way to open the location, + * based on the modifier keys that the user is pressing at the time the selection is made. + * + * If the application never calls nautilus_gtk_places_sidebar_set_open_flags(), then the sidebar will only + * use NAUTILUS_GTK_PLACES_OPEN_NORMAL in the NautilusGtkPlacesSidebar::open-location signal. This is the + * default mode of operation. + */ +typedef enum { + NAUTILUS_GTK_PLACES_OPEN_NORMAL = 1 << 0, + NAUTILUS_GTK_PLACES_OPEN_NEW_TAB = 1 << 1, + NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW = 1 << 2 +} NautilusGtkPlacesOpenFlags; + +GType nautilus_gtk_places_sidebar_get_type (void) G_GNUC_CONST; +GtkWidget * nautilus_gtk_places_sidebar_new (void); + +NautilusGtkPlacesOpenFlags nautilus_gtk_places_sidebar_get_open_flags (NautilusGtkPlacesSidebar *sidebar); +void nautilus_gtk_places_sidebar_set_open_flags (NautilusGtkPlacesSidebar *sidebar, + NautilusGtkPlacesOpenFlags flags); + +GFile * nautilus_gtk_places_sidebar_get_location (NautilusGtkPlacesSidebar *sidebar); +void nautilus_gtk_places_sidebar_set_location (NautilusGtkPlacesSidebar *sidebar, + GFile *location); + +gboolean nautilus_gtk_places_sidebar_get_show_recent (NautilusGtkPlacesSidebar *sidebar); +void nautilus_gtk_places_sidebar_set_show_recent (NautilusGtkPlacesSidebar *sidebar, + gboolean show_recent); + +gboolean nautilus_gtk_places_sidebar_get_show_desktop (NautilusGtkPlacesSidebar *sidebar); +void nautilus_gtk_places_sidebar_set_show_desktop (NautilusGtkPlacesSidebar *sidebar, + gboolean show_desktop); + +gboolean nautilus_gtk_places_sidebar_get_show_enter_location (NautilusGtkPlacesSidebar *sidebar); +void nautilus_gtk_places_sidebar_set_show_enter_location (NautilusGtkPlacesSidebar *sidebar, + gboolean show_enter_location); + +void nautilus_gtk_places_sidebar_add_shortcut (NautilusGtkPlacesSidebar *sidebar, + GFile *location); +void nautilus_gtk_places_sidebar_remove_shortcut (NautilusGtkPlacesSidebar *sidebar, + GFile *location); +GListModel * nautilus_gtk_places_sidebar_get_shortcuts (NautilusGtkPlacesSidebar *sidebar); + +GFile * nautilus_gtk_places_sidebar_get_nth_bookmark (NautilusGtkPlacesSidebar *sidebar, + int n); +void nautilus_gtk_places_sidebar_set_drop_targets_visible (NautilusGtkPlacesSidebar *sidebar, + gboolean visible); +gboolean nautilus_gtk_places_sidebar_get_show_trash (NautilusGtkPlacesSidebar *sidebar); +void nautilus_gtk_places_sidebar_set_show_trash (NautilusGtkPlacesSidebar *sidebar, + gboolean show_trash); + +void nautilus_gtk_places_sidebar_set_show_other_locations (NautilusGtkPlacesSidebar *sidebar, + gboolean show_other_locations); +gboolean nautilus_gtk_places_sidebar_get_show_other_locations (NautilusGtkPlacesSidebar *sidebar); + +void nautilus_gtk_places_sidebar_set_show_starred_location (NautilusGtkPlacesSidebar *sidebar, + gboolean show_starred_location); +gboolean nautilus_gtk_places_sidebar_get_show_starred_location (NautilusGtkPlacesSidebar *sidebar); + +/* Keep order, since it's used for the sort functions */ +typedef enum { + NAUTILUS_GTK_PLACES_SECTION_INVALID, + NAUTILUS_GTK_PLACES_SECTION_COMPUTER, + NAUTILUS_GTK_PLACES_SECTION_MOUNTS, + NAUTILUS_GTK_PLACES_SECTION_CLOUD, + NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS, + NAUTILUS_GTK_PLACES_SECTION_OTHER_LOCATIONS, + NAUTILUS_GTK_PLACES_N_SECTIONS +} NautilusGtkPlacesSectionType; + +typedef enum { + NAUTILUS_GTK_PLACES_INVALID, + NAUTILUS_GTK_PLACES_BUILT_IN, + NAUTILUS_GTK_PLACES_XDG_DIR, + NAUTILUS_GTK_PLACES_MOUNTED_VOLUME, + NAUTILUS_GTK_PLACES_BOOKMARK, + NAUTILUS_GTK_PLACES_HEADING, + NAUTILUS_GTK_PLACES_CONNECT_TO_SERVER, + NAUTILUS_GTK_PLACES_ENTER_LOCATION, + NAUTILUS_GTK_PLACES_DROP_FEEDBACK, + NAUTILUS_GTK_PLACES_BOOKMARK_PLACEHOLDER, + NAUTILUS_GTK_PLACES_OTHER_LOCATIONS, + NAUTILUS_GTK_PLACES_STARRED_LOCATION, + NAUTILUS_GTK_PLACES_N_PLACES +} NautilusGtkPlacesPlaceType; + +char *nautilus_gtk_places_sidebar_get_location_title (NautilusGtkPlacesSidebar *sidebar); + +G_END_DECLS + +#endif /* __NAUTILUS_GTK_PLACES_SIDEBAR_PRIVATE_H__ */ diff --git a/src/gtk/nautilusgtkplacesview.c b/src/gtk/nautilusgtkplacesview.c new file mode 100644 index 0000000..0d062c9 --- /dev/null +++ b/src/gtk/nautilusgtkplacesview.c @@ -0,0 +1,2635 @@ +/* nautilusgtkplacesview.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 Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include "nautilus-enum-types.h" + +#include <gio/gio.h> +#include <gio/gvfs.h> +#include <gtk/gtk.h> + +#include "nautilusgtkplacesviewprivate.h" +#include "nautilusgtkplacesviewrowprivate.h" +#include "nautilus-file.h" +#include "nautilus-file-utilities.h" +#include "nautilus-properties-window.h" + +/*< private > + * NautilusGtkPlacesView: + * + * NautilusGtkPlacesView is a widget that displays a list of persistent drives + * such as harddisk partitions and networks. NautilusGtkPlacesView 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 NautilusGtkPlacesView::open-location signal. This is emitted when the user + * selects a location to open in the view. + */ + +struct _NautilusGtkPlacesViewClass +{ + GtkBoxClass parent_class; + + void (* open_location) (NautilusGtkPlacesView *view, + GFile *location, + NautilusGtkPlacesOpenFlags open_flags); + + void (* show_error_message) (NautilusGtkPlacesSidebar *sidebar, + const char *primary, + const char *secondary); +}; + +struct _NautilusGtkPlacesView +{ + GtkBox parent_instance; + + GVolumeMonitor *volume_monitor; + NautilusGtkPlacesOpenFlags open_flags; + NautilusGtkPlacesOpenFlags current_open_flags; + + GFile *server_list_file; + GFileMonitor *server_list_monitor; + GFileMonitor *network_monitor; + + GCancellable *cancellable; + + char *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 *server_adresses_popover; + GtkWidget *available_protocols_grid; + GtkWidget *network_placeholder; + GtkWidget *network_placeholder_label; + + GtkSizeGroup *path_size_group; + GtkSizeGroup *space_size_group; + + GtkEntryCompletion *address_entry_completion; + GtkListStore *completion_store; + + GCancellable *networks_fetching_cancellable; + + NautilusGtkPlacesViewRow *row_for_action; + + guint should_open_location : 1; + guint should_pulse_entry : 1; + guint entry_pulse_timeout_id; + guint connecting_to_server : 1; + guint mounting_volume : 1; + guint unmounting_mount : 1; + guint fetching_networks : 1; + guint loading : 1; + guint destroyed : 1; +}; + +static void mount_volume (NautilusGtkPlacesView *view, + GVolume *volume); + +static void on_eject_button_clicked (GtkWidget *widget, + NautilusGtkPlacesViewRow *row); + +static gboolean on_row_popup_menu (GtkWidget *widget, + GVariant *args, + gpointer user_data); + +static void click_cb (GtkGesture *gesture, + int n_press, + double x, + double y, + gpointer user_data); + +static void populate_servers (NautilusGtkPlacesView *view); + +static gboolean nautilus_gtk_places_view_get_fetching_networks (NautilusGtkPlacesView *view); + +static void nautilus_gtk_places_view_set_fetching_networks (NautilusGtkPlacesView *view, + gboolean fetching_networks); + +static void nautilus_gtk_places_view_set_loading (NautilusGtkPlacesView *view, + gboolean loading); + +static void update_loading (NautilusGtkPlacesView *view); + +G_DEFINE_TYPE (NautilusGtkPlacesView, nautilus_gtk_places_view, GTK_TYPE_BOX) + +/* NautilusGtkPlacesView properties & signals */ +enum { + PROP_0, + PROP_OPEN_FLAGS, + PROP_FETCHING_NETWORKS, + PROP_LOADING, + LAST_PROP +}; + +enum { + OPEN_LOCATION, + SHOW_ERROR_MESSAGE, + LAST_SIGNAL +}; + +const char *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 (NautilusGtkPlacesView *view, + GFile *location, + NautilusGtkPlacesOpenFlags open_flags) +{ + if ((open_flags & view->open_flags) == 0) + open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + + g_signal_emit (view, places_view_signals[OPEN_LOCATION], 0, location, open_flags); +} + +static void +emit_show_error_message (NautilusGtkPlacesView *view, + char *primary_message, + char *secondary_message) +{ + g_signal_emit (view, places_view_signals[SHOW_ERROR_MESSAGE], + 0, primary_message, secondary_message); +} + +static void +server_file_changed_cb (NautilusGtkPlacesView *view) +{ + populate_servers (view); +} + +static GBookmarkFile * +server_list_load (NautilusGtkPlacesView *view) +{ + GBookmarkFile *bookmarks; + GError *error = NULL; + char *datadir; + char *filename; + + bookmarks = g_bookmark_file_new (); + datadir = g_build_filename (g_get_user_config_dir (), "gtk-4.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 (!view->server_list_monitor) + { + view->server_list_file = g_file_new_for_path (filename); + + if (view->server_list_file) + { + view->server_list_monitor = g_file_monitor_file (view->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 (view->server_list_monitor, + "changed", + G_CALLBACK (server_file_changed_cb), + view); + } + } + + g_clear_object (&view->server_list_file); + } + + g_free (datadir); + g_free (filename); + + return bookmarks; +} + +static void +server_list_save (GBookmarkFile *bookmarks) +{ + char *filename; + + filename = g_build_filename (g_get_user_config_dir (), "gtk-4.0", "servers", NULL); + g_bookmark_file_to_file (bookmarks, filename, NULL); + g_free (filename); +} + +static void +server_list_add_server (NautilusGtkPlacesView *view, + GFile *file) +{ + GBookmarkFile *bookmarks; + GFileInfo *info; + GError *error; + char *title; + char *uri; + GDateTime *now; + + 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); + now = g_date_time_new_now_utc (); + g_bookmark_file_set_visited_date_time (bookmarks, uri, now); + g_date_time_unref (now); + 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 (NautilusGtkPlacesView *view, + const char *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 (gtk_widget_get_root (widget)); + if (GTK_IS_WINDOW (toplevel)) + return GTK_WINDOW (toplevel); + else + return NULL; +} + +static void +set_busy_cursor (NautilusGtkPlacesView *view, + gboolean busy) +{ + GtkWidget *widget; + GtkWindow *toplevel; + + toplevel = get_toplevel (GTK_WIDGET (view)); + widget = GTK_WIDGET (toplevel); + if (!toplevel || !gtk_widget_get_realized (widget)) + return; + + if (busy) + gtk_widget_set_cursor_from_name (widget, "progress"); + else + gtk_widget_set_cursor (widget, NULL); +} + +/* Activates the given row, with the given flags as parameter */ +static void +activate_row (NautilusGtkPlacesView *view, + NautilusGtkPlacesViewRow *row, + NautilusGtkPlacesOpenFlags flags) +{ + GVolume *volume; + GMount *mount; + GFile *file; + + mount = nautilus_gtk_places_view_row_get_mount (row); + volume = nautilus_gtk_places_view_row_get_volume (row); + file = nautilus_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. + */ + view->should_open_location = TRUE; + + nautilus_gtk_places_view_row_set_busy (row, TRUE); + mount_volume (view, volume); + } +} + +static void update_places (NautilusGtkPlacesView *view); + +static void +nautilus_gtk_places_view_finalize (GObject *object) +{ + NautilusGtkPlacesView *view = (NautilusGtkPlacesView *)object; + + if (view->entry_pulse_timeout_id > 0) + g_source_remove (view->entry_pulse_timeout_id); + + g_clear_pointer (&view->search_query, g_free); + g_clear_object (&view->server_list_file); + g_clear_object (&view->server_list_monitor); + g_clear_object (&view->volume_monitor); + g_clear_object (&view->network_monitor); + g_clear_object (&view->cancellable); + g_clear_object (&view->networks_fetching_cancellable); + g_clear_object (&view->path_size_group); + g_clear_object (&view->space_size_group); + + G_OBJECT_CLASS (nautilus_gtk_places_view_parent_class)->finalize (object); +} + +static void +nautilus_gtk_places_view_dispose (GObject *object) +{ + NautilusGtkPlacesView *view = (NautilusGtkPlacesView *)object; + + view->destroyed = 1; + + g_signal_handlers_disconnect_by_func (view->volume_monitor, update_places, object); + + if (view->network_monitor) + g_signal_handlers_disconnect_by_func (view->network_monitor, update_places, object); + + if (view->server_list_monitor) + g_signal_handlers_disconnect_by_func (view->server_list_monitor, server_file_changed_cb, object); + + g_cancellable_cancel (view->cancellable); + g_cancellable_cancel (view->networks_fetching_cancellable); + g_clear_pointer (&view->popup_menu, gtk_widget_unparent); + + G_OBJECT_CLASS (nautilus_gtk_places_view_parent_class)->dispose (object); +} + +static void +nautilus_gtk_places_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + NautilusGtkPlacesView *self = NAUTILUS_GTK_PLACES_VIEW (object); + + switch (prop_id) + { + case PROP_LOADING: + g_value_set_boolean (value, nautilus_gtk_places_view_get_loading (self)); + break; + + case PROP_OPEN_FLAGS: + g_value_set_flags (value, nautilus_gtk_places_view_get_open_flags (self)); + break; + + case PROP_FETCHING_NETWORKS: + g_value_set_boolean (value, nautilus_gtk_places_view_get_fetching_networks (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +nautilus_gtk_places_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + NautilusGtkPlacesView *self = NAUTILUS_GTK_PLACES_VIEW (object); + + switch (prop_id) + { + case PROP_OPEN_FLAGS: + nautilus_gtk_places_view_set_open_flags (self, g_value_get_flags (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +is_external_volume (GVolume *volume) +{ + gboolean is_external; + GDrive *drive; + char *id; + + drive = g_volume_get_drive (volume); + id = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_CLASS); + + is_external = g_volume_can_eject (volume); + + /* NULL volume identifier only happens on removable devices */ + is_external |= !id; + + if (drive) + is_external |= g_drive_is_removable (drive); + + g_clear_object (&drive); + g_free (id); + + return is_external; +} + +typedef struct +{ + char *uri; + NautilusGtkPlacesView *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 (NautilusGtkPlacesView *view) +{ + GBookmarkFile *server_list; + GtkWidget *child; + char **uris; + gsize num_uris; + int i; + + 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 (view->recent_servers_stack), + num_uris > 0 ? "list" : "empty"); + + if (!uris) + { + g_bookmark_file_free (server_list); + return; + } + + /* clear previous items */ + while ((child = gtk_widget_get_first_child (GTK_WIDGET (view->recent_servers_listbox)))) + gtk_list_box_remove (GTK_LIST_BOX (view->recent_servers_listbox), child); + + gtk_list_store_clear (view->completion_store); + + for (i = 0; i < num_uris; i++) + { + RemoveServerData *data; + GtkTreeIter iter; + GtkWidget *row; + GtkWidget *grid; + GtkWidget *button; + GtkWidget *label; + char *name; + char *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 (view->completion_store, &iter); + gtk_list_store_set (view->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, + 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_grid_attach (GTK_GRID (grid), label, 0, 0, 1, 1); + + /* 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_widget_add_css_class (label, "dim-label"); + gtk_grid_attach (GTK_GRID (grid), label, 0, 1, 1, 1); + + /* remove button */ + button = gtk_button_new_from_icon_name ("window-close-symbolic"); + gtk_widget_set_halign (button, GTK_ALIGN_END); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + gtk_button_set_has_frame (GTK_BUTTON (button), FALSE); + gtk_widget_add_css_class (button, "sidebar-button"); + gtk_grid_attach (GTK_GRID (grid), button, 1, 0, 1, 2); + + gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), grid); + gtk_list_box_insert (GTK_LIST_BOX (view->recent_servers_listbox), row, -1); + + /* 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); + + g_free (name); + } + + g_strfreev (uris); + g_bookmark_file_free (server_list); +} + +static void +update_view_mode (NautilusGtkPlacesView *view) +{ + GtkWidget *child; + gboolean show_listbox; + + show_listbox = FALSE; + + /* drives */ + for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox)); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + /* GtkListBox filter rows by changing their GtkWidget::child-visible property */ + if (gtk_widget_get_child_visible (child)) + { + show_listbox = TRUE; + break; + } + } + + if (!show_listbox && + view->search_query && + view->search_query[0] != '\0') + { + gtk_stack_set_visible_child_name (GTK_STACK (view->stack), "empty-search"); + } + else + { + gtk_stack_set_visible_child_name (GTK_STACK (view->stack), "browse"); + } +} + +static void +insert_row (NautilusGtkPlacesView *view, + GtkWidget *row, + gboolean is_network) +{ + GtkEventController *controller; + GtkShortcutTrigger *trigger; + GtkShortcutAction *action; + GtkShortcut *shortcut; + GtkGesture *gesture; + + g_object_set_data (G_OBJECT (row), "is-network", GINT_TO_POINTER (is_network)); + + controller = gtk_shortcut_controller_new (); + trigger = gtk_alternative_trigger_new (gtk_keyval_trigger_new (GDK_KEY_F10, GDK_SHIFT_MASK), + gtk_keyval_trigger_new (GDK_KEY_Menu, 0)); + action = gtk_callback_action_new (on_row_popup_menu, row, NULL); + shortcut = gtk_shortcut_new (trigger, action); + gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (controller), shortcut); + gtk_widget_add_controller (GTK_WIDGET (row), controller); + + gesture = gtk_gesture_click_new (); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_SECONDARY); + g_signal_connect (gesture, "pressed", G_CALLBACK (click_cb), row); + gtk_widget_add_controller (row, GTK_EVENT_CONTROLLER (gesture)); + + g_signal_connect (nautilus_gtk_places_view_row_get_eject_button (NAUTILUS_GTK_PLACES_VIEW_ROW (row)), + "clicked", + G_CALLBACK (on_eject_button_clicked), + row); + + nautilus_gtk_places_view_row_set_path_size_group (NAUTILUS_GTK_PLACES_VIEW_ROW (row), view->path_size_group); + nautilus_gtk_places_view_row_set_space_size_group (NAUTILUS_GTK_PLACES_VIEW_ROW (row), view->space_size_group); + + gtk_list_box_insert (GTK_LIST_BOX (view->listbox), row, -1); +} + +static void +add_volume (NautilusGtkPlacesView *view, + GVolume *volume) +{ + gboolean is_network; + GMount *mount; + GFile *root; + GIcon *icon; + char *identifier; + char *name; + char *path; + + if (is_external_volume (volume)) + 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 || !g_mount_is_shadowed (mount)) + { + GtkWidget *row; + + row = g_object_new (NAUTILUS_TYPE_GTK_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 (NautilusGtkPlacesView *view, + GMount *mount) +{ + gboolean is_network; + GFile *root; + GIcon *icon; + char *name; + char *path; + char *uri; + char *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 (NAUTILUS_TYPE_GTK_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 (NautilusGtkPlacesView *view, + GDrive *drive) +{ + GList *volumes; + GList *l; + + 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 (NautilusGtkPlacesView *view, + GFile *file, + GIcon *icon, + const char *display_name, + const char *path, + gboolean is_network) +{ + GtkWidget *row; + row = g_object_new (NAUTILUS_TYPE_GTK_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 (NautilusGtkPlacesView *view) +{ + GtkWidget *child; + gboolean has_network = FALSE; + + for (child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox)); + child != NULL; + child = gtk_widget_get_next_sibling (child)) + { + if (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (child), "is-network")) && + g_object_get_data (G_OBJECT (child), "is-placeholder") == NULL) + { + has_network = TRUE; + break; + } + } + + return has_network; +} + +static void +update_network_state (NautilusGtkPlacesView *view) +{ + if (view->network_placeholder == NULL) + { + view->network_placeholder = gtk_list_box_row_new (); + view->network_placeholder_label = gtk_label_new (""); + gtk_label_set_xalign (GTK_LABEL (view->network_placeholder_label), 0.0); + gtk_widget_set_margin_start (view->network_placeholder_label, 12); + gtk_widget_set_margin_end (view->network_placeholder_label, 12); + gtk_widget_set_margin_top (view->network_placeholder_label, 6); + gtk_widget_set_margin_bottom (view->network_placeholder_label, 6); + gtk_widget_set_hexpand (view->network_placeholder_label, TRUE); + gtk_widget_set_sensitive (view->network_placeholder, FALSE); + gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (view->network_placeholder), + view->network_placeholder_label); + g_object_set_data (G_OBJECT (view->network_placeholder), + "is-network", GINT_TO_POINTER (TRUE)); + /* mark the row as placeholder, so it always goes first */ + g_object_set_data (G_OBJECT (view->network_placeholder), + "is-placeholder", GINT_TO_POINTER (TRUE)); + gtk_list_box_insert (GTK_LIST_BOX (view->listbox), view->network_placeholder, -1); + } + + if (nautilus_gtk_places_view_get_fetching_networks (view)) + { + /* 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 (view->network_placeholder); + gtk_label_set_text (GTK_LABEL (view->network_placeholder_label), + _("Searching for network locations")); + } + } + else if (!has_networks (view)) + { + gtk_widget_show (view->network_placeholder); + gtk_label_set_text (GTK_LABEL (view->network_placeholder_label), + _("No network locations found")); + } + else + { + gtk_widget_hide (view->network_placeholder); + } +} + +static void +monitor_network (NautilusGtkPlacesView *view) +{ + GFile *network_file; + GError *error; + + if (view->network_monitor) + return; + + error = NULL; + network_file = g_file_new_for_uri ("network:///"); + view->network_monitor = g_file_monitor (network_file, + G_FILE_MONITOR_NONE, + NULL, + &error); + + g_clear_object (&network_file); + + if (error) + { + g_warning ("Error monitoring network: %s", error->message); + g_clear_error (&error); + return; + } + + g_signal_connect_swapped (view->network_monitor, + "changed", + G_CALLBACK (update_places), + view); +} + +static void +populate_networks (NautilusGtkPlacesView *view, + GFileEnumerator *enumerator, + GList *detected_networks) +{ + GList *l; + GFile *file; + GFile *activatable_file; + char *uri; + GFileType type; + GIcon *icon; + char *display_name; + + for (l = 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); + } +} + +static void +network_enumeration_next_files_finished (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesView *view; + GList *detected_networks; + GError *error; + + view = NAUTILUS_GTK_PLACES_VIEW (user_data); + error = NULL; + + 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_clear_error (&error); + g_object_unref (view); + return; + } + + g_warning ("Failed to fetch network locations: %s", error->message); + g_clear_error (&error); + } + else + { + nautilus_gtk_places_view_set_fetching_networks (view, FALSE); + populate_networks (view, G_FILE_ENUMERATOR (source_object), detected_networks); + + g_list_free_full (detected_networks, g_object_unref); + } + + update_network_state (view); + monitor_network (view); + update_loading (view); + + g_object_unref (view); +} + +static void +network_enumeration_finished (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (user_data); + GFileEnumerator *enumerator; + GError *error; + + error = NULL; + enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, &error); + + if (error) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) + g_warning ("Failed to fetch network locations: %s", error->message); + + g_clear_error (&error); + g_object_unref (view); + } + else + { + g_file_enumerator_next_files_async (enumerator, + G_MAXINT32, + G_PRIORITY_DEFAULT, + view->networks_fetching_cancellable, + network_enumeration_next_files_finished, + user_data); + g_object_unref (enumerator); + } +} + +static void +fetch_networks (NautilusGtkPlacesView *view) +{ + GFile *network_file; + const char * const *supported_uris; + gboolean found; + + supported_uris = g_vfs_get_supported_uri_schemes (g_vfs_get_default ()); + + for (found = FALSE; !found && supported_uris && supported_uris[0]; supported_uris++) + if (g_strcmp0 (supported_uris[0], "network") == 0) + found = TRUE; + + if (!found) + return; + + network_file = g_file_new_for_uri ("network:///"); + + g_cancellable_cancel (view->networks_fetching_cancellable); + g_clear_object (&view->networks_fetching_cancellable); + view->networks_fetching_cancellable = g_cancellable_new (); + nautilus_gtk_places_view_set_fetching_networks (view, TRUE); + update_network_state (view); + + g_object_ref (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, + view->networks_fetching_cancellable, + network_enumeration_finished, + view); + + g_clear_object (&network_file); +} + +static void +update_places (NautilusGtkPlacesView *view) +{ + GList *mounts; + GList *volumes; + GList *drives; + GList *l; + GIcon *icon; + GFile *file; + GtkWidget *child; + gchar *osname; + + /* Clear all previously added items */ + while ((child = gtk_widget_get_first_child (GTK_WIDGET (view->listbox)))) + gtk_list_box_remove (GTK_LIST_BOX (view->listbox), child); + + view->network_placeholder = NULL; + /* Inform clients that we started loading */ + nautilus_gtk_places_view_set_loading (view, TRUE); + + /* Add "Computer" row */ + file = g_file_new_for_path ("/"); + icon = g_themed_icon_new_with_default_fallbacks ("drive-harddisk"); + osname = g_get_os_info (G_OS_INFO_KEY_NAME); + + add_file (view, file, icon, ((osname != NULL) ? osname : _("Operating System")), "/", FALSE); + + g_clear_object (&file); + g_clear_object (&icon); + g_free (osname); + + /* Add currently connected drives */ + drives = g_volume_monitor_get_connected_drives (view->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 (view->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 (view->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); + /* Check whether we still are in a loading state */ + update_loading (view); +} + +static void +server_mount_ready_cb (GObject *source_file, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (user_data); + gboolean should_show; + GError *error; + GFile *location; + + location = G_FILE (source_file); + should_show = TRUE; + error = NULL; + + g_file_mount_enclosing_volume_finish (location, res, &error); + 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); + } + + /* The operation got cancelled by the user and or the error + has been handled already. */ + g_clear_error (&error); + } + + if (view->destroyed) + { + g_object_unref (view); + return; + } + + view->should_pulse_entry = FALSE; + gtk_entry_set_progress_fraction (GTK_ENTRY (view->address_entry), 0); + + /* Restore from Cancel to Connect */ + gtk_button_set_label (GTK_BUTTON (view->connect_button), _("Con_nect")); + gtk_widget_set_sensitive (view->address_entry, TRUE); + view->connecting_to_server = FALSE; + + 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_editable_set_text (GTK_EDITABLE (view->address_entry), ""); + + if (view->should_open_location) + { + GMount *mount; + GFile *root; + + /* + * If the mount is not found at this point, it is probably user- + * invisible, which happens e.g for smb-browse, but the location + * should be opened anyway... + */ + mount = g_file_find_enclosing_mount (location, view->cancellable, NULL); + if (mount) + { + root = g_mount_get_default_location (mount); + + emit_open_location (view, root, view->open_flags); + + g_object_unref (root); + g_object_unref (mount); + } + else + { + emit_open_location (view, location, view->open_flags); + } + } + } + + update_places (view); + g_object_unref (view); +} + +static void +volume_mount_ready_cb (GObject *source_volume, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (user_data); + gboolean should_show; + GVolume *volume; + GError *error; + + volume = G_VOLUME (source_volume); + should_show = TRUE; + error = NULL; + + 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 (NAUTILUS_GTK_PLACES_VIEW (user_data), _("Unable to access location"), error->message); + should_show = FALSE; + } + + /* The operation got cancelled by the user and or the error + has been handled already. */ + g_clear_error (&error); + } + + if (view->destroyed) + { + g_object_unref(view); + return; + } + + view->mounting_volume = FALSE; + update_loading (view); + + if (should_show) + { + GMount *mount; + GFile *root; + + mount = g_volume_get_mount (volume); + root = g_mount_get_default_location (mount); + + if (view->should_open_location) + emit_open_location (NAUTILUS_GTK_PLACES_VIEW (user_data), root, view->open_flags); + + g_object_unref (mount); + g_object_unref (root); + } + + update_places (view); + g_object_unref (view); +} + +static void +unmount_ready_cb (GObject *source_mount, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesView *view; + GMount *mount; + GError *error; + + view = NAUTILUS_GTK_PLACES_VIEW (user_data); + mount = G_MOUNT (source_mount); + error = NULL; + + 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); + } + + if (view->destroyed) { + g_object_unref (view); + return; + } + + view->row_for_action = NULL; + view->unmounting_mount = FALSE; + update_loading (view); + + g_object_unref (view); +} + +static gboolean +pulse_entry_cb (gpointer user_data) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (user_data); + + if (view->destroyed) + { + view->entry_pulse_timeout_id = 0; + + return G_SOURCE_REMOVE; + } + else if (view->should_pulse_entry) + { + gtk_entry_progress_pulse (GTK_ENTRY (view->address_entry)); + + return G_SOURCE_CONTINUE; + } + else + { + gtk_entry_set_progress_fraction (GTK_ENTRY (view->address_entry), 0); + view->entry_pulse_timeout_id = 0; + + return G_SOURCE_REMOVE; + } +} + +static void +unmount_mount (NautilusGtkPlacesView *view, + GMount *mount) +{ + GMountOperation *operation; + GtkWidget *toplevel; + + toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (view))); + + g_cancellable_cancel (view->cancellable); + g_clear_object (&view->cancellable); + view->cancellable = g_cancellable_new (); + + view->unmounting_mount = TRUE; + update_loading (view); + + g_object_ref (view); + + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + g_mount_unmount_with_operation (mount, + 0, + operation, + view->cancellable, + unmount_ready_cb, + view); + g_object_unref (operation); +} + +static void +mount_server (NautilusGtkPlacesView *view, + GFile *location) +{ + GMountOperation *operation; + GtkWidget *toplevel; + + g_cancellable_cancel (view->cancellable); + g_clear_object (&view->cancellable); + /* User cliked when the operation was ongoing, so wanted to cancel it */ + if (view->connecting_to_server) + return; + + view->cancellable = g_cancellable_new (); + toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (view))); + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + + view->should_pulse_entry = TRUE; + gtk_entry_set_progress_pulse_step (GTK_ENTRY (view->address_entry), 0.1); + gtk_entry_set_progress_fraction (GTK_ENTRY (view->address_entry), 0.1); + /* Allow to cancel the operation */ + gtk_button_set_label (GTK_BUTTON (view->connect_button), _("Cance_l")); + gtk_widget_set_sensitive (view->address_entry, FALSE); + view->connecting_to_server = TRUE; + update_loading (view); + + if (view->entry_pulse_timeout_id == 0) + view->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); + + /* make sure we keep the view around for as long as we are running */ + g_object_ref (view); + + g_file_mount_enclosing_volume (location, + 0, + operation, + view->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 (NautilusGtkPlacesView *view, + GVolume *volume) +{ + GMountOperation *operation; + GtkWidget *toplevel; + + toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (view))); + operation = gtk_mount_operation_new (GTK_WINDOW (toplevel)); + + g_cancellable_cancel (view->cancellable); + g_clear_object (&view->cancellable); + view->cancellable = g_cancellable_new (); + + view->mounting_volume = TRUE; + update_loading (view); + + g_mount_operation_set_password_save (operation, G_PASSWORD_SAVE_FOR_SESSION); + + /* make sure we keep the view around for as long as we are running */ + g_object_ref (view); + + g_volume_mount (volume, + 0, + operation, + view->cancellable, + volume_mount_ready_cb, + view); + + /* unref operation here - g_file_mount_enclosing_volume() does ref for itself */ + g_object_unref (operation); +} + +static void +open_cb (GtkWidget *widget, + const char *action_name, + GVariant *parameter) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (widget); + NautilusGtkPlacesOpenFlags flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + + if (view->row_for_action == NULL) + return; + + if (strcmp (action_name, "location.open") == 0) + flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + else if (strcmp (action_name, "location.open-tab") == 0) + flags = NAUTILUS_GTK_PLACES_OPEN_NEW_TAB; + else if (strcmp (action_name, "location.open-window") == 0) + flags = NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW; + + activate_row (view, view->row_for_action, flags); +} + +static void +properties_cb (GtkWidget *widget, + const char *action_name, + GVariant *parameter) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (widget); + GList *list; + GMount *mount; + g_autoptr (GFile) location = NULL; + NautilusFile *file; + + if (view->row_for_action == NULL) + return; + + file = NULL; + mount = nautilus_gtk_places_view_row_get_mount (view->row_for_action); + + if (mount) + { + location = g_mount_get_root (mount); + file = nautilus_file_get (location); + } + else + file = nautilus_file_get (nautilus_gtk_places_view_row_get_file (view->row_for_action)); + + if (file) + { + list = g_list_append (NULL, file); + nautilus_properties_window_present (list, widget, NULL, NULL, NULL); + + nautilus_file_list_unref (list); + } +} + +static void +mount_cb (GtkWidget *widget, + const char *action_name, + GVariant *parameter) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (widget); + GVolume *volume; + + if (view->row_for_action == NULL) + return; + + volume = nautilus_gtk_places_view_row_get_volume (view->row_for_action); + + /* + * When the mount item is activated, it's expected that + * the volume only gets mounted, without opening it after + * the operation is complete. + */ + view->should_open_location = FALSE; + + nautilus_gtk_places_view_row_set_busy (view->row_for_action, TRUE); + mount_volume (view, volume); +} + +static void +unmount_cb (GtkWidget *widget, + const char *action_name, + GVariant *parameter) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (widget); + GMount *mount; + + if (view->row_for_action == NULL) + return; + + mount = nautilus_gtk_places_view_row_get_mount (view->row_for_action); + + nautilus_gtk_places_view_row_set_busy (view->row_for_action, TRUE); + + unmount_mount (view, mount); +} + +static void +attach_protocol_row_to_grid (GtkGrid *grid, + const char *protocol_name, + const char *protocol_prefix) +{ + GtkWidget *name_label; + GtkWidget *prefix_label; + + name_label = gtk_label_new (protocol_name); + gtk_widget_set_halign (name_label, GTK_ALIGN_START); + gtk_grid_attach_next_to (grid, name_label, NULL, GTK_POS_BOTTOM, 1, 1); + + prefix_label = gtk_label_new (protocol_prefix); + gtk_widget_set_halign (prefix_label, GTK_ALIGN_START); + gtk_grid_attach_next_to (grid, prefix_label, name_label, GTK_POS_RIGHT, 1, 1); +} + +static void +populate_available_protocols_grid (GtkGrid *grid) +{ + const char * const *supported_protocols; + gboolean has_any = FALSE; + + supported_protocols = g_vfs_get_supported_uri_schemes (g_vfs_get_default ()); + + if (g_strv_contains (supported_protocols, "afp")) + { + attach_protocol_row_to_grid (grid, _("AppleTalk"), "afp://"); + has_any = TRUE; + } + + if (g_strv_contains (supported_protocols, "ftp")) + { + attach_protocol_row_to_grid (grid, _("File Transfer Protocol"), + /* Translators: do not translate ftp:// and ftps:// */ + _("ftp:// or ftps://")); + has_any = TRUE; + } + + if (g_strv_contains (supported_protocols, "nfs")) + { + attach_protocol_row_to_grid (grid, _("Network File System"), "nfs://"); + has_any = TRUE; + } + + if (g_strv_contains (supported_protocols, "smb")) + { + attach_protocol_row_to_grid (grid, _("Samba"), "smb://"); + has_any = TRUE; + } + + if (g_strv_contains (supported_protocols, "ssh")) + { + attach_protocol_row_to_grid (grid, _("SSH File Transfer Protocol"), + /* Translators: do not translate sftp:// and ssh:// */ + _("sftp:// or ssh://")); + has_any = TRUE; + } + + if (g_strv_contains (supported_protocols, "dav")) + { + attach_protocol_row_to_grid (grid, _("WebDAV"), + /* Translators: do not translate dav:// and davs:// */ + _("dav:// or davs://")); + has_any = TRUE; + } + + if (!has_any) + gtk_widget_hide (GTK_WIDGET (grid)); +} + +static GMenuModel * +get_menu_model (void) +{ + GMenu *menu; + GMenu *section; + GMenuItem *item; + + menu = g_menu_new (); + section = g_menu_new (); + item = g_menu_item_new (_("_Open"), "location.open"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("Open in New _Tab"), "location.open-tab"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("Open in New _Window"), "location.open-window"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + section = g_menu_new (); + item = g_menu_item_new (_("_Disconnect"), "location.disconnect"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("_Unmount"), "location.unmount"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + + item = g_menu_item_new (_("_Connect"), "location.connect"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("_Mount"), "location.mount"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + item = g_menu_item_new (_("_Properties"), "location.properties"); + g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); + g_menu_append_item (section, item); + g_object_unref (item); + + g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); + g_object_unref (section); + + return G_MENU_MODEL (menu); +} + +static void +_popover_set_pointing_to_widget (GtkPopover *popover, + GtkWidget *target) +{ + GtkWidget *parent; + double x, y, w, h; + + parent = gtk_widget_get_parent (GTK_WIDGET (popover)); + + if (!gtk_widget_translate_coordinates (target, parent, 0, 0, &x, &y)) + return; + + w = gtk_widget_get_allocated_width (GTK_WIDGET (target)); + h = gtk_widget_get_allocated_height (GTK_WIDGET (target)); + + gtk_popover_set_pointing_to (popover, &(GdkRectangle){x, y, w, h}); +} + +static gboolean +real_popup_menu (GtkWidget *widget, + double x, + double y) +{ + NautilusGtkPlacesViewRow *row = NAUTILUS_GTK_PLACES_VIEW_ROW (widget); + NautilusGtkPlacesView *view; + GMount *mount; + GFile *file; + gboolean is_root, is_network; + double x_in_view, y_in_view; + + view = NAUTILUS_GTK_PLACES_VIEW (gtk_widget_get_ancestor (GTK_WIDGET (row), NAUTILUS_TYPE_GTK_PLACES_VIEW)); + + mount = nautilus_gtk_places_view_row_get_mount (row); + file = nautilus_gtk_places_view_row_get_file (row); + is_root = file && nautilus_is_root_directory (file); + is_network = nautilus_gtk_places_view_row_get_is_network (row); + + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.disconnect", + !file && mount && is_network); + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.unmount", + !file && mount && !is_network); + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.connect", + !file && !mount && is_network); + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.mount", + !file && !mount && !is_network); + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.properties", + (is_root || + (mount && !(file && is_network)))); + + if (!view->popup_menu) + { + GMenuModel *model = get_menu_model (); + + view->popup_menu = gtk_popover_menu_new_from_model (model); + + gtk_popover_set_has_arrow (GTK_POPOVER (view->popup_menu), FALSE); + gtk_widget_set_parent (view->popup_menu, GTK_WIDGET (view)); + gtk_widget_set_halign (view->popup_menu, GTK_ALIGN_START); + + g_object_unref (model); + } + + if (view->row_for_action) + g_object_set_data (G_OBJECT (view->row_for_action), "menu", NULL); + + if (x == -1 && y == -1) + _popover_set_pointing_to_widget (GTK_POPOVER (view->popup_menu), widget); + else + { + gtk_widget_translate_coordinates (widget, GTK_WIDGET (view), + x, y, &x_in_view, &y_in_view); + gtk_popover_set_pointing_to (GTK_POPOVER (view->popup_menu), + &(GdkRectangle){x_in_view, y_in_view, 0, 0}); + } + + view->row_for_action = row; + if (view->row_for_action) + g_object_set_data (G_OBJECT (view->row_for_action), "menu", view->popup_menu); + + gtk_popover_popup (GTK_POPOVER (view->popup_menu)); + + return TRUE; +} + +static gboolean +on_row_popup_menu (GtkWidget *widget, + GVariant *args, + gpointer user_data) +{ + return real_popup_menu (widget, -1, -1); +} + +static void +click_cb (GtkGesture *gesture, + int n_press, + double x, + double y, + gpointer user_data) +{ + real_popup_menu (GTK_WIDGET (user_data), x, y); +} + +static gboolean +on_key_press_event (GtkEventController *controller, + guint keyval, + guint keycode, + GdkModifierType state, + NautilusGtkPlacesView *view) +{ + GdkModifierType modifiers; + + modifiers = gtk_accelerator_get_default_mod_mask (); + + if (keyval == GDK_KEY_Return || + keyval == GDK_KEY_KP_Enter || + keyval == GDK_KEY_ISO_Enter || + keyval == GDK_KEY_space) + { + GtkWidget *focus_widget; + GtkWindow *toplevel; + + view->current_open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + toplevel = get_toplevel (GTK_WIDGET (view)); + + if (!toplevel) + return FALSE; + + focus_widget = gtk_root_get_focus (GTK_ROOT (toplevel)); + + if (!NAUTILUS_IS_GTK_PLACES_VIEW_ROW (focus_widget)) + return FALSE; + + if ((state & modifiers) == GDK_SHIFT_MASK) + view->current_open_flags = NAUTILUS_GTK_PLACES_OPEN_NEW_TAB; + else if ((state & modifiers) == GDK_CONTROL_MASK) + view->current_open_flags = NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW; + + activate_row (view, NAUTILUS_GTK_PLACES_VIEW_ROW (focus_widget), view->current_open_flags); + + return TRUE; + } + + return FALSE; +} + +static void +on_middle_click_row_event (GtkGestureClick *gesture, + guint n_press, + double x, + double y, + NautilusGtkPlacesView *view) +{ + GtkListBoxRow *row; + + if (n_press != 1) + return; + + row = gtk_list_box_get_row_at_y (GTK_LIST_BOX (view->listbox), y); + if (row != NULL && gtk_widget_is_sensitive (GTK_WIDGET (row))) + activate_row (view, NAUTILUS_GTK_PLACES_VIEW_ROW (row), NAUTILUS_GTK_PLACES_OPEN_NEW_TAB); +} + + +static void +on_eject_button_clicked (GtkWidget *widget, + NautilusGtkPlacesViewRow *row) +{ + if (row) + { + GtkWidget *view = gtk_widget_get_ancestor (GTK_WIDGET (row), NAUTILUS_TYPE_GTK_PLACES_VIEW); + + unmount_mount (NAUTILUS_GTK_PLACES_VIEW (view), nautilus_gtk_places_view_row_get_mount (row)); + } +} + +static void +on_connect_button_clicked (NautilusGtkPlacesView *view) +{ + const char *uri; + GFile *file; + + 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 (view->connect_button)) + return; + + uri = gtk_editable_get_text (GTK_EDITABLE (view->address_entry)); + + if (uri != NULL && uri[0] != '\0') + file = g_file_new_for_commandline_arg (uri); + + if (file) + { + view->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 (NautilusGtkPlacesView *view) +{ + const char * const *supported_protocols; + char *address, *scheme; + gboolean supported; + + supported = FALSE; + supported_protocols = g_vfs_get_supported_uri_schemes (g_vfs_get_default ()); + address = g_strdup (gtk_editable_get_text (GTK_EDITABLE (view->address_entry))); + scheme = g_uri_parse_scheme (address); + + if (!supported_protocols) + goto out; + + if (!scheme) + goto out; + + supported = g_strv_contains (supported_protocols, scheme) && + !g_strv_contains (unsupported_protocols, scheme); + +out: + gtk_widget_set_sensitive (view->connect_button, supported); + if (scheme && !supported) + gtk_widget_add_css_class (view->address_entry, "error"); + else + gtk_widget_remove_css_class (view->address_entry, "error"); + + g_free (address); + g_free (scheme); +} + +static void +on_address_entry_show_help_pressed (NautilusGtkPlacesView *view, + GtkEntryIconPosition icon_pos, + GtkEntry *entry) +{ + GdkRectangle rect; + double x, y; + + /* Setup the auxiliary popover's rectangle */ + gtk_entry_get_icon_area (GTK_ENTRY (view->address_entry), + GTK_ENTRY_ICON_SECONDARY, + &rect); + gtk_widget_translate_coordinates (view->address_entry, GTK_WIDGET (view), + rect.x, rect.y, &x, &y); + + rect.x = x; + rect.y = y; + gtk_popover_set_pointing_to (GTK_POPOVER (view->server_adresses_popover), &rect); + gtk_widget_set_visible (view->server_adresses_popover, TRUE); +} + +static void +on_recent_servers_listbox_row_activated (NautilusGtkPlacesView *view, + NautilusGtkPlacesViewRow *row, + GtkWidget *listbox) +{ + char *uri; + + uri = g_object_get_data (G_OBJECT (row), "uri"); + + gtk_editable_set_text (GTK_EDITABLE (view->address_entry), uri); + + gtk_widget_hide (view->recent_servers_popover); +} + +static void +on_listbox_row_activated (NautilusGtkPlacesView *view, + NautilusGtkPlacesViewRow *row, + GtkWidget *listbox) +{ + activate_row (view, row, view->current_open_flags); +} + +static gboolean +listbox_filter_func (GtkListBoxRow *row, + gpointer user_data) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (user_data); + gboolean is_placeholder; + gboolean retval; + gboolean searching; + char *name; + char *path; + + retval = FALSE; + searching = view->search_query && view->search_query[0] != '\0'; + + is_placeholder = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-placeholder")); + + if (is_placeholder && searching) + return FALSE; + + if (!searching) + return TRUE; + + g_object_get (row, + "name", &name, + "path", &path, + NULL); + + if (name) + { + char *lowercase_name = g_utf8_strdown (name, -1); + + retval |= strstr (lowercase_name, view->search_query) != NULL; + + g_free (lowercase_name); + } + + if (path) + { + char *lowercase_path = g_utf8_strdown (path, -1); + + retval |= strstr (lowercase_path, view->search_query) != NULL; + + g_free (lowercase_path); + } + + g_free (name); + g_free (path); + + return retval; +} + +static void +listbox_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + gboolean row_is_network; + char *text; + + 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); + gtk_widget_set_margin_top (header, 6); + + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + + label = g_object_new (GTK_TYPE_LABEL, + "use_markup", TRUE, + "margin-start", 12, + "label", text, + "xalign", 0.0f, + NULL); + if (row_is_network) + { + GtkWidget *header_name; + GtkWidget *network_header_spinner; + + gtk_widget_set_margin_end (label, 6); + + header_name = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); + network_header_spinner = gtk_spinner_new (); + gtk_widget_set_margin_end (network_header_spinner, 12); + g_object_bind_property (NAUTILUS_GTK_PLACES_VIEW (user_data), + "fetching-networks", + network_header_spinner, + "spinning", + G_BINDING_SYNC_CREATE); + + gtk_box_append (GTK_BOX (header_name), label); + gtk_box_append (GTK_BOX (header_name), network_header_spinner); + gtk_box_append (GTK_BOX (header), header_name); + } + else + { + gtk_widget_set_hexpand (label, TRUE); + gtk_widget_set_margin_end (label, 12); + gtk_box_append (GTK_BOX (header), label); + } + + gtk_box_append (GTK_BOX (header), separator); + + gtk_list_box_row_set_header (row, header); + + g_free (text); + } + else + { + gtk_list_box_row_set_header (row, NULL); + } +} + +static int +listbox_sort_func (GtkListBoxRow *row1, + GtkListBoxRow *row2, + gpointer user_data) +{ + gboolean row1_is_network; + gboolean row2_is_network; + char *path1; + char *path2; + gboolean *is_placeholder1; + gboolean *is_placeholder2; + int 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", &path1, NULL); + g_object_get (row2, "path", &path2, NULL); + + retval = g_utf8_collate (path1, path2); + + g_free (path1); + g_free (path2); + + return retval; +} + +static void +nautilus_gtk_places_view_constructed (GObject *object) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (object); + + G_OBJECT_CLASS (nautilus_gtk_places_view_parent_class)->constructed (object); + + gtk_list_box_set_sort_func (GTK_LIST_BOX (view->listbox), + listbox_sort_func, + object, + NULL); + gtk_list_box_set_filter_func (GTK_LIST_BOX (view->listbox), + listbox_filter_func, + object, + NULL); + gtk_list_box_set_header_func (GTK_LIST_BOX (view->listbox), + listbox_header_func, + object, + NULL); + + /* load drives */ + update_places (view); + + g_signal_connect_swapped (view->volume_monitor, + "mount-added", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (view->volume_monitor, + "mount-changed", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (view->volume_monitor, + "mount-removed", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (view->volume_monitor, + "volume-added", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (view->volume_monitor, + "volume-changed", + G_CALLBACK (update_places), + object); + g_signal_connect_swapped (view->volume_monitor, + "volume-removed", + G_CALLBACK (update_places), + object); +} + +static void +nautilus_gtk_places_view_map (GtkWidget *widget) +{ + NautilusGtkPlacesView *view = NAUTILUS_GTK_PLACES_VIEW (widget); + + gtk_editable_set_text (GTK_EDITABLE (view->address_entry), ""); + + GTK_WIDGET_CLASS (nautilus_gtk_places_view_parent_class)->map (widget); +} + +static void +nautilus_gtk_places_view_class_init (NautilusGtkPlacesViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = nautilus_gtk_places_view_finalize; + object_class->dispose = nautilus_gtk_places_view_dispose; + object_class->constructed = nautilus_gtk_places_view_constructed; + object_class->get_property = nautilus_gtk_places_view_get_property; + object_class->set_property = nautilus_gtk_places_view_set_property; + + widget_class->map = nautilus_gtk_places_view_map; + + /* + * NautilusGtkPlacesView::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 NautilusGtkPlacesOpenFlags 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. + */ + places_view_signals [OPEN_LOCATION] = + g_signal_new ("open-location", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET (NautilusGtkPlacesViewClass, open_location), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_OBJECT, + NAUTILUS_TYPE_OPEN_FLAGS); + + /* + * NautilusGtkPlacesView::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. + */ + 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 (NautilusGtkPlacesViewClass, show_error_message), + NULL, NULL, + NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_STRING); + + properties[PROP_LOADING] = + g_param_spec_boolean ("loading", + "Loading", + "Whether the view is loading locations", + FALSE, + G_PARAM_READABLE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + + properties[PROP_FETCHING_NETWORKS] = + g_param_spec_boolean ("fetching-networks", + "Fetching networks", + "Whether the view is fetching networks", + FALSE, + G_PARAM_READABLE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + + 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", + NAUTILUS_TYPE_OPEN_FLAGS, + NAUTILUS_GTK_PLACES_OPEN_NORMAL, + G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB); + + 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/ui/nautilusgtkplacesview.ui"); + + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, actionbar); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, address_entry); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, address_entry_completion); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, completion_store); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, connect_button); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, listbox); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, recent_servers_listbox); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, recent_servers_popover); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, recent_servers_stack); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, stack); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, server_adresses_popover); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesView, available_protocols_grid); + + gtk_widget_class_bind_template_callback (widget_class, on_address_entry_text_changed); + gtk_widget_class_bind_template_callback (widget_class, on_address_entry_show_help_pressed); + gtk_widget_class_bind_template_callback (widget_class, on_connect_button_clicked); + 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); + + /** + * NautilusGtkPlacesView|location.open: + * + * Opens the location in the current window. + */ + gtk_widget_class_install_action (widget_class, "location.open", NULL, open_cb); + + /** + * NautilusGtkPlacesView|location.open-tab: + * + * Opens the location in a new tab. + */ + gtk_widget_class_install_action (widget_class, "location.open-tab", NULL, open_cb); + + /** + * NautilusGtkPlacesView|location.open-window: + * + * Opens the location in a new window. + */ + gtk_widget_class_install_action (widget_class, "location.open-window", NULL, open_cb); + + /** + * NautilusGtkPlacesView|location.mount: + * + * Mount the location. + */ + gtk_widget_class_install_action (widget_class, "location.mount", NULL, mount_cb); + + /** + * NautilusGtkPlacesView|location.connect: + * + * Connect the location. + */ + gtk_widget_class_install_action (widget_class, "location.connect", NULL, mount_cb); + + /** + * NautilusGtkPlacesView|location.unmount: + * + * Unmount the location. + */ + gtk_widget_class_install_action (widget_class, "location.unmount", NULL, unmount_cb); + + /** + * NautilusGtkPlacesView|location.disconnect: + * + * Disconnect the location. + */ + gtk_widget_class_install_action (widget_class, "location.disconnect", NULL, unmount_cb); + + /** + * NautilusGtkPlacesView|location.properties: + * + * Show location properties. + */ + gtk_widget_class_install_action (widget_class, "location.properties", NULL, properties_cb); + + gtk_widget_class_set_css_name (widget_class, "placesview"); + gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_LIST); +} + +static void +nautilus_gtk_places_view_init (NautilusGtkPlacesView *self) +{ + GtkEventController *controller; + + self->volume_monitor = g_volume_monitor_get (); + self->open_flags = NAUTILUS_GTK_PLACES_OPEN_NORMAL; + self->path_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + self->space_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + gtk_widget_action_set_enabled (GTK_WIDGET (self), "location.open-tab", FALSE); + gtk_widget_action_set_enabled (GTK_WIDGET (self), "location.open-window", FALSE); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_widget_set_parent (self->server_adresses_popover, GTK_WIDGET (self)); + controller = gtk_event_controller_key_new (); + g_signal_connect (controller, "key-pressed", G_CALLBACK (on_key_press_event), self); + gtk_widget_add_controller (GTK_WIDGET (self), controller); + + /* We need an additional controller because GtkListBox only + * activates rows for GDK_BUTTON_PRIMARY clicks + */ + controller = (GtkEventController *) gtk_gesture_click_new (); + gtk_event_controller_set_propagation_phase (controller, GTK_PHASE_BUBBLE); + gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_MIDDLE); + g_signal_connect (controller, "released", + G_CALLBACK (on_middle_click_row_event), self); + gtk_widget_add_controller (self->listbox, controller); + + populate_available_protocols_grid (GTK_GRID (self->available_protocols_grid)); +} + +/* + * nautilus_gtk_places_view_new: + * + * Creates a new NautilusGtkPlacesView widget. + * + * The application should connect to at least the + * NautilusGtkPlacesView::open-location signal to be notified + * when the user makes a selection in the view. + * + * Returns: a newly created NautilusGtkPlacesView + */ +GtkWidget * +nautilus_gtk_places_view_new (void) +{ + return g_object_new (NAUTILUS_TYPE_GTK_PLACES_VIEW, NULL); +} + +/* + * nautilus_gtk_places_view_set_open_flags: + * @view: a NautilusGtkPlacesView + * @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 NautilusGtkPlacesView::open-location signal is emitted, its flags + * argument will be set to one of the @flags that was passed in + * nautilus_gtk_places_view_set_open_flags(). + * + * Passing 0 for @flags will cause NAUTILUS_GTK_PLACES_OPEN_NORMAL to always be sent + * to callbacks for the “open-location” signal. + */ +void +nautilus_gtk_places_view_set_open_flags (NautilusGtkPlacesView *view, + NautilusGtkPlacesOpenFlags flags) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view)); + + if (view->open_flags == flags) + return; + + view->open_flags = flags; + + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.open-tab", + (flags & NAUTILUS_GTK_PLACES_OPEN_NEW_TAB) != 0); + gtk_widget_action_set_enabled (GTK_WIDGET (view), "location.open-window", + (flags & NAUTILUS_GTK_PLACES_OPEN_NEW_WINDOW) != 0); + + g_object_notify_by_pspec (G_OBJECT (view), properties[PROP_OPEN_FLAGS]); +} + +/* + * nautilus_gtk_places_view_get_open_flags: + * @view: a NautilusGtkPlacesSidebar + * + * Gets the open flags. + * + * Returns: the NautilusGtkPlacesOpenFlags of @view + */ +NautilusGtkPlacesOpenFlags +nautilus_gtk_places_view_get_open_flags (NautilusGtkPlacesView *view) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view), 0); + + return view->open_flags; +} + +/* + * nautilus_gtk_places_view_get_search_query: + * @view: a NautilusGtkPlacesView + * + * Retrieves the current search query from @view. + * + * Returns: (transfer none): the current search query. + */ +const char * +nautilus_gtk_places_view_get_search_query (NautilusGtkPlacesView *view) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view), NULL); + + return view->search_query; +} + +/* + * nautilus_gtk_places_view_set_search_query: + * @view: a NautilusGtkPlacesView + * @query_text: the query, or NULL. + * + * Sets the search query of @view. The search is immediately performed + * once the query is set. + */ +void +nautilus_gtk_places_view_set_search_query (NautilusGtkPlacesView *view, + const char *query_text) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view)); + + if (g_strcmp0 (view->search_query, query_text) != 0) + { + g_clear_pointer (&view->search_query, g_free); + view->search_query = g_utf8_strdown (query_text, -1); + + gtk_list_box_invalidate_filter (GTK_LIST_BOX (view->listbox)); + gtk_list_box_invalidate_headers (GTK_LIST_BOX (view->listbox)); + + update_view_mode (view); + } +} + +/* + * nautilus_gtk_places_view_get_loading: + * @view: a NautilusGtkPlacesView + * + * Returns %TRUE if the view is loading locations. + */ +gboolean +nautilus_gtk_places_view_get_loading (NautilusGtkPlacesView *view) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view), FALSE); + + return view->loading; +} + +static void +update_loading (NautilusGtkPlacesView *view) +{ + gboolean loading; + + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view)); + + loading = view->fetching_networks || view->connecting_to_server || + view->mounting_volume || view->unmounting_mount; + + set_busy_cursor (view, loading); + nautilus_gtk_places_view_set_loading (view, loading); +} + +static void +nautilus_gtk_places_view_set_loading (NautilusGtkPlacesView *view, + gboolean loading) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view)); + + if (view->loading != loading) + { + view->loading = loading; + g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_LOADING]); + } +} + +static gboolean +nautilus_gtk_places_view_get_fetching_networks (NautilusGtkPlacesView *view) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view), FALSE); + + return view->fetching_networks; +} + +static void +nautilus_gtk_places_view_set_fetching_networks (NautilusGtkPlacesView *view, + gboolean fetching_networks) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW (view)); + + if (view->fetching_networks != fetching_networks) + { + view->fetching_networks = fetching_networks; + g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_FETCHING_NETWORKS]); + } +} diff --git a/src/gtk/nautilusgtkplacesview.ui b/src/gtk/nautilusgtkplacesview.ui new file mode 100644 index 0000000..fbedf70 --- /dev/null +++ b/src/gtk/nautilusgtkplacesview.ui @@ -0,0 +1,261 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="gtk40"> + <object class="GtkListStore" id="completion_store"> + <columns> + <column type="gchararray"/> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkEntryCompletion" id="address_entry_completion"> + <property name="model">completion_store</property> + <property name="text-column">1</property> + <property name="inline-completion">1</property> + <property name="popup-completion">0</property> + </object> + <object class="GtkPopover" id="server_adresses_popover"> + <property name="position">2</property> + <child> + <object class="GtkBox"> + <property name="orientation">1</property> + <property name="spacing">6</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="label" translatable="yes">Server Addresses</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="label" translatable="yes">Server addresses are made up of a protocol prefix and an address. Examples:</property> + <property name="wrap">1</property> + <property name="width-chars">40</property> + <property name="max-width-chars">40</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="label">smb://gnome.org, ssh://192.168.0.1, ftp://[2001:db8::1]</property> + <property name="wrap">1</property> + <property name="width-chars">40</property> + <property name="max-width-chars">40</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkGrid" id="available_protocols_grid"> + <property name="margin-top">12</property> + <property name="hexpand">1</property> + <property name="row-spacing">6</property> + <property name="column-spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</property> + <property name="label" translatable="yes">Available Protocols</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Prefix</property> + <property name="xalign">0</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <object class="GtkPopover" id="recent_servers_popover"> + <child> + <object class="GtkStack" id="recent_servers_stack"> + <child> + <object class="GtkStackPage"> + <property name="name">empty</property> + <property name="child"> + <object class="AdwStatusPage"> + <property name="icon-name">network-server-symbolic</property> + <property name="title" translatable="yes" comments="Translators: Server as any successfully connected network address">No Recent Servers</property> + <style> + <class name="compact"/> + </style> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="name">list</property> + <property name="child"> + <object class="GtkBox"> + <property name="orientation">1</property> + <property name="spacing">12</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Recent Servers</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="vexpand">1</property> + <property name="has-frame">1</property> + <property name="min-content-width">250</property> + <property name="min-content-height">200</property> + <child> + <object class="GtkViewport"> + <child> + <object class="GtkListBox" id="recent_servers_listbox"> + <property name="selection-mode">0</property> + <signal name="row-activated" handler="on_recent_servers_listbox_row_activated" object="NautilusGtkPlacesView" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + </object> + </child> + </object> + <template class="NautilusGtkPlacesView" parent="GtkBox"> + <accessibility> + <property name="label" translatable="yes">Other Locations</property> + <property name="description" translatable="yes">List of common local and remote mountpoints.</property> + </accessibility> + <property name="orientation">1</property> + <child> + <object class="GtkStack" id="stack"> + <property name="vhomogeneous">0</property> + <property name="transition-type">1</property> + <child> + <object class="GtkStackPage"> + <property name="name">browse</property> + <property name="child"> + <object class="GtkScrolledWindow"> + <property name="hexpand">1</property> + <property name="vexpand">1</property> + <child> + <object class="GtkViewport"> + <child> + <object class="GtkListBox" id="listbox"> + <property name="selection-mode">0</property> + <signal name="row-activated" handler="on_listbox_row_activated" object="NautilusGtkPlacesView" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="name">empty-search</property> + <property name="child"> + <object class="AdwStatusPage"> + <property name="icon-name">edit-find-symbolic</property> + <property name="title" translatable="yes">No Results Found</property> + <property name="description" translatable="yes">Try a different search.</property> + </object> + </property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkActionBar" id="actionbar"> + <property name="hexpand">1</property> + <style> + <class name="background"/> + </style> + <child> + <object class="GtkLabel"> + <property name="hexpand">1</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">1</property> + <attributes> + <attribute name="weight" value="bold"></attribute> + </attributes> + </object> + </child> + <child type="end"> + <object class="GtkButton" id="connect_button"> + <property name="label" translatable="yes">Con_nect</property> + <property name="use-underline">1</property> + <property name="sensitive">0</property> + <property name="valign">3</property> + <signal name="clicked" handler="on_connect_button_clicked" object="NautilusGtkPlacesView" swapped="yes"/> + </object> + </child> + <child type="end"> + <object class="GtkBox"> + <property name="hexpand">1</property> + <child> + <object class="GtkEntry" id="address_entry"> + <property name="hexpand">1</property> + <property name="width-chars">20</property> + <property name="placeholder-text" translatable="yes">Enter server address…</property> + <property name="secondary-icon-name">dialog-question-symbolic</property> + <property name="completion">address_entry_completion</property> + <signal name="notify::text" handler="on_address_entry_text_changed" object="NautilusGtkPlacesView" swapped="yes"/> + <signal name="activate" handler="on_connect_button_clicked" object="NautilusGtkPlacesView" swapped="yes"/> + <signal name="icon-press" handler="on_address_entry_show_help_pressed" object="NautilusGtkPlacesView" swapped="yes"/> + </object> + </child> + <child> + <object class="GtkMenuButton" id="server_list_button"> + <property name="direction">0</property> + <property name="popover">recent_servers_popover</property> + <property name="icon-name">pan-down-symbolic</property> + <style> + <class name="server-list-button"/> + </style> + </object> + </child> + <style> + <class name="linked"/> + </style> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/src/gtk/nautilusgtkplacesviewprivate.h b/src/gtk/nautilusgtkplacesviewprivate.h new file mode 100644 index 0000000..4cf6e3e --- /dev/null +++ b/src/gtk/nautilusgtkplacesviewprivate.h @@ -0,0 +1,55 @@ +/* nautilusgtkplacesview.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 Lesser General Public License as published + * by the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef NAUTILUS_GTK_PLACES_VIEW_H +#define NAUTILUS_GTK_PLACES_VIEW_H + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#endif + +#include "nautilusgtkplacessidebarprivate.h" + +G_BEGIN_DECLS + +#define NAUTILUS_TYPE_GTK_PLACES_VIEW (nautilus_gtk_places_view_get_type ()) +#define NAUTILUS_GTK_PLACES_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NAUTILUS_TYPE_GTK_PLACES_VIEW, NautilusGtkPlacesView)) +#define NAUTILUS_GTK_PLACES_VIEW_CLASS(klass)(G_TYPE_CHECK_CLASS_CAST ((klass), NAUTILUS_TYPE_GTK_PLACES_VIEW, NautilusGtkPlacesViewClass)) +#define NAUTILUS_IS_GTK_PLACES_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NAUTILUS_TYPE_GTK_PLACES_VIEW)) +#define NAUTILUS_IS_GTK_PLACES_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NAUTILUS_TYPE_GTK_PLACES_VIEW)) +#define NAUTILUS_GTK_PLACES_VIEW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NAUTILUS_TYPE_GTK_PLACES_VIEW, NautilusGtkPlacesViewClass)) + +typedef struct _NautilusGtkPlacesView NautilusGtkPlacesView; +typedef struct _NautilusGtkPlacesViewClass NautilusGtkPlacesViewClass; + +GType nautilus_gtk_places_view_get_type (void) G_GNUC_CONST; + +NautilusGtkPlacesOpenFlags nautilus_gtk_places_view_get_open_flags (NautilusGtkPlacesView *view); +void nautilus_gtk_places_view_set_open_flags (NautilusGtkPlacesView *view, + NautilusGtkPlacesOpenFlags flags); + +const char * nautilus_gtk_places_view_get_search_query (NautilusGtkPlacesView *view); +void nautilus_gtk_places_view_set_search_query (NautilusGtkPlacesView *view, + const char *query_text); + +gboolean nautilus_gtk_places_view_get_loading (NautilusGtkPlacesView *view); + +GtkWidget * nautilus_gtk_places_view_new (void); + +G_END_DECLS + +#endif /* NAUTILUS_GTK_PLACES_VIEW_H */ diff --git a/src/gtk/nautilusgtkplacesviewrow.c b/src/gtk/nautilusgtkplacesviewrow.c new file mode 100644 index 0000000..64d8896 --- /dev/null +++ b/src/gtk/nautilusgtkplacesviewrow.c @@ -0,0 +1,508 @@ +/* nautilusgtkplacesviewrow.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 Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include "nautilus-application.h" +#include "nautilus-enum-types.h" + +#include <gio/gio.h> + +#include "nautilusgtkplacesviewrowprivate.h" + +/* As this widget is shared with Nautilus, we use this guard to + * ensure that internally we only include the files that we need + * instead of including gtk.h + */ +#ifdef GTK_COMPILATION +#else +#include <gtk/gtk.h> +#endif + +struct _NautilusGtkPlacesViewRow +{ + GtkListBoxRow parent_instance; + + GtkLabel *available_space_label; + GtkStack *mount_stack; + GtkSpinner *busy_spinner; + GtkButton *eject_button; + GtkImage *eject_icon; + GtkImage *icon_image; + GtkLabel *name_label; + GtkLabel *path_label; + + GVolume *volume; + GMount *mount; + GFile *file; + + GCancellable *cancellable; + + int is_network : 1; +}; + +G_DEFINE_TYPE (NautilusGtkPlacesViewRow, nautilus_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 +measure_available_space_finished (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + NautilusGtkPlacesViewRow *row = user_data; + GFileInfo *info; + GError *error; + guint64 free_space; + guint64 total_space; + char *formatted_free_size; + char *formatted_total_size; + char *label; + guint plural_form; + + error = NULL; + + info = g_file_query_filesystem_info_finish (G_FILE (object), + res, + &error); + + if (error) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED)) + { + g_warning ("Failed to measure available space: %s", error->message); + } + + g_clear_error (&error); + goto out; + } + + if (!g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE) || + !g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE)) + { + g_object_unref (info); + goto out; + } + + free_space = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE); + total_space = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE); + + formatted_free_size = g_format_size (free_space); + formatted_total_size = g_format_size (total_space); + + /* read g_format_size code in glib for further understanding */ + plural_form = free_space < 1000 ? free_space : free_space % 1000 + 1000; + + /* Translators: respectively, free and total space of the drive. The plural form + * should be based on the free space available. + * i.e. 1 GB / 24 GB available. + */ + label = g_strdup_printf (dngettext (GETTEXT_PACKAGE, "%s / %s available", "%s / %s available", plural_form), + formatted_free_size, formatted_total_size); + + gtk_label_set_label (row->available_space_label, label); + + g_object_unref (info); + g_free (formatted_total_size); + g_free (formatted_free_size); + g_free (label); +out: + g_object_unref (object); +} + +static void +measure_available_space (NautilusGtkPlacesViewRow *row) +{ + gboolean skip_measure; + gboolean should_measure; + g_autoptr (GFile) root = NULL; + + skip_measure = FALSE; + if (nautilus_application_is_sandboxed ()) + { + root = g_file_new_for_uri ("file:///"); + if (row->file != NULL) + skip_measure = g_file_equal (root, row->file); + } + + should_measure = ((row->volume || row->mount || row->file) && + !row->is_network && !skip_measure); + + gtk_label_set_label (row->available_space_label, ""); + gtk_widget_set_visible (GTK_WIDGET (row->available_space_label), should_measure); + + if (should_measure) + { + GFile *file = NULL; + + if (row->file) + { + file = g_object_ref (row->file); + } + else if (row->mount) + { + file = g_mount_get_root (row->mount); + } + else if (row->volume) + { + GMount *mount; + + mount = g_volume_get_mount (row->volume); + + if (mount) + file = g_mount_get_root (row->mount); + + g_clear_object (&mount); + } + + if (file) + { + g_cancellable_cancel (row->cancellable); + g_clear_object (&row->cancellable); + row->cancellable = g_cancellable_new (); + + g_file_query_filesystem_info_async (file, + G_FILE_ATTRIBUTE_FILESYSTEM_FREE "," G_FILE_ATTRIBUTE_FILESYSTEM_SIZE, + G_PRIORITY_DEFAULT, + row->cancellable, + measure_available_space_finished, + row); + } + } +} + +static void +nautilus_gtk_places_view_row_finalize (GObject *object) +{ + NautilusGtkPlacesViewRow *self = NAUTILUS_GTK_PLACES_VIEW_ROW (object); + + g_cancellable_cancel (self->cancellable); + + g_clear_object (&self->volume); + g_clear_object (&self->mount); + g_clear_object (&self->file); + g_clear_object (&self->cancellable); + + G_OBJECT_CLASS (nautilus_gtk_places_view_row_parent_class)->finalize (object); +} + +static void +nautilus_gtk_places_view_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + NautilusGtkPlacesViewRow *self; + + self = NAUTILUS_GTK_PLACES_VIEW_ROW (object); + + switch (prop_id) + { + case PROP_ICON: + g_value_set_object (value, gtk_image_get_gicon (self->icon_image)); + 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 +nautilus_gtk_places_view_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + NautilusGtkPlacesViewRow *self = NAUTILUS_GTK_PLACES_VIEW_ROW (object); + + switch (prop_id) + { + case PROP_ICON: + gtk_image_set_from_gicon (self->icon_image, g_value_get_object (value)); + 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)); + if (self->mount != NULL) + { + gtk_stack_set_visible_child (self->mount_stack, GTK_WIDGET (self->eject_button)); + gtk_widget_set_child_visible (GTK_WIDGET (self->mount_stack), TRUE); + } + else + { + gtk_widget_set_child_visible (GTK_WIDGET (self->mount_stack), FALSE); + } + measure_available_space (self); + break; + + case PROP_FILE: + g_set_object (&self->file, g_value_get_object (value)); + measure_available_space (self); + break; + + case PROP_IS_NETWORK: + nautilus_gtk_places_view_row_set_is_network (self, g_value_get_boolean (value)); + measure_available_space (self); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +nautilus_gtk_places_view_row_size_allocate (GtkWidget *widget, + int width, + int height, + int baseline) +{ + GtkWidget *menu = GTK_WIDGET (g_object_get_data (G_OBJECT (widget), "menu")); + + GTK_WIDGET_CLASS (nautilus_gtk_places_view_row_parent_class)->size_allocate (widget, width, height, baseline); + if (menu) + gtk_popover_present (GTK_POPOVER (menu)); +} + +static void +nautilus_gtk_places_view_row_class_init (NautilusGtkPlacesViewRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = nautilus_gtk_places_view_row_finalize; + object_class->get_property = nautilus_gtk_places_view_row_get_property; + object_class->set_property = nautilus_gtk_places_view_row_set_property; + + widget_class->size_allocate = nautilus_gtk_places_view_row_size_allocate; + + 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/ui/nautilusgtkplacesviewrow.ui"); + + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, available_space_label); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, mount_stack); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, busy_spinner); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, eject_button); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, eject_icon); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, icon_image); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, name_label); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkPlacesViewRow, path_label); +} + +static void +nautilus_gtk_places_view_row_init (NautilusGtkPlacesViewRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget* +nautilus_gtk_places_view_row_new (GVolume *volume, + GMount *mount) +{ + return g_object_new (NAUTILUS_TYPE_GTK_PLACES_VIEW_ROW, + "volume", volume, + "mount", mount, + NULL); +} + +GMount* +nautilus_gtk_places_view_row_get_mount (NautilusGtkPlacesViewRow *row) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW_ROW (row), NULL); + + return row->mount; +} + +GVolume* +nautilus_gtk_places_view_row_get_volume (NautilusGtkPlacesViewRow *row) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW_ROW (row), NULL); + + return row->volume; +} + +GFile* +nautilus_gtk_places_view_row_get_file (NautilusGtkPlacesViewRow *row) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW_ROW (row), NULL); + + return row->file; +} + +GtkWidget* +nautilus_gtk_places_view_row_get_eject_button (NautilusGtkPlacesViewRow *row) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW_ROW (row), NULL); + + return GTK_WIDGET (row->eject_button); +} + +void +nautilus_gtk_places_view_row_set_busy (NautilusGtkPlacesViewRow *row, + gboolean is_busy) +{ + g_return_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW_ROW (row)); + + if (is_busy) + { + gtk_stack_set_visible_child (row->mount_stack, GTK_WIDGET (row->busy_spinner)); + gtk_widget_set_child_visible (GTK_WIDGET (row->mount_stack), TRUE); + gtk_spinner_start (row->busy_spinner); + } + else + { + gtk_widget_set_child_visible (GTK_WIDGET (row->mount_stack), FALSE); + gtk_spinner_stop (row->busy_spinner); + } +} + +gboolean +nautilus_gtk_places_view_row_get_is_network (NautilusGtkPlacesViewRow *row) +{ + g_return_val_if_fail (NAUTILUS_IS_GTK_PLACES_VIEW_ROW (row), FALSE); + + return row->is_network; +} + +void +nautilus_gtk_places_view_row_set_is_network (NautilusGtkPlacesViewRow *row, + gboolean is_network) +{ + if (row->is_network != is_network) + { + row->is_network = is_network; + + gtk_image_set_from_icon_name (row->eject_icon, "media-eject-symbolic"); + gtk_widget_set_tooltip_text (GTK_WIDGET (row->eject_button), is_network ? _("Disconnect") : _("Unmount")); + } +} + +void +nautilus_gtk_places_view_row_set_path_size_group (NautilusGtkPlacesViewRow *row, + GtkSizeGroup *group) +{ + if (group) + gtk_size_group_add_widget (group, GTK_WIDGET (row->path_label)); +} + +void +nautilus_gtk_places_view_row_set_space_size_group (NautilusGtkPlacesViewRow *row, + GtkSizeGroup *group) +{ + if (group) + gtk_size_group_add_widget (group, GTK_WIDGET (row->available_space_label)); +} diff --git a/src/gtk/nautilusgtkplacesviewrow.ui b/src/gtk/nautilusgtkplacesviewrow.ui new file mode 100644 index 0000000..06c8041 --- /dev/null +++ b/src/gtk/nautilusgtkplacesviewrow.ui @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="gtk40"> + <template class="NautilusGtkPlacesViewRow" parent="GtkListBoxRow"> + <property name="width-request">100</property> + <property name="child"> + <object class="GtkBox" id="box"> + <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="spacing">18</property> + <child> + <object class="GtkImage" id="icon_image"> + <property name="pixel-size">32</property> + </object> + </child> + <child> + <object class="GtkLabel" id="name_label"> + <property name="hexpand">1</property> + <property name="xalign">0</property> + <property name="ellipsize">3</property> + </object> + </child> + <child> + <object class="GtkLabel" id="available_space_label"> + <property name="xalign">1</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="path_label"> + <property name="justify">1</property> + <property name="ellipsize">2</property> + <property name="xalign">0</property> + <property name="max-width-chars">15</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkStack" id="mount_stack"> + <child> + <object class="GtkStackPage"> + <property name="name">button</property> + <property name="child"> + <object class="GtkButton" id="eject_button"> + <property name="visible">0</property> + <property name="halign">3</property> + <property name="valign">3</property> + <property name="tooltip-text" translatable="yes">Unmount</property> + <child> + <object class="GtkImage" id="eject_icon"> + <property name="icon-name">media-eject-symbolic</property> + </object> + </child> + <style> + <class name="image-button"/> + <class name="sidebar-button"/> + </style> + </object> + </property> + </object> + </child> + <child> + <object class="GtkStackPage"> + <property name="name">spinner</property> + <property name="child"> + <object class="GtkSpinner" id="busy_spinner"> + <property name="halign">3</property> + <property name="valign">3</property> + </object> + </property> + </object> + </child> + </object> + </child> + </object> + </property> + </template> +</interface> diff --git a/src/gtk/nautilusgtkplacesviewrowprivate.h b/src/gtk/nautilusgtkplacesviewrowprivate.h new file mode 100644 index 0000000..d54b918 --- /dev/null +++ b/src/gtk/nautilusgtkplacesviewrowprivate.h @@ -0,0 +1,59 @@ +/* nautilusgtkplacesviewrow.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 Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef NAUTILUS_GTK_PLACES_VIEW_ROW_H +#define NAUTILUS_GTK_PLACES_VIEW_ROW_H + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#endif + + +G_BEGIN_DECLS + +#define NAUTILUS_TYPE_GTK_PLACES_VIEW_ROW (nautilus_gtk_places_view_row_get_type()) + + G_DECLARE_FINAL_TYPE (NautilusGtkPlacesViewRow, nautilus_gtk_places_view_row, NAUTILUS, GTK_PLACES_VIEW_ROW, GtkListBoxRow) + +GtkWidget* nautilus_gtk_places_view_row_new (GVolume *volume, + GMount *mount); + +GtkWidget* nautilus_gtk_places_view_row_get_eject_button (NautilusGtkPlacesViewRow *row); + +GMount* nautilus_gtk_places_view_row_get_mount (NautilusGtkPlacesViewRow *row); + +GVolume* nautilus_gtk_places_view_row_get_volume (NautilusGtkPlacesViewRow *row); + +GFile* nautilus_gtk_places_view_row_get_file (NautilusGtkPlacesViewRow *row); + +void nautilus_gtk_places_view_row_set_busy (NautilusGtkPlacesViewRow *row, + gboolean is_busy); + +gboolean nautilus_gtk_places_view_row_get_is_network (NautilusGtkPlacesViewRow *row); + +void nautilus_gtk_places_view_row_set_is_network (NautilusGtkPlacesViewRow *row, + gboolean is_network); + +void nautilus_gtk_places_view_row_set_path_size_group (NautilusGtkPlacesViewRow *row, + GtkSizeGroup *group); + +void nautilus_gtk_places_view_row_set_space_size_group (NautilusGtkPlacesViewRow *row, + GtkSizeGroup *group); + +G_END_DECLS + +#endif /* NAUTILUS_GTK_PLACES_VIEW_ROW_H */ diff --git a/src/gtk/nautilusgtksidebarrow.c b/src/gtk/nautilusgtksidebarrow.c new file mode 100644 index 0000000..9b6ebaf --- /dev/null +++ b/src/gtk/nautilusgtksidebarrow.c @@ -0,0 +1,692 @@ +/* nautilusgtksidebarrow.c + * + * Copyright (C) 2015 Carlos Soriano <csoriano@gnome.org> + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include "nautilus-enum-types.h" +#include "nautilus-file.h" + +#include "nautilusgtksidebarrowprivate.h" +/* For section and place type enums */ +#include "nautilusgtkplacessidebarprivate.h" + +#include <cloudproviders.h> + +struct _NautilusGtkSidebarRow +{ + GtkListBoxRow parent_instance; + GIcon *start_icon; + GIcon *end_icon; + GtkWidget *start_icon_widget; + GtkWidget *end_icon_widget; + char *label; + char *tooltip; + char *eject_tooltip; + GtkWidget *label_widget; + gboolean ejectable; + GtkWidget *eject_button; + int order_index; + NautilusGtkPlacesSectionType section_type; + NautilusGtkPlacesPlaceType place_type; + char *uri; + NautilusFile *file; + GDrive *drive; + GVolume *volume; + GMount *mount; + GObject *cloud_provider_account; + gboolean placeholder; + NautilusGtkPlacesSidebar *sidebar; + GtkWidget *revealer; + GtkWidget *busy_spinner; +}; + +G_DEFINE_TYPE (NautilusGtkSidebarRow, nautilus_gtk_sidebar_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + PROP_0, + PROP_START_ICON, + PROP_END_ICON, + PROP_LABEL, + PROP_TOOLTIP, + PROP_EJECT_TOOLTIP, + PROP_EJECTABLE, + PROP_SIDEBAR, + PROP_ORDER_INDEX, + PROP_SECTION_TYPE, + PROP_PLACE_TYPE, + PROP_URI, + PROP_NAUTILUS_FILE, + PROP_DRIVE, + PROP_VOLUME, + PROP_MOUNT, + PROP_CLOUD_PROVIDER_ACCOUNT, + PROP_PLACEHOLDER, + LAST_PROP +}; + +static GParamSpec *properties [LAST_PROP]; + +static void +cloud_row_update (NautilusGtkSidebarRow *self) +{ + CloudProvidersAccount *account; + GIcon *end_icon; + int provider_status; + + account = CLOUD_PROVIDERS_ACCOUNT (self->cloud_provider_account); + provider_status = cloud_providers_account_get_status (account); + switch (provider_status) + { + case CLOUD_PROVIDERS_ACCOUNT_STATUS_IDLE: + end_icon = NULL; + break; + + case CLOUD_PROVIDERS_ACCOUNT_STATUS_SYNCING: + end_icon = g_themed_icon_new ("emblem-synchronizing-symbolic"); + break; + + case CLOUD_PROVIDERS_ACCOUNT_STATUS_ERROR: + end_icon = g_themed_icon_new ("dialog-warning-symbolic"); + break; + + default: + return; + } + + g_object_set (self, + "label", cloud_providers_account_get_name (account), + NULL); + g_object_set (self, + "tooltip", cloud_providers_account_get_status_details (account), + NULL); + g_object_set (self, + "end-icon", end_icon, + NULL); + + if (end_icon != NULL) + g_object_unref (end_icon); +} + +static void +nautilus_gtk_sidebar_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + NautilusGtkSidebarRow *self = NAUTILUS_GTK_SIDEBAR_ROW (object); + + switch (prop_id) + { + case PROP_SIDEBAR: + g_value_set_object (value, self->sidebar); + break; + + case PROP_START_ICON: + g_value_set_object (value, self->start_icon); + break; + + case PROP_END_ICON: + g_value_set_object (value, self->end_icon); + break; + + case PROP_LABEL: + g_value_set_string (value, self->label); + break; + + case PROP_TOOLTIP: + g_value_set_string (value, self->tooltip); + break; + + case PROP_EJECT_TOOLTIP: + g_value_set_string (value, self->eject_tooltip); + break; + + case PROP_EJECTABLE: + g_value_set_boolean (value, self->ejectable); + break; + + case PROP_ORDER_INDEX: + g_value_set_int (value, self->order_index); + break; + + case PROP_SECTION_TYPE: + g_value_set_enum (value, self->section_type); + break; + + case PROP_PLACE_TYPE: + g_value_set_enum (value, self->place_type); + break; + + case PROP_URI: + g_value_set_string (value, self->uri); + break; + + case PROP_NAUTILUS_FILE: + g_value_set_object (value, self->file); + break; + + case PROP_DRIVE: + g_value_set_object (value, self->drive); + 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_CLOUD_PROVIDER_ACCOUNT: + g_value_set_object (value, self->cloud_provider_account); + break; + + case PROP_PLACEHOLDER: + g_value_set_boolean (value, self->placeholder); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +nautilus_gtk_sidebar_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + NautilusGtkSidebarRow *self = NAUTILUS_GTK_SIDEBAR_ROW (object); + + switch (prop_id) + { + case PROP_SIDEBAR: + self->sidebar = g_value_get_object (value); + break; + + case PROP_START_ICON: + { + g_clear_object (&self->start_icon); + object = g_value_get_object (value); + if (object != NULL) + { + self->start_icon = G_ICON (g_object_ref (object)); + gtk_image_set_from_gicon (GTK_IMAGE (self->start_icon_widget), self->start_icon); + } + else + { + gtk_image_clear (GTK_IMAGE (self->start_icon_widget)); + } + break; + } + + case PROP_END_ICON: + { + g_clear_object (&self->end_icon); + object = g_value_get_object (value); + if (object != NULL) + { + self->end_icon = G_ICON (g_object_ref (object)); + gtk_image_set_from_gicon (GTK_IMAGE (self->end_icon_widget), self->end_icon); + gtk_widget_show (self->end_icon_widget); + } + else + { + gtk_image_clear (GTK_IMAGE (self->end_icon_widget)); + gtk_widget_hide (self->end_icon_widget); + } + break; + } + + case PROP_LABEL: + g_free (self->label); + self->label = g_strdup (g_value_get_string (value)); + gtk_label_set_text (GTK_LABEL (self->label_widget), self->label); + break; + + case PROP_TOOLTIP: + g_free (self->tooltip); + self->tooltip = g_strdup (g_value_get_string (value)); + gtk_widget_set_tooltip_text (GTK_WIDGET (self), self->tooltip); + break; + + case PROP_EJECT_TOOLTIP: + g_free (self->eject_tooltip); + self->eject_tooltip = g_strdup (g_value_get_string (value)); + gtk_widget_set_tooltip_text (GTK_WIDGET (self->eject_button), self->eject_tooltip); + break; + + case PROP_EJECTABLE: + self->ejectable = g_value_get_boolean (value); + if (self->ejectable) + gtk_widget_show (self->eject_button); + else + gtk_widget_hide (self->eject_button); + break; + + case PROP_ORDER_INDEX: + self->order_index = g_value_get_int (value); + break; + + case PROP_SECTION_TYPE: + self->section_type = g_value_get_enum (value); + if (self->section_type == NAUTILUS_GTK_PLACES_SECTION_COMPUTER || + self->section_type == NAUTILUS_GTK_PLACES_SECTION_OTHER_LOCATIONS) + gtk_label_set_ellipsize (GTK_LABEL (self->label_widget), PANGO_ELLIPSIZE_NONE); + else + gtk_label_set_ellipsize (GTK_LABEL (self->label_widget), PANGO_ELLIPSIZE_END); + break; + + case PROP_PLACE_TYPE: + self->place_type = g_value_get_enum (value); + break; + + case PROP_URI: + g_free (self->uri); + self->uri = g_strdup (g_value_get_string (value)); + if (self->uri != NULL) + { + self->file = nautilus_file_get_by_uri (self->uri); + if (self->file != NULL) + nautilus_file_call_when_ready (self->file, NAUTILUS_FILE_ATTRIBUTE_MOUNT, NULL, NULL); + } + break; + + case PROP_DRIVE: + g_set_object (&self->drive, g_value_get_object (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)); + break; + + case PROP_CLOUD_PROVIDER_ACCOUNT: + if (self->cloud_provider_account != NULL) + g_signal_handlers_disconnect_by_data (self->cloud_provider_account, self); + + self->cloud_provider_account = g_value_dup_object (value); + + if (self->cloud_provider_account != NULL) + { + g_signal_connect_swapped (self->cloud_provider_account, "notify::name", + G_CALLBACK (cloud_row_update), self); + g_signal_connect_swapped (self->cloud_provider_account, "notify::status", + G_CALLBACK (cloud_row_update), self); + g_signal_connect_swapped (self->cloud_provider_account, "notify::status-details", + G_CALLBACK (cloud_row_update), self); + } + break; + + case PROP_PLACEHOLDER: + { + self->placeholder = g_value_get_boolean (value); + if (self->placeholder) + { + g_clear_object (&self->start_icon); + g_clear_object (&self->end_icon); + g_free (self->label); + self->label = NULL; + g_free (self->tooltip); + self->tooltip = NULL; + self->eject_tooltip = NULL; + gtk_widget_set_tooltip_text (GTK_WIDGET (self), NULL); + self->ejectable = FALSE; + self->section_type = NAUTILUS_GTK_PLACES_SECTION_BOOKMARKS; + self->place_type = NAUTILUS_GTK_PLACES_BOOKMARK_PLACEHOLDER; + g_free (self->uri); + self->uri = NULL; + g_clear_object (&self->drive); + g_clear_object (&self->volume); + g_clear_object (&self->mount); + g_clear_object (&self->cloud_provider_account); + + gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (self), NULL); + + gtk_widget_add_css_class (GTK_WIDGET (self), "sidebar-placeholder-row"); + } + + break; + } + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +on_child_revealed (GObject *self, + GParamSpec *pspec, + gpointer user_data) +{ + /* We need to hide the actual widget because if not the GtkListBoxRow will + * still allocate the paddings, even if the revealer is not revealed, and + * therefore the row will be still somewhat visible. */ + if (!gtk_revealer_get_reveal_child (GTK_REVEALER (self))) + gtk_widget_hide (GTK_WIDGET (NAUTILUS_GTK_SIDEBAR_ROW (user_data))); +} + +void +nautilus_gtk_sidebar_row_reveal (NautilusGtkSidebarRow *self) +{ + gtk_widget_show (GTK_WIDGET (self)); + gtk_revealer_set_reveal_child (GTK_REVEALER (self->revealer), TRUE); +} + +void +nautilus_gtk_sidebar_row_hide (NautilusGtkSidebarRow *self, + gboolean immediate) +{ + guint transition_duration; + + transition_duration = gtk_revealer_get_transition_duration (GTK_REVEALER (self->revealer)); + if (immediate) + gtk_revealer_set_transition_duration (GTK_REVEALER (self->revealer), 0); + + gtk_revealer_set_reveal_child (GTK_REVEALER (self->revealer), FALSE); + + gtk_revealer_set_transition_duration (GTK_REVEALER (self->revealer), transition_duration); +} + +void +nautilus_gtk_sidebar_row_set_start_icon (NautilusGtkSidebarRow *self, + GIcon *icon) +{ + g_return_if_fail (NAUTILUS_IS_GTK_SIDEBAR_ROW (self)); + + if (self->start_icon != icon) + { + g_set_object (&self->start_icon, icon); + if (self->start_icon != NULL) + gtk_image_set_from_gicon (GTK_IMAGE (self->start_icon_widget), self->start_icon); + else + gtk_image_clear (GTK_IMAGE (self->start_icon_widget)); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_START_ICON]); + } +} + +void +nautilus_gtk_sidebar_row_set_end_icon (NautilusGtkSidebarRow *self, + GIcon *icon) +{ + g_return_if_fail (NAUTILUS_IS_GTK_SIDEBAR_ROW (self)); + + if (self->end_icon != icon) + { + g_set_object (&self->end_icon, icon); + if (self->end_icon != NULL) + gtk_image_set_from_gicon (GTK_IMAGE (self->end_icon_widget), self->end_icon); + else + if (self->end_icon_widget != NULL) + gtk_image_clear (GTK_IMAGE (self->end_icon_widget)); + + g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_END_ICON]); + } +} + +static void +nautilus_gtk_sidebar_row_finalize (GObject *object) +{ + NautilusGtkSidebarRow *self = NAUTILUS_GTK_SIDEBAR_ROW (object); + + g_clear_object (&self->start_icon); + g_clear_object (&self->end_icon); + g_free (self->label); + self->label = NULL; + g_free (self->tooltip); + self->tooltip = NULL; + g_free (self->eject_tooltip); + self->eject_tooltip = NULL; + g_free (self->uri); + self->uri = NULL; + nautilus_file_unref (self->file); + g_clear_object (&self->drive); + g_clear_object (&self->volume); + g_clear_object (&self->mount); + if (self->cloud_provider_account != NULL) + g_signal_handlers_disconnect_by_data (self->cloud_provider_account, self); + g_clear_object (&self->cloud_provider_account); + + G_OBJECT_CLASS (nautilus_gtk_sidebar_row_parent_class)->finalize (object); +} + +static void +nautilus_gtk_sidebar_row_init (NautilusGtkSidebarRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_widget_set_focus_on_click (GTK_WIDGET (self), FALSE); + + self->file = NULL; +} + +static void +nautilus_gtk_sidebar_row_class_init (NautilusGtkSidebarRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = nautilus_gtk_sidebar_row_get_property; + object_class->set_property = nautilus_gtk_sidebar_row_set_property; + object_class->finalize = nautilus_gtk_sidebar_row_finalize; + + properties [PROP_SIDEBAR] = + g_param_spec_object ("sidebar", + "Sidebar", + "Sidebar", + NAUTILUS_TYPE_GTK_PLACES_SIDEBAR, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_START_ICON] = + g_param_spec_object ("start-icon", + "start-icon", + "The start icon.", + G_TYPE_ICON, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_END_ICON] = + g_param_spec_object ("end-icon", + "end-icon", + "The end icon.", + G_TYPE_ICON, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_LABEL] = + g_param_spec_string ("label", + "label", + "The label text.", + NULL, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_TOOLTIP] = + g_param_spec_string ("tooltip", + "Tooltip", + "Tooltip", + NULL, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_EJECT_TOOLTIP] = + g_param_spec_string ("eject-tooltip", + "Eject Tooltip", + "Eject Tooltip", + NULL, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_EJECTABLE] = + g_param_spec_boolean ("ejectable", + "Ejectable", + "Ejectable", + FALSE, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_ORDER_INDEX] = + g_param_spec_int ("order-index", + "OrderIndex", + "Order Index", + 0, G_MAXINT, 0, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_SECTION_TYPE] = + g_param_spec_enum ("section-type", + "section type", + "The section type.", + NAUTILUS_TYPE_GTK_PLACES_SECTION_TYPE, + NAUTILUS_GTK_PLACES_SECTION_INVALID, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY)); + + properties [PROP_PLACE_TYPE] = + g_param_spec_enum ("place-type", + "place type", + "The place type.", + NAUTILUS_TYPE_GTK_PLACES_PLACE_TYPE, + NAUTILUS_GTK_PLACES_INVALID, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY)); + + properties [PROP_URI] = + g_param_spec_string ("uri", + "Uri", + "Uri", + NULL, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_NAUTILUS_FILE] = + g_param_spec_object ("file", + "File", + "Nautilus File", + NAUTILUS_TYPE_FILE, + (G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS )); + + properties [PROP_DRIVE] = + g_param_spec_object ("drive", + "Drive", + "Drive", + G_TYPE_DRIVE, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_VOLUME] = + g_param_spec_object ("volume", + "Volume", + "Volume", + G_TYPE_VOLUME, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_MOUNT] = + g_param_spec_object ("mount", + "Mount", + "Mount", + G_TYPE_MOUNT, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_CLOUD_PROVIDER_ACCOUNT] = + g_param_spec_object ("cloud-provider-account", + "CloudProvidersAccount", + "CloudProvidersAccount", + G_TYPE_OBJECT, + (G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties [PROP_PLACEHOLDER] = + g_param_spec_boolean ("placeholder", + "Placeholder", + "Placeholder", + FALSE, + (G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, LAST_PROP, properties); + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/nautilus/gtk/ui/nautilusgtksidebarrow.ui"); + + gtk_widget_class_bind_template_child (widget_class, NautilusGtkSidebarRow, start_icon_widget); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkSidebarRow, end_icon_widget); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkSidebarRow, label_widget); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkSidebarRow, eject_button); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkSidebarRow, revealer); + gtk_widget_class_bind_template_child (widget_class, NautilusGtkSidebarRow, busy_spinner); + + gtk_widget_class_bind_template_callback (widget_class, on_child_revealed); + gtk_widget_class_set_css_name (widget_class, "row"); +} + +NautilusGtkSidebarRow* +nautilus_gtk_sidebar_row_clone (NautilusGtkSidebarRow *self) +{ + return g_object_new (NAUTILUS_TYPE_GTK_SIDEBAR_ROW, + "sidebar", self->sidebar, + "start-icon", self->start_icon, + "end-icon", self->end_icon, + "label", self->label, + "tooltip", self->tooltip, + "eject-tooltip", self->eject_tooltip, + "ejectable", self->ejectable, + "order-index", self->order_index, + "section-type", self->section_type, + "place-type", self->place_type, + "uri", self->uri, + "file", self->file, + "drive", self->drive, + "volume", self->volume, + "mount", self->mount, + "cloud-provider-account", self->cloud_provider_account, + NULL); +} + +GtkWidget* +nautilus_gtk_sidebar_row_get_eject_button (NautilusGtkSidebarRow *self) +{ + return self->eject_button; +} + +void +nautilus_gtk_sidebar_row_set_busy (NautilusGtkSidebarRow *row, + gboolean is_busy) +{ + g_return_if_fail (NAUTILUS_IS_GTK_SIDEBAR_ROW (row)); + + gtk_widget_set_visible (row->busy_spinner, is_busy); +} diff --git a/src/gtk/nautilusgtksidebarrow.ui b/src/gtk/nautilusgtksidebarrow.ui new file mode 100644 index 0000000..95d91f9 --- /dev/null +++ b/src/gtk/nautilusgtksidebarrow.ui @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="gtk40"> + <template class="NautilusGtkSidebarRow" parent="GtkListBoxRow"> + <property name="focus-on-click">0</property> + <style> + <class name="sidebar-row"/> + </style> + <property name="child"> + <object class="GtkRevealer" id="revealer"> + <property name="reveal-child">1</property> + <signal name="notify::child-revealed" handler="on_child_revealed"/> + <style> + <class name="sidebar-revealer"/> + </style> + <child> + <object class="GtkBox"> + <child> + <object class="GtkImage" id="start_icon_widget"> + <style> + <class name="sidebar-icon"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="label_widget"> + <property name="hexpand">1</property> + <property name="xalign">0</property> + <style> + <class name="sidebar-label"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="end_icon_widget"> + <property name="visible">0</property> + <property name="hexpand">1</property> + <property name="halign">2</property> + <property name="valign">3</property> + <style> + <class name="sidebar-icon"/> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="eject_button"> + <property name="halign">3</property> + <property name="valign">3</property> + <property name="margin-start">4px</property> + <property name="icon-name">media-eject-symbolic</property> + <style> + <class name="sidebar-button"/> + </style> + </object> + </child> + <child> + <object class="GtkSpinner" id="busy_spinner"> + <property name="spinning">1</property> + <property name="halign">3</property> + <property name="valign">3</property> + <property name="margin-start">4px</property> + <property name="visible">0</property> + </object> + </child> + </object> + </child> + </object> + </property> + </template> +</interface> diff --git a/src/gtk/nautilusgtksidebarrowprivate.h b/src/gtk/nautilusgtksidebarrowprivate.h new file mode 100644 index 0000000..0bd9355 --- /dev/null +++ b/src/gtk/nautilusgtksidebarrowprivate.h @@ -0,0 +1,60 @@ +/* nautilusgtksidebarrowprivate.h + * + * Copyright (C) 2015 Carlos Soriano <csoriano@gnome.org> + * + * This file is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of the + * License, or (at your option) any later version. + * + * This file 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef NAUTILUS_GTK_SIDEBAR_ROW_PRIVATE_H +#define NAUTILUS_GTK_SIDEBAR_ROW_PRIVATE_H + +#include <glib.h> + +G_BEGIN_DECLS + +#define NAUTILUS_TYPE_GTK_SIDEBAR_ROW (nautilus_gtk_sidebar_row_get_type()) +#define NAUTILUS_GTK_SIDEBAR_ROW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NAUTILUS_TYPE_GTK_SIDEBAR_ROW, NautilusGtkSidebarRow)) +#define NAUTILUS_GTK_SIDEBAR_ROW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NAUTILUS_TYPE_GTK_SIDEBAR_ROW, NautilusGtkSidebarRowClass)) +#define NAUTILUS_IS_GTK_SIDEBAR_ROW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NAUTILUS_TYPE_GTK_SIDEBAR_ROW)) +#define NAUTILUS_IS_GTK_SIDEBAR_ROW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NAUTILUS_TYPE_GTK_SIDEBAR_ROW)) +#define NAUTILUS_GTK_SIDEBAR_ROW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NAUTILUS_TYPE_GTK_SIDEBAR_ROW, NautilusGtkSidebarRowClass)) + +typedef struct _NautilusGtkSidebarRow NautilusGtkSidebarRow; +typedef struct _NautilusGtkSidebarRowClass NautilusGtkSidebarRowClass; + +struct _NautilusGtkSidebarRowClass +{ + GtkListBoxRowClass parent; +}; + +GType nautilus_gtk_sidebar_row_get_type (void) G_GNUC_CONST; + +NautilusGtkSidebarRow *nautilus_gtk_sidebar_row_new (void); +NautilusGtkSidebarRow *nautilus_gtk_sidebar_row_clone (NautilusGtkSidebarRow *self); + +/* Use these methods instead of gtk_widget_hide/show to use an animation */ +void nautilus_gtk_sidebar_row_hide (NautilusGtkSidebarRow *self, + gboolean immediate); +void nautilus_gtk_sidebar_row_reveal (NautilusGtkSidebarRow *self); + +GtkWidget *nautilus_gtk_sidebar_row_get_eject_button (NautilusGtkSidebarRow *self); +void nautilus_gtk_sidebar_row_set_start_icon (NautilusGtkSidebarRow *self, + GIcon *icon); +void nautilus_gtk_sidebar_row_set_end_icon (NautilusGtkSidebarRow *self, + GIcon *icon); +void nautilus_gtk_sidebar_row_set_busy (NautilusGtkSidebarRow *row, + gboolean is_busy); + +G_END_DECLS + +#endif /* NAUTILUS_GTK_SIDEBAR_ROW_PRIVATE_H */ |