diff options
Diffstat (limited to 'panels/search')
-rw-r--r-- | panels/search/cc-search-locations-dialog.c | 787 | ||||
-rw-r--r-- | panels/search/cc-search-locations-dialog.h | 30 | ||||
-rw-r--r-- | panels/search/cc-search-locations-dialog.ui | 85 | ||||
-rw-r--r-- | panels/search/cc-search-panel-row.c | 233 | ||||
-rw-r--r-- | panels/search/cc-search-panel-row.h | 38 | ||||
-rw-r--r-- | panels/search/cc-search-panel-row.ui | 61 | ||||
-rw-r--r-- | panels/search/cc-search-panel.c | 683 | ||||
-rw-r--r-- | panels/search/cc-search-panel.h | 30 | ||||
-rw-r--r-- | panels/search/cc-search-panel.ui | 57 | ||||
-rw-r--r-- | panels/search/gnome-search-panel.desktop.in.in | 18 | ||||
-rw-r--r-- | panels/search/icons/meson.build | 4 | ||||
-rw-r--r-- | panels/search/icons/scalable/org.gnome.Settings-search-symbolic.svg | 4 | ||||
-rw-r--r-- | panels/search/meson.build | 50 | ||||
-rw-r--r-- | panels/search/search.gresource.xml | 8 |
14 files changed, 2088 insertions, 0 deletions
diff --git a/panels/search/cc-search-locations-dialog.c b/panels/search/cc-search-locations-dialog.c new file mode 100644 index 0000000..9a0e437 --- /dev/null +++ b/panels/search/cc-search-locations-dialog.c @@ -0,0 +1,787 @@ +/* -*- 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 <http://www.gnu.org/licenses/>. + * + * Author: Cosimo Cecchi <cosimoc@gnome.org> + */ + +#include "cc-search-locations-dialog.h" + +#include <glib/gi18n.h> + +#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); +} diff --git a/panels/search/cc-search-locations-dialog.h b/panels/search/cc-search-locations-dialog.h new file mode 100644 index 0000000..4e2b5b3 --- /dev/null +++ b/panels/search/cc-search-locations-dialog.h @@ -0,0 +1,30 @@ +/* -*- 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 <http://www.gnu.org/licenses/>. + * + * Author: Cosimo Cecchi <cosimoc@gnome.org> + */ + +#pragma once + +#include "cc-search-panel.h" + +#define CC_SEARCH_LOCATIONS_DIALOG_TYPE (cc_search_locations_dialog_get_type ()) +G_DECLARE_FINAL_TYPE (CcSearchLocationsDialog, cc_search_locations_dialog, CC, SEARCH_LOCATIONS_DIALOG, AdwPreferencesWindow) + +CcSearchLocationsDialog *cc_search_locations_dialog_new (CcSearchPanel *panel); + +gboolean cc_search_locations_dialog_is_available (void); diff --git a/panels/search/cc-search-locations-dialog.ui b/panels/search/cc-search-locations-dialog.ui new file mode 100644 index 0000000..20ad732 --- /dev/null +++ b/panels/search/cc-search-locations-dialog.ui @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.0 --> + <template class="CcSearchLocationsDialog" parent="AdwPreferencesWindow"> + <property name="modal">True</property> + <property name="hide-on-close">True</property> + <property name="search-enabled">False</property> + <property name="title" translatable="yes">Search Locations</property> + <child> + <object class="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup"> + <property name="description" translatable="yes">Folders which are searched by system applications, such as Files, Photos and Videos.</property> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="places_group"> + <property name="title" translatable="yes">Places</property> + <child> + <object class="GtkListBox" id="places_list"> + <property name="selection-mode">none</property> + <signal name="keynav-failed" handler="keynav_failed_cb" object="CcSearchLocationsDialog" swapped="yes"/> + <accessibility> + <relation name="labelled-by">places_group</relation> + </accessibility> + <style> + <class name="boxed-list"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="bookmarks_group"> + <property name="title" translatable="yes">Bookmarks</property> + <child> + <object class="GtkListBox" id="bookmarks_list"> + <property name="selection-mode">none</property> + <signal name="keynav-failed" handler="keynav_failed_cb" object="CcSearchLocationsDialog" swapped="yes"/> + <accessibility> + <relation name="labelled-by">bookmarks_group</relation> + </accessibility> + <style> + <class name="boxed-list"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="others_group"> + <property name="title" translatable="yes">Others</property> + <property name="header-suffix"> + <object class="GtkButton" id="locations_add"> + <property name="valign">center</property> + <property name="child"> + <object class="AdwButtonContent"> + <property name="icon-name">list-add-symbolic</property> + <property name="label" translatable="yes">Add Location</property> + </object> + </property> + <signal name="clicked" handler="add_button_clicked" object="CcSearchLocationsDialog" swapped="yes"/> + <style> + <class name="flat"/> + </style> + </object> + </property> + <child> + <object class="GtkListBox" id="others_list"> + <property name="selection-mode">none</property> + <signal name="keynav-failed" handler="keynav_failed_cb" object="CcSearchLocationsDialog" swapped="yes"/> + <accessibility> + <relation name="labelled-by">others_group</relation> + </accessibility> + <style> + <class name="boxed-list"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/panels/search/cc-search-panel-row.c b/panels/search/cc-search-panel-row.c new file mode 100644 index 0000000..7cae2a3 --- /dev/null +++ b/panels/search/cc-search-panel-row.c @@ -0,0 +1,233 @@ +/* + * Copyright © 2019 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 <http://www.gnu.org/licenses/>. + * + * Author: Felipe Borges <felipeborges@gnome.org> + */ + +#include "cc-search-panel-row.h" + +struct _CcSearchPanelRow +{ + AdwActionRow parent_instance; + + GAppInfo *app_info; + + GtkImage *icon; + GtkSwitch *switcher; + + GtkListBox *drag_widget; + gdouble drag_x; + gdouble drag_y; +}; + +G_DEFINE_TYPE (CcSearchPanelRow, cc_search_panel_row, ADW_TYPE_ACTION_ROW) + +enum +{ + SIGNAL_MOVE_ROW, + SIGNAL_LAST +}; + +static guint signals[SIGNAL_LAST] = { 0, }; + +static void +move_up_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + CcSearchPanelRow *self = CC_SEARCH_PANEL_ROW (user_data); + GtkListBox *list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (self))); + gint previous_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (self)) - 1; + GtkListBoxRow *previous_row = gtk_list_box_get_row_at_index (list_box, previous_idx); + + if (previous_row == NULL) + return; + + g_signal_emit (self, + signals[SIGNAL_MOVE_ROW], + 0, + previous_row); +} + +static void +move_down_cb (GSimpleAction *action, + GVariant *parameter, + gpointer user_data) +{ + CcSearchPanelRow *self = CC_SEARCH_PANEL_ROW (user_data); + GtkListBox *list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (self))); + gint next_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (self)) + 1; + GtkListBoxRow *next_row = gtk_list_box_get_row_at_index (list_box, next_idx); + + if (next_row == NULL) + return; + + g_signal_emit (next_row, + signals[SIGNAL_MOVE_ROW], + 0, + self); +} + +static GdkContentProvider * +drag_prepare_cb (GtkDragSource *source, + double x, + double y, + CcSearchPanelRow *self) +{ + self->drag_x = x; + self->drag_y = y; + + return gdk_content_provider_new_typed (CC_TYPE_SEARCH_PANEL_ROW, self); +} + +static void +drag_begin_cb (GtkDragSource *source, + GdkDrag *drag, + CcSearchPanelRow *self) +{ + CcSearchPanelRow *panel_row; + GtkAllocation alloc; + GtkWidget *drag_icon; + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + self->drag_widget = GTK_LIST_BOX (gtk_list_box_new ()); + gtk_widget_set_size_request (GTK_WIDGET (self->drag_widget), alloc.width, alloc.height); + + panel_row = cc_search_panel_row_new (self->app_info); + gtk_switch_set_active (panel_row->switcher, gtk_switch_get_active (self->switcher)); + + gtk_list_box_append (GTK_LIST_BOX (self->drag_widget), GTK_WIDGET (panel_row)); + gtk_list_box_drag_highlight_row (self->drag_widget, GTK_LIST_BOX_ROW (panel_row)); + + drag_icon = gtk_drag_icon_get_for_drag (drag); + gtk_drag_icon_set_child (GTK_DRAG_ICON (drag_icon), GTK_WIDGET (self->drag_widget)); + gdk_drag_set_hotspot (drag, self->drag_x, self->drag_y); + +} + +static gboolean +drop_cb (GtkDropTarget *drop_target, + const GValue *value, + gdouble x, + gdouble y, + CcSearchPanelRow *self) +{ + CcSearchPanelRow *source; + + g_message ("Drop"); + + if (!G_VALUE_HOLDS (value, CC_TYPE_SEARCH_PANEL_ROW)) + return FALSE; + + source = g_value_get_object (value); + + g_signal_emit (source, + signals[SIGNAL_MOVE_ROW], + 0, + self); + + return TRUE; +} + +static void +cc_search_panel_row_class_init (CcSearchPanelRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/search/cc-search-panel-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcSearchPanelRow, icon); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanelRow, switcher); + + signals[SIGNAL_MOVE_ROW] = + g_signal_new ("move-row", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, + 1, CC_TYPE_SEARCH_PANEL_ROW); +} + +const GActionEntry row_entries[] = { + { "move-up", move_up_cb, NULL, NULL, NULL, { 0 } }, + { "move-down", move_down_cb, NULL, NULL, NULL, { 0 } } +}; + +static void +cc_search_panel_row_init (CcSearchPanelRow *self) +{ + GtkDragSource *drag_source; + GtkDropTarget *drop_target; + GSimpleActionGroup *group; + + gtk_widget_init_template (GTK_WIDGET (self)); + + drag_source = gtk_drag_source_new (); + gtk_drag_source_set_actions (drag_source, GDK_ACTION_MOVE); + g_signal_connect (drag_source, "prepare", G_CALLBACK (drag_prepare_cb), self); + g_signal_connect (drag_source, "drag-begin", G_CALLBACK (drag_begin_cb), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag_source)); + + drop_target = gtk_drop_target_new (CC_TYPE_SEARCH_PANEL_ROW, GDK_ACTION_MOVE); + gtk_drop_target_set_preload (drop_target, TRUE); + g_signal_connect (drop_target, "drop", G_CALLBACK (drop_cb), self); + gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drop_target)); + + group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (group), + row_entries, + G_N_ELEMENTS (row_entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self), "row", G_ACTION_GROUP (group)); +} + +CcSearchPanelRow * +cc_search_panel_row_new (GAppInfo *app_info) +{ + CcSearchPanelRow *self; + g_autoptr(GIcon) gicon = NULL; + + self = g_object_new (CC_TYPE_SEARCH_PANEL_ROW, NULL); + self->app_info = g_object_ref (app_info); + + gicon = g_app_info_get_icon (app_info); + if (gicon == NULL) + gicon = g_themed_icon_new ("application-x-executable"); + else + g_object_ref (gicon); + gtk_image_set_from_gicon (self->icon, gicon); + + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self), + g_app_info_get_name (app_info)); + + return self; +} + +GAppInfo * +cc_search_panel_row_get_app_info (CcSearchPanelRow *self) +{ + return self->app_info; +} + +GtkWidget * +cc_search_panel_row_get_switch (CcSearchPanelRow *self) +{ + return GTK_WIDGET (self->switcher); +} diff --git a/panels/search/cc-search-panel-row.h b/panels/search/cc-search-panel-row.h new file mode 100644 index 0000000..f7bae83 --- /dev/null +++ b/panels/search/cc-search-panel-row.h @@ -0,0 +1,38 @@ +/* + * Copyright © 2019 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 <http://www.gnu.org/licenses/>. + * + * Author: Felipe Borges <felipeborges@gnome.org> + */ + +#pragma once + +#include <gio/gdesktopappinfo.h> +#include <adwaita.h> + +G_BEGIN_DECLS + +#define CC_TYPE_SEARCH_PANEL_ROW (cc_search_panel_row_get_type()) + +G_DECLARE_FINAL_TYPE (CcSearchPanelRow, cc_search_panel_row, CC, SEARCH_PANEL_ROW, AdwActionRow) + + +CcSearchPanelRow *cc_search_panel_row_new (GAppInfo *app_info); + +GAppInfo *cc_search_panel_row_get_app_info (CcSearchPanelRow *row); + +GtkWidget *cc_search_panel_row_get_switch (CcSearchPanelRow *row); + +G_END_DECLS diff --git a/panels/search/cc-search-panel-row.ui b/panels/search/cc-search-panel-row.ui new file mode 100644 index 0000000..89e74ae --- /dev/null +++ b/panels/search/cc-search-panel-row.ui @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="CcSearchPanelRow" parent="AdwActionRow"> + <property name="activatable-widget">switcher</property> + <child type="prefix"> + <object class="GtkImage" id="icon"> + <property name="icon-size">large</property> + <style> + <class name="lowres-icon"/> + </style> + </object> + </child> + <child type="prefix"> + <object class="GtkImage"> + <property name="icon-name">list-drag-handle-symbolic</property> + <style> + <class name="drag-handle"/> + </style> + </object> + </child> + <child type="suffix"> + <object class="GtkSwitch" id="switcher"> + <property name="valign">center</property> + </object> + </child> + <child type="suffix"> + <object class="GtkSeparator"> + <property name="orientation">vertical</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + </object> + </child> + <child type="suffix"> + <object class="GtkMenuButton"> + <property name="valign">center</property> + <property name="icon-name">view-more-symbolic</property> + <property name="menu-model">move_row_menu</property> + <accessibility> + <property name="label" translatable="yes">More options…</property> + </accessibility> + <style> + <class name="flat"/> + </style> + </object> + </child> + </template> + + <!-- Move Row Menu --> + <menu id="move_row_menu"> + <section> + <item> + <attribute name="label" translatable="yes">Move Up</attribute> + <attribute name="action">row.move-up</attribute> + </item> + <item> + <attribute name="label" translatable="yes">Move Down</attribute> + <attribute name="action">row.move-down</attribute> + </item> + </section> + </menu> +</interface> diff --git a/panels/search/cc-search-panel.c b/panels/search/cc-search-panel.c new file mode 100644 index 0000000..80c8c94 --- /dev/null +++ b/panels/search/cc-search-panel.c @@ -0,0 +1,683 @@ +/* -*- 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 <http://www.gnu.org/licenses/>. + * + * Author: Cosimo Cecchi <cosimoc@gnome.org> + */ + +#include "cc-search-panel.h" +#include "cc-search-panel-row.h" +#include "cc-search-locations-dialog.h" +#include "cc-search-resources.h" + +#include <gio/gdesktopappinfo.h> +#include <glib/gi18n.h> + +struct _CcSearchPanel +{ + CcPanel parent_instance; + + GtkWidget *list_box; + GtkSwitch *main_switch; + GtkWidget *search_group; + GtkWidget *settings_row; + CcSearchPanelRow *selected_row; + + GSettings *search_settings; + GHashTable *sort_order; + + CcSearchLocationsDialog *locations_dialog; +}; + +CC_PANEL_REGISTER (CcSearchPanel, cc_search_panel) + +#define SHELL_PROVIDER_GROUP "Shell Search Provider" + +static gboolean +keynav_failed_cb (CcSearchPanel *self, GtkDirectionType direction, GtkWidget *list) +{ + 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 gint +list_sort_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + CcSearchPanel *self = user_data; + GAppInfo *app_a, *app_b; + const gchar *id_a, *id_b; + gint idx_a, idx_b; + gpointer lookup; + + app_a = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW ((gpointer*)a)); + app_b = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW ((gpointer*)b)); + + id_a = g_app_info_get_id (app_a); + id_b = g_app_info_get_id (app_b); + + /* find the index of the application in the GSettings preferences */ + idx_a = -1; + idx_b = -1; + + lookup = g_hash_table_lookup (self->sort_order, id_a); + if (lookup) + idx_a = GPOINTER_TO_INT (lookup) - 1; + + lookup = g_hash_table_lookup (self->sort_order, id_b); + if (lookup) + idx_b = GPOINTER_TO_INT (lookup) - 1; + + /* if neither app is found, use alphabetical order */ + if ((idx_a == -1) && (idx_b == -1)) + return g_utf8_collate (g_app_info_get_name (app_a), g_app_info_get_name (app_b)); + + /* if app_a isn't found, it's sorted after app_b */ + if (idx_a == -1) + return 1; + + /* if app_b isn't found, it's sorted after app_a */ + if (idx_b == -1) + return -1; + + /* finally, if both apps are found, return their order in the list */ + return (idx_a - idx_b); +} + +static void +search_panel_invalidate_sort_order (CcSearchPanel *self) +{ + g_auto(GStrv) sort_order = NULL; + gint idx; + + g_hash_table_remove_all (self->sort_order); + sort_order = g_settings_get_strv (self->search_settings, "sort-order"); + + for (idx = 0; sort_order[idx] != NULL; idx++) + g_hash_table_insert (self->sort_order, g_strdup (sort_order[idx]), GINT_TO_POINTER (idx + 1)); + + gtk_list_box_invalidate_sort (GTK_LIST_BOX (self->list_box)); +} + +static gint +propagate_compare_func (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + CcSearchPanel *self = user_data; + const gchar *key_a = a, *key_b = b; + gint idx_a, idx_b; + + idx_a = GPOINTER_TO_INT (g_hash_table_lookup (self->sort_order, key_a)); + idx_b = GPOINTER_TO_INT (g_hash_table_lookup (self->sort_order, key_b)); + + return (idx_a - idx_b); +} + +static void +search_panel_propagate_sort_order (CcSearchPanel *self) +{ + g_autoptr(GList) keys = NULL; + GList *l; + g_autoptr(GPtrArray) sort_order = NULL; + + sort_order = g_ptr_array_new (); + keys = g_hash_table_get_keys (self->sort_order); + keys = g_list_sort_with_data (keys, propagate_compare_func, self); + + for (l = keys; l != NULL; l = l->next) + g_ptr_array_add (sort_order, l->data); + + g_ptr_array_add (sort_order, NULL); + g_settings_set_strv (self->search_settings, "sort-order", + (const gchar **) sort_order->pdata); +} + +static void +search_panel_set_no_providers (CcSearchPanel *self) +{ + /* center the list box in the scrolled window */ + gtk_widget_set_valign (self->list_box, GTK_ALIGN_CENTER); + + gtk_list_box_append (GTK_LIST_BOX (self->list_box), + gtk_label_new (_("No applications found"))); +} + +static void +search_panel_move_selected (CcSearchPanel *self, + gboolean down) +{ + GtkListBoxRow *other_row; + GAppInfo *app_info, *other_app_info; + const gchar *app_id, *other_app_id; + const gchar *last_good_app, *target_app; + GtkWidget *aux; + gint idx, other_idx; + gpointer idx_ptr; + gboolean found; + + app_info = cc_search_panel_row_get_app_info (self->selected_row); + app_id = g_app_info_get_id (app_info); + + /* The assertions are valid only as long as we don't move the first + or the last item. */ + + aux = GTK_WIDGET (self->selected_row); + other_row = down ? GTK_LIST_BOX_ROW (gtk_widget_get_next_sibling (aux)) : + GTK_LIST_BOX_ROW (gtk_widget_get_prev_sibling (aux)); + + other_app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (other_row)); + other_app_id = g_app_info_get_id (other_app_info); + + g_assert (other_app_id != NULL); + + /* Check if we're moving one of the unsorted providers at the end of + the list; in that case, the value we obtain from the sort order table + is garbage. + We need to find the last app with a valid sort order, and + then set the sort order on all intermediate apps until we find the + one we want to move, if moving up, or the neighbor, if moving down. + */ + last_good_app = target_app = app_id; + found = g_hash_table_lookup_extended (self->sort_order, last_good_app, NULL, &idx_ptr); + while (!found) + { + GAppInfo *tmp; + const char *tmp_id; + + aux = gtk_widget_get_prev_sibling (aux); + if (aux == NULL) + { + last_good_app = NULL; + break; + } + + tmp = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (aux)); + tmp_id = g_app_info_get_id (tmp); + + last_good_app = tmp_id; + found = g_hash_table_lookup_extended (self->sort_order, tmp_id, NULL, &idx_ptr); + } + + /* For simplicity's sake, set all sort orders to the previously visible state + first, and only then do the modification requested. + + The loop actually sets the sort order on last_good_app even if we found a + valid one already, but I preferred to keep the logic simple, at the expense + of a small performance penalty. + */ + if (found) + { + idx = GPOINTER_TO_INT (idx_ptr); + } + else + { + /* If not found, there is no configured app that has a sort order, so we start + from the first position and walk the entire list. + Sort orders are 1 based, so that 0 (NULL) is not a valid value. + */ + idx = 1; + aux = gtk_widget_get_first_child (GTK_WIDGET (self->list_box)); + } + + while (last_good_app != target_app) + { + GAppInfo *tmp; + const char *tmp_id; + + tmp = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (aux)); + tmp_id = g_app_info_get_id (tmp); + + g_hash_table_replace (self->sort_order, g_strdup (tmp_id), GINT_TO_POINTER (idx)); + + aux = gtk_widget_get_next_sibling (aux); + idx++; + last_good_app = tmp_id; + } + + other_idx = GPOINTER_TO_INT (g_hash_table_lookup (self->sort_order, app_id)); + idx = down ? (other_idx + 1) : (other_idx - 1); + + g_hash_table_replace (self->sort_order, g_strdup (other_app_id), GINT_TO_POINTER (other_idx)); + g_hash_table_replace (self->sort_order, g_strdup (app_id), GINT_TO_POINTER (idx)); + + search_panel_propagate_sort_order (self); +} + +static void +row_moved_cb (CcSearchPanel *self, + CcSearchPanelRow *dest_row, + CcSearchPanelRow *row) +{ + gint source_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (row)); + gint dest_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (dest_row)); + gboolean down; + + self->selected_row = row; + + down = (source_idx - dest_idx) < 0; + for (int i = 0; i < ABS (source_idx - dest_idx); i++) + search_panel_move_selected (self, down); +} + +static void +settings_row_activated (GtkWidget *widget, + gpointer user_data) +{ + CcSearchPanel *self = user_data; + + if (self->locations_dialog == NULL) + { + self->locations_dialog = cc_search_locations_dialog_new (self); + g_object_add_weak_pointer (G_OBJECT (self->locations_dialog), + (gpointer *) &self->locations_dialog); + } + + gtk_window_present (GTK_WINDOW (self->locations_dialog)); +} + +static GVariant * +switch_settings_mapping_set_generic (const GValue *value, + const GVariantType *expected_type, + GtkWidget *row, + gboolean default_enabled) +{ + CcSearchPanel *self = g_object_get_data (G_OBJECT (row), "self"); + GAppInfo *app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (row)); + g_auto(GStrv) apps = NULL; + g_autoptr(GPtrArray) new_apps = NULL; + gint idx; + gboolean remove, found; + + remove = !!g_value_get_boolean (value) == !!default_enabled; + found = FALSE; + new_apps = g_ptr_array_new_with_free_func (g_free); + apps = g_settings_get_strv (self->search_settings, + default_enabled ? "disabled" : "enabled"); + + for (idx = 0; apps[idx] != NULL; idx++) + { + if (g_strcmp0 (apps[idx], g_app_info_get_id (app_info)) == 0) + { + found = TRUE; + + if (remove) + continue; + } + + g_ptr_array_add (new_apps, g_strdup (apps[idx])); + } + + if (!found && !remove) + g_ptr_array_add (new_apps, g_strdup (g_app_info_get_id (app_info))); + + g_ptr_array_add (new_apps, NULL); + + return g_variant_new_strv ((const gchar **) new_apps->pdata, -1); +} + +static GVariant * +switch_settings_mapping_set_default_enabled (const GValue *value, + const GVariantType *expected_type, + gpointer user_data) +{ + return switch_settings_mapping_set_generic (value, expected_type, + user_data, TRUE); +} + +static GVariant * +switch_settings_mapping_set_default_disabled (const GValue *value, + const GVariantType *expected_type, + gpointer user_data) +{ + return switch_settings_mapping_set_generic (value, expected_type, + user_data, FALSE); +} + +static gboolean +switch_settings_mapping_get_generic (GValue *value, + GVariant *variant, + GtkWidget *row, + gboolean default_enabled) +{ + GAppInfo *app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (row)); + g_autofree const gchar **apps = NULL; + gint idx; + gboolean found; + + found = FALSE; + apps = g_variant_get_strv (variant, NULL); + + for (idx = 0; apps[idx] != NULL; idx++) + { + if (g_strcmp0 (apps[idx], g_app_info_get_id (app_info)) == 0) + { + found = TRUE; + break; + } + } + + g_value_set_boolean (value, !!default_enabled != !!found); + + return TRUE; +} + +static gboolean +switch_settings_mapping_get_default_enabled (GValue *value, + GVariant *variant, + gpointer user_data) +{ + return switch_settings_mapping_get_generic (value, variant, + user_data, TRUE); +} + +static gboolean +switch_settings_mapping_get_default_disabled (GValue *value, + GVariant *variant, + gpointer user_data) +{ + return switch_settings_mapping_get_generic (value, variant, + user_data, FALSE); +} + +static void +search_panel_add_one_app_info (CcSearchPanel *self, + GAppInfo *app_info, + gboolean default_enabled) +{ + CcSearchPanelRow *row; + + /* gnome-control-center is special cased in the shell, + and is not configurable */ + if (g_strcmp0 (g_app_info_get_id (app_info), + "gnome-control-center.desktop") == 0) + return; + + /* reset valignment of the list box */ + gtk_widget_set_valign (self->list_box, GTK_ALIGN_FILL); + + row = cc_search_panel_row_new (app_info); + g_signal_connect_object (row, "move-row", + G_CALLBACK (row_moved_cb), self, + G_CONNECT_SWAPPED); + g_object_set_data (G_OBJECT (row), "self", self); + gtk_list_box_append (GTK_LIST_BOX (self->list_box), GTK_WIDGET (row)); + + if (default_enabled) + { + g_settings_bind_with_mapping (self->search_settings, "disabled", + cc_search_panel_row_get_switch (row), "active", + G_SETTINGS_BIND_DEFAULT, + switch_settings_mapping_get_default_enabled, + switch_settings_mapping_set_default_enabled, + row, NULL); + } + else + { + g_settings_bind_with_mapping (self->search_settings, "enabled", + cc_search_panel_row_get_switch (row), "active", + G_SETTINGS_BIND_DEFAULT, + switch_settings_mapping_get_default_disabled, + switch_settings_mapping_set_default_disabled, + row, NULL); + } +} + +static void +search_panel_add_one_provider (CcSearchPanel *self, + GFile *provider) +{ + g_autofree gchar *path = NULL; + g_autofree gchar *desktop_id = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + g_autoptr(GAppInfo) app_info = NULL; + g_autoptr(GError) error = NULL; + gboolean default_disabled; + + path = g_file_get_path (provider); + keyfile = g_key_file_new (); + g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, &error); + + if (error != NULL) + { + g_warning ("Error loading %s: %s - search provider will be ignored", + path, error->message); + return; + } + + if (!g_key_file_has_group (keyfile, SHELL_PROVIDER_GROUP)) + { + g_debug ("Shell search provider group missing from '%s', ignoring", path); + return; + } + + desktop_id = g_key_file_get_string (keyfile, SHELL_PROVIDER_GROUP, + "DesktopId", &error); + + if (error != NULL) + { + g_warning ("Unable to read desktop ID from %s: %s - search provider will be ignored", + path, error->message); + return; + } + + app_info = G_APP_INFO (g_desktop_app_info_new (desktop_id)); + + if (app_info == NULL) + { + g_debug ("Could not find application with desktop ID '%s' referenced in '%s', ignoring", + desktop_id, path); + return; + } + + default_disabled = g_key_file_get_boolean (keyfile, SHELL_PROVIDER_GROUP, + "DefaultDisabled", NULL); + search_panel_add_one_app_info (self, app_info, !default_disabled); +} + +static void +search_providers_discover_ready (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GList) providers = NULL; + GList *l; + CcSearchPanel *self = CC_SEARCH_PANEL (source); + g_autoptr(GError) error = NULL; + + providers = g_task_propagate_pointer (G_TASK (result), &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + if (providers == NULL) + { + search_panel_set_no_providers (self); + return; + } + + for (l = providers; l != NULL; l = l->next) + { + g_autoptr(GFile) provider = l->data; + search_panel_add_one_provider (self, provider); + } + + /* propagate a write to GSettings, to make sure we always have + * all the providers in the list. + */ + search_panel_propagate_sort_order (self); +} + +static GList * +search_providers_discover_one_directory (const gchar *system_dir, + GCancellable *cancellable) +{ + GList *providers = NULL; + g_autofree gchar *providers_path = NULL; + g_autoptr(GFile) providers_location = NULL; + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GError) error = NULL; + + providers_path = g_build_filename (system_dir, "gnome-shell", "search-providers", NULL); + providers_location = g_file_new_for_path (providers_path); + + enumerator = g_file_enumerate_children (providers_location, + "standard::type,standard::name,standard::content-type", + G_FILE_QUERY_INFO_NONE, + cancellable, &error); + + if (error != NULL) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error opening %s: %s - search provider configuration won't be possible", + providers_path, error->message); + + return NULL; + } + + while (TRUE) + { + g_autoptr(GFileInfo) info = NULL; + GFile *provider; + + info = g_file_enumerator_next_file (enumerator, cancellable, &error); + if (info == NULL) + { + if (error != NULL && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Error reading from %s: %s - search providers might be missing from the panel", + providers_path, error->message); + return providers; + } + provider = g_file_get_child (providers_location, g_file_info_get_name (info)); + providers = g_list_prepend (providers, provider); + } +} + +static void +search_providers_discover_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GList *providers = NULL; + const gchar * const *system_data_dirs; + int idx; + + system_data_dirs = g_get_system_data_dirs (); + for (idx = 0; system_data_dirs[idx] != NULL; idx++) + { + providers = g_list_concat (search_providers_discover_one_directory (system_data_dirs[idx], cancellable), + providers); + + if (g_task_return_error_if_cancelled (task)) + { + g_list_free_full (providers, g_object_unref); + return; + } + } + + g_task_return_pointer (task, providers, NULL); +} + +static void +populate_search_providers (CcSearchPanel *self) +{ + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cc_panel_get_cancellable (CC_PANEL (self)), + search_providers_discover_ready, self); + g_task_run_in_thread (task, search_providers_discover_thread); +} + +static void +cc_search_panel_finalize (GObject *object) +{ + CcSearchPanel *self = CC_SEARCH_PANEL (object); + + g_clear_object (&self->search_settings); + g_hash_table_destroy (self->sort_order); + + if (self->locations_dialog) + gtk_window_destroy (GTK_WINDOW (self->locations_dialog)); + + G_OBJECT_CLASS (cc_search_panel_parent_class)->finalize (object); +} + +static void +cc_search_panel_init (CcSearchPanel *self) +{ + g_resources_register (cc_search_get_resource ()); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box), + (GtkListBoxSortFunc)list_sort_func, self, NULL); + + gtk_widget_set_sensitive (self->settings_row, cc_search_locations_dialog_is_available ()); + + self->search_settings = g_settings_new ("org.gnome.desktop.search-providers"); + g_settings_bind (self->search_settings, + "disable-external", + self->main_switch, + "active", + G_SETTINGS_BIND_DEFAULT | + G_SETTINGS_BIND_INVERT_BOOLEAN); + + g_object_bind_property (self->main_switch, + "active", + self->search_group, + "sensitive", + G_BINDING_DEFAULT | + G_BINDING_SYNC_CREATE); + + self->sort_order = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + g_signal_connect_swapped (self->search_settings, "changed::sort-order", + G_CALLBACK (search_panel_invalidate_sort_order), self); + search_panel_invalidate_sort_order (self); + + populate_search_providers (self); +} + +static void +cc_search_panel_class_init (CcSearchPanelClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *oclass = G_OBJECT_CLASS (klass); + + oclass->finalize = cc_search_panel_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/control-center/search/cc-search-panel.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, list_box); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, main_switch); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, search_group); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, settings_row); + + gtk_widget_class_bind_template_callback (widget_class, settings_row_activated); + gtk_widget_class_bind_template_callback (widget_class, keynav_failed_cb); +} diff --git a/panels/search/cc-search-panel.h b/panels/search/cc-search-panel.h new file mode 100644 index 0000000..34d75a5 --- /dev/null +++ b/panels/search/cc-search-panel.h @@ -0,0 +1,30 @@ +/* -*- 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 <http://www.gnu.org/licenses/>. + * + * Author: Cosimo Cecchi <cosimoc@gnome.org> + */ + +#pragma once + +#include <shell/cc-panel.h> + +G_BEGIN_DECLS + +#define CC_TYPE_SEARCH_PANEL (cc_search_panel_get_type ()) +G_DECLARE_FINAL_TYPE (CcSearchPanel, cc_search_panel, CC, SEARCH_PANEL, CcPanel) + +G_END_DECLS diff --git a/panels/search/cc-search-panel.ui b/panels/search/cc-search-panel.ui new file mode 100644 index 0000000..7876fe8 --- /dev/null +++ b/panels/search/cc-search-panel.ui @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="CcSearchPanel" parent="CcPanel"> + <child type="content"> + <object class="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Application Search</property> + <property name="subtitle" translatable="yes">Include application-provided search results.</property> + <property name="activatable-widget">main_switch</property> + <child type="suffix"> + <object class="GtkSwitch" id="main_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="settings_row"> + <property name="title" translatable="yes">Search Locations</property> + <property name="subtitle" translatable="yes">Folders which are searched by system applications.</property> + <property name="activatable">True</property> + <signal name="activated" handler="settings_row_activated" object="CcSearchPanel" swapped="no"/> + <child type="suffix"> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="search_group"> + <property name="title" translatable="yes">Search Results</property> + <property name="description" translatable="yes">Results are displayed according to the list order.</property> + <child> + <object class="GtkListBox" id="list_box"> + <property name="selection-mode">none</property> + <signal name="keynav-failed" handler="keynav_failed_cb" object="CcSearchPanel" swapped="yes"/> + <accessibility> + <relation name="labelled-by">search_group</relation> + </accessibility> + <style> + <class name="boxed-list"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + +</interface> diff --git a/panels/search/gnome-search-panel.desktop.in.in b/panels/search/gnome-search-panel.desktop.in.in new file mode 100644 index 0000000..5df64e4 --- /dev/null +++ b/panels/search/gnome-search-panel.desktop.in.in @@ -0,0 +1,18 @@ +[Desktop Entry] +Name=Search +Comment=Control which applications show search results in the Activities Overview +Exec=gnome-control-center search +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=org.gnome.Settings-search-symbolic +Terminal=false +Type=Application +NoDisplay=true +StartupNotify=true +Categories=GNOME;GTK;Settings;DesktopSettings;X-GNOME-Settings-Panel;X-GNOME-PersonalizationSettings; +OnlyShowIn=GNOME;Unity; +X-GNOME-Bugzilla-Bugzilla=GNOME +X-GNOME-Bugzilla-Product=gnome-control-center +X-GNOME-Bugzilla-Component=search +X-GNOME-Bugzilla-Version=@VERSION@ +# Translators: Search terms to find the Search panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Search;Find;Index;Hide;Privacy;Results; diff --git a/panels/search/icons/meson.build b/panels/search/icons/meson.build new file mode 100644 index 0000000..d2d33e2 --- /dev/null +++ b/panels/search/icons/meson.build @@ -0,0 +1,4 @@ +install_data( + 'scalable/org.gnome.Settings-search-symbolic.svg', + install_dir: join_paths(control_center_icondir, 'hicolor', 'scalable', 'apps') +) diff --git a/panels/search/icons/scalable/org.gnome.Settings-search-symbolic.svg b/panels/search/icons/scalable/org.gnome.Settings-search-symbolic.svg new file mode 100644 index 0000000..3bc527b --- /dev/null +++ b/panels/search/icons/scalable/org.gnome.Settings-search-symbolic.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"> + <path d="m 6.5 0 c -3.578125 0 -6.5 2.921875 -6.5 6.5 s 2.921875 6.496094 6.5 6.496094 c 1.429688 0 2.753906 -0.464844 3.828125 -1.253906 l 3.945313 3.945312 c 0.957031 0.9375 2.363281 -0.5 1.40625 -1.4375 l -3.929688 -3.929688 c 0.785156 -1.074218 1.25 -2.394531 1.25 -3.820312 c 0 -3.578125 -2.921875 -6.5 -6.5 -6.5 z m 0 2 c 2.496094 0 4.5 2.003906 4.5 4.5 s -2.003906 4.496094 -4.5 4.496094 s -4.5 -2 -4.5 -4.496094 s 2.003906 -4.5 4.5 -4.5 z m 0 0" fill="#2e3436"/> +</svg> diff --git a/panels/search/meson.build b/panels/search/meson.build new file mode 100644 index 0000000..3b66562 --- /dev/null +++ b/panels/search/meson.build @@ -0,0 +1,50 @@ +panels_list += cappletname +desktop = 'gnome-@0@-panel.desktop'.format(cappletname) + +desktop_in = configure_file( + input: desktop + '.in.in', + output: desktop + '.in', + configuration: desktop_conf +) + +i18n.merge_file( + type: 'desktop', + input: desktop_in, + output: desktop, + po_dir: po_dir, + install: true, + install_dir: control_center_desktopdir +) + +sources = files( + 'cc-search-panel.c', + 'cc-search-locations-dialog.c', + 'cc-search-panel-row.c' +) + +resource_data = files( + 'cc-search-locations-dialog.ui', + 'cc-search-panel.ui', +) + +sources += gnome.compile_resources( + 'cc-' + cappletname + '-resources', + cappletname + '.gresource.xml', + c_name: 'cc_' + cappletname, + dependencies: resource_data, + export: true +) + +cflags += [ + '-DDATADIR="@0@"'.format(control_center_datadir) +] + +panels_libs += static_library( + cappletname, + sources: sources, + include_directories: [ top_inc, common_inc ], + dependencies: common_deps, + c_args: cflags +) + +subdir('icons') diff --git a/panels/search/search.gresource.xml b/panels/search/search.gresource.xml new file mode 100644 index 0000000..60c403e --- /dev/null +++ b/panels/search/search.gresource.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/control-center/search"> + <file preprocess="xml-stripblanks">cc-search-locations-dialog.ui</file> + <file preprocess="xml-stripblanks">cc-search-panel.ui</file> + <file preprocess="xml-stripblanks">cc-search-panel-row.ui</file> + </gresource> +</gresources> |