/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2012 Red Hat, Inc * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * Author: Cosimo Cecchi */ #include "cc-search-locations-dialog.h" #include #define TRACKER_SCHEMA "org.freedesktop.Tracker.Miner.Files" #define TRACKER3_SCHEMA "org.freedesktop.Tracker3.Miner.Files" #define TRACKER_KEY_RECURSIVE_DIRECTORIES "index-recursive-directories" #define TRACKER_KEY_SINGLE_DIRECTORIES "index-single-directories" typedef enum { PLACE_XDG, PLACE_BOOKMARKS, PLACE_OTHER } PlaceType; typedef struct { CcSearchLocationsDialog *dialog; GFile *location; gchar *display_name; PlaceType place_type; GCancellable *cancellable; const gchar *settings_key; } Place; typedef struct { GtkWidget *row; GtkWidget *switch_; } PlaceRowWidgets; struct _CcSearchLocationsDialog { AdwPreferencesWindow parent; GSettings *tracker_preferences; GtkWidget *places_group; GtkWidget *places_list; GtkWidget *bookmarks_group; GtkWidget *bookmarks_list; GtkWidget *others_list; GtkWidget *locations_add; }; struct _CcSearchLocationsDialogClass { AdwPreferencesWindowClass parent_class; }; G_DEFINE_TYPE (CcSearchLocationsDialog, cc_search_locations_dialog, ADW_TYPE_PREFERENCES_WINDOW) static gboolean keynav_failed_cb (CcSearchLocationsDialog *self, GtkDirectionType direction) { GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self))); if (!toplevel) return FALSE; if (direction != GTK_DIR_UP && direction != GTK_DIR_DOWN) return FALSE; return gtk_widget_child_focus (toplevel, direction == GTK_DIR_UP ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD); } static void cc_search_locations_dialog_finalize (GObject *object) { CcSearchLocationsDialog *self = CC_SEARCH_LOCATIONS_DIALOG (object); g_clear_object (&self->tracker_preferences); G_OBJECT_CLASS (cc_search_locations_dialog_parent_class)->finalize (object); } static void cc_search_locations_dialog_init (CcSearchLocationsDialog *self) { gtk_widget_init_template (GTK_WIDGET (self)); } static Place * place_new (CcSearchLocationsDialog *dialog, GFile *location, gchar *display_name, PlaceType place_type) { Place *new_place = g_new0 (Place, 1); new_place->dialog = dialog; new_place->location = location; if (display_name != NULL) new_place->display_name = display_name; else new_place->display_name = g_file_get_basename (location); if (g_strcmp0 (g_file_get_path (location), g_get_home_dir ()) == 0) new_place->settings_key = TRACKER_KEY_SINGLE_DIRECTORIES; else new_place->settings_key = TRACKER_KEY_RECURSIVE_DIRECTORIES; new_place->place_type = place_type; return new_place; } static void place_free (Place * p) { g_cancellable_cancel (p->cancellable); g_clear_object (&p->cancellable); g_object_unref (p->location); g_free (p->display_name); g_free (p); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (Place, place_free) static GList * get_bookmarks (CcSearchLocationsDialog *self) { g_autoptr(GFile) file = NULL; g_autofree gchar *contents = NULL; g_autofree gchar *path = NULL; GList *bookmarks = NULL; GError *error = NULL; path = g_build_filename (g_get_user_config_dir (), "gtk-3.0", "bookmarks", NULL); file = g_file_new_for_path (path); if (g_file_load_contents (file, NULL, &contents, NULL, NULL, &error)) { gint idx; g_auto(GStrv) lines = NULL; lines = g_strsplit (contents, "\n", -1); for (idx = 0; lines[idx]; idx++) { /* Ignore empty or invalid lines that cannot be parsed properly */ if (lines[idx][0] != '\0' && lines[idx][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; Place *bookmark; label = NULL; space = strchr (lines[idx], ' '); if (space) { *space = '\0'; label = g_strdup (space + 1); } bookmark = place_new (self, g_file_new_for_uri (lines[idx]), label, PLACE_BOOKMARKS); bookmarks = g_list_prepend (bookmarks, bookmark); } } } return g_list_reverse (bookmarks); } static const gchar * get_user_special_dir_if_not_home (GUserDirectory idx) { const gchar *path; path = g_get_user_special_dir (idx); if (g_strcmp0 (path, g_get_home_dir ()) == 0) return NULL; return path; } static GList * get_xdg_dirs (CcSearchLocationsDialog *self) { GList *xdg_dirs = NULL; gint idx; const gchar *path; Place *xdg_dir; for (idx = 0; idx < G_USER_N_DIRECTORIES; idx++) { path = get_user_special_dir_if_not_home (idx); if (path == NULL) continue; if (idx == G_USER_DIRECTORY_TEMPLATES || idx == G_USER_DIRECTORY_PUBLIC_SHARE || idx == G_USER_DIRECTORY_DESKTOP) continue; xdg_dir = place_new (self, g_file_new_for_path (path), NULL, PLACE_XDG); xdg_dirs = g_list_prepend (xdg_dirs, xdg_dir); } return g_list_reverse (xdg_dirs); } static const gchar * path_to_tracker_dir (const gchar *path) { const gchar *value; if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_DESKTOP)) == 0) value = "&DESKTOP"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOCUMENTS)) == 0) value = "&DOCUMENTS"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOWNLOAD)) == 0) value = "&DOWNLOAD"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_MUSIC)) == 0) value = "&MUSIC"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_PICTURES)) == 0) value = "&PICTURES"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_PUBLIC_SHARE)) == 0) value = "&PUBLIC_SHARE"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_TEMPLATES)) == 0) value = "&TEMPLATES"; else if (g_strcmp0 (path, get_user_special_dir_if_not_home (G_USER_DIRECTORY_VIDEOS)) == 0) value = "&VIDEOS"; else if (g_strcmp0 (path, g_get_home_dir ()) == 0) value = "$HOME"; else value = path; return value; } static const gchar * path_from_tracker_dir (const gchar *value) { const gchar *path; if (g_strcmp0 (value, "&DESKTOP") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DESKTOP); else if (g_strcmp0 (value, "&DOCUMENTS") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOCUMENTS); else if (g_strcmp0 (value, "&DOWNLOAD") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_DOWNLOAD); else if (g_strcmp0 (value, "&MUSIC") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_MUSIC); else if (g_strcmp0 (value, "&PICTURES") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PICTURES); else if (g_strcmp0 (value, "&PUBLIC_SHARE") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_PUBLIC_SHARE); else if (g_strcmp0 (value, "&TEMPLATES") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_TEMPLATES); else if (g_strcmp0 (value, "&VIDEOS") == 0) path = get_user_special_dir_if_not_home (G_USER_DIRECTORY_VIDEOS); else if (g_strcmp0 (value, "$HOME") == 0) path = g_get_home_dir (); else path = value; return path; } static GPtrArray * place_get_new_settings_values (CcSearchLocationsDialog *self, Place *place, gboolean remove) { g_auto(GStrv) values = NULL; g_autofree gchar *path = NULL; GPtrArray *new_values; const gchar *tracker_dir; gboolean found; gint idx; new_values = g_ptr_array_new_with_free_func (g_free); values = g_settings_get_strv (self->tracker_preferences, place->settings_key); path = g_file_get_path (place->location); tracker_dir = path_to_tracker_dir (path); found = FALSE; for (idx = 0; values[idx] != NULL; idx++) { if (g_strcmp0 (values[idx], tracker_dir) == 0) { found = TRUE; if (remove) continue; } g_ptr_array_add (new_values, g_strdup (values[idx])); } if (!found && !remove) g_ptr_array_add (new_values, g_strdup (tracker_dir)); g_ptr_array_add (new_values, NULL); return new_values; } static GList * get_tracker_locations (CcSearchLocationsDialog *self) { g_auto(GStrv) locations = NULL; GFile *file; GList *list; gint idx; Place *location; const gchar *path; locations = g_settings_get_strv (self->tracker_preferences, TRACKER_KEY_RECURSIVE_DIRECTORIES); list = NULL; for (idx = 0; locations[idx] != NULL; idx++) { path = path_from_tracker_dir (locations[idx]); file = g_file_new_for_commandline_arg (path); location = place_new (self, file, NULL, PLACE_OTHER); if (file != NULL && g_file_query_exists (file, NULL)) { list = g_list_prepend (list, location); } else { g_autoptr(GPtrArray) new_values = NULL; new_values = place_get_new_settings_values (self, location, TRUE); g_settings_set_strv (self->tracker_preferences, TRACKER_KEY_RECURSIVE_DIRECTORIES, (const gchar **) new_values->pdata); } } return g_list_reverse (list); } static GList * get_places_list (CcSearchLocationsDialog *self) { g_autoptr(GList) xdg_list = NULL; g_autoptr(GList) tracker_list = NULL; g_autoptr(GList) bookmark_list = NULL; GList *l; g_autoptr(GHashTable) places = NULL; Place *place, *old_place; GList *places_list; places = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, NULL, (GDestroyNotify) place_free); /* add home */ place = place_new (self, g_file_new_for_path (g_get_home_dir ()), g_strdup (_("Home")), PLACE_XDG); g_hash_table_insert (places, place->location, place); /* first, load the XDG dirs */ xdg_list = get_xdg_dirs (self); for (l = xdg_list; l != NULL; l = l->next) { place = l->data; g_hash_table_insert (places, place->location, place); } /* then, insert all the tracker locations that are not XDG dirs */ tracker_list = get_tracker_locations (self); for (l = tracker_list; l != NULL; l = l->next) { g_autoptr(Place) p = l->data; old_place = g_hash_table_lookup (places, p->location); if (old_place == NULL) { g_hash_table_insert (places, p->location, p); g_steal_pointer (&p); } } /* finally, load bookmarks, and possibly update attributes */ bookmark_list = get_bookmarks (self); for (l = bookmark_list; l != NULL; l = l->next) { g_autoptr(Place) p = l->data; old_place = g_hash_table_lookup (places, p->location); if (old_place == NULL) { g_hash_table_insert (places, p->location, p); g_steal_pointer (&p); } else { g_free (old_place->display_name); old_place->display_name = g_strdup (p->display_name); if (old_place->place_type == PLACE_OTHER) old_place->place_type = PLACE_BOOKMARKS; } } places_list = g_hash_table_get_values (places); g_hash_table_steal_all (places); return places_list; } static gboolean switch_tracker_get_mapping (GValue *value, GVariant *variant, gpointer user_data) { Place *place = user_data; g_autofree const gchar **locations = NULL; GFile *location; gint idx; gboolean found; found = FALSE; locations = g_variant_get_strv (variant, NULL); for (idx = 0; locations[idx] != NULL; idx++) { location = g_file_new_for_path (path_from_tracker_dir(locations[idx])); found = g_file_equal (location, place->location); g_object_unref (location); if (found) break; } g_value_set_boolean (value, found); return TRUE; } static GVariant * switch_tracker_set_mapping (const GValue *value, const GVariantType *expected_type, gpointer user_data) { Place *place = user_data; g_autoptr(GPtrArray) new_values = NULL; gboolean remove; remove = !g_value_get_boolean (value); new_values = place_get_new_settings_values (place->dialog, place, remove); return g_variant_new_strv ((const gchar **) new_values->pdata, -1); } static void place_query_info_ready (GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GFileInfo) info = NULL; PlaceRowWidgets *widgets; Place *place; info = g_file_query_info_finish (G_FILE (source), res, NULL); if (!info) return; widgets = user_data; place = g_object_get_data (G_OBJECT (widgets->row), "place"); g_clear_object (&place->cancellable); gtk_widget_set_visible (widgets->switch_, TRUE); g_settings_bind_with_mapping (place->dialog->tracker_preferences, place->settings_key, widgets->switch_, "active", G_SETTINGS_BIND_DEFAULT, switch_tracker_get_mapping, switch_tracker_set_mapping, place, NULL); adw_preferences_row_set_title (ADW_PREFERENCES_ROW (widgets->row), place->display_name); } static void remove_button_clicked (CcSearchLocationsDialog *self, GtkWidget *button) { g_autoptr(GPtrArray) new_values = NULL; Place *place; place = g_object_get_data (G_OBJECT (button), "place"); new_values = place_get_new_settings_values (self, place, TRUE); g_settings_set_strv (self->tracker_preferences, place->settings_key, (const gchar **) new_values->pdata); } static gint place_compare_func (gconstpointer a, gconstpointer b, gpointer user_data) { GtkWidget *child_a, *child_b; Place *place_a, *place_b; g_autofree gchar *path = NULL; gboolean is_home; child_a = GTK_WIDGET (a); child_b = GTK_WIDGET (b); place_a = g_object_get_data (G_OBJECT (child_a), "place"); place_b = g_object_get_data (G_OBJECT (child_b), "place"); path = g_file_get_path (place_a->location); is_home = (g_strcmp0 (path, g_get_home_dir ()) == 0); if (is_home) return -1; if (place_a->place_type == place_b->place_type) return g_utf8_collate (place_a->display_name, place_b->display_name); if (place_a->place_type == PLACE_XDG) return -1; if ((place_a->place_type == PLACE_BOOKMARKS) && (place_b->place_type == PLACE_OTHER)) return -1; return 1; } static GtkWidget * create_row_for_place (CcSearchLocationsDialog *self, Place *place) { PlaceRowWidgets *widgets; GtkWidget *remove_button, *separator; widgets = g_new0 (PlaceRowWidgets, 1); widgets->row = adw_action_row_new (); widgets->switch_ = gtk_switch_new (); gtk_widget_set_visible (widgets->switch_, FALSE); gtk_widget_set_valign (widgets->switch_, GTK_ALIGN_CENTER); adw_action_row_add_suffix (ADW_ACTION_ROW (widgets->row), widgets->switch_); adw_action_row_set_activatable_widget (ADW_ACTION_ROW (widgets->row), widgets->switch_); g_object_set_data_full (G_OBJECT (widgets->row), "place", place, (GDestroyNotify) place_free); if (place->place_type == PLACE_OTHER) { separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL); gtk_widget_set_margin_top (separator, 12); gtk_widget_set_margin_bottom (separator, 12); adw_action_row_add_suffix (ADW_ACTION_ROW (widgets->row), separator); remove_button = gtk_button_new_from_icon_name ("window-close-symbolic"); g_object_set_data (G_OBJECT (remove_button), "place", place); gtk_widget_set_valign (remove_button, GTK_ALIGN_CENTER); gtk_style_context_add_class (gtk_widget_get_style_context (remove_button), "flat"); adw_action_row_add_suffix (ADW_ACTION_ROW (widgets->row), remove_button); g_signal_connect_swapped (remove_button, "clicked", G_CALLBACK (remove_button_clicked), self); } place->cancellable = g_cancellable_new (); g_file_query_info_async (place->location, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, place->cancellable, place_query_info_ready, widgets); return widgets->row; } static void update_list_visibility (CcSearchLocationsDialog *self) { gtk_widget_set_visible (self->places_group, gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->places_list), 0) != NULL); gtk_widget_set_visible (self->bookmarks_group, gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->bookmarks_list), 0) != NULL); gtk_widget_set_visible (self->others_list, gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->others_list), 0) != NULL); } static void populate_list_boxes (CcSearchLocationsDialog *self) { g_autoptr(GList) places = NULL; GList *l; Place *place; GtkWidget *row; places = get_places_list (self); for (l = places; l != NULL; l = l->next) { place = l->data; row = create_row_for_place (self, place); switch (place->place_type) { case PLACE_XDG: gtk_list_box_append (GTK_LIST_BOX (self->places_list), row); break; case PLACE_BOOKMARKS: gtk_list_box_append (GTK_LIST_BOX (self->bookmarks_list), row); break; case PLACE_OTHER: gtk_list_box_append (GTK_LIST_BOX (self->others_list), row); break; default: g_assert_not_reached (); } } update_list_visibility (self); } static void add_file_chooser_response (CcSearchLocationsDialog *self, GtkResponseType response, GtkWidget *widget) { g_autoptr(Place) place = NULL; g_autoptr(GPtrArray) new_values = NULL; if (response != GTK_RESPONSE_OK) { gtk_window_destroy (GTK_WINDOW (widget)); return; } place = place_new (self, gtk_file_chooser_get_file (GTK_FILE_CHOOSER (widget)), NULL, 0); place->settings_key = TRACKER_KEY_RECURSIVE_DIRECTORIES; new_values = place_get_new_settings_values (self, place, FALSE); g_settings_set_strv (self->tracker_preferences, place->settings_key, (const gchar **) new_values->pdata); gtk_window_destroy (GTK_WINDOW (widget)); } static void add_button_clicked (CcSearchLocationsDialog *self) { GtkWidget *file_chooser; file_chooser = gtk_file_chooser_dialog_new (_("Select Location"), GTK_WINDOW (self), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_OK"), GTK_RESPONSE_OK, NULL); gtk_window_set_modal (GTK_WINDOW (file_chooser), TRUE); g_signal_connect_swapped (file_chooser, "response", G_CALLBACK (add_file_chooser_response), self); gtk_window_present (GTK_WINDOW (file_chooser)); } static void other_places_refresh (CcSearchLocationsDialog *self) { g_autoptr(GList) places = NULL; GList *l; GtkListBoxRow *widget; while ((widget = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->others_list), 0))) gtk_list_box_remove (GTK_LIST_BOX (self->others_list), GTK_WIDGET (widget)); places = get_places_list (self); for (l = places; l != NULL; l = l->next) { GtkWidget *row; Place *place; place = l->data; if (place->place_type != PLACE_OTHER) continue; row = create_row_for_place (self, place); gtk_list_box_append (GTK_LIST_BOX (self->others_list), row); } update_list_visibility (self); } CcSearchLocationsDialog * cc_search_locations_dialog_new (CcSearchPanel *panel) { CcSearchLocationsDialog *self; GSettingsSchemaSource *source; g_autoptr(GSettingsSchema) schema = NULL; GtkWidget *toplevel; CcShell *shell; self = g_object_new (CC_SEARCH_LOCATIONS_DIALOG_TYPE, NULL); source = g_settings_schema_source_get_default (); schema = g_settings_schema_source_lookup (source, TRACKER3_SCHEMA, TRUE); if (schema) self->tracker_preferences = g_settings_new (TRACKER3_SCHEMA); else self->tracker_preferences = g_settings_new (TRACKER_SCHEMA); populate_list_boxes (self); gtk_list_box_set_sort_func (GTK_LIST_BOX (self->others_list), (GtkListBoxSortFunc)place_compare_func, NULL, NULL); g_signal_connect_swapped (self->tracker_preferences, "changed::" TRACKER_KEY_RECURSIVE_DIRECTORIES, G_CALLBACK (other_places_refresh), self); shell = cc_panel_get_shell (CC_PANEL (panel)); toplevel = cc_shell_get_toplevel (shell); gtk_window_set_transient_for (GTK_WINDOW (self), GTK_WINDOW (toplevel)); return self; } gboolean cc_search_locations_dialog_is_available (void) { GSettingsSchemaSource *source; g_autoptr(GSettingsSchema) schema = NULL; source = g_settings_schema_source_get_default (); if (!source) return FALSE; schema = g_settings_schema_source_lookup (source, TRACKER3_SCHEMA, TRUE); if (schema) return TRUE; schema = g_settings_schema_source_lookup (source, TRACKER_SCHEMA, TRUE); if (schema) return TRUE; return FALSE; } static void cc_search_locations_dialog_class_init (CcSearchLocationsDialogClass *klass) { GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cc_search_locations_dialog_finalize; gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/search/cc-search-locations-dialog.ui"); gtk_widget_class_bind_template_child (widget_class, CcSearchLocationsDialog, places_group); gtk_widget_class_bind_template_child (widget_class, CcSearchLocationsDialog, places_list); gtk_widget_class_bind_template_child (widget_class, CcSearchLocationsDialog, bookmarks_group); gtk_widget_class_bind_template_child (widget_class, CcSearchLocationsDialog, bookmarks_list); gtk_widget_class_bind_template_child (widget_class, CcSearchLocationsDialog, others_list); gtk_widget_class_bind_template_child (widget_class, CcSearchLocationsDialog, locations_add); gtk_widget_class_bind_template_callback (widget_class, add_button_clicked); gtk_widget_class_bind_template_callback (widget_class, keynav_failed_cb); }