summaryrefslogtreecommitdiffstats
path: root/panels/search/cc-search-panel.c
diff options
context:
space:
mode:
Diffstat (limited to 'panels/search/cc-search-panel.c')
-rw-r--r--panels/search/cc-search-panel.c683
1 files changed, 683 insertions, 0 deletions
diff --git a/panels/search/cc-search-panel.c b/panels/search/cc-search-panel.c
new file mode 100644
index 0000000..80c8c94
--- /dev/null
+++ b/panels/search/cc-search-panel.c
@@ -0,0 +1,683 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2012 Red Hat, Inc
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Cosimo Cecchi <cosimoc@gnome.org>
+ */
+
+#include "cc-search-panel.h"
+#include "cc-search-panel-row.h"
+#include "cc-search-locations-dialog.h"
+#include "cc-search-resources.h"
+
+#include <gio/gdesktopappinfo.h>
+#include <glib/gi18n.h>
+
+struct _CcSearchPanel
+{
+ CcPanel parent_instance;
+
+ GtkWidget *list_box;
+ GtkSwitch *main_switch;
+ GtkWidget *search_group;
+ GtkWidget *settings_row;
+ CcSearchPanelRow *selected_row;
+
+ GSettings *search_settings;
+ GHashTable *sort_order;
+
+ CcSearchLocationsDialog *locations_dialog;
+};
+
+CC_PANEL_REGISTER (CcSearchPanel, cc_search_panel)
+
+#define SHELL_PROVIDER_GROUP "Shell Search Provider"
+
+static gboolean
+keynav_failed_cb (CcSearchPanel *self, GtkDirectionType direction, GtkWidget *list)
+{
+ GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
+
+ if (!toplevel)
+ return FALSE;
+
+ if (direction != GTK_DIR_UP && direction != GTK_DIR_DOWN)
+ return FALSE;
+
+ return gtk_widget_child_focus (toplevel, direction == GTK_DIR_UP ?
+ GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD);
+}
+
+static gint
+list_sort_func (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ CcSearchPanel *self = user_data;
+ GAppInfo *app_a, *app_b;
+ const gchar *id_a, *id_b;
+ gint idx_a, idx_b;
+ gpointer lookup;
+
+ app_a = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW ((gpointer*)a));
+ app_b = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW ((gpointer*)b));
+
+ id_a = g_app_info_get_id (app_a);
+ id_b = g_app_info_get_id (app_b);
+
+ /* find the index of the application in the GSettings preferences */
+ idx_a = -1;
+ idx_b = -1;
+
+ lookup = g_hash_table_lookup (self->sort_order, id_a);
+ if (lookup)
+ idx_a = GPOINTER_TO_INT (lookup) - 1;
+
+ lookup = g_hash_table_lookup (self->sort_order, id_b);
+ if (lookup)
+ idx_b = GPOINTER_TO_INT (lookup) - 1;
+
+ /* if neither app is found, use alphabetical order */
+ if ((idx_a == -1) && (idx_b == -1))
+ return g_utf8_collate (g_app_info_get_name (app_a), g_app_info_get_name (app_b));
+
+ /* if app_a isn't found, it's sorted after app_b */
+ if (idx_a == -1)
+ return 1;
+
+ /* if app_b isn't found, it's sorted after app_a */
+ if (idx_b == -1)
+ return -1;
+
+ /* finally, if both apps are found, return their order in the list */
+ return (idx_a - idx_b);
+}
+
+static void
+search_panel_invalidate_sort_order (CcSearchPanel *self)
+{
+ g_auto(GStrv) sort_order = NULL;
+ gint idx;
+
+ g_hash_table_remove_all (self->sort_order);
+ sort_order = g_settings_get_strv (self->search_settings, "sort-order");
+
+ for (idx = 0; sort_order[idx] != NULL; idx++)
+ g_hash_table_insert (self->sort_order, g_strdup (sort_order[idx]), GINT_TO_POINTER (idx + 1));
+
+ gtk_list_box_invalidate_sort (GTK_LIST_BOX (self->list_box));
+}
+
+static gint
+propagate_compare_func (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ CcSearchPanel *self = user_data;
+ const gchar *key_a = a, *key_b = b;
+ gint idx_a, idx_b;
+
+ idx_a = GPOINTER_TO_INT (g_hash_table_lookup (self->sort_order, key_a));
+ idx_b = GPOINTER_TO_INT (g_hash_table_lookup (self->sort_order, key_b));
+
+ return (idx_a - idx_b);
+}
+
+static void
+search_panel_propagate_sort_order (CcSearchPanel *self)
+{
+ g_autoptr(GList) keys = NULL;
+ GList *l;
+ g_autoptr(GPtrArray) sort_order = NULL;
+
+ sort_order = g_ptr_array_new ();
+ keys = g_hash_table_get_keys (self->sort_order);
+ keys = g_list_sort_with_data (keys, propagate_compare_func, self);
+
+ for (l = keys; l != NULL; l = l->next)
+ g_ptr_array_add (sort_order, l->data);
+
+ g_ptr_array_add (sort_order, NULL);
+ g_settings_set_strv (self->search_settings, "sort-order",
+ (const gchar **) sort_order->pdata);
+}
+
+static void
+search_panel_set_no_providers (CcSearchPanel *self)
+{
+ /* center the list box in the scrolled window */
+ gtk_widget_set_valign (self->list_box, GTK_ALIGN_CENTER);
+
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box),
+ gtk_label_new (_("No applications found")));
+}
+
+static void
+search_panel_move_selected (CcSearchPanel *self,
+ gboolean down)
+{
+ GtkListBoxRow *other_row;
+ GAppInfo *app_info, *other_app_info;
+ const gchar *app_id, *other_app_id;
+ const gchar *last_good_app, *target_app;
+ GtkWidget *aux;
+ gint idx, other_idx;
+ gpointer idx_ptr;
+ gboolean found;
+
+ app_info = cc_search_panel_row_get_app_info (self->selected_row);
+ app_id = g_app_info_get_id (app_info);
+
+ /* The assertions are valid only as long as we don't move the first
+ or the last item. */
+
+ aux = GTK_WIDGET (self->selected_row);
+ other_row = down ? GTK_LIST_BOX_ROW (gtk_widget_get_next_sibling (aux)) :
+ GTK_LIST_BOX_ROW (gtk_widget_get_prev_sibling (aux));
+
+ other_app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (other_row));
+ other_app_id = g_app_info_get_id (other_app_info);
+
+ g_assert (other_app_id != NULL);
+
+ /* Check if we're moving one of the unsorted providers at the end of
+ the list; in that case, the value we obtain from the sort order table
+ is garbage.
+ We need to find the last app with a valid sort order, and
+ then set the sort order on all intermediate apps until we find the
+ one we want to move, if moving up, or the neighbor, if moving down.
+ */
+ last_good_app = target_app = app_id;
+ found = g_hash_table_lookup_extended (self->sort_order, last_good_app, NULL, &idx_ptr);
+ while (!found)
+ {
+ GAppInfo *tmp;
+ const char *tmp_id;
+
+ aux = gtk_widget_get_prev_sibling (aux);
+ if (aux == NULL)
+ {
+ last_good_app = NULL;
+ break;
+ }
+
+ tmp = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (aux));
+ tmp_id = g_app_info_get_id (tmp);
+
+ last_good_app = tmp_id;
+ found = g_hash_table_lookup_extended (self->sort_order, tmp_id, NULL, &idx_ptr);
+ }
+
+ /* For simplicity's sake, set all sort orders to the previously visible state
+ first, and only then do the modification requested.
+
+ The loop actually sets the sort order on last_good_app even if we found a
+ valid one already, but I preferred to keep the logic simple, at the expense
+ of a small performance penalty.
+ */
+ if (found)
+ {
+ idx = GPOINTER_TO_INT (idx_ptr);
+ }
+ else
+ {
+ /* If not found, there is no configured app that has a sort order, so we start
+ from the first position and walk the entire list.
+ Sort orders are 1 based, so that 0 (NULL) is not a valid value.
+ */
+ idx = 1;
+ aux = gtk_widget_get_first_child (GTK_WIDGET (self->list_box));
+ }
+
+ while (last_good_app != target_app)
+ {
+ GAppInfo *tmp;
+ const char *tmp_id;
+
+ tmp = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (aux));
+ tmp_id = g_app_info_get_id (tmp);
+
+ g_hash_table_replace (self->sort_order, g_strdup (tmp_id), GINT_TO_POINTER (idx));
+
+ aux = gtk_widget_get_next_sibling (aux);
+ idx++;
+ last_good_app = tmp_id;
+ }
+
+ other_idx = GPOINTER_TO_INT (g_hash_table_lookup (self->sort_order, app_id));
+ idx = down ? (other_idx + 1) : (other_idx - 1);
+
+ g_hash_table_replace (self->sort_order, g_strdup (other_app_id), GINT_TO_POINTER (other_idx));
+ g_hash_table_replace (self->sort_order, g_strdup (app_id), GINT_TO_POINTER (idx));
+
+ search_panel_propagate_sort_order (self);
+}
+
+static void
+row_moved_cb (CcSearchPanel *self,
+ CcSearchPanelRow *dest_row,
+ CcSearchPanelRow *row)
+{
+ gint source_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (row));
+ gint dest_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (dest_row));
+ gboolean down;
+
+ self->selected_row = row;
+
+ down = (source_idx - dest_idx) < 0;
+ for (int i = 0; i < ABS (source_idx - dest_idx); i++)
+ search_panel_move_selected (self, down);
+}
+
+static void
+settings_row_activated (GtkWidget *widget,
+ gpointer user_data)
+{
+ CcSearchPanel *self = user_data;
+
+ if (self->locations_dialog == NULL)
+ {
+ self->locations_dialog = cc_search_locations_dialog_new (self);
+ g_object_add_weak_pointer (G_OBJECT (self->locations_dialog),
+ (gpointer *) &self->locations_dialog);
+ }
+
+ gtk_window_present (GTK_WINDOW (self->locations_dialog));
+}
+
+static GVariant *
+switch_settings_mapping_set_generic (const GValue *value,
+ const GVariantType *expected_type,
+ GtkWidget *row,
+ gboolean default_enabled)
+{
+ CcSearchPanel *self = g_object_get_data (G_OBJECT (row), "self");
+ GAppInfo *app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (row));
+ g_auto(GStrv) apps = NULL;
+ g_autoptr(GPtrArray) new_apps = NULL;
+ gint idx;
+ gboolean remove, found;
+
+ remove = !!g_value_get_boolean (value) == !!default_enabled;
+ found = FALSE;
+ new_apps = g_ptr_array_new_with_free_func (g_free);
+ apps = g_settings_get_strv (self->search_settings,
+ default_enabled ? "disabled" : "enabled");
+
+ for (idx = 0; apps[idx] != NULL; idx++)
+ {
+ if (g_strcmp0 (apps[idx], g_app_info_get_id (app_info)) == 0)
+ {
+ found = TRUE;
+
+ if (remove)
+ continue;
+ }
+
+ g_ptr_array_add (new_apps, g_strdup (apps[idx]));
+ }
+
+ if (!found && !remove)
+ g_ptr_array_add (new_apps, g_strdup (g_app_info_get_id (app_info)));
+
+ g_ptr_array_add (new_apps, NULL);
+
+ return g_variant_new_strv ((const gchar **) new_apps->pdata, -1);
+}
+
+static GVariant *
+switch_settings_mapping_set_default_enabled (const GValue *value,
+ const GVariantType *expected_type,
+ gpointer user_data)
+{
+ return switch_settings_mapping_set_generic (value, expected_type,
+ user_data, TRUE);
+}
+
+static GVariant *
+switch_settings_mapping_set_default_disabled (const GValue *value,
+ const GVariantType *expected_type,
+ gpointer user_data)
+{
+ return switch_settings_mapping_set_generic (value, expected_type,
+ user_data, FALSE);
+}
+
+static gboolean
+switch_settings_mapping_get_generic (GValue *value,
+ GVariant *variant,
+ GtkWidget *row,
+ gboolean default_enabled)
+{
+ GAppInfo *app_info = cc_search_panel_row_get_app_info (CC_SEARCH_PANEL_ROW (row));
+ g_autofree const gchar **apps = NULL;
+ gint idx;
+ gboolean found;
+
+ found = FALSE;
+ apps = g_variant_get_strv (variant, NULL);
+
+ for (idx = 0; apps[idx] != NULL; idx++)
+ {
+ if (g_strcmp0 (apps[idx], g_app_info_get_id (app_info)) == 0)
+ {
+ found = TRUE;
+ break;
+ }
+ }
+
+ g_value_set_boolean (value, !!default_enabled != !!found);
+
+ return TRUE;
+}
+
+static gboolean
+switch_settings_mapping_get_default_enabled (GValue *value,
+ GVariant *variant,
+ gpointer user_data)
+{
+ return switch_settings_mapping_get_generic (value, variant,
+ user_data, TRUE);
+}
+
+static gboolean
+switch_settings_mapping_get_default_disabled (GValue *value,
+ GVariant *variant,
+ gpointer user_data)
+{
+ return switch_settings_mapping_get_generic (value, variant,
+ user_data, FALSE);
+}
+
+static void
+search_panel_add_one_app_info (CcSearchPanel *self,
+ GAppInfo *app_info,
+ gboolean default_enabled)
+{
+ CcSearchPanelRow *row;
+
+ /* gnome-control-center is special cased in the shell,
+ and is not configurable */
+ if (g_strcmp0 (g_app_info_get_id (app_info),
+ "gnome-control-center.desktop") == 0)
+ return;
+
+ /* reset valignment of the list box */
+ gtk_widget_set_valign (self->list_box, GTK_ALIGN_FILL);
+
+ row = cc_search_panel_row_new (app_info);
+ g_signal_connect_object (row, "move-row",
+ G_CALLBACK (row_moved_cb), self,
+ G_CONNECT_SWAPPED);
+ g_object_set_data (G_OBJECT (row), "self", self);
+ gtk_list_box_append (GTK_LIST_BOX (self->list_box), GTK_WIDGET (row));
+
+ if (default_enabled)
+ {
+ g_settings_bind_with_mapping (self->search_settings, "disabled",
+ cc_search_panel_row_get_switch (row), "active",
+ G_SETTINGS_BIND_DEFAULT,
+ switch_settings_mapping_get_default_enabled,
+ switch_settings_mapping_set_default_enabled,
+ row, NULL);
+ }
+ else
+ {
+ g_settings_bind_with_mapping (self->search_settings, "enabled",
+ cc_search_panel_row_get_switch (row), "active",
+ G_SETTINGS_BIND_DEFAULT,
+ switch_settings_mapping_get_default_disabled,
+ switch_settings_mapping_set_default_disabled,
+ row, NULL);
+ }
+}
+
+static void
+search_panel_add_one_provider (CcSearchPanel *self,
+ GFile *provider)
+{
+ g_autofree gchar *path = NULL;
+ g_autofree gchar *desktop_id = NULL;
+ g_autoptr(GKeyFile) keyfile = NULL;
+ g_autoptr(GAppInfo) app_info = NULL;
+ g_autoptr(GError) error = NULL;
+ gboolean default_disabled;
+
+ path = g_file_get_path (provider);
+ keyfile = g_key_file_new ();
+ g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, &error);
+
+ if (error != NULL)
+ {
+ g_warning ("Error loading %s: %s - search provider will be ignored",
+ path, error->message);
+ return;
+ }
+
+ if (!g_key_file_has_group (keyfile, SHELL_PROVIDER_GROUP))
+ {
+ g_debug ("Shell search provider group missing from '%s', ignoring", path);
+ return;
+ }
+
+ desktop_id = g_key_file_get_string (keyfile, SHELL_PROVIDER_GROUP,
+ "DesktopId", &error);
+
+ if (error != NULL)
+ {
+ g_warning ("Unable to read desktop ID from %s: %s - search provider will be ignored",
+ path, error->message);
+ return;
+ }
+
+ app_info = G_APP_INFO (g_desktop_app_info_new (desktop_id));
+
+ if (app_info == NULL)
+ {
+ g_debug ("Could not find application with desktop ID '%s' referenced in '%s', ignoring",
+ desktop_id, path);
+ return;
+ }
+
+ default_disabled = g_key_file_get_boolean (keyfile, SHELL_PROVIDER_GROUP,
+ "DefaultDisabled", NULL);
+ search_panel_add_one_app_info (self, app_info, !default_disabled);
+}
+
+static void
+search_providers_discover_ready (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GList) providers = NULL;
+ GList *l;
+ CcSearchPanel *self = CC_SEARCH_PANEL (source);
+ g_autoptr(GError) error = NULL;
+
+ providers = g_task_propagate_pointer (G_TASK (result), &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ if (providers == NULL)
+ {
+ search_panel_set_no_providers (self);
+ return;
+ }
+
+ for (l = providers; l != NULL; l = l->next)
+ {
+ g_autoptr(GFile) provider = l->data;
+ search_panel_add_one_provider (self, provider);
+ }
+
+ /* propagate a write to GSettings, to make sure we always have
+ * all the providers in the list.
+ */
+ search_panel_propagate_sort_order (self);
+}
+
+static GList *
+search_providers_discover_one_directory (const gchar *system_dir,
+ GCancellable *cancellable)
+{
+ GList *providers = NULL;
+ g_autofree gchar *providers_path = NULL;
+ g_autoptr(GFile) providers_location = NULL;
+ g_autoptr(GFileEnumerator) enumerator = NULL;
+ g_autoptr(GError) error = NULL;
+
+ providers_path = g_build_filename (system_dir, "gnome-shell", "search-providers", NULL);
+ providers_location = g_file_new_for_path (providers_path);
+
+ enumerator = g_file_enumerate_children (providers_location,
+ "standard::type,standard::name,standard::content-type",
+ G_FILE_QUERY_INFO_NONE,
+ cancellable, &error);
+
+ if (error != NULL)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Error opening %s: %s - search provider configuration won't be possible",
+ providers_path, error->message);
+
+ return NULL;
+ }
+
+ while (TRUE)
+ {
+ g_autoptr(GFileInfo) info = NULL;
+ GFile *provider;
+
+ info = g_file_enumerator_next_file (enumerator, cancellable, &error);
+ if (info == NULL)
+ {
+ if (error != NULL && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Error reading from %s: %s - search providers might be missing from the panel",
+ providers_path, error->message);
+ return providers;
+ }
+ provider = g_file_get_child (providers_location, g_file_info_get_name (info));
+ providers = g_list_prepend (providers, provider);
+ }
+}
+
+static void
+search_providers_discover_thread (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GList *providers = NULL;
+ const gchar * const *system_data_dirs;
+ int idx;
+
+ system_data_dirs = g_get_system_data_dirs ();
+ for (idx = 0; system_data_dirs[idx] != NULL; idx++)
+ {
+ providers = g_list_concat (search_providers_discover_one_directory (system_data_dirs[idx], cancellable),
+ providers);
+
+ if (g_task_return_error_if_cancelled (task))
+ {
+ g_list_free_full (providers, g_object_unref);
+ return;
+ }
+ }
+
+ g_task_return_pointer (task, providers, NULL);
+}
+
+static void
+populate_search_providers (CcSearchPanel *self)
+{
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cc_panel_get_cancellable (CC_PANEL (self)),
+ search_providers_discover_ready, self);
+ g_task_run_in_thread (task, search_providers_discover_thread);
+}
+
+static void
+cc_search_panel_finalize (GObject *object)
+{
+ CcSearchPanel *self = CC_SEARCH_PANEL (object);
+
+ g_clear_object (&self->search_settings);
+ g_hash_table_destroy (self->sort_order);
+
+ if (self->locations_dialog)
+ gtk_window_destroy (GTK_WINDOW (self->locations_dialog));
+
+ G_OBJECT_CLASS (cc_search_panel_parent_class)->finalize (object);
+}
+
+static void
+cc_search_panel_init (CcSearchPanel *self)
+{
+ g_resources_register (cc_search_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box),
+ (GtkListBoxSortFunc)list_sort_func, self, NULL);
+
+ gtk_widget_set_sensitive (self->settings_row, cc_search_locations_dialog_is_available ());
+
+ self->search_settings = g_settings_new ("org.gnome.desktop.search-providers");
+ g_settings_bind (self->search_settings,
+ "disable-external",
+ self->main_switch,
+ "active",
+ G_SETTINGS_BIND_DEFAULT |
+ G_SETTINGS_BIND_INVERT_BOOLEAN);
+
+ g_object_bind_property (self->main_switch,
+ "active",
+ self->search_group,
+ "sensitive",
+ G_BINDING_DEFAULT |
+ G_BINDING_SYNC_CREATE);
+
+ self->sort_order = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+ g_signal_connect_swapped (self->search_settings, "changed::sort-order",
+ G_CALLBACK (search_panel_invalidate_sort_order), self);
+ search_panel_invalidate_sort_order (self);
+
+ populate_search_providers (self);
+}
+
+static void
+cc_search_panel_class_init (CcSearchPanelClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *oclass = G_OBJECT_CLASS (klass);
+
+ oclass->finalize = cc_search_panel_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/control-center/search/cc-search-panel.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, list_box);
+ gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, main_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, search_group);
+ gtk_widget_class_bind_template_child (widget_class, CcSearchPanel, settings_row);
+
+ gtk_widget_class_bind_template_callback (widget_class, settings_row_activated);
+ gtk_widget_class_bind_template_callback (widget_class, keynav_failed_cb);
+}