From 5aacce4e472883342e169804b8c85e5931b1ac7c Mon Sep 17 00:00:00 2001 From: Tobias Berz <46005508+BerziOnline@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:56:31 +0200 Subject: [PATCH] places-sidebar: Add user-defined bookmark groups Users can create, rename and delete groups in the Places sidebar and move bookmarks into them via a context submenu or drag and drop. Bookmarks stay in the GTK bookmarks file; Nemo stores only the group membership, order and collapsed state in ~/.config/nemo/bookmark-groups (GKeyFile). Without that file the sidebar is unchanged. Related: #2489 --- src/meson.build | 2 + src/nemo-bookmark-groups.c | 167 ++++++++++++ src/nemo-bookmark-groups.h | 17 ++ src/nemo-bookmark-list.c | 39 +++ src/nemo-places-sidebar.c | 488 ++++++++++++++++++++++++++++++++++-- test/meson.build | 8 + test/test-bookmark-groups.c | 137 ++++++++++ 7 files changed, 839 insertions(+), 19 deletions(-) create mode 100644 src/nemo-bookmark-groups.c create mode 100644 src/nemo-bookmark-groups.h create mode 100644 test/test-bookmark-groups.c diff --git a/src/meson.build b/src/meson.build index 023443ef85..f84015c132 100644 --- a/src/meson.build +++ b/src/meson.build @@ -21,6 +21,7 @@ nemoCommon_sources = [ 'nemo-application.c', 'nemo-blank-desktop-window.c', 'nemo-bookmark-list.c', + 'nemo-bookmark-groups.c', 'nemo-bookmarks-window.c', 'nemo-config-base-widget.c', 'nemo-connect-server-dialog-nonmain.c', @@ -146,6 +147,7 @@ nemo_autorun_software = executable('nemo-autorun-software', nemo_connect_server = executable('nemo-connect-server', [ 'nemo-bookmark-list.c', + 'nemo-bookmark-groups.c', 'nemo-connect-server-dialog.c', 'nemo-connect-server-dialog-main.c', 'nemo-connect-server-operation.c' diff --git a/src/nemo-bookmark-groups.c b/src/nemo-bookmark-groups.c new file mode 100644 index 0000000000..3219e55218 --- /dev/null +++ b/src/nemo-bookmark-groups.c @@ -0,0 +1,167 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#include "nemo-bookmark-groups.h" +#include +#include + +char * +nemo_bookmark_groups_filename (void) +{ + return g_build_filename (g_get_user_config_dir (), "nemo", "bookmark-groups", NULL); +} + +GKeyFile * +nemo_bookmark_groups_load (const char *filename, GError **error) +{ + GKeyFile *groups = g_key_file_new (); + GError *local_error = NULL; + if (!g_key_file_load_from_file (groups, filename, G_KEY_FILE_NONE, &local_error)) { + if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { + g_propagate_error (error, local_error); + g_key_file_unref (groups); + return NULL; + } + g_clear_error (&local_error); + } + return groups; +} + +gboolean +nemo_bookmark_groups_save (GKeyFile *groups, const char *filename, GError **error) +{ + char *directory = g_path_get_dirname (filename); + if (g_mkdir_with_parents (directory, 0700) < 0) { + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), + "Could not create %s: %s", directory, g_strerror (errno)); + g_free (directory); + return FALSE; + } + g_free (directory); + return g_key_file_save_to_file (groups, filename, error); +} + +char ** +nemo_bookmark_groups_get_ids (GKeyFile *groups) +{ + char **ids = g_key_file_get_string_list (groups, "Groups", "Order", NULL, NULL); + return ids != NULL ? ids : g_new0 (char *, 1); +} + +char * +nemo_bookmark_groups_create (GKeyFile *groups, const char *name) +{ + char **ids = nemo_bookmark_groups_get_ids (groups); + guint length = g_strv_length (ids); + char *id = g_uuid_string_random (); + ids = g_realloc_n (ids, length + 2, sizeof (char *)); + ids[length] = g_strdup (id); + ids[length + 1] = NULL; + g_key_file_set_string_list (groups, "Groups", "Order", (const char * const *) ids, length + 1); + g_key_file_set_string (groups, id, "Name", name); + g_key_file_set_boolean (groups, id, "Collapsed", FALSE); + g_strfreev (ids); + return id; +} + +gboolean +nemo_bookmark_groups_rename (GKeyFile *groups, const char *id, const char *name) +{ + if (!g_key_file_has_group (groups, id) || name == NULL || *name == '\0') + return FALSE; + g_key_file_set_string (groups, id, "Name", name); + return TRUE; +} +void +nemo_bookmark_groups_delete (GKeyFile *groups, const char *id) +{ + char **ids = nemo_bookmark_groups_get_ids (groups); + GPtrArray *remaining = g_ptr_array_new (); + for (guint i = 0; ids[i] != NULL; i++) { + if (g_strcmp0 (ids[i], id) != 0) + g_ptr_array_add (remaining, ids[i]); + } + g_key_file_set_string_list (groups, "Groups", "Order", + (const char * const *) remaining->pdata, remaining->len); + g_key_file_remove_group (groups, id, NULL); + g_ptr_array_unref (remaining); + g_strfreev (ids); +} +char * +nemo_bookmark_groups_find (GKeyFile *groups, const char *uri) +{ + char **ids = nemo_bookmark_groups_get_ids (groups); + char *result = NULL; + for (guint i = 0; ids[i] != NULL; i++) { + char **uris = g_key_file_get_string_list (groups, ids[i], "URIs", NULL, NULL); + if (uris != NULL && g_strv_contains ((const char * const *) uris, uri)) { + result = g_strdup (ids[i]); + g_strfreev (uris); + break; + } + g_strfreev (uris); + } + g_strfreev (ids); + return result; +} + +void +nemo_bookmark_groups_move (GKeyFile *groups, const char *uri, const char *id, const char *before_uri) +{ + if (g_strcmp0 (uri, before_uri) == 0) { + char *current_id = nemo_bookmark_groups_find (groups, uri); + gboolean same_group = g_strcmp0 (current_id, id) == 0; + g_free (current_id); + if (same_group) + return; + } + char **ids = nemo_bookmark_groups_get_ids (groups); + /* Never lose a membership because a stale menu targets a deleted group. */ + if (id != NULL && !g_strv_contains ((const char * const *) ids, id)) { + g_strfreev (ids); + return; + } + for (guint i = 0; ids[i] != NULL; i++) { + char **uris = g_key_file_get_string_list (groups, ids[i], "URIs", NULL, NULL); + GPtrArray *ordered = g_ptr_array_new (); + gboolean inserted = FALSE; + for (guint j = 0; uris != NULL && uris[j] != NULL; j++) { + if (g_strcmp0 (uris[j], uri) == 0) + continue; + if (g_strcmp0 (ids[i], id) == 0 && g_strcmp0 (uris[j], before_uri) == 0) { + g_ptr_array_add (ordered, (gpointer) uri); + inserted = TRUE; + } + g_ptr_array_add (ordered, uris[j]); + } + if (g_strcmp0 (ids[i], id) == 0 && !inserted) + g_ptr_array_add (ordered, (gpointer) uri); + g_key_file_set_string_list (groups, ids[i], "URIs", + (const char * const *) ordered->pdata, ordered->len); + g_ptr_array_unref (ordered); + g_strfreev (uris); + } + g_strfreev (ids); +} +gboolean +nemo_bookmark_groups_prune (GKeyFile *groups, const char * const *uris) +{ + char **ids = nemo_bookmark_groups_get_ids (groups); + gboolean changed = FALSE; + for (guint i = 0; ids[i] != NULL; i++) { + char **assigned = g_key_file_get_string_list (groups, ids[i], "URIs", NULL, NULL); + GPtrArray *remaining = g_ptr_array_new (); + guint length = assigned != NULL ? g_strv_length (assigned) : 0; + for (guint j = 0; j < length; j++) { + if (g_strv_contains (uris, assigned[j])) + g_ptr_array_add (remaining, assigned[j]); + } + if (remaining->len != length) { + g_key_file_set_string_list (groups, ids[i], "URIs", + (const char * const *) remaining->pdata, remaining->len); + changed = TRUE; + } + g_ptr_array_unref (remaining); + g_strfreev (assigned); + } + g_strfreev (ids); + return changed; +} diff --git a/src/nemo-bookmark-groups.h b/src/nemo-bookmark-groups.h new file mode 100644 index 0000000000..f2001f5282 --- /dev/null +++ b/src/nemo-bookmark-groups.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifndef NEMO_BOOKMARK_GROUPS_H +#define NEMO_BOOKMARK_GROUPS_H +#include + +/* Private, GLib-only metadata. GTK's bookmarks remain authoritative. */ +GKeyFile *nemo_bookmark_groups_load (const char *filename, GError **error); +gboolean nemo_bookmark_groups_save (GKeyFile *groups, const char *filename, GError **error); +char *nemo_bookmark_groups_filename (void); +char **nemo_bookmark_groups_get_ids (GKeyFile *groups); +char *nemo_bookmark_groups_create (GKeyFile *groups, const char *name); +gboolean nemo_bookmark_groups_rename (GKeyFile *groups, const char *id, const char *name); +void nemo_bookmark_groups_delete (GKeyFile *groups, const char *id); +char *nemo_bookmark_groups_find (GKeyFile *groups, const char *uri); +void nemo_bookmark_groups_move (GKeyFile *groups, const char *uri, const char *id, const char *before_uri); +gboolean nemo_bookmark_groups_prune (GKeyFile *groups, const char * const *uris); +#endif diff --git a/src/nemo-bookmark-list.c b/src/nemo-bookmark-list.c index f515a0609f..ff58cd78f7 100644 --- a/src/nemo-bookmark-list.c +++ b/src/nemo-bookmark-list.c @@ -27,6 +27,7 @@ #include #include "nemo-bookmark-list.h" +#include "nemo-bookmark-groups.h" #include #include @@ -424,6 +425,29 @@ nemo_bookmark_list_contains (NemoBookmarkList *bookmarks, != NULL; } +static void +prune_bookmark_groups (NemoBookmarkList *bookmarks) +{ + char *filename = nemo_bookmark_groups_filename (); + GKeyFile *groups = nemo_bookmark_groups_load (filename, NULL); + if (groups != NULL) { + GPtrArray *uris = g_ptr_array_new_with_free_func (g_free); + for (GList *l = bookmarks->list; l != NULL; l = l->next) + g_ptr_array_add (uris, nemo_bookmark_get_uri (l->data)); + g_ptr_array_add (uris, NULL); + if (nemo_bookmark_groups_prune (groups, (const char * const *) uris->pdata)) { + GError *error = NULL; + if (!nemo_bookmark_groups_save (groups, filename, &error)) { + g_warning ("Could not prune bookmark groups: %s", error->message); + g_error_free (error); + } + } + g_ptr_array_unref (uris); + g_key_file_unref (groups); + } + g_free (filename); +} + /** * nemo_bookmark_list_delete_item_at: * @@ -451,6 +475,7 @@ nemo_bookmark_list_delete_item_at (NemoBookmarkList *bookmarks, g_list_free_1 (doomed); + prune_bookmark_groups (bookmarks); nemo_bookmark_list_save_file (bookmarks); } @@ -556,6 +581,7 @@ nemo_bookmark_list_delete_items_with_uri (NemoBookmarkList *bookmarks, } if (list_changed) { + prune_bookmark_groups (bookmarks); nemo_bookmark_list_save_file (bookmarks); } } @@ -722,6 +748,9 @@ load_files_finish (NemoBookmarkList *bookmarks, clear_bookmarks (bookmarks, old_list); + if (error == NULL && GPOINTER_TO_INT (g_task_get_task_data (G_TASK (res)))) + prune_bookmark_groups (bookmarks); + g_clear_error (&error); g_list_foreach (bookmarks->list, (GFunc) connect_bookmark_signals, bookmarks); g_signal_emit (bookmarks, signals[CHANGED], 0); } @@ -745,8 +774,10 @@ load_files_thread (GTask *task, GFile *file; file = nemo_bookmark_list_get_file (); + gboolean authoritative = TRUE; if (!g_file_query_exists (file, NULL)) { + authoritative = FALSE; g_object_unref (file); file = nemo_bookmark_list_get_legacy_file (); } @@ -767,8 +798,15 @@ load_files_thread (GTask *task, gchar **lines; gint i; + authoritative = authoritative && g_utf8_validate (contents, -1, NULL); lines = g_strsplit (contents, "\n", -1); for (i = 0; lines[i]; i++) { + if (lines[i][0] != '\0') { + char *scheme = g_uri_parse_scheme (lines[i]); + if (scheme == NULL || lines[i][0] == ' ') + authoritative = FALSE; + g_free (scheme); + } /* 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 */ @@ -805,6 +843,7 @@ load_files_thread (GTask *task, g_free (contents); g_strfreev (lines); + g_task_set_task_data (task, GINT_TO_POINTER (authoritative), NULL); g_task_return_pointer (task, new_list, NULL); } else { g_task_return_error (task, error); diff --git a/src/nemo-places-sidebar.c b/src/nemo-places-sidebar.c index bd952ea2f9..00fb9b1ea9 100644 --- a/src/nemo-places-sidebar.c +++ b/src/nemo-places-sidebar.c @@ -59,6 +59,7 @@ #include "nemo-actions.h" #include "nemo-application.h" #include "nemo-bookmark-list.h" +#include "nemo-bookmark-groups.h" #include "nemo-places-sidebar.h" #include "nemo-properties-window.h" #include "nemo-window.h" @@ -94,6 +95,8 @@ typedef struct { NemoWindow *window; NemoBookmarkList *bookmarks; + GKeyFile *bookmark_groups; + GFileMonitor *groups_monitor; GVolumeMonitor *volume_monitor; NemoActionManager *action_manager; @@ -183,6 +186,7 @@ enum { PLACES_SIDEBAR_COLUMN_HEADING_TEXT, PLACES_SIDEBAR_COLUMN_DF_PERCENT, PLACES_SIDEBAR_COLUMN_SHOW_DF, + PLACES_SIDEBAR_COLUMN_GROUP_ID, PLACES_SIDEBAR_COLUMN_COUNT }; @@ -201,6 +205,7 @@ typedef enum { SECTION_BOOKMARKS, SECTION_DEVICES, SECTION_NETWORK, + SECTION_BOOKMARK_GROUP, } SectionType; enum { @@ -494,6 +499,100 @@ add_place (NemoPlacesSidebar *sidebar, return cat_iter; } +static void +add_bookmark_groups (NemoPlacesSidebar *sidebar) +{ + char **ids = nemo_bookmark_groups_get_ids (sidebar->bookmark_groups); + for (guint i = 0; ids[i] != NULL; i++) { + char *name = g_key_file_get_string (sidebar->bookmark_groups, ids[i], "Name", NULL); + if (name == NULL || *name == '\0') { + g_free (name); + continue; + } + GtkTreeIter heading = add_heading (sidebar, SECTION_BOOKMARK_GROUP, name); + gtk_tree_store_set (sidebar->store, &heading, + PLACES_SIDEBAR_COLUMN_GROUP_ID, ids[i], -1); + char **uris = g_key_file_get_string_list (sidebar->bookmark_groups, ids[i], "URIs", NULL, NULL); + for (guint j = 0; uris != NULL && uris[j] != NULL; j++) { + for (guint k = 0; k < nemo_bookmark_list_length (sidebar->bookmarks); k++) { + NemoBookmark *bookmark = nemo_bookmark_list_item_at (sidebar->bookmarks, k); + char *uri = nemo_bookmark_get_uri (bookmark); + gboolean match = g_strcmp0 (uri, uris[j]) == 0; + g_free (uri); + if (!match) + continue; + GFile *location = nemo_bookmark_get_location (bookmark); + char *tooltip = g_file_get_parse_name (location); + char *icon = nemo_bookmark_get_icon_name (bookmark); + add_place (sidebar, PLACES_BOOKMARK, SECTION_BOOKMARK_GROUP, + nemo_bookmark_get_name (bookmark), icon, uris[j], + NULL, NULL, NULL, k, tooltip, 0, FALSE, heading); + GtkTreeIter child; + gint count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (sidebar->store), &heading); + gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (sidebar->store), &child, &heading, count - 1); + gtk_tree_store_set (sidebar->store, &child, + PLACES_SIDEBAR_COLUMN_GROUP_ID, ids[i], -1); + g_free (icon); + g_free (tooltip); + g_object_unref (location); + break; + } + } + g_strfreev (uris); + g_free (name); + } + g_strfreev (ids); +} + +static GKeyFile * +load_groups_for_edit (NemoPlacesSidebar *sidebar) +{ + char *filename = nemo_bookmark_groups_filename (); + GError *error = NULL; + GKeyFile *groups = nemo_bookmark_groups_load (filename, &error); + if (error != NULL) { + eel_show_error_dialog (_("Unable to load bookmark groups"), error->message, GTK_WINDOW (sidebar->window)); + g_error_free (error); + } + g_free (filename); + return groups; +} + +static gboolean +save_groups (NemoPlacesSidebar *sidebar, GKeyFile *groups) +{ + char *filename = nemo_bookmark_groups_filename (); + GError *error = NULL; + gboolean success = nemo_bookmark_groups_save (groups, filename, &error); + if (!success) { + eel_show_error_dialog (_("Unable to save bookmark groups"), error->message, GTK_WINDOW (sidebar->window)); + g_error_free (error); + } else { + update_places_on_idle (sidebar); + } + g_free (filename); + return success; +} + +/* Move out of the legacy My Computer bookmark range when joining a group. */ +static void +move_bookmark_below_breakpoint (NemoPlacesSidebar *sidebar, const char *uri) +{ + for (guint i = 0; i < nemo_bookmark_list_length (sidebar->bookmarks); i++) { + char *candidate = nemo_bookmark_get_uri (nemo_bookmark_list_item_at (sidebar->bookmarks, i)); + gboolean match = g_strcmp0 (candidate, uri) == 0; + g_free (candidate); + if (match) { + if ((gint) i < sidebar->bookmark_breakpoint) { + decrement_bookmark_breakpoint (sidebar); + nemo_bookmark_list_move_item (sidebar->bookmarks, i, + nemo_bookmark_list_length (sidebar->bookmarks)); + } + break; + } + } +} + typedef struct { const gchar *location; const gchar *last_uri; @@ -549,6 +648,14 @@ restore_expand_state_foreach (GtkTreeModel *model, gtk_tree_view_expand_to_path (sidebar->tree_view, path); else gtk_tree_view_collapse_row (sidebar->tree_view, path); + } else if (section_type == SECTION_BOOKMARK_GROUP) { + char *id = NULL; + gtk_tree_model_get (model, iter, PLACES_SIDEBAR_COLUMN_GROUP_ID, &id, -1); + if (id != NULL && !g_key_file_get_boolean (sidebar->bookmark_groups, id, "Collapsed", NULL)) + gtk_tree_view_expand_to_path (sidebar->tree_view, path); + else + gtk_tree_view_collapse_row (sidebar->tree_view, path); + g_free (id); } else if (section_type == SECTION_DEVICES) { if (sidebar->devices_expanded) gtk_tree_view_expand_to_path (sidebar->tree_view, path); @@ -752,6 +859,14 @@ update_places (NemoPlacesSidebar *sidebar) sidebar->updating_sidebar = TRUE; + g_clear_pointer (&sidebar->bookmark_groups, g_key_file_unref); + char *groups_filename = nemo_bookmark_groups_filename (); + sidebar->bookmark_groups = nemo_bookmark_groups_load (groups_filename, NULL); + g_free (groups_filename); + if (sidebar->bookmark_groups == NULL) { + sidebar->bookmark_groups = g_key_file_new (); + } + model = NULL; last_uri = NULL; @@ -836,6 +951,14 @@ update_places (NemoPlacesSidebar *sidebar) for (bookmark_index = 0; bookmark_index < temp_breakpoint; ++bookmark_index) { bookmark = nemo_bookmark_list_item_at (sidebar->bookmarks, bookmark_index); + char *bookmark_uri = nemo_bookmark_get_uri (bookmark); + char *group_id = nemo_bookmark_groups_find (sidebar->bookmark_groups, bookmark_uri); + g_free (bookmark_uri); + if (group_id != NULL) { + g_free (group_id); + continue; + } + root = nemo_bookmark_get_location (bookmark); bookmark_name = nemo_bookmark_get_name (bookmark); @@ -927,9 +1050,17 @@ update_places (NemoPlacesSidebar *sidebar) cat_iter = add_heading (sidebar, SECTION_BOOKMARKS, _("Bookmarks")); - while (bookmark_index < bookmark_count) { + for (; bookmark_index < bookmark_count; bookmark_index++) { bookmark = nemo_bookmark_list_item_at (sidebar->bookmarks, bookmark_index); + char *bookmark_uri = nemo_bookmark_get_uri (bookmark); + char *group_id = nemo_bookmark_groups_find (sidebar->bookmark_groups, bookmark_uri); + g_free (bookmark_uri); + if (group_id != NULL) { + g_free (group_id); + continue; + } + root = nemo_bookmark_get_location (bookmark); bookmark_name = nemo_bookmark_get_name (bookmark); @@ -947,9 +1078,10 @@ update_places (NemoPlacesSidebar *sidebar) g_free (icon); g_free (mount_uri); g_free (tooltip); - ++bookmark_index; } + add_bookmark_groups (sidebar); + GList *place_infos = NULL; PlaceInfo *place_info; @@ -1583,10 +1715,10 @@ get_drag_type (NemoPlacesSidebar *sidebar, return GTK_TREE_VIEW_DROP_BEFORE; } - if ((section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS) + if ((section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS || section_type == SECTION_BOOKMARK_GROUP) && zone == POSITION_UPPER) { return GTK_TREE_VIEW_DROP_BEFORE; - } else if ((section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS) + } else if ((section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS || section_type == SECTION_BOOKMARK_GROUP) && zone == POSITION_LOWER) { return GTK_TREE_VIEW_DROP_AFTER; } else { @@ -1624,6 +1756,21 @@ compute_drop_position (GtkTreeView *tree_view, PLACES_SIDEBAR_COLUMN_SECTION_TYPE, §ion_type, PLACES_SIDEBAR_COLUMN_URI, &drop_target_uri, -1); + if (sidebar->drag_data_received && sidebar->drag_data_info == GTK_TREE_MODEL_ROW && + (section_type == SECTION_BOOKMARK_GROUP || + (section_type == SECTION_BOOKMARKS && place_type == PLACES_HEADING))) { + if (place_type == PLACES_HEADING) { + *pos = GTK_TREE_VIEW_DROP_AFTER; + } else { + GdkRectangle rect; + gint bin_y; + gtk_tree_view_convert_widget_to_bin_window_coords (tree_view, x, y, NULL, &bin_y); + gtk_tree_view_get_cell_area (tree_view, *path, NULL, &rect); + *pos = bin_y < rect.y + rect.height / 2 ? GTK_TREE_VIEW_DROP_BEFORE : GTK_TREE_VIEW_DROP_AFTER; + } + g_free (drop_target_uri); + return TRUE; + } if (!cat_is_expanded (sidebar, section_type) && place_type == PLACES_HEADING) { if (sidebar->expand_timeout_source > 0) { goto fail; @@ -1976,18 +2123,14 @@ update_bookmark_breakpoint (NemoPlacesSidebar *sidebar, static void reorder_bookmarks (NemoPlacesSidebar *sidebar, int new_position, - SectionType new_section_type) + SectionType new_section_type, + GtkTreeIter *source) { - GtkTreeIter iter; + GtkTreeIter iter = *source; PlaceType type; SectionType old_section_type; int old_position; - /* Get the selected path */ - if (!get_selected_iter (sidebar, &iter)) { - return; - } - gtk_tree_model_get (GTK_TREE_MODEL (sidebar->store_filter), &iter, PLACES_SIDEBAR_COLUMN_ROW_TYPE, &type, PLACES_SIDEBAR_COLUMN_SECTION_TYPE, &old_section_type, @@ -2009,6 +2152,90 @@ reorder_bookmarks (NemoPlacesSidebar *sidebar, update_places (sidebar); } +/* Resolve the row encoded by GTK, not the selection which may change mid-drag. */ +static gboolean +get_drag_source_iter (NemoPlacesSidebar *sidebar, GtkSelectionData *data, GtkTreeIter *iter) +{ + GtkTreeModel *source_model = NULL; + GtkTreePath *source_path = NULL; + gboolean valid = FALSE; + if (!gtk_tree_get_row_drag_data (data, &source_model, &source_path)) + return FALSE; + if (source_model == sidebar->store_filter) { + valid = gtk_tree_model_get_iter (source_model, iter, source_path); + } else if (source_model == GTK_TREE_MODEL (sidebar->store)) { + GtkTreeIter child; + if (gtk_tree_model_get_iter (source_model, &child, source_path)) + valid = gtk_tree_model_filter_convert_child_iter_to_iter ( + GTK_TREE_MODEL_FILTER (sidebar->store_filter), iter, &child); + } + gtk_tree_path_free (source_path); + if (valid) { + PlaceType type; + gtk_tree_model_get (sidebar->store_filter, iter, + PLACES_SIDEBAR_COLUMN_ROW_TYPE, &type, -1); + valid = type == PLACES_BOOKMARK; + } + return valid; +} + +static void +reorder_group_bookmark (NemoPlacesSidebar *sidebar, GtkTreeModel *model, + GtkTreeIter *target, GtkTreeViewDropPosition pos, + gint position, SectionType section, GtkTreeIter *source) +{ + char *uri = NULL, *id = NULL, *target_uri = NULL; + PlaceType type; + SectionType old_section; + gtk_tree_model_get (model, source, + PLACES_SIDEBAR_COLUMN_URI, &uri, + PLACES_SIDEBAR_COLUMN_ROW_TYPE, &type, + PLACES_SIDEBAR_COLUMN_SECTION_TYPE, &old_section, -1); + if (type != PLACES_BOOKMARK || uri == NULL) { + g_free (uri); + return; + } + gtk_tree_model_get (model, target, + PLACES_SIDEBAR_COLUMN_GROUP_ID, &id, + PLACES_SIDEBAR_COLUMN_URI, &target_uri, -1); + GKeyFile *groups = load_groups_for_edit (sidebar); + if (groups == NULL) + goto out; + char *before = g_strdup (target_uri); + if (pos == GTK_TREE_VIEW_DROP_AFTER) { + g_clear_pointer (&before, g_free); + if (id != NULL && target_uri != NULL) { + char **uris = g_key_file_get_string_list (groups, id, "URIs", NULL, NULL); + for (guint i = 0; uris != NULL && uris[i] != NULL; i++) { + if (g_strcmp0 (uris[i], target_uri) == 0) { + before = g_strdup (uris[i + 1]); + break; + } + } + g_strfreev (uris); + } + } + if (g_strcmp0 (uri, target_uri) != 0) { + nemo_bookmark_groups_move (groups, uri, id, before); + if (save_groups (sidebar, groups)) { + if (section == SECTION_BOOKMARK_GROUP) + move_bookmark_below_breakpoint (sidebar, uri); + else { + if (old_section == SECTION_BOOKMARK_GROUP && + (section == SECTION_XDG_BOOKMARKS || section == SECTION_COMPUTER)) + increment_bookmark_breakpoint (sidebar); + reorder_bookmarks (sidebar, position, section, source); + } + } + } + g_free (before); + g_key_file_unref (groups); +out: + g_free (uri); + g_free (id); + g_free (target_uri); +} + static gboolean idle_hide_bookmarks (gpointer user_data) { @@ -2092,7 +2319,7 @@ drag_data_received_callback (GtkWidget *widget, */ position = nemo_bookmark_list_length (sidebar->bookmarks); } else if (tree_pos == GTK_TREE_VIEW_DROP_AFTER && - (section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS)) { + (section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS || section_type == SECTION_BOOKMARK_GROUP)) { position++; } @@ -2102,7 +2329,15 @@ drag_data_received_callback (GtkWidget *widget, success = TRUE; break; case GTK_TREE_MODEL_ROW: - reorder_bookmarks (sidebar, position, section_type); + GtkTreeIter source; + SectionType old_section = SECTION_BOOKMARKS; + if (!get_drag_source_iter (sidebar, selection_data, &source)) + break; + gtk_tree_model_get (model, &source, PLACES_SIDEBAR_COLUMN_SECTION_TYPE, &old_section, -1); + if (section_type == SECTION_BOOKMARK_GROUP || old_section == SECTION_BOOKMARK_GROUP) + reorder_group_bookmark (sidebar, model, &iter, tree_pos, position, section_type, &source); + else + reorder_bookmarks (sidebar, position, section_type, &source); success = TRUE; break; default: @@ -3404,11 +3639,163 @@ popup_menu_realized (GtkWidget *menu, } #endif +static void +group_menu_activate (GtkMenuItem *item, NemoPlacesSidebar *sidebar) +{ + gint action = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "group-action")); + const char *id = g_object_get_data (G_OBJECT (item), "group-id"); + const char *uri = g_object_get_data (G_OBJECT (item), "bookmark-uri"); + char *name = NULL; + if (action < 2) { + GtkWidget *dialog = gtk_dialog_new_with_buttons (action == 0 ? _("New group…") : _("Rename group…"), + GTK_WINDOW (sidebar->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + _("Cancel"), GTK_RESPONSE_CANCEL, _("Save"), GTK_RESPONSE_OK, NULL); + GtkWidget *entry = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); + gtk_entry_set_placeholder_text (GTK_ENTRY (entry), _("Group name")); + if (action == 1 && id != NULL) { + char *old_name = g_key_file_get_string (sidebar->bookmark_groups, id, "Name", NULL); + gtk_entry_set_text (GTK_ENTRY (entry), old_name != NULL ? old_name : ""); + g_free (old_name); + } + gtk_container_set_border_width (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), 12); + gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), entry); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK); + gtk_widget_show_all (dialog); + if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_OK) + name = g_strstrip (g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)))); + gtk_widget_destroy (dialog); + if (name == NULL) + return; + if (*name == '\0') { + eel_show_error_dialog (_("A group name cannot be empty"), NULL, GTK_WINDOW (sidebar->window)); + g_free (name); + return; + } + } else if (action == 2) { + GtkWidget *dialog = gtk_message_dialog_new (GTK_WINDOW (sidebar->window), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, "%s", _("Delete this bookmark group?")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", + _("Its bookmarks will return to Bookmarks. No bookmarks or files will be deleted.")); + gtk_dialog_add_buttons (GTK_DIALOG (dialog), _("Cancel"), GTK_RESPONSE_CANCEL, + _("Delete group"), GTK_RESPONSE_OK, NULL); + gint response = gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); + if (response != GTK_RESPONSE_OK) + return; + } + GKeyFile *groups = load_groups_for_edit (sidebar); + if (groups == NULL) { + g_free (name); + return; + } + if (action == 0) { + char *new_id = nemo_bookmark_groups_create (groups, name); + g_free (new_id); + } else if (action == 1) { + nemo_bookmark_groups_rename (groups, id, name); + } else if (action == 2) { + nemo_bookmark_groups_delete (groups, id); + } else { + nemo_bookmark_groups_move (groups, uri, id, NULL); + } + if (save_groups (sidebar, groups)) { + if (action == 3) { + move_bookmark_below_breakpoint (sidebar, uri); + } else if (action == 2) { + char **uris = g_key_file_get_string_list (sidebar->bookmark_groups, id, "URIs", NULL, NULL); + for (guint i = 0; uris != NULL && uris[i] != NULL; i++) + move_bookmark_below_breakpoint (sidebar, uris[i]); + g_strfreev (uris); + } + } + g_key_file_unref (groups); + g_free (name); +} + +static GtkWidget * +group_menu_item (NemoPlacesSidebar *sidebar, const char *label, gint action, + const char *id, const char *uri) +{ + GtkWidget *item = gtk_menu_item_new_with_label (label); + g_object_set_data (G_OBJECT (item), "group-menu-item", GINT_TO_POINTER (1)); + g_object_set_data (G_OBJECT (item), "group-action", GINT_TO_POINTER (action)); + g_object_set_data_full (G_OBJECT (item), "group-id", g_strdup (id), g_free); + g_object_set_data_full (G_OBJECT (item), "bookmark-uri", g_strdup (uri), g_free); + g_signal_connect_object (item, "activate", G_CALLBACK (group_menu_activate), sidebar, 0); + return item; +} + +static void +append_group_menu (NemoPlacesSidebar *sidebar, GtkWidget *menu, const char *id, const char *uri) +{ + GList *children = gtk_container_get_children (GTK_CONTAINER (menu)); + for (GList *l = children; l != NULL; l = l->next) { + if (g_object_get_data (G_OBJECT (l->data), "group-menu-item") != NULL) + gtk_widget_destroy (l->data); + } + g_list_free (children); + GtkWidget *item = group_menu_item (sidebar, _("New group…"), 0, NULL, NULL); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + item = group_menu_item (sidebar, _("Rename group…"), 1, id, NULL); + gtk_widget_set_sensitive (item, id != NULL); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + item = group_menu_item (sidebar, _("Delete group"), 2, id, NULL); + gtk_widget_set_sensitive (item, id != NULL); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + if (uri != NULL) { + GtkWidget *submenu = gtk_menu_new (); + item = group_menu_item (sidebar, _("Move to group"), 3, NULL, NULL); + gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); + gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); + gtk_menu_shell_append (GTK_MENU_SHELL (submenu), + group_menu_item (sidebar, _("Bookmarks"), 3, NULL, uri)); + char **ids = nemo_bookmark_groups_get_ids (sidebar->bookmark_groups); + for (guint i = 0; ids[i] != NULL; i++) { + char *name = g_key_file_get_string (sidebar->bookmark_groups, ids[i], "Name", NULL); + if (name != NULL) + gtk_menu_shell_append (GTK_MENU_SHELL (submenu), + group_menu_item (sidebar, name, 3, ids[i], uri)); + g_free (name); + } + g_strfreev (ids); + gtk_widget_show_all (submenu); + } + /* Only show our items: show_all(menu) would reveal hidden volume actions. */ + children = gtk_container_get_children (GTK_CONTAINER (menu)); + for (GList *l = children; l != NULL; l = l->next) { + if (g_object_get_data (G_OBJECT (l->data), "group-menu-item") != NULL) + gtk_widget_show (l->data); + } + g_list_free (children); +} + static void bookmarks_popup_menu (NemoPlacesSidebar *sidebar, GdkEventButton *event) { update_menu_states (sidebar); + /* The popup is reused for devices, network locations and bookmarks. */ + GList *children = gtk_container_get_children (GTK_CONTAINER (sidebar->popup_menu)); + for (GList *l = children; l != NULL; l = l->next) { + if (g_object_get_data (G_OBJECT (l->data), "group-menu-item") != NULL) + gtk_widget_hide (l->data); + } + g_list_free (children); + GtkTreeIter iter; + if (get_selected_iter (sidebar, &iter)) { + char *id = NULL, *uri = NULL; + PlaceType type; + gtk_tree_model_get (sidebar->store_filter, &iter, + PLACES_SIDEBAR_COLUMN_GROUP_ID, &id, + PLACES_SIDEBAR_COLUMN_URI, &uri, + PLACES_SIDEBAR_COLUMN_ROW_TYPE, &type, -1); + if (type == PLACES_BOOKMARK) + append_group_menu (sidebar, sidebar->popup_menu, id, uri); + g_free (id); + g_free (uri); + } eel_pop_up_context_menu (GTK_MENU(sidebar->popup_menu), (GdkEvent *) event, GTK_WIDGET (sidebar)); @@ -3645,9 +4032,20 @@ bookmarks_button_press_event_cb (GtkWidget *widget, PLACES_SIDEBAR_COLUMN_ROW_TYPE, &row_type, -1); - if (row_type != PLACES_HEADING) { + if (row_type == PLACES_HEADING) { + char *id = NULL; + gtk_tree_model_get (model, &iter, PLACES_SIDEBAR_COLUMN_GROUP_ID, &id, -1); + GtkWidget *menu = gtk_menu_new (); + gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (sidebar), NULL); + append_group_menu (sidebar, menu, id, NULL); + g_signal_connect_swapped (menu, "selection-done", G_CALLBACK (gtk_widget_destroy), menu); + eel_pop_up_context_menu (GTK_MENU (menu), (GdkEvent *) event, GTK_WIDGET (sidebar)); + g_free (id); + } else { + gtk_tree_selection_select_path (gtk_tree_view_get_selection (tree_view), path); bookmarks_popup_menu (sidebar, event); } + retval = TRUE; } else if (event->button == GDK_BUTTON_MIDDLE) { NemoWindowOpenFlags flags = 0; @@ -3856,7 +4254,17 @@ update_expanded_state (GtkTreeView *tree_view, gtk_tree_model_get (model, iter, PLACES_SIDEBAR_COLUMN_SECTION_TYPE, &type, -1); - if (type == SECTION_COMPUTER) { + if (type == SECTION_BOOKMARK_GROUP) { + char *id = NULL; + gtk_tree_model_get (model, iter, PLACES_SIDEBAR_COLUMN_GROUP_ID, &id, -1); + GKeyFile *groups = load_groups_for_edit (sidebar); + if (groups != NULL && id != NULL) { + g_key_file_set_boolean (groups, id, "Collapsed", !expanded); + save_groups (sidebar, groups); + } + g_clear_pointer (&groups, g_key_file_unref); + g_free (id); + } else if (type == SECTION_COMPUTER) { sidebar->my_computer_expanded = expanded; g_settings_set_boolean (nemo_window_state, NEMO_WINDOW_STATE_MY_COMPUTER_EXPANDED, expanded); } else if (type == SECTION_BOOKMARKS) { @@ -3916,6 +4324,13 @@ row_activated_cb (GtkTreeView *tree_view, PLACES_SIDEBAR_COLUMN_ROW_TYPE, &place_type, -1); if (place_type == PLACES_HEADING) { + if (section_type == SECTION_BOOKMARK_GROUP) { + if (gtk_tree_view_row_expanded (tree_view, path)) + gtk_tree_view_collapse_row (tree_view, path); + else + gtk_tree_view_expand_row (tree_view, path, FALSE); + return; + } if (section_type == SECTION_COMPUTER) { sidebar->my_computer_expanded = !sidebar->my_computer_expanded; } else if (section_type == SECTION_BOOKMARKS) { @@ -4102,7 +4517,10 @@ row_visibility_function (GtkTreeModel *model, if (type != PLACES_HEADING || section_type != SECTION_BOOKMARKS) return TRUE; - if (sidebar->in_drag) + char **ids = nemo_bookmark_groups_get_ids (sidebar->bookmark_groups); + gboolean have_groups = ids[0] != NULL; + g_strfreev (ids); + if (have_groups || sidebar->in_drag) return TRUE; if ((int)nemo_bookmark_list_length (sidebar->bookmarks) > sidebar->bookmark_breakpoint) @@ -4111,6 +4529,19 @@ row_visibility_function (GtkTreeModel *model, return FALSE; } +static void +groups_changed_cb (GFileMonitor *monitor, GFile *file, GFile *other_file, + GFileMonitorEvent event, NemoPlacesSidebar *sidebar) +{ + char *basename = file != NULL ? g_file_get_basename (file) : NULL; + char *other_basename = other_file != NULL ? g_file_get_basename (other_file) : NULL; + if (g_strcmp0 (basename, "bookmark-groups") == 0 || + g_strcmp0 (other_basename, "bookmark-groups") == 0) + update_places_on_idle (sidebar); + g_free (basename); + g_free (other_basename); +} + static void nemo_places_sidebar_init (NemoPlacesSidebar *sidebar) { @@ -4120,6 +4551,18 @@ nemo_places_sidebar_init (NemoPlacesSidebar *sidebar) GtkTreeSelection *selection; GtkStyleContext *style_context; + sidebar->bookmark_groups = g_key_file_new (); + char *filename = nemo_bookmark_groups_filename (); + char *directory = g_path_get_dirname (filename); + g_mkdir_with_parents (directory, 0700); + GFile *groups_dir = g_file_new_for_path (directory); + sidebar->groups_monitor = g_file_monitor_directory (groups_dir, G_FILE_MONITOR_NONE, NULL, NULL); + if (sidebar->groups_monitor != NULL) + g_signal_connect_object (sidebar->groups_monitor, "changed", G_CALLBACK (groups_changed_cb), sidebar, 0); + g_object_unref (groups_dir); + g_free (directory); + g_free (filename); + sidebar->action_manager = nemo_action_manager_new (); sidebar->actions_changed_id = g_signal_connect_swapped (sidebar->action_manager, "changed", @@ -4463,6 +4906,12 @@ nemo_places_sidebar_dispose (GObject *object) favorites_changed_cb, sidebar); + if (sidebar->groups_monitor != NULL) { + g_file_monitor_cancel (sidebar->groups_monitor); + g_clear_object (&sidebar->groups_monitor); + } + g_clear_pointer (&sidebar->bookmark_groups, g_key_file_unref); + if (sidebar->volume_monitor != NULL) { g_signal_handlers_disconnect_by_func (sidebar->volume_monitor, volume_added_callback, sidebar); @@ -4631,7 +5080,7 @@ nemo_shortcuts_model_row_draggable (GtkTreeDragSource *drag_source, -1); if (place_type != PLACES_HEADING && - (section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS)) + (section_type == SECTION_XDG_BOOKMARKS || section_type == SECTION_BOOKMARKS || section_type == SECTION_BOOKMARK_GROUP)) return TRUE; return FALSE; @@ -4677,8 +5126,9 @@ nemo_shortcuts_model_new (NemoPlacesSidebar *sidebar) G_TYPE_INT, G_TYPE_STRING, G_TYPE_INT, - G_TYPE_BOOLEAN - }; + G_TYPE_BOOLEAN, + G_TYPE_STRING + }; model = g_object_new (NEMO_TYPE_SHORTCUTS_MODEL, NULL); model->sidebar = sidebar; diff --git a/test/meson.build b/test/meson.build index 166d9f98bd..21ab78f7c8 100644 --- a/test/meson.build +++ b/test/meson.build @@ -35,3 +35,11 @@ test('Content Search test', ), args: [] ) + +test('Bookmark groups', + executable('test-bookmark-groups', + [ 'test-bookmark-groups.c', '../src/nemo-bookmark-groups.c' ], + include_directories: include_directories('../src'), + dependencies: [ glib ], + ), +) diff --git a/test/test-bookmark-groups.c b/test/test-bookmark-groups.c new file mode 100644 index 0000000000..427991794c --- /dev/null +++ b/test/test-bookmark-groups.c @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#include "nemo-bookmark-groups.h" +#include + +static void +roundtrip (void) +{ + GError *error = NULL; + char *directory = g_dir_make_tmp ("nemo-groups-XXXXXX", &error); + g_assert_no_error (error); + char *filename = g_build_filename (directory, "nemo", "bookmark-groups", NULL); + GKeyFile *groups = nemo_bookmark_groups_load (filename, &error); + g_assert_no_error (error); + char **ids = nemo_bookmark_groups_get_ids (groups); + g_assert_cmpuint (g_strv_length (ids), ==, 0); + g_strfreev (ids); + char *lan = nemo_bookmark_groups_create (groups, "LAN"); + g_assert_nonnull (lan); + char *work = nemo_bookmark_groups_create (groups, "Arbeit"); + g_assert_nonnull (work); + g_assert_true (nemo_bookmark_groups_save (groups, filename, &error)); + g_assert_no_error (error); + g_key_file_unref (groups); + groups = nemo_bookmark_groups_load (filename, &error); + g_assert_no_error (error); + ids = nemo_bookmark_groups_get_ids (groups); + g_assert_cmpuint (g_strv_length (ids), ==, 2); + g_assert_cmpstr (ids[0], ==, lan); + g_assert_cmpstr (ids[1], ==, work); + char *name = g_key_file_get_string (groups, lan, "Name", NULL); + g_assert_cmpstr (name, ==, "LAN"); + g_free (name); + g_strfreev (ids); + g_key_file_unref (groups); + g_unlink (filename); + char *parent = g_path_get_dirname (filename); + g_rmdir (parent); + g_rmdir (directory); + g_free (parent); + g_free (filename); + g_free (directory); + g_free (lan); + g_free (work); +} +static void +membership (void) +{ + GKeyFile *groups = g_key_file_new (); + char *lan = nemo_bookmark_groups_create (groups, "LAN"); + char *work = nemo_bookmark_groups_create (groups, "Arbeit"); + nemo_bookmark_groups_move (groups, "file:///mnt/nas", lan, NULL); + char *id = nemo_bookmark_groups_find (groups, "file:///mnt/nas"); + g_assert_cmpstr (id, ==, lan); + g_free (id); + nemo_bookmark_groups_move (groups, "file:///mnt/hermes", lan, "file:///mnt/nas"); + char **uris = g_key_file_get_string_list (groups, lan, "URIs", NULL, NULL); + g_assert_cmpuint (g_strv_length (uris), ==, 2); + g_assert_cmpstr (uris[0], ==, "file:///mnt/hermes"); + g_strfreev (uris); + nemo_bookmark_groups_move (groups, "file:///mnt/nas", work, NULL); + id = nemo_bookmark_groups_find (groups, "file:///mnt/nas"); + g_assert_cmpstr (id, ==, work); + g_free (id); + nemo_bookmark_groups_move (groups, "file:///mnt/nas", NULL, NULL); + g_assert_null (nemo_bookmark_groups_find (groups, "file:///mnt/nas")); + g_free (lan); + g_free (work); + g_key_file_unref (groups); +} +static void +lifecycle (void) +{ + GKeyFile *groups = g_key_file_new (); + char *lan = nemo_bookmark_groups_create (groups, "LAN"); + nemo_bookmark_groups_move (groups, "file:///mnt/nas", lan, NULL); + nemo_bookmark_groups_delete (groups, lan); + char **ids = nemo_bookmark_groups_get_ids (groups); + g_assert_cmpuint (g_strv_length (ids), ==, 0); + g_assert_null (nemo_bookmark_groups_find (groups, "file:///mnt/nas")); + g_strfreev (ids); + g_free (lan); + g_key_file_unref (groups); +} +static void +rename_group (void) +{ + GKeyFile *groups = g_key_file_new (); + char *id = nemo_bookmark_groups_create (groups, "LAN"); + g_assert_true (nemo_bookmark_groups_rename (groups, id, "Netzwerk")); + char *name = g_key_file_get_string (groups, id, "Name", NULL); + g_assert_cmpstr (name, ==, "Netzwerk"); + g_free (name); + g_free (id); + g_key_file_unref (groups); +} +static void +prune (void) +{ + GKeyFile *groups = g_key_file_new (); + char *id = nemo_bookmark_groups_create (groups, "LAN"); + nemo_bookmark_groups_move (groups, "file:///keep", id, NULL); + nemo_bookmark_groups_move (groups, "file:///deleted", id, NULL); + const char *uris[] = { "file:///keep", NULL }; + g_assert_true (nemo_bookmark_groups_prune (groups, uris)); + g_assert_false (nemo_bookmark_groups_prune (groups, uris)); + g_assert_null (nemo_bookmark_groups_find (groups, "file:///deleted")); + char *found = nemo_bookmark_groups_find (groups, "file:///keep"); + g_assert_cmpstr (found, ==, id); + g_free (found); + g_free (id); + g_key_file_unref (groups); +} +static void +move_before_self (void) +{ + GKeyFile *groups = g_key_file_new (); + char *id = nemo_bookmark_groups_create (groups, "LAN"); + nemo_bookmark_groups_move (groups, "file:///first", id, NULL); + nemo_bookmark_groups_move (groups, "file:///second", id, NULL); + nemo_bookmark_groups_move (groups, "file:///first", id, "file:///first"); + char **uris = g_key_file_get_string_list (groups, id, "URIs", NULL, NULL); + g_assert_cmpstr (uris[0], ==, "file:///first"); + g_assert_cmpstr (uris[1], ==, "file:///second"); + g_strfreev (uris); + g_free (id); + g_key_file_unref (groups); +} +int main (int argc, char **argv) { + g_test_init (&argc, &argv, NULL); + g_test_add_func ("/bookmark-groups/roundtrip", roundtrip); + g_test_add_func ("/bookmark-groups/membership", membership); + g_test_add_func ("/bookmark-groups/delete", lifecycle); + g_test_add_func ("/bookmark-groups/rename", rename_group); + g_test_add_func ("/bookmark-groups/prune", prune); + g_test_add_func ("/bookmark-groups/move-before-self", move_before_self); + return g_test_run (); +}