/* * Nautilus * * Copyright (C) 1999, 2000 Eazel, Inc. * * Nautilus is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * Nautilus is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * Authors: John Sullivan */ /* nautilus-bookmark-list.c - implementation of centralized list of bookmarks. */ #include #include "nautilus-bookmark-list.h" #include "nautilus-bookmark.h" #include "nautilus-file-utilities.h" #include "nautilus-file.h" #include "nautilus-icon-names.h" #include "nautilus-scheme.h" #include #include #include #define MAX_BOOKMARK_LENGTH 80 #define LOAD_JOB 1 #define SAVE_JOB 2 struct _NautilusBookmarkList { GObject parent_instance; GList *list; GFileMonitor *monitor; GCancellable *load_cancellable; GCancellable *save_cancellable; }; enum { CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; /* forward declarations */ #define NAUTILUS_BOOKMARK_LIST_ERROR (nautilus_bookmark_list_error_quark ()) static GQuark nautilus_bookmark_list_error_quark (void); static void nautilus_bookmark_list_load_file (NautilusBookmarkList *bookmarks); static void nautilus_bookmark_list_save_file (NautilusBookmarkList *bookmarks); G_DEFINE_FINAL_TYPE (NautilusBookmarkList, nautilus_bookmark_list, G_TYPE_OBJECT) static GQuark nautilus_bookmark_list_error_quark (void) { return g_quark_from_static_string ("nautilus-bookmark-list-error-quark"); } static GFile * nautilus_bookmark_list_get_file (void) { g_autofree char *filename = NULL; if (g_test_initialized () && g_getenv ("XDG_CONFIG_HOME")) { filename = g_build_filename (g_getenv ("XDG_CONFIG_HOME"), "gtk-3.0", "bookmarks", NULL); } else { filename = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "bookmarks", NULL); } return g_file_new_for_path (filename); } /* Initialization. */ static void bookmark_in_list_changed_callback (NautilusBookmark *bookmark, NautilusBookmarkList *bookmarks) { g_assert (NAUTILUS_IS_BOOKMARK (bookmark)); g_assert (NAUTILUS_IS_BOOKMARK_LIST (bookmarks)); /* save changes to the list */ nautilus_bookmark_list_save_file (bookmarks); } static void bookmark_in_list_name_changed (NautilusBookmarkList *bookmarks) { nautilus_bookmark_list_save_file (bookmarks); } static void stop_monitoring_bookmark (NautilusBookmark *bookmark, gpointer user_data) { g_signal_handlers_disconnect_by_func (bookmark, bookmark_in_list_changed_callback, user_data); g_signal_handlers_disconnect_by_func (bookmark, bookmark_in_list_name_changed, user_data); } static void clear (NautilusBookmarkList *bookmarks) { g_list_foreach (bookmarks->list, (GFunc) stop_monitoring_bookmark, bookmarks); g_list_free_full (bookmarks->list, g_object_unref); bookmarks->list = NULL; } static void do_finalize (GObject *object) { NautilusBookmarkList *self = NAUTILUS_BOOKMARK_LIST (object); if (self->monitor != NULL) { g_file_monitor_cancel (self->monitor); g_clear_object (&self->monitor); } if (self->load_cancellable != NULL) { g_cancellable_cancel (self->load_cancellable); g_clear_object (&self->load_cancellable); } if (self->save_cancellable != NULL) { g_cancellable_cancel (self->save_cancellable); g_clear_object (&self->save_cancellable); } clear (self); G_OBJECT_CLASS (nautilus_bookmark_list_parent_class)->finalize (object); } static void nautilus_bookmark_list_class_init (NautilusBookmarkListClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); object_class->finalize = do_finalize; signals[CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void bookmark_monitor_changed_cb (GFileMonitor *monitor, GFile *child, GFile *other_file, GFileMonitorEvent eflags, gpointer user_data) { if (eflags == G_FILE_MONITOR_EVENT_CHANGED || eflags == G_FILE_MONITOR_EVENT_CREATED) { g_return_if_fail (NAUTILUS_IS_BOOKMARK_LIST (NAUTILUS_BOOKMARK_LIST (user_data))); nautilus_bookmark_list_load_file (NAUTILUS_BOOKMARK_LIST (user_data)); } } static void nautilus_bookmarks_monitor_file (NautilusBookmarkList *bookmarks) { g_autoptr (GFile) file = nautilus_bookmark_list_get_file (); bookmarks->monitor = g_file_monitor_file (file, 0, NULL, NULL); g_file_monitor_set_rate_limit (bookmarks->monitor, 1000); g_signal_connect_object (bookmarks->monitor, "changed", G_CALLBACK (bookmark_monitor_changed_cb), bookmarks, G_CONNECT_DEFAULT); } static void nautilus_bookmark_list_init (NautilusBookmarkList *bookmarks) { nautilus_bookmark_list_load_file (bookmarks); nautilus_bookmarks_monitor_file (bookmarks); } static GList * bookmark_list_get_node (NautilusBookmarkList *bookmarks, GFile *location, guint *index_ptr) { guint index = 0; for (GList *node = bookmarks->list; node != NULL; node = node->next, index += 1) { NautilusBookmark *bookmark = node->data; GFile *bookmark_location = nautilus_bookmark_get_location (bookmark); if (g_file_equal (location, bookmark_location)) { if (index_ptr) { *index_ptr = index; } return node; } } return NULL; } /** * insert_bookmark_internal: * @bookmarks: pointer to a #NautilusBookmarkList * @bookmark: (transfer full): pointer to a #NautilusBookmark to insert * @index: Position to store bookmark index in the list * * Adds a bookmark to the given #NautilusBookmarkList if it doesn't exist. * * Returns: %TRUE when the bookmark was inserted and %FALSE if the bookmark * already exists. */ static gboolean insert_bookmark_internal (NautilusBookmarkList *bookmarks, NautilusBookmark *bookmark, int index) { GFile *location = nautilus_bookmark_get_location (bookmark); if (bookmark_list_get_node (bookmarks, location, NULL) != NULL) { g_object_unref (bookmark); return FALSE; } bookmarks->list = g_list_insert (bookmarks->list, bookmark, index); g_signal_connect_object (bookmark, "contents-changed", G_CALLBACK (bookmark_in_list_changed_callback), bookmarks, G_CONNECT_DEFAULT); g_signal_connect_object (bookmark, "notify::name", G_CALLBACK (bookmark_in_list_name_changed), bookmarks, G_CONNECT_SWAPPED); return TRUE; } /** * nautilus_bookmark_list_get_bookmark: * * Get the bookmark with the specified location, if any * @bookmarks: the list of bookmarks. * @location: a #GFile * * Returns: (transfer none): the bookmark with location @location, or %NULL. **/ NautilusBookmark * nautilus_bookmark_list_get_bookmark (NautilusBookmarkList *bookmarks, GFile *location) { g_return_val_if_fail (NAUTILUS_IS_BOOKMARK_LIST (bookmarks), NULL); g_return_val_if_fail (G_IS_FILE (location), NULL); GList *node = bookmark_list_get_node (bookmarks, location, NULL); return node != NULL ? node->data : NULL; } /** * nautilus_bookmark_list_add: * @bookmarks: NautilusBookmarkList to append to. * @location: (transfer none): location to insert a bookmark for * @position: position at which to add the bookmark, see g_list_insert * * Adds a bookmark for the given @location at at a specified @position. * * Returns: (transfer none): A new bookmark if one was appended or %NULL otherwise **/ void nautilus_bookmark_list_add (NautilusBookmarkList *bookmarks, GFile *location, int position) { g_return_if_fail (NAUTILUS_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (G_IS_FILE (location)); NautilusBookmark *bookmark = nautilus_bookmark_new (location, NULL); if (insert_bookmark_internal (bookmarks, bookmark, position)) { nautilus_bookmark_list_save_file (bookmarks); } } /** * nautilus_bookmark_list_contains: * * Check whether a bookmark for the given @location exists * @bookmarks: NautilusBookmarkList to check contents of. * @location: a #GFile to check for. * * Return value: TRUE if matching bookmark is in list, FALSE otherwise **/ gboolean nautilus_bookmark_list_contains (NautilusBookmarkList *bookmarks, GFile *location) { g_return_val_if_fail (NAUTILUS_IS_BOOKMARK_LIST (bookmarks), FALSE); g_return_val_if_fail (G_IS_FILE (location), FALSE); return bookmark_list_get_node (bookmarks, location, NULL) != NULL; } /** * nautilus_bookmark_list_move_item: * * Move the given item to the destination. * @location: the location of the bookmark to move. * @destination: index to which to move the bookmark. **/ void nautilus_bookmark_list_move_item (NautilusBookmarkList *bookmarks, GFile *location, guint destination) { guint index; GList *link_to_move = bookmark_list_get_node (bookmarks, location, &index); if (link_to_move == NULL) { g_autofree char *uri = g_file_get_uri (location); g_warning ("Attempted moving unknown bookmark of %s", uri); return; } if (index == destination) { return; } GList *link_at_destination = g_list_nth (bookmarks->list, destination); bookmarks->list = g_list_remove_link (bookmarks->list, link_to_move); /* NULL link at destination means end of the list */ bookmarks->list = g_list_insert_before_link (bookmarks->list, link_at_destination, link_to_move); nautilus_bookmark_list_save_file (bookmarks); } /** * nautilus_bookmark_list_remove: * * Removes any bookmark for @location * @bookmarks: the list of bookmarks. * @location: The location to remove a bookmark for. **/ void nautilus_bookmark_list_remove (NautilusBookmarkList *bookmarks, GFile *location) { g_return_if_fail (NAUTILUS_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (location != NULL); GList *link_to_remove = bookmark_list_get_node (bookmarks, location, NULL); if (link_to_remove == NULL) { g_autofree char *uri = g_file_get_uri (location); g_warning ("Attempted removing unknown bookmark of %s", uri); return; } bookmarks->list = g_list_remove_link (bookmarks->list, link_to_remove); NautilusBookmark *bookmark = link_to_remove->data; stop_monitoring_bookmark (bookmark, bookmarks); g_list_free_full (link_to_remove, g_object_unref); nautilus_bookmark_list_save_file (bookmarks); } static void load_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { g_autoptr (GError) error = NULL; g_autofree gchar *contents = g_task_propagate_pointer (G_TASK (res), &error); if (g_cancellable_is_cancelled (g_task_get_cancellable (G_TASK (res)))) { return; } NautilusBookmarkList *self = NAUTILUS_BOOKMARK_LIST (source_object); g_clear_object (&self->load_cancellable); if (error != NULL) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_warning ("Unable to get contents of the bookmarks file: %s", error->message); } return; } /* Wipe out old list. */ clear (self); char **lines = g_strsplit (contents, "\n", -1); for (guint i = 0; lines[i]; i++) { char *uri = lines[i]; /* Ignore empty or invalid lines that cannot be parsed properly */ if (uri[0] == '\0' || g_unichar_isspace (uri[0])) { continue; } /* Split bookmarks file entries into URI and label, which are separated by * a space. This behavior is used since GTK 2.7's file chooser. */ char *label = NULL; char *space = strchr (uri, ' '); if (space != NULL) { *space = '\0'; label = space + 1; } g_autoptr (GFile) location = g_file_new_for_uri (uri); NautilusBookmark *new_bookmark = nautilus_bookmark_new (location, label); insert_bookmark_internal (self, new_bookmark, -1); } g_signal_emit (self, signals[CHANGED], 0); g_strfreev (lines); } static void load_io_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { GFile *file; gchar *contents; GError *error = NULL; file = nautilus_bookmark_list_get_file (); g_file_load_contents (file, cancellable, &contents, NULL, NULL, &error); g_object_unref (file); if (error != NULL) { g_task_return_error (task, error); } else { g_task_return_pointer (task, contents, g_free); } } static void load_file_async (NautilusBookmarkList *self) { /* This didn't come from a monitor since it would be temperorarly disabled. * The only other source of load is the initialization, which would never * occur after another operation */ g_return_if_fail (self->save_cancellable == NULL); g_autoptr (GTask) task = NULL; if (self->load_cancellable != NULL) { g_cancellable_cancel (self->load_cancellable); g_clear_object (&self->load_cancellable); } self->load_cancellable = g_cancellable_new (); task = g_task_new (G_OBJECT (self), self->load_cancellable, load_callback, NULL); g_task_run_in_thread (task, load_io_thread); } static void save_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { if (g_cancellable_is_cancelled (g_task_get_cancellable (G_TASK (res)))) { return; } NautilusBookmarkList *self = NAUTILUS_BOOKMARK_LIST (source_object); g_autoptr (GError) error = NULL; gboolean success; success = g_task_propagate_boolean (G_TASK (res), &error); g_clear_object (&self->save_cancellable); if (error != NULL) { g_warning ("Unable to replace contents of the bookmarks file: %s", error->message); } /* g_file_replace_contents() returned FALSE, but did not set an error. */ if (!success) { g_warning ("Unable to replace contents of the bookmarks file."); } /* re-enable bookmark file monitoring */ nautilus_bookmarks_monitor_file (self); } static void save_io_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { if (g_task_return_error_if_cancelled (task)) { return; } g_autoptr (GFile) file = nautilus_bookmark_list_get_file (); g_autoptr (GFile) parent = g_file_get_parent (file); const gchar *path = g_file_peek_path (parent); gchar *contents; gboolean success; GError *error = NULL; if (g_mkdir_with_parents (path, 0700) == -1) { int saved_errno = errno; g_set_error (&error, NAUTILUS_BOOKMARK_LIST_ERROR, 0, "Failed to create bookmarks folder %s: %s", path, g_strerror (saved_errno)); g_task_return_error (task, error); return; } contents = (gchar *) g_task_get_task_data (task); success = g_file_replace_contents (file, contents, strlen (contents), NULL, FALSE, 0, NULL, cancellable, &error); if (error != NULL) { g_task_return_error (task, error); } else { g_task_return_boolean (task, success); } } static void save_file_async (NautilusBookmarkList *self) { g_autoptr (GTask) task = NULL; GString *bookmark_string = g_string_new (NULL); /* temporarily disable bookmark file monitoring when writing file */ if (self->monitor != NULL) { g_file_monitor_cancel (self->monitor); g_clear_object (&self->monitor); } if (self->save_cancellable != NULL) { g_cancellable_cancel (self->save_cancellable); g_clear_object (&self->save_cancellable); } if (self->load_cancellable != NULL) { g_cancellable_cancel (self->load_cancellable); g_clear_object (&self->load_cancellable); } self->save_cancellable = g_cancellable_new (); for (GList *l = self->list; l != NULL; l = l->next) { NautilusBookmark *bookmark = NAUTILUS_BOOKMARK (l->data); /* Store bookmark URI followed by label */ g_autofree char *uri = nautilus_bookmark_get_uri (bookmark); const char *label = nautilus_bookmark_get_name (bookmark); g_string_append_printf (bookmark_string, "%s %s\n", uri, label); } task = g_task_new (G_OBJECT (self), self->save_cancellable, save_callback, NULL); gchar *contents = g_string_free_and_steal (bookmark_string); g_task_set_task_data (task, contents, g_free); g_task_run_in_thread (task, save_io_thread); } /** * nautilus_bookmark_list_load_file: * * Reads bookmarks from file, clobbering contents in memory. * @bookmarks: the list of bookmarks to fill with file contents. **/ static void nautilus_bookmark_list_load_file (NautilusBookmarkList *bookmarks) { load_file_async (bookmarks); } /** * nautilus_bookmark_list_save_file: * * Save bookmarks to disk. * @bookmarks: the list of bookmarks to save. **/ static void nautilus_bookmark_list_save_file (NautilusBookmarkList *bookmarks) { g_signal_emit (bookmarks, signals[CHANGED], 0); save_file_async (bookmarks); } gboolean nautilus_bookmark_list_can_bookmark (NautilusBookmarkList *list, GFile *location) { if (bookmark_list_get_node (list, location, NULL) != NULL) { /* Already bookmarked */ return FALSE; } if (g_file_has_uri_scheme (location, SCHEME_SEARCH)) { return FALSE; } if (g_file_has_uri_scheme (location, SCHEME_RECENT) || g_file_has_uri_scheme (location, SCHEME_STARRED) || g_file_has_uri_scheme (location, SCHEME_NETWORK_VIEW) || nautilus_is_home_directory (location) || g_file_has_uri_scheme (location, SCHEME_TRASH)) { /* Already in the sidebar */ return FALSE; } return TRUE; } /** * nautilus_bookmark_list_new: * * Create a new bookmark_list, with contents read from disk. * * Return value: A pointer to the new widget. **/ NautilusBookmarkList * nautilus_bookmark_list_new (void) { return g_object_new (NAUTILUS_TYPE_BOOKMARK_LIST, NULL); } /** * nautilus_bookmark_list_get_all: * * Get a GList of all NautilusBookmark. * @bookmarks: NautilusBookmarkList from where to get the bookmarks. **/ GList * nautilus_bookmark_list_get_all (NautilusBookmarkList *bookmarks) { g_return_val_if_fail (NAUTILUS_IS_BOOKMARK_LIST (bookmarks), NULL); return bookmarks->list; }