/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ /* * Caja * * Copyright (C) 1999, 2000 Eazel, Inc. * * Caja 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. * * Caja is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * * Authors: John Sullivan <sullivan@eazel.com> */ /* caja-bookmark-list.c - implementation of centralized list of bookmarks. */ #include <config.h> #include "caja-bookmark-list.h" #include <libcaja-private/caja-file-utilities.h> #include <libcaja-private/caja-file.h> #include <libcaja-private/caja-icon-names.h> #include <gio/gio.h> #include <string.h> #define LOAD_JOB 1 #define SAVE_JOB 2 enum { CONTENTS_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; static char *window_geometry; static CajaBookmarkList *singleton = NULL; /* forward declarations */ static void caja_bookmark_list_load_file (CajaBookmarkList *bookmarks); static void caja_bookmark_list_save_file (CajaBookmarkList *bookmarks); G_DEFINE_TYPE(CajaBookmarkList, caja_bookmark_list, G_TYPE_OBJECT) static CajaBookmark * new_bookmark_from_uri (const char *uri, const char *label) { CajaBookmark *new_bookmark; CajaFile *file; char *name; GIcon *icon; gboolean has_label; GFile *location; gboolean native; location = NULL; if (uri) { location = g_file_new_for_uri (uri); } has_label = FALSE; if (!label) { name = caja_compute_title_for_location (location); } else { name = g_strdup (label); has_label = TRUE; } new_bookmark = NULL; if (uri) { native = g_file_is_native (location); file = caja_file_get (location); icon = NULL; if (caja_file_check_if_ready (file, CAJA_FILE_ATTRIBUTES_FOR_ICON)) { icon = caja_file_get_gicon (file, 0); } caja_file_unref (file); if (icon == NULL) { icon = native ? g_themed_icon_new (CAJA_ICON_FOLDER) : g_themed_icon_new (CAJA_ICON_FOLDER_REMOTE); } new_bookmark = caja_bookmark_new (location, name, has_label, icon); g_object_unref (icon); } g_free (name); g_object_unref (location); return new_bookmark; } static GFile * caja_bookmark_list_get_legacy_file (void) { char *filename; GFile *file; 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 * caja_bookmark_list_get_file (void) { char *filename; GFile *file; 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; } /* Initialization. */ static void bookmark_in_list_changed_callback (CajaBookmark *bookmark, CajaBookmarkList *bookmarks) { g_assert (CAJA_IS_BOOKMARK (bookmark)); g_assert (CAJA_IS_BOOKMARK_LIST (bookmarks)); /* Save changes so we'll have the good icon next time. */ caja_bookmark_list_save_file (bookmarks); } static void stop_monitoring_bookmark (CajaBookmarkList *bookmarks, CajaBookmark *bookmark) { g_signal_handlers_disconnect_by_func (bookmark, bookmark_in_list_changed_callback, bookmarks); } static void stop_monitoring_one (gpointer data, gpointer user_data) { g_assert (CAJA_IS_BOOKMARK (data)); g_assert (CAJA_IS_BOOKMARK_LIST (user_data)); stop_monitoring_bookmark (CAJA_BOOKMARK_LIST (user_data), CAJA_BOOKMARK (data)); } static void clear (CajaBookmarkList *bookmarks) { g_list_foreach (bookmarks->list, stop_monitoring_one, bookmarks); g_list_free_full (bookmarks->list, g_object_unref); bookmarks->list = NULL; } static void do_finalize (GObject *object) { if (CAJA_BOOKMARK_LIST (object)->monitor != NULL) { g_file_monitor_cancel (CAJA_BOOKMARK_LIST (object)->monitor); CAJA_BOOKMARK_LIST (object)->monitor = NULL; } g_queue_free (CAJA_BOOKMARK_LIST (object)->pending_ops); clear (CAJA_BOOKMARK_LIST (object)); G_OBJECT_CLASS (caja_bookmark_list_parent_class)->finalize (object); } static GObject * do_constructor (GType type, guint n_construct_params, GObjectConstructParam *construct_params) { GObject *retval; if (singleton != NULL) { return g_object_ref (singleton); } retval = G_OBJECT_CLASS (caja_bookmark_list_parent_class)->constructor (type, n_construct_params, construct_params); singleton = CAJA_BOOKMARK_LIST (retval); g_object_add_weak_pointer (retval, (gpointer) &singleton); return retval; } static void caja_bookmark_list_class_init (CajaBookmarkListClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); object_class->finalize = do_finalize; object_class->constructor = do_constructor; signals[CONTENTS_CHANGED] = g_signal_new ("contents_changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (CajaBookmarkListClass, contents_changed), 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 (CAJA_IS_BOOKMARK_LIST (CAJA_BOOKMARK_LIST (user_data))); caja_bookmark_list_load_file (CAJA_BOOKMARK_LIST (user_data)); } } static void caja_bookmark_list_init (CajaBookmarkList *bookmarks) { GFile *file; bookmarks->pending_ops = g_queue_new (); caja_bookmark_list_load_file (bookmarks); file = caja_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 (bookmarks->monitor, "changed", G_CALLBACK (bookmark_monitor_changed_cb), bookmarks); g_object_unref (file); } static void insert_bookmark_internal (CajaBookmarkList *bookmarks, CajaBookmark *bookmark, int index) { bookmarks->list = g_list_insert (bookmarks->list, bookmark, index); g_signal_connect_object (bookmark, "contents_changed", G_CALLBACK (bookmark_in_list_changed_callback), bookmarks, 0); } /** * caja_bookmark_list_append: * * Append a bookmark to a bookmark list. * @bookmarks: CajaBookmarkList to append to. * @bookmark: Bookmark to append a copy of. **/ void caja_bookmark_list_append (CajaBookmarkList *bookmarks, CajaBookmark *bookmark) { g_return_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (CAJA_IS_BOOKMARK (bookmark)); insert_bookmark_internal (bookmarks, caja_bookmark_copy (bookmark), -1); caja_bookmark_list_save_file (bookmarks); } /** * caja_bookmark_list_contains: * * Check whether a bookmark with matching name and url is already in the list. * @bookmarks: CajaBookmarkList to check contents of. * @bookmark: CajaBookmark to match against. * * Return value: TRUE if matching bookmark is in list, FALSE otherwise **/ gboolean caja_bookmark_list_contains (CajaBookmarkList *bookmarks, CajaBookmark *bookmark) { g_return_val_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks), FALSE); g_return_val_if_fail (CAJA_IS_BOOKMARK (bookmark), FALSE); return g_list_find_custom (bookmarks->list, (gpointer)bookmark, caja_bookmark_compare_with) != NULL; } /** * caja_bookmark_list_delete_item_at: * * Delete the bookmark at the specified position. * @bookmarks: the list of bookmarks. * @index: index, must be less than length of list. **/ void caja_bookmark_list_delete_item_at (CajaBookmarkList *bookmarks, guint index) { GList *doomed; g_return_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (index < g_list_length (bookmarks->list)); doomed = g_list_nth (bookmarks->list, index); g_return_if_fail (doomed != NULL); bookmarks->list = g_list_remove_link (bookmarks->list, doomed); g_assert (CAJA_IS_BOOKMARK (doomed->data)); stop_monitoring_bookmark (bookmarks, CAJA_BOOKMARK (doomed->data)); g_object_unref (doomed->data); g_list_free_1 (doomed); caja_bookmark_list_save_file (bookmarks); } /** * caja_bookmark_list_move_item: * * Move the item from the given position to the destination. * @index: the index of the first bookmark. * @destination: the index of the second bookmark. **/ void caja_bookmark_list_move_item (CajaBookmarkList *bookmarks, guint index, guint destination) { GList *bookmark_item; if (index == destination) { return; } bookmark_item = g_list_nth (bookmarks->list, index); g_return_if_fail (bookmark_item != NULL); bookmarks->list = g_list_remove_link (bookmarks->list, bookmark_item); if (index < destination) { bookmarks->list = g_list_insert (bookmarks->list, bookmark_item->data, destination - 1); } else { bookmarks->list = g_list_insert (bookmarks->list, bookmark_item->data, destination); } caja_bookmark_list_save_file (bookmarks); } /** * caja_bookmark_list_delete_items_with_uri: * * Delete all bookmarks with the given uri. * @bookmarks: the list of bookmarks. * @uri: The uri to match. **/ void caja_bookmark_list_delete_items_with_uri (CajaBookmarkList *bookmarks, const char *uri) { GList *node, *next; gboolean list_changed; char *bookmark_uri; g_return_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (uri != NULL); list_changed = FALSE; for (node = bookmarks->list; node != NULL; node = next) { next = node->next; bookmark_uri = caja_bookmark_get_uri (CAJA_BOOKMARK (node->data)); if (g_strcmp0 (bookmark_uri, uri) == 0) { bookmarks->list = g_list_remove_link (bookmarks->list, node); stop_monitoring_bookmark (bookmarks, CAJA_BOOKMARK (node->data)); g_object_unref (node->data); g_list_free_1 (node); list_changed = TRUE; } g_free (bookmark_uri); } if (list_changed) { caja_bookmark_list_save_file (bookmarks); } } /** * caja_bookmark_list_get_window_geometry: * * Get a string representing the bookmark_list's window's geometry. * This is the value set earlier by caja_bookmark_list_set_window_geometry. * @bookmarks: the list of bookmarks associated with the window. * Return value: string representation of window's geometry, suitable for * passing to mate_parse_geometry(), or NULL if * no window geometry has yet been saved for this bookmark list. **/ const char * caja_bookmark_list_get_window_geometry (CajaBookmarkList *bookmarks) { return window_geometry; } /** * caja_bookmark_list_insert_item: * * Insert a bookmark at a specified position. * @bookmarks: the list of bookmarks. * @index: the position to insert the bookmark at. * @new_bookmark: the bookmark to insert a copy of. **/ void caja_bookmark_list_insert_item (CajaBookmarkList *bookmarks, CajaBookmark *new_bookmark, guint index) { g_return_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (index <= g_list_length (bookmarks->list)); insert_bookmark_internal (bookmarks, caja_bookmark_copy (new_bookmark), index); caja_bookmark_list_save_file (bookmarks); } /** * caja_bookmark_list_item_at: * * Get the bookmark at the specified position. * @bookmarks: the list of bookmarks. * @index: index, must be less than length of list. * * Return value: the bookmark at position @index in @bookmarks. **/ CajaBookmark * caja_bookmark_list_item_at (CajaBookmarkList *bookmarks, guint index) { g_return_val_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks), NULL); g_return_val_if_fail (index < g_list_length (bookmarks->list), NULL); return CAJA_BOOKMARK (g_list_nth_data (bookmarks->list, index)); } /** * caja_bookmark_list_length: * * Get the number of bookmarks in the list. * @bookmarks: the list of bookmarks. * * Return value: the length of the bookmark list. **/ guint caja_bookmark_list_length (CajaBookmarkList *bookmarks) { g_return_val_if_fail (CAJA_IS_BOOKMARK_LIST(bookmarks), 0); return g_list_length (bookmarks->list); } static void load_file_finish (CajaBookmarkList *bookmarks, GObject *source, GAsyncResult *res) { GError *error = NULL; gchar *contents = NULL; g_file_load_contents_finish (G_FILE (source), res, &contents, NULL, NULL, &error); if (error == NULL) { char **lines; int i; lines = g_strsplit (contents, "\n", -1); for (i = 0; lines[i]; i++) { /* Ignore empty or invalid lines that cannot be parsed properly */ if (lines[i][0] != '\0' && lines[i][0] != ' ') { /* gtk 2.7/2.8 might have labels appended to bookmarks which are separated by a space */ /* we must seperate the bookmark uri and the potential label */ char *space, *label; label = NULL; space = strchr (lines[i], ' '); if (space) { *space = '\0'; label = g_strdup (space + 1); } insert_bookmark_internal (bookmarks, new_bookmark_from_uri (lines[i], label), -1); g_free (label); } } g_free (contents); g_strfreev (lines); g_signal_emit (bookmarks, signals[CONTENTS_CHANGED], 0); } else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_warning ("Could not load bookmark file: %s\n", error->message); g_error_free (error); } } static void load_file_async (CajaBookmarkList *self, GAsyncReadyCallback callback) { GFile *file; file = caja_bookmark_list_get_file (); if (!g_file_query_exists (file, NULL)) { file = caja_bookmark_list_get_legacy_file (); } /* Wipe out old list. */ clear (self); /* keep the bookmark list alive */ g_object_ref (self); g_file_load_contents_async (file, NULL, callback, self); g_object_unref (file); } static void save_file_finish (CajaBookmarkList *bookmarks, GObject *source, GAsyncResult *res) { GError *error = NULL; GFile *file; g_file_replace_contents_finish (G_FILE (source), res, NULL, &error); if (error != NULL) { g_warning ("Unable to replace contents of the bookmarks file: %s", error->message); g_error_free (error); } file = caja_bookmark_list_get_file (); /* re-enable bookmark file monitoring */ bookmarks->monitor = g_file_monitor_file (file, 0, NULL, NULL); g_file_monitor_set_rate_limit (bookmarks->monitor, 1000); g_signal_connect (bookmarks->monitor, "changed", G_CALLBACK (bookmark_monitor_changed_cb), bookmarks); g_object_unref (file); } static void save_file_async (CajaBookmarkList *bookmarks, GAsyncReadyCallback callback) { GFile *file; GList *l; GString *bookmark_string; GFile *parent; char *path; /* temporarily disable bookmark file monitoring when writing file */ if (bookmarks->monitor != NULL) { g_file_monitor_cancel (bookmarks->monitor); bookmarks->monitor = NULL; } file = caja_bookmark_list_get_file (); bookmark_string = g_string_new (NULL); for (l = bookmarks->list; l; l = l->next) { CajaBookmark *bookmark; bookmark = CAJA_BOOKMARK (l->data); /* make sure we save label if it has one for compatibility with GTK 2.7 and 2.8 */ if (caja_bookmark_get_has_custom_name (bookmark)) { char *label, *uri; label = caja_bookmark_get_name (bookmark); uri = caja_bookmark_get_uri (bookmark); g_string_append_printf (bookmark_string, "%s %s\n", uri, label); g_free (uri); g_free (label); } else { char *uri; uri = caja_bookmark_get_uri (bookmark); g_string_append_printf (bookmark_string, "%s\n", uri); g_free (uri); } } /* keep the bookmark list alive */ g_object_ref (bookmarks); parent = g_file_get_parent (file); path = g_file_get_path (parent); g_mkdir_with_parents (path, 0700); g_free (path); g_object_unref (parent); g_file_replace_contents_async (file, bookmark_string->str, bookmark_string->len, NULL, FALSE, 0, NULL, callback, bookmarks); g_object_unref (file); } static void process_next_op (CajaBookmarkList *bookmarks); static void op_processed_cb (GObject *source, GAsyncResult *res, gpointer user_data) { CajaBookmarkList *self = user_data; int op; op = GPOINTER_TO_INT (g_queue_pop_tail (self->pending_ops)); if (op == LOAD_JOB) { load_file_finish (self, source, res); } else { save_file_finish (self, source, res); } if (!g_queue_is_empty (self->pending_ops)) { process_next_op (self); } /* release the reference acquired during the _async method */ g_object_unref (self); } static void process_next_op (CajaBookmarkList *bookmarks) { gint op; op = GPOINTER_TO_INT (g_queue_peek_tail (bookmarks->pending_ops)); if (op == LOAD_JOB) { load_file_async (bookmarks, op_processed_cb); } else { save_file_async (bookmarks, op_processed_cb); } } /** * caja_bookmark_list_load_file: * * Reads bookmarks from file, clobbering contents in memory. * @bookmarks: the list of bookmarks to fill with file contents. **/ static void caja_bookmark_list_load_file (CajaBookmarkList *bookmarks) { g_queue_push_head (bookmarks->pending_ops, GINT_TO_POINTER (LOAD_JOB)); if (g_queue_get_length (bookmarks->pending_ops) == 1) { process_next_op (bookmarks); } } /** * caja_bookmark_list_save_file: * * Save bookmarks to disk. * @bookmarks: the list of bookmarks to save. **/ static void caja_bookmark_list_save_file (CajaBookmarkList *bookmarks) { g_signal_emit (bookmarks, signals[CONTENTS_CHANGED], 0); g_queue_push_head (bookmarks->pending_ops, GINT_TO_POINTER (SAVE_JOB)); if (g_queue_get_length (bookmarks->pending_ops) == 1) { process_next_op (bookmarks); } } /** * caja_bookmark_list_new: * * Create a new bookmark_list, with contents read from disk. * * Return value: A pointer to the new widget. **/ CajaBookmarkList * caja_bookmark_list_new (void) { CajaBookmarkList *list; list = CAJA_BOOKMARK_LIST (g_object_new (CAJA_TYPE_BOOKMARK_LIST, NULL)); return list; } /** * caja_bookmark_list_set_window_geometry: * * Set a bookmarks window's geometry (position & size), in string form. This is * stored to disk by this class, and can be retrieved later in * the same session or in a future session. * @bookmarks: the list of bookmarks associated with the window. * @geometry: the new window geometry string. **/ void caja_bookmark_list_set_window_geometry (CajaBookmarkList *bookmarks, const char *geometry) { g_return_if_fail (CAJA_IS_BOOKMARK_LIST (bookmarks)); g_return_if_fail (geometry != NULL); g_free (window_geometry); window_geometry = g_strdup (geometry); caja_bookmark_list_save_file (bookmarks); }