summaryrefslogtreecommitdiffstats
path: root/panels/search
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--panels/search/cc-search-locations-dialog.c726
-rw-r--r--panels/search/cc-search-locations-dialog.h30
-rw-r--r--panels/search/cc-search-locations-dialog.ui164
-rw-r--r--panels/search/cc-search-panel-row.c229
-rw-r--r--panels/search/cc-search-panel-row.h38
-rw-r--r--panels/search/cc-search-panel-row.ui112
-rw-r--r--panels/search/cc-search-panel.c699
-rw-r--r--panels/search/cc-search-panel.h30
-rw-r--r--panels/search/cc-search-panel.ui62
-rw-r--r--panels/search/gnome-search-panel.desktop.in.in18
-rw-r--r--panels/search/meson.build49
-rw-r--r--panels/search/search.gresource.xml8
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>