diff options
Diffstat (limited to '')
-rw-r--r-- | panels/search/cc-search-locations-dialog.c | 726 | ||||
-rw-r--r-- | panels/search/cc-search-locations-dialog.h | 30 | ||||
-rw-r--r-- | panels/search/cc-search-locations-dialog.ui | 164 | ||||
-rw-r--r-- | panels/search/cc-search-panel-row.c | 229 | ||||
-rw-r--r-- | panels/search/cc-search-panel-row.h | 38 | ||||
-rw-r--r-- | panels/search/cc-search-panel-row.ui | 112 | ||||
-rw-r--r-- | panels/search/cc-search-panel.c | 699 | ||||
-rw-r--r-- | panels/search/cc-search-panel.h | 30 | ||||
-rw-r--r-- | panels/search/cc-search-panel.ui | 62 | ||||
-rw-r--r-- | panels/search/gnome-search-panel.desktop.in.in | 18 | ||||
-rw-r--r-- | panels/search/meson.build | 49 | ||||
-rw-r--r-- | panels/search/search.gresource.xml | 8 |
12 files changed, 2165 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..48749da --- /dev/null +++ b/panels/search/cc-search-locations-dialog.c @@ -0,0 +1,726 @@ +/* -*- 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 "list-box-helper.h" + +#include <glib/gi18n.h> + +#define TRACKER_SCHEMA "org.freedesktop.Tracker.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; + +struct _CcSearchLocationsDialog { + GtkDialog parent; + + GSettings *tracker_preferences; + + GtkWidget *places_list; + GtkWidget *bookmarks_list; + GtkWidget *others_list; + GtkWidget *locations_add; +}; + +struct _CcSearchLocationsDialogClass { + GtkDialogClass parent_class; +}; + +G_DEFINE_TYPE (CcSearchLocationsDialog, cc_search_locations_dialog, GTK_TYPE_DIALOG) + +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) +{ + GtkWidget *row, *box, *w; + Place *place; + g_autoptr(GFileInfo) info = NULL; + g_autofree gchar *path = NULL; + + info = g_file_query_info_finish (G_FILE (source), res, NULL); + if (!info) + return; + + row = user_data; + place = g_object_get_data (G_OBJECT (row), "place"); + g_clear_object (&place->cancellable); + + box = gtk_bin_get_child (GTK_BIN (row)); + gtk_widget_show (box); + + w = gtk_label_new (place->display_name); + gtk_widget_show (w); + gtk_container_add (GTK_CONTAINER (box), w); + + w = gtk_switch_new (); + gtk_widget_show (w); + gtk_widget_set_valign (w, GTK_ALIGN_CENTER); + gtk_box_pack_end (GTK_BOX (box), w, FALSE, FALSE, 0); + g_settings_bind_with_mapping (place->dialog->tracker_preferences, place->settings_key, + w, "active", + G_SETTINGS_BIND_DEFAULT, + switch_tracker_get_mapping, + switch_tracker_set_mapping, + place, NULL); +} + +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) +{ + GtkWidget *child, *row, *remove_button; + + row = gtk_list_box_row_new (); + gtk_widget_show (row); + gtk_list_box_row_set_selectable (GTK_LIST_BOX_ROW (row), FALSE); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE); + child = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_show (child); + gtk_container_add (GTK_CONTAINER (row), child); + g_object_set (row, "margin", 5, "margin-left", 16, NULL); + g_object_set_data_full (G_OBJECT (row), "place", place, (GDestroyNotify) place_free); + + if (place->place_type == PLACE_OTHER) + { + remove_button = gtk_button_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU); + gtk_widget_show (remove_button); + g_object_set_data (G_OBJECT (remove_button), "place", place); + gtk_style_context_add_class (gtk_widget_get_style_context (remove_button), "flat"); + gtk_box_pack_end (GTK_BOX (child), remove_button, FALSE, FALSE, 2); + + 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, row); + + return row; +} + +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_container_add (GTK_CONTAINER (self->places_list), row); + break; + case PLACE_BOOKMARKS: + gtk_container_add (GTK_CONTAINER (self->bookmarks_list), row); + break; + case PLACE_OTHER: + gtk_container_add (GTK_CONTAINER (self->others_list), row); + break; + default: + g_assert_not_reached (); + } + } +} + +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_widget_destroy (GTK_WIDGET (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_widget_destroy (GTK_WIDGET (widget)); +} + +static void +add_button_clicked (CcSearchLocationsDialog *self) +{ + GtkWidget *file_chooser; + + file_chooser = gtk_file_chooser_dialog_new (_("Select Location"), + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (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_widget_show (file_chooser); +} + +static void +other_places_refresh (CcSearchLocationsDialog *self) +{ + g_autoptr(GList) places = NULL; + GList *l; + Place *place; + GtkWidget *row; + + gtk_container_foreach (GTK_CONTAINER (self->others_list), (GtkCallback) gtk_widget_destroy, NULL); + + places = get_places_list (self); + for (l = places; l != NULL; l = l->next) + { + place = l->data; + if (place->place_type != PLACE_OTHER) + continue; + + row = create_row_for_place (self, place); + gtk_container_add (GTK_CONTAINER (self->others_list), row); + } +} + +CcSearchLocationsDialog * +cc_search_locations_dialog_new (CcSearchPanel *panel) +{ + CcSearchLocationsDialog *self; + + self = g_object_new (CC_SEARCH_LOCATIONS_DIALOG_TYPE, + "use-header-bar", TRUE, + NULL); + + 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); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->others_list), cc_list_box_update_header_func, NULL, NULL); + + g_signal_connect_swapped (self->tracker_preferences, "changed::" TRACKER_KEY_RECURSIVE_DIRECTORIES, + G_CALLBACK (other_places_refresh), self); + + gtk_window_set_transient_for (GTK_WINDOW (self), + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (panel)))); + + 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, TRACKER_SCHEMA, TRUE); + return schema != NULL; +} + +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_list); + 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); +} diff --git a/panels/search/cc-search-locations-dialog.h b/panels/search/cc-search-locations-dialog.h new file mode 100644 index 0000000..912f2f0 --- /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, GtkDialog) + +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..147829e --- /dev/null +++ b/panels/search/cc-search-locations-dialog.ui @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.0 --> + <template class="CcSearchLocationsDialog" parent="GtkDialog"> + <property name="can_focus">False</property> + <property name="default_height">400</property> + <property name="default_width">360</property> + <property name="modal">True</property> + <property name="title" translatable="yes">Search Locations</property> + <property name="type_hint">dialog</property> + <property name="use_header_bar">1</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="border-width">0</property> + <child> + <object class="GtkNotebook"> + <property name="show-border">False</property> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="vexpand">True</property> + <property name="border-width">35</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="margin-bottom">35</property> + <property name="label" translatable="yes">Folders which are searched by system applications, such as Files, Photos and Videos.</property> + </object> + </child> + <child> + <object class="GtkListBox" id="places_list"> + <property name="visible">True</property> + <property name="expand">True</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + <packing> + <property name="tab-expand">True</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="label" translatable="yes">Places</property> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="vexpand">True</property> + <property name="border-width">35</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="margin-bottom">35</property> + <property name="label" translatable="yes">Folders which are searched by system applications, such as Files, Photos and Videos.</property> + </object> + </child> + <child> + <object class="GtkListBox" id="bookmarks_list"> + <property name="visible">True</property> + <property name="expand">True</property> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="tab-expand">True</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="label" translatable="yes">Bookmarks</property> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="vexpand">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="wrap">True</property> + <property name="margin">35</property> + <property name="label" translatable="yes">Folders which are searched by system applications, such as Files, Photos and Videos.</property> + </object> + </child> + <child> + <object class="GtkListBox" id="others_list"> + <property name="visible">True</property> + <child> + <placeholder/> + </child> + </object> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + </object> + </child> + <child> + <object class="GtkButton" id="locations_add"> + <property name="visible">True</property> + <property name="halign">center</property> + <property name="margin">5</property> + <signal name="clicked" handler="add_button_clicked" object="CcSearchLocationsDialog" swapped="yes"/> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">list-add-symbolic</property> + <property name="icon-size">1</property> + </object> + </child> + <style> + <class name="flat"/> + </style> + </object> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="tab-expand">True</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="label" translatable="yes">Other</property> + </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..e8ae5fb --- /dev/null +++ b/panels/search/cc-search-panel-row.c @@ -0,0 +1,229 @@ +/* + * 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 +{ + GtkListBoxRow parent_instance; + + GAppInfo *app_info; + + GtkEventBox *drag_handle; + GtkImage *icon; + GtkLabel *app_name; + GtkSwitch *switcher; + + GtkListBox *drag_widget; +}; + +G_DEFINE_TYPE (CcSearchPanelRow, cc_search_panel_row, GTK_TYPE_LIST_BOX_ROW) + +enum +{ + SIGNAL_MOVE_ROW, + SIGNAL_LAST +}; + +static guint signals[SIGNAL_LAST] = { 0, }; + +static void +move_up_button_clicked (GtkButton *button, + CcSearchPanelRow *self) +{ + 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_button_clicked (GtkButton *button, + CcSearchPanelRow *self) +{ + 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 void +drag_begin_cb (CcSearchPanelRow *self, + GdkDragContext *drag_context) +{ + CcSearchPanelRow *drag_row; + GtkAllocation alloc; + gint x = 0, y = 0; + + gtk_widget_get_allocation (GTK_WIDGET (self), &alloc); + + gdk_window_get_device_position (gtk_widget_get_window (GTK_WIDGET (self)), + gdk_drag_context_get_device (drag_context), + &x, &y, NULL); + + self->drag_widget = GTK_LIST_BOX (gtk_list_box_new ()); + gtk_widget_show (GTK_WIDGET (self->drag_widget)); + gtk_widget_set_size_request (GTK_WIDGET (self->drag_widget), alloc.width, alloc.height); + + drag_row = cc_search_panel_row_new (self->app_info); + gtk_switch_set_active (drag_row->switcher, gtk_switch_get_active (self->switcher)); + gtk_widget_show (GTK_WIDGET (drag_row)); + gtk_container_add (GTK_CONTAINER (self->drag_widget), GTK_WIDGET (drag_row)); + gtk_list_box_drag_highlight_row (self->drag_widget, GTK_LIST_BOX_ROW (drag_row)); + + gtk_drag_set_icon_widget (drag_context, GTK_WIDGET (self->drag_widget), x - alloc.x, y - alloc.y); +} + +static void +drag_end_cb (CcSearchPanelRow *self) +{ + g_clear_pointer ((GtkWidget **) &self->drag_widget, gtk_widget_destroy); +} + +static void +drag_data_get_cb (CcSearchPanelRow *self, + GdkDragContext *context, + GtkSelectionData *selection_data, + guint info, + guint time_) +{ + gtk_selection_data_set (selection_data, + gdk_atom_intern_static_string ("GTK_LIST_BOX_ROW"), + 32, + (const guchar *)&self, + sizeof (gpointer)); +} + +static void +drag_data_received_cb (CcSearchPanelRow *self, + GdkDragContext *context, + gint x, + gint y, + GtkSelectionData *selection_data, + guint info, + guint time_) +{ + CcSearchPanelRow *source; + + source = *((CcSearchPanelRow **) gtk_selection_data_get_data (selection_data)); + if (source == self) + return; + + g_signal_emit (source, + signals[SIGNAL_MOVE_ROW], + 0, + self); +} + +static GtkTargetEntry entries[] = +{ + { "GTK_LIST_BOX_ROW", GTK_TARGET_SAME_APP, 0 } +}; + +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, drag_handle); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanelRow, icon); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanelRow, app_name); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanelRow, switcher); + + gtk_widget_class_bind_template_callback (widget_class, drag_begin_cb); + gtk_widget_class_bind_template_callback (widget_class, drag_end_cb); + gtk_widget_class_bind_template_callback (widget_class, drag_data_get_cb); + gtk_widget_class_bind_template_callback (widget_class, drag_data_received_cb); + gtk_widget_class_bind_template_callback (widget_class, move_up_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, move_down_button_clicked); + + 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); +} + +static void +cc_search_panel_row_init (CcSearchPanelRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_drag_source_set (GTK_WIDGET (self->drag_handle), GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE); + gtk_drag_dest_set (GTK_WIDGET (self), GTK_DEST_DEFAULT_ALL, entries, 1, GDK_ACTION_MOVE); +} + +CcSearchPanelRow * +cc_search_panel_row_new (GAppInfo *app_info) +{ + CcSearchPanelRow *self; + g_autoptr(GIcon) gicon = NULL; + gint width, height; + + 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, GTK_ICON_SIZE_DND); + gtk_icon_size_lookup (GTK_ICON_SIZE_DND, &width, &height); + gtk_image_set_pixel_size (self->icon, MAX (width, height)); + + gtk_label_set_text (self->app_name, g_app_info_get_name (app_info)); + + gtk_drag_source_set (GTK_WIDGET (self->drag_handle), GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE); + + 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..97fe9f1 --- /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 <gtk/gtk.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, GtkListBoxRow) + + +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..dc53aad --- /dev/null +++ b/panels/search/cc-search-panel-row.ui @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.0 --> + <template class="CcSearchPanelRow" parent="GtkListBoxRow"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="selectable">False</property> + <signal name="drag_data_received" handler="drag_data_received_cb" object="CcSearchPanelRow" swapped="yes"/> + <child> + <object class="GtkEventBox" id="drag_handle"> + <property name="visible">True</property> + <signal name="drag_data_get" handler="drag_data_get_cb" object="CcSearchPanelRow" swapped="yes"/> + <signal name="drag-begin" handler="drag_begin_cb" object="CcSearchPanelRow" swapped="yes"/> + <signal name="drag_end" handler="drag_end_cb" object="CcSearchPanelRow" swapped="yes"/> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="spacing">10</property> + <property name="border-width">10</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon-name">list-drag-handle-symbolic</property> + <style> + <class name="drag-handle"/> + </style> + </object> + </child> + <child> + <object class="GtkImage" id="icon"> + <property name="visible">True</property> + <style> + <class name="lowres-icon"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="app_name"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="ellipsize">end</property> + <property name="xalign">0.0</property> + </object> + </child> + <child> + <object class="GtkSwitch" id="switcher"> + <property name="visible">True</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + </object> + </child> + <child> + <object class="GtkMenuButton"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="popover">move_row_menu</property> + <style> + <class name="flat"/> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="icon-name">view-more-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + + <!-- Move Row Menu --> + <object class="GtkPopoverMenu" id="move_row_menu"> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin">12</property> + <property name="spacing">6</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkModelButton"> + <property name="visible">True</property> + <property name="label" translatable="yes">Move Up</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="xalign">0.0</property> + <signal name="clicked" handler="move_up_button_clicked"/> + </object> + </child> + <child> + <object class="GtkModelButton"> + <property name="visible">True</property> + <property name="label" translatable="yes">Move Down</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="xalign">0.0</property> + <signal name="clicked" handler="move_down_button_clicked"/> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/panels/search/cc-search-panel.c b/panels/search/cc-search-panel.c new file mode 100644 index 0000000..b0d4d42 --- /dev/null +++ b/panels/search/cc-search-panel.c @@ -0,0 +1,699 @@ +/* -*- 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 "list-box-helper.h" + +#include <gio/gdesktopappinfo.h> +#include <glib/gi18n.h> + +struct _CcSearchPanel +{ + CcPanel parent_instance; + + GtkWidget *list_box; + GtkWidget *search_vbox; + GtkWidget *search_frame; + GtkWidget *settings_button; + 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 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) +{ + GtkWidget *w; + + /* center the list box in the scrolled window */ + gtk_widget_set_valign (self->list_box, GTK_ALIGN_CENTER); + + w = gtk_label_new (_("No applications found")); + gtk_widget_show (w); + + gtk_container_add (GTK_CONTAINER (self->list_box), w); +} + +static void +search_panel_move_selected (CcSearchPanel *self, + gboolean down) +{ + GtkListBoxRow *row, *other_row; + GAppInfo *app_info, *other_app_info; + const gchar *app_id, *other_app_id; + const gchar *last_good_app, *target_app; + gint idx, other_idx; + gpointer idx_ptr; + gboolean found; + g_autoptr(GList) children = NULL; + GList *l, *other; + + row = GTK_LIST_BOX_ROW (self->selected_row); + app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (row)); + app_id = g_app_info_get_id (app_info); + + children = gtk_container_get_children (GTK_CONTAINER (self->list_box)); + + /* The assertions are valid only as long as we don't move the first + or the last item. */ + + l = g_list_find (children, row); + g_assert (l != NULL); + + other = down ? g_list_next(l) : g_list_previous(l); + g_assert (other != NULL); + + other_row = other->data; + 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; + + l = g_list_previous (l); + if (l == NULL) + { + last_good_app = NULL; + break; + } + + tmp = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (l->data)); + 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; + l = children; + } + + while (last_good_app != target_app) + { + GAppInfo *tmp; + const char *tmp_id; + + tmp = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (l->data)); + tmp_id = g_app_info_get_id (tmp); + + g_hash_table_replace (self->sort_order, g_strdup (tmp_id), GINT_TO_POINTER (idx)); + + l = g_list_next (l); + 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_button_clicked (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; + g_autoptr(GIcon) icon = NULL; + + /* 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_container_add (GTK_CONTAINER (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_widget_destroy (GTK_WIDGET (self->locations_dialog)); + + G_OBJECT_CLASS (cc_search_panel_parent_class)->finalize (object); +} + +static void +cc_search_panel_constructed (GObject *object) +{ + CcSearchPanel *self = CC_SEARCH_PANEL (object); + GtkWidget *box, *widget; + + G_OBJECT_CLASS (cc_search_panel_parent_class)->constructed (object); + + /* add the disable all switch */ + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_show (box); + + widget = gtk_switch_new (); + gtk_widget_show (widget); + gtk_widget_set_valign (widget, GTK_ALIGN_CENTER); + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 4); + + g_settings_bind (self->search_settings, "disable-external", + widget, "active", + G_SETTINGS_BIND_DEFAULT | + G_SETTINGS_BIND_INVERT_BOOLEAN); + + g_object_bind_property (widget, "active", + self->search_vbox, "sensitive", + G_BINDING_DEFAULT | + G_BINDING_SYNC_CREATE); + + cc_shell_embed_widget_in_header (cc_panel_get_shell (CC_PANEL (self)), self->settings_button, GTK_POS_LEFT); + cc_shell_embed_widget_in_header (cc_panel_get_shell (CC_PANEL (self)), box, GTK_POS_RIGHT); +} + +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_list_box_set_header_func (GTK_LIST_BOX (self->list_box), cc_list_box_update_header_func, NULL, NULL); + + gtk_widget_set_sensitive (self->settings_button, cc_search_locations_dialog_is_available ()); + + self->search_settings = g_settings_new ("org.gnome.desktop.search-providers"); + 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->constructed = cc_search_panel_constructed; + 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, search_vbox); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, search_frame); + gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, settings_button); + + gtk_widget_class_bind_template_callback (widget_class, settings_button_clicked); +} 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..731cc95 --- /dev/null +++ b/panels/search/cc-search-panel.ui @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.0 --> + <template class="CcSearchPanel" parent="CcPanel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + + <child> + <object class="GtkScrolledWindow" id="search_vbox"> + <property name="visible">True</property> + <property name="hscrollbar_policy">never</property> + <property name="can_focus">False</property> + <child> + <object class="HdyClamp"> + <property name="visible">True</property> + <property name="margin_top">32</property> + <property name="margin_bottom">32</property> + <property name="margin_start">12</property> + <property name="margin_end">12</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="orientation">vertical</property> + <property name="hexpand">True</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="xalign">0.0</property> + <property name="margin-bottom">20</property> + <property name="wrap">True</property> + <property name="label" translatable="yes">Control which search results are shown in the Activities Overview. The order of search results can also be changed by moving rows in the list.</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkFrame" id="search_frame"> + <property name="visible">True</property> + <child> + <object class="GtkListBox" id="list_box"> + <property name="visible">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + + <!-- Header widget --> + <object class="GtkButton" id="settings_button"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="label" translatable="yes">Search Locations</property> + <signal name="clicked" handler="settings_button_clicked" object="CcSearchPanel" swapped="no"/> + </object> +</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..c4ea2d0 --- /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=preferences-system-search +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/meson.build b/panels/search/meson.build new file mode 100644 index 0000000..39b076b --- /dev/null +++ b/panels/search/meson.build @@ -0,0 +1,49 @@ +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( + desktop, + 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 +) 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> |