summaryrefslogtreecommitdiffstats
path: root/shell/cc-panel-list.c
diff options
context:
space:
mode:
Diffstat (limited to 'shell/cc-panel-list.c')
-rw-r--r--shell/cc-panel-list.c1112
1 files changed, 1112 insertions, 0 deletions
diff --git a/shell/cc-panel-list.c b/shell/cc-panel-list.c
new file mode 100644
index 0000000..d425067
--- /dev/null
+++ b/shell/cc-panel-list.c
@@ -0,0 +1,1112 @@
+/* cc-panel-list.c
+ *
+ * Copyright (C) 2016 Endless, 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: Georges Basile Stavracas Neto <gbsneto@gnome.org>
+ */
+
+#define G_LOG_DOMAIN "cc-panel-list"
+
+#include <string.h>
+
+#include "cc-debug.h"
+#include "cc-panel-list.h"
+#include "cc-util.h"
+
+typedef struct
+{
+ GtkWidget *row;
+ GtkWidget *description_label;
+ CcPanelCategory category;
+ gchar *id;
+ gchar *name;
+ gchar *description;
+ gchar **keywords;
+ CcPanelVisibility visibility;
+} RowData;
+
+struct _CcPanelList
+{
+ GtkStack parent;
+
+ GtkWidget *privacy_listbox;
+ GtkWidget *main_listbox;
+ GtkWidget *search_listbox;
+
+ /* When clicking on Details or Devices row, show it
+ * automatically select the first panel of the list.
+ */
+ gboolean autoselect_panel : 1;
+
+ GtkListBoxRow *privacy_row;
+
+ gchar *current_panel_id;
+ gchar *search_query;
+
+ CcPanelListView previous_view;
+ CcPanelListView view;
+ GHashTable *id_to_data;
+ GHashTable *id_to_search_data;
+};
+
+G_DEFINE_TYPE (CcPanelList, cc_panel_list, GTK_TYPE_STACK)
+
+enum
+{
+ PROP_0,
+ PROP_SEARCH_MODE,
+ PROP_SEARCH_QUERY,
+ PROP_VIEW,
+ N_PROPS
+};
+
+enum
+{
+ SHOW_PANEL,
+ LAST_SIGNAL
+};
+
+static GParamSpec *properties [N_PROPS] = { NULL, };
+static gint signals [LAST_SIGNAL] = { 0, };
+
+/*
+ * Auxiliary methods
+ */
+static GtkWidget*
+get_widget_from_view (CcPanelList *self,
+ CcPanelListView view)
+{
+ switch (view)
+ {
+ case CC_PANEL_LIST_MAIN:
+ return self->main_listbox;
+
+ case CC_PANEL_LIST_PRIVACY:
+ return self->privacy_listbox;
+
+ case CC_PANEL_LIST_SEARCH:
+ return self->search_listbox;
+
+ case CC_PANEL_LIST_WIDGET:
+ return gtk_stack_get_child_by_name (GTK_STACK (self), "custom-widget");
+
+ default:
+ return NULL;
+ }
+}
+
+static GtkWidget *
+get_listbox_from_category (CcPanelList *self,
+ CcPanelCategory category)
+{
+
+ switch (category)
+ {
+ case CC_CATEGORY_PRIVACY:
+ return self->privacy_listbox;
+ break;
+
+ default:
+ return self->main_listbox;
+ break;
+ }
+
+ return NULL;
+}
+
+static void
+activate_row_below (CcPanelList *self,
+ RowData *data)
+{
+ GtkListBoxRow *next_row;
+ GtkListBox *listbox;
+ guint row_index;
+
+ row_index = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (data->row));
+ listbox = GTK_LIST_BOX (get_listbox_from_category (self, data->category));
+ next_row = gtk_list_box_get_row_at_index (listbox, row_index + 1);
+
+ /* Try the previous one if the current is invalid */
+ if (!next_row)
+ next_row = gtk_list_box_get_row_at_index (listbox, row_index - 1);
+
+ if (next_row)
+ g_signal_emit_by_name (next_row, "activate");
+}
+
+static CcPanelListView
+get_view_from_listbox (CcPanelList *self,
+ GtkWidget *listbox)
+{
+ if (listbox == self->main_listbox)
+ return CC_PANEL_LIST_MAIN;
+
+ if (listbox == self->privacy_listbox)
+ return CC_PANEL_LIST_PRIVACY;
+
+ return CC_PANEL_LIST_SEARCH;
+}
+
+static void
+switch_to_view (CcPanelList *self,
+ CcPanelListView view)
+{
+ GtkWidget *visible_child;
+ gboolean should_crossfade;
+
+ CC_ENTRY;
+
+ if (self->view == view)
+ CC_RETURN ();
+
+ CC_TRACE_MSG ("Switching to view: %d", view);
+
+ self->previous_view = self->view;
+ self->view = view;
+
+ /*
+ * When changing to or from the search view, the animation should
+ * be crossfade. Otherwise, it's the previous-forward movement.
+ */
+ should_crossfade = view == CC_PANEL_LIST_SEARCH ||
+ self->previous_view == CC_PANEL_LIST_SEARCH;
+
+ gtk_stack_set_transition_type (GTK_STACK (self),
+ should_crossfade ? GTK_STACK_TRANSITION_TYPE_CROSSFADE :
+ GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
+
+ visible_child = get_widget_from_view (self, view);
+
+ gtk_stack_set_visible_child (GTK_STACK (self), visible_child);
+
+ /* For non-search views, make sure the displayed panel matches the
+ * newly selected row
+ */
+ if (self->autoselect_panel &&
+ view != CC_PANEL_LIST_SEARCH &&
+ self->previous_view != CC_PANEL_LIST_WIDGET)
+ {
+ cc_panel_list_activate (self);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VIEW]);
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_MODE]);
+
+ CC_EXIT;
+}
+
+static void
+update_search (CcPanelList *self)
+{
+ /*
+ * Only change to the search view is there's a
+ * search query available.
+ */
+ if (self->search_query &&
+ g_utf8_strlen (self->search_query, -1) > 0)
+ {
+ if (self->view == CC_PANEL_LIST_MAIN)
+ switch_to_view (self, CC_PANEL_LIST_SEARCH);
+ }
+ else
+ {
+ if (self->view == CC_PANEL_LIST_SEARCH)
+ switch_to_view (self, self->previous_view);
+ }
+
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->search_listbox));
+ gtk_list_box_unselect_all (GTK_LIST_BOX (self->search_listbox));
+}
+
+static const gchar*
+get_panel_id_from_row (CcPanelList *self,
+ GtkListBoxRow *row)
+{
+
+ RowData *row_data;
+
+ if (row == self->privacy_row)
+ return "privacy";
+
+ row_data = g_object_get_data (G_OBJECT (row), "data");
+
+ g_assert (row_data != NULL);
+ return row_data->id;
+}
+
+/*
+ * RowData functions
+ */
+static void
+row_data_free (RowData *data)
+{
+ g_strfreev (data->keywords);
+ g_free (data->description);
+ g_free (data->name);
+ g_free (data->id);
+ g_free (data);
+}
+
+static RowData*
+row_data_new (CcPanelCategory category,
+ const gchar *id,
+ const gchar *name,
+ const gchar *description,
+ const GStrv keywords,
+ const gchar *icon,
+ CcPanelVisibility visibility,
+ gboolean has_sidebar)
+{
+ GtkWidget *label, *grid, *image;
+ RowData *data;
+
+ data = g_new0 (RowData, 1);
+ data->category = category;
+ data->row = gtk_list_box_row_new ();
+ data->id = g_strdup (id);
+ data->name = g_strdup (name);
+ data->description = g_strdup (description);
+ data->keywords = g_strdupv (keywords);
+
+ /* Setup the row */
+ grid = g_object_new (GTK_TYPE_GRID,
+ "visible", TRUE,
+ "hexpand", TRUE,
+ "border-width", 12,
+ "column-spacing", 12,
+ NULL);
+
+ /* Icon */
+ image = gtk_image_new_from_icon_name (icon, GTK_ICON_SIZE_BUTTON);
+ gtk_style_context_add_class (gtk_widget_get_style_context (image), "sidebar-icon");
+
+ gtk_grid_attach (GTK_GRID (grid), image, 0, 0, 1, 1);
+
+ gtk_widget_show (image);
+
+ /* Name label */
+ label = g_object_new (GTK_TYPE_LABEL,
+ "label", name,
+ "visible", TRUE,
+ "xalign", 0.0,
+ "hexpand", TRUE,
+ NULL);
+ gtk_grid_attach (GTK_GRID (grid), label, 1, 0, 1, 1);
+
+ /* Description label */
+ label = g_object_new (GTK_TYPE_LABEL,
+ "label", description,
+ "visible", FALSE,
+ "xalign", 0.0,
+ "hexpand", TRUE,
+ NULL);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 25);
+ gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+
+ if (has_sidebar)
+ {
+ image = gtk_image_new_from_icon_name ("go-next-symbolic", GTK_ICON_SIZE_BUTTON);
+ gtk_style_context_add_class (gtk_widget_get_style_context (image), "sidebar-icon");
+ gtk_grid_attach (GTK_GRID (grid), image, 2, 0, 1, 1);
+ gtk_widget_show (image);
+ }
+
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+ gtk_grid_attach (GTK_GRID (grid), label, 1, 1, 1, 1);
+
+ data->description_label = label;
+
+ gtk_container_add (GTK_CONTAINER (data->row), grid);
+ gtk_widget_show (data->row);
+
+ g_object_set_data_full (G_OBJECT (data->row), "data", data, (GDestroyNotify) row_data_free);
+
+ data->visibility = visibility;
+
+ return data;
+}
+
+/*
+ * GtkListBox functions
+ */
+static gboolean
+filter_func (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcPanelList *self;
+ RowData *data;
+ g_autofree gchar *search_text = NULL;
+ g_autofree gchar *panel_text = NULL;
+ g_autofree gchar *panel_description = NULL;
+ gboolean retval = FALSE;
+ gint i;
+
+ self = CC_PANEL_LIST (user_data);
+ data = g_object_get_data (G_OBJECT (row), "data");
+
+ if (!self->search_query)
+ return TRUE;
+
+ panel_text = cc_util_normalize_casefold_and_unaccent (data->name);
+ search_text = cc_util_normalize_casefold_and_unaccent (self->search_query);
+ panel_description = cc_util_normalize_casefold_and_unaccent (data->description);
+
+ g_strstrip (panel_text);
+ g_strstrip (search_text);
+ g_strstrip (panel_description);
+
+ /*
+ * The description label is only visible when the search is
+ * happening.
+ */
+ gtk_widget_set_visible (data->description_label, self->view == CC_PANEL_LIST_SEARCH);
+
+ for (i = 0; !retval && data->keywords[i] != NULL; i++)
+ retval = (strstr (data->keywords[i], search_text) == data->keywords[i]);
+
+ retval = retval || g_strstr_len (panel_text, -1, search_text) != NULL ||
+ g_strstr_len (panel_description, -1, search_text) != NULL;
+
+ return retval;
+}
+
+static const gchar * const panel_order[] = {
+ /* Main page */
+ "wifi",
+ "network",
+ "mobile-broadband",
+ "bluetooth",
+ "background",
+ "notifications",
+ "search",
+ "applications",
+ "privacy",
+ "online-accounts",
+ "sharing",
+
+ /* Privacy page */
+ "location",
+ "camera",
+ "microphone",
+ "thunderbolt",
+ "usage",
+ "lock",
+ "diagnostics",
+
+ /* Devices page */
+ "sound",
+ "power",
+ "display",
+ "mouse",
+ "keyboard",
+ "printers",
+ "removable-media",
+ "wacom",
+ "color",
+
+ /* Details page */
+ "region",
+ "universal-access",
+ "user-accounts",
+ "default-apps",
+ "reset-settings",
+ "datetime",
+ "info-overview",
+};
+
+static guint
+get_panel_id_index (const gchar *panel_id)
+{
+ guint i;
+
+ for (i = 0; i < G_N_ELEMENTS (panel_order); i++)
+ {
+ if (g_str_equal (panel_order[i], panel_id))
+ return i;
+ }
+
+ return 0;
+}
+
+static gint
+sort_function (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ CcPanelList *self = CC_PANEL_LIST (user_data);
+ const gchar *a_id, *b_id;
+
+ a_id = get_panel_id_from_row (self, a);
+ b_id = get_panel_id_from_row (self, b);
+
+ return get_panel_id_index (a_id) - get_panel_id_index (b_id);
+}
+
+static gint
+search_sort_function (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ CcPanelList *self;
+ RowData *a_data, *b_data;
+ g_autofree gchar *a_name = NULL;
+ g_autofree gchar *b_name = NULL;
+ g_autofree gchar *search = NULL;
+ gchar *a_strstr, *b_strstr;
+ gint a_distance, b_distance;
+
+ self = CC_PANEL_LIST (user_data);
+ search = NULL;
+ a_data = g_object_get_data (G_OBJECT (a), "data");
+ b_data = g_object_get_data (G_OBJECT (b), "data");
+
+ a_distance = b_distance = G_MAXINT;
+
+ a_name = cc_util_normalize_casefold_and_unaccent (a_data->name);
+ b_name = cc_util_normalize_casefold_and_unaccent (b_data->name);
+ g_strstrip (a_name);
+ g_strstrip (b_name);
+
+ if (self->search_query)
+ {
+ search = cc_util_normalize_casefold_and_unaccent (self->search_query);
+ g_strstrip (search);
+ }
+
+ /* Default result for empty search */
+ if (!search || g_utf8_strlen (search, -1) == 0)
+ return g_strcmp0 (a_name, b_name);
+
+ a_strstr = g_strstr_len (a_name, -1, search);
+ b_strstr = g_strstr_len (b_name, -1, search);
+
+ if (a_strstr)
+ a_distance = g_strstr_len (a_name, -1, search) - a_name;
+
+ if (b_strstr)
+ b_distance = g_strstr_len (b_name, -1, search) - b_name;
+
+ return a_distance - b_distance;
+}
+
+static void
+header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data)
+{
+ CcPanelList *self = CC_PANEL_LIST (user_data);
+ RowData *row_data, *before_data;
+
+ if (!before)
+ return;
+
+ if (row == self->privacy_row || before == self->privacy_row)
+ return;
+
+ /*
+ * We can only retrieve the data after assuring that none
+ * of the rows are the Privacy row.
+ */
+ row_data = g_object_get_data (G_OBJECT (row), "data");
+ before_data = g_object_get_data (G_OBJECT (before), "data");
+
+ if (row_data->category != before_data->category)
+ {
+ GtkWidget *separator;
+
+ separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_widget_set_hexpand (separator, TRUE);
+ gtk_widget_show (separator);
+
+ gtk_list_box_row_set_header (row, separator);
+ }
+ else
+ {
+ gtk_list_box_row_set_header (row, NULL);
+ }
+}
+
+/*
+ * Callbacks
+ */
+static void
+row_activated_cb (GtkWidget *listbox,
+ GtkListBoxRow *row,
+ CcPanelList *self)
+{
+ RowData *data;
+
+ if (row == self->privacy_row)
+ {
+ switch_to_view (self, CC_PANEL_LIST_PRIVACY);
+ goto out;
+ }
+
+ /*
+ * When a panel is selected, the previous one should be
+ * unselected, except when it's search.
+ */
+ if (listbox != self->search_listbox)
+ {
+ if (listbox != self->main_listbox)
+ gtk_list_box_unselect_all (GTK_LIST_BOX (self->main_listbox));
+
+ if (listbox != self->privacy_listbox)
+ gtk_list_box_unselect_all (GTK_LIST_BOX (self->privacy_listbox));
+ }
+
+ /*
+ * Since we're not sure that the activated row is in the
+ * current view, set the view here.
+ */
+ switch_to_view (self, get_view_from_listbox (self, listbox));
+
+ data = g_object_get_data (G_OBJECT (row), "data");
+
+ /* If the activated row is relative to the current panel, and it has
+ * a custom widget, show the custom widget again.
+ */
+ if (g_strcmp0 (data->id, self->current_panel_id) == 0 &&
+ self->previous_view != CC_PANEL_LIST_SEARCH &&
+ gtk_stack_get_child_by_name (GTK_STACK (self), "custom-widget") != NULL)
+ {
+ CC_TRACE_MSG ("Switching to panel widget");
+
+ switch_to_view (self, CC_PANEL_LIST_WIDGET);
+ }
+
+ g_signal_emit (self, signals[SHOW_PANEL], 0, data->id);
+
+out:
+ /* After selecting the panel and eventually changing the view, reset the
+ * autoselect flag. If necessary, cc_panel_list_set_active_panel() will
+ * set it to FALSE again.
+ */
+ self->autoselect_panel = TRUE;
+}
+
+static void
+search_row_activated_cb (GtkWidget *listbox,
+ GtkListBoxRow *row,
+ CcPanelList *self)
+{
+ GtkWidget *real_listbox;
+ RowData *data;
+ GList *children, *l;
+
+ CC_ENTRY;
+
+ data = g_object_get_data (G_OBJECT (row), "data");
+
+ if (data->category == CC_CATEGORY_PRIVACY)
+ real_listbox = self->privacy_listbox;
+ else
+ real_listbox = self->main_listbox;
+
+ /* Select the correct row */
+ children = gtk_container_get_children (GTK_CONTAINER (real_listbox));
+
+ for (l = children; l != NULL; l = l->next)
+ {
+ RowData *real_row_data;
+
+ real_row_data = g_object_get_data (l->data, "data");
+
+ /*
+ * The main listbox has the Details & Devices rows, and neither
+ * of them contains "data", so we have to ensure we have valid
+ * data before going on.
+ */
+ if (!real_row_data)
+ continue;
+
+ if (g_strcmp0 (real_row_data->id, data->id) == 0)
+ {
+ GtkListBoxRow *real_row;
+
+ real_row = GTK_LIST_BOX_ROW (real_row_data->row);
+
+ gtk_list_box_select_row (GTK_LIST_BOX (real_listbox), real_row);
+ gtk_widget_grab_focus (GTK_WIDGET (real_row));
+
+ g_signal_emit_by_name (real_row, "activate");
+ break;
+ }
+ }
+
+ g_list_free (children);
+
+ CC_EXIT;
+}
+
+static void
+cc_panel_list_finalize (GObject *object)
+{
+ CcPanelList *self = (CcPanelList *)object;
+
+ g_clear_pointer (&self->search_query, g_free);
+ g_clear_pointer (&self->current_panel_id, g_free);
+ g_clear_pointer (&self->id_to_data, g_hash_table_destroy);
+ g_clear_pointer (&self->id_to_search_data, g_hash_table_destroy);
+
+ G_OBJECT_CLASS (cc_panel_list_parent_class)->finalize (object);
+}
+
+static void
+cc_panel_list_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcPanelList *self = CC_PANEL_LIST (object);
+
+ switch (prop_id)
+ {
+ case PROP_SEARCH_MODE:
+ g_value_set_boolean (value, self->view == CC_PANEL_LIST_SEARCH);
+ break;
+
+ case PROP_SEARCH_QUERY:
+ g_value_set_string (value, self->search_query);
+ break;
+
+ case PROP_VIEW:
+ g_value_set_int (value, self->view);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_panel_list_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcPanelList *self = CC_PANEL_LIST (object);
+
+ switch (prop_id)
+ {
+ case PROP_SEARCH_MODE:
+ update_search (self);
+ break;
+
+ case PROP_SEARCH_QUERY:
+ cc_panel_list_set_search_query (self, g_value_get_string (value));
+ break;
+
+ case PROP_VIEW:
+ switch_to_view (self, g_value_get_int (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_panel_list_class_init (CcPanelListClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = cc_panel_list_finalize;
+ object_class->get_property = cc_panel_list_get_property;
+ object_class->set_property = cc_panel_list_set_property;
+
+ /**
+ * CcPanelList:show-panel:
+ *
+ * Emitted when a panel is selected.
+ */
+ signals[SHOW_PANEL] = g_signal_new ("show-panel",
+ CC_TYPE_PANEL_LIST,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ /**
+ * CcPanelList:search-mode:
+ *
+ * Whether the search is visible or not.
+ */
+ properties[PROP_SEARCH_MODE] = g_param_spec_boolean ("search-mode",
+ "Search mode",
+ "Whether it's in search mode or not",
+ FALSE,
+ G_PARAM_READWRITE);
+
+ /**
+ * CcPanelList:search-query:
+ *
+ * The search that is being applied to sidelist.
+ */
+ properties[PROP_SEARCH_QUERY] = g_param_spec_string ("search-query",
+ "Search query",
+ "The current search query",
+ NULL,
+ G_PARAM_READWRITE);
+
+ /**
+ * CcPanelList:view:
+ *
+ * The current view of the sidelist.
+ */
+ properties[PROP_VIEW] = g_param_spec_int ("view",
+ "View",
+ "The current view of the sidelist",
+ CC_PANEL_LIST_MAIN,
+ CC_PANEL_LIST_SEARCH,
+ CC_PANEL_LIST_MAIN,
+ G_PARAM_READWRITE);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/ControlCenter/gtk/cc-panel-list.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcPanelList, privacy_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcPanelList, privacy_row);
+ gtk_widget_class_bind_template_child (widget_class, CcPanelList, main_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcPanelList, search_listbox);
+
+ gtk_widget_class_bind_template_callback (widget_class, row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_row_activated_cb);
+}
+
+static void
+cc_panel_list_init (CcPanelList *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->id_to_data = g_hash_table_new (g_str_hash, g_str_equal);
+ self->id_to_search_data = g_hash_table_new (g_str_hash, g_str_equal);
+ self->view = CC_PANEL_LIST_MAIN;
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->main_listbox),
+ sort_function,
+ self,
+ NULL);
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->privacy_listbox),
+ sort_function,
+ self,
+ NULL);
+
+ gtk_list_box_set_header_func (GTK_LIST_BOX (self->main_listbox),
+ header_func,
+ self,
+ NULL);
+
+ /* Search listbox */
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->search_listbox),
+ search_sort_function,
+ self,
+ NULL);
+
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (self->search_listbox),
+ filter_func,
+ self,
+ NULL);
+}
+
+GtkWidget*
+cc_panel_list_new (void)
+{
+ return g_object_new (CC_TYPE_PANEL_LIST, NULL);
+}
+
+gboolean
+cc_panel_list_activate (CcPanelList *self)
+{
+ GtkListBoxRow *row;
+ GtkWidget *listbox;
+ guint i = 0;
+
+ CC_ENTRY;
+
+ g_return_val_if_fail (CC_IS_PANEL_LIST (self), FALSE);
+
+ listbox = get_widget_from_view (self, self->view);
+ if (!GTK_IS_LIST_BOX (listbox))
+ CC_RETURN (FALSE);
+
+ /* Select the first visible row */
+ do
+ row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (listbox), i++);
+ while (row && !gtk_widget_get_visible (GTK_WIDGET (row)));
+
+ /* If the row is valid, activate it */
+ if (row)
+ {
+ gtk_list_box_select_row (GTK_LIST_BOX (listbox), row);
+ gtk_widget_grab_focus (GTK_WIDGET (row));
+
+ g_signal_emit_by_name (row, "activate");
+ }
+
+ CC_RETURN (row != NULL);
+}
+
+const gchar*
+cc_panel_list_get_search_query (CcPanelList *self)
+{
+ g_return_val_if_fail (CC_IS_PANEL_LIST (self), NULL);
+
+ return self->search_query;
+}
+
+void
+cc_panel_list_set_search_query (CcPanelList *self,
+ const gchar *search)
+{
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ if (g_strcmp0 (self->search_query, search) != 0)
+ {
+ g_clear_pointer (&self->search_query, g_free);
+ self->search_query = g_strdup (search);
+
+ update_search (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_QUERY]);
+
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->search_listbox));
+ gtk_list_box_invalidate_sort (GTK_LIST_BOX (self->search_listbox));
+ }
+}
+
+CcPanelListView
+cc_panel_list_get_view (CcPanelList *self)
+{
+ g_return_val_if_fail (CC_IS_PANEL_LIST (self), -1);
+
+ return self->view;
+}
+
+void
+cc_panel_list_go_previous (CcPanelList *self)
+{
+ CcPanelListView previous_view;
+
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ previous_view = self->previous_view;
+
+ /* The back button is only visible and clickable outside the main view. If
+ * the previous view is the widget view itself, it means we went from:
+ *
+ * Main → Details or Devices → Widget
+ *
+ * to
+ *
+ * Main → Details or Devices
+ *
+ * A similar situation may happen with search.
+ *
+ * To avoid a loop (Details or Devices → Widget → Details or Devices → ...),
+ * make sure to go back to the main view when the current view is details or
+ * devices.
+ */
+ if (previous_view == CC_PANEL_LIST_WIDGET || previous_view == CC_PANEL_LIST_SEARCH)
+ previous_view = CC_PANEL_LIST_MAIN;
+
+ switch_to_view (self, previous_view);
+}
+
+void
+cc_panel_list_add_panel (CcPanelList *self,
+ CcPanelCategory category,
+ const gchar *id,
+ const gchar *title,
+ const gchar *description,
+ const GStrv keywords,
+ const gchar *icon,
+ CcPanelVisibility visibility,
+ gboolean has_sidebar)
+{
+ GtkWidget *listbox;
+ RowData *data, *search_data;
+
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ /* Add the panel to the proper listbox */
+ data = row_data_new (category, id, title, description, keywords, icon, visibility, has_sidebar);
+ gtk_widget_set_visible (data->row, visibility == CC_PANEL_VISIBLE);
+
+ listbox = get_listbox_from_category (self, category);
+ gtk_container_add (GTK_CONTAINER (listbox), data->row);
+
+ /* And add to the search listbox too */
+ search_data = row_data_new (category, id, title, description, keywords, icon, visibility, has_sidebar);
+ gtk_widget_set_visible (search_data->row, visibility != CC_PANEL_HIDDEN);
+
+ gtk_container_add (GTK_CONTAINER (self->search_listbox), search_data->row);
+
+ g_hash_table_insert (self->id_to_data, data->id, data);
+ g_hash_table_insert (self->id_to_search_data, search_data->id, search_data);
+
+ /* Only show the Devices/Details rows when there's at least one panel */
+ if (category == CC_CATEGORY_PRIVACY)
+ gtk_widget_show (GTK_WIDGET (self->privacy_row));
+}
+
+/**
+ * cc_panel_list_set_active_panel:
+ * @self: a #CcPanelList
+ * @id: the id of the panel to be activated
+ *
+ * Sets the current active panel.
+ */
+void
+cc_panel_list_set_active_panel (CcPanelList *self,
+ const gchar *id)
+{
+ GtkWidget *listbox;
+ RowData *data;
+
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ data = g_hash_table_lookup (self->id_to_data, id);
+
+ g_assert (data != NULL);
+
+ /* Stop if row is supposed to be always hidden */
+ if (data->visibility == CC_PANEL_HIDDEN)
+ {
+ g_debug ("Panel '%s' is always hidden, stopping.", id);
+ cc_panel_list_activate (self);
+ return;
+ }
+
+ /* If the currently selected panel is not always visible, for example when
+ * the panel is only visible on search and we're temporarily seeing it, make
+ * sure to hide it after the user moves out.
+ */
+ if (self->current_panel_id != NULL && g_strcmp0 (self->current_panel_id, id) != 0)
+ {
+ RowData *current_row_data;
+
+ current_row_data = g_hash_table_lookup (self->id_to_data, self->current_panel_id);
+
+ /* We cannot be showing a non-existent panel */
+ g_assert (current_row_data != NULL);
+
+ gtk_widget_set_visible (current_row_data->row, current_row_data->visibility == CC_PANEL_VISIBLE);
+ }
+
+ listbox = gtk_widget_get_parent (data->row);
+
+ /* The row might be hidden now, so make sure it's visible */
+ gtk_widget_show (data->row);
+
+ gtk_list_box_select_row (GTK_LIST_BOX (listbox), GTK_LIST_BOX_ROW (data->row));
+ gtk_widget_grab_focus (data->row);
+
+ /* When setting the active panel programatically, prevent from
+ * autoselecting the first panel of the new view.
+ */
+ self->autoselect_panel = FALSE;
+
+ if (self->view != CC_PANEL_LIST_WIDGET)
+ g_signal_emit_by_name (data->row, "activate");
+
+ /* Store the current panel id */
+ g_clear_pointer (&self->current_panel_id, g_free);
+ self->current_panel_id = g_strdup (id);
+}
+
+/**
+ * cc_panel_list_set_panel_visibility:
+ * @self: a #CcPanelList
+ * @id: the id of the panel
+ * @visibility: visibility of panel with @id
+ *
+ * Sets the visibility of panel with @id. @id must be a valid
+ * id with a corresponding panel.
+ */
+void
+cc_panel_list_set_panel_visibility (CcPanelList *self,
+ const gchar *id,
+ CcPanelVisibility visibility)
+{
+ RowData *data, *search_data;
+
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ data = g_hash_table_lookup (self->id_to_data, id);
+ search_data = g_hash_table_lookup (self->id_to_search_data, id);
+
+ g_assert (data != NULL);
+ g_assert (search_data != NULL);
+
+ data->visibility = visibility;
+
+ /* If this is the currently selected row, and the panel can't be displayed
+ * (i.e. visibility != VISIBLE), then select the next possible row */
+ if (gtk_list_box_row_is_selected (GTK_LIST_BOX_ROW (data->row)) &&
+ visibility != CC_PANEL_VISIBLE)
+ {
+ activate_row_below (self, data);
+ }
+
+ gtk_widget_set_visible (data->row, visibility == CC_PANEL_VISIBLE);
+ gtk_widget_set_visible (search_data->row, visibility =! CC_PANEL_HIDDEN);
+}
+
+void
+cc_panel_list_add_sidebar_widget (CcPanelList *self,
+ GtkWidget *widget)
+{
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ if (widget)
+ {
+ gtk_stack_add_named (GTK_STACK (self), widget, "custom-widget");
+ switch_to_view (self, CC_PANEL_LIST_WIDGET);
+ }
+ else
+ {
+ widget = get_widget_from_view (self, CC_PANEL_LIST_WIDGET);
+
+ if (widget)
+ gtk_container_remove (GTK_CONTAINER (self), widget);
+ }
+}
+
+void
+cc_panel_list_set_selection_mode (CcPanelList *self,
+ GtkSelectionMode selection_mode)
+{
+ g_return_if_fail (CC_IS_PANEL_LIST (self));
+
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (self->main_listbox), selection_mode);
+
+ /* When selection mode changed, selection will be lost. So reselect */
+ if (selection_mode == GTK_SELECTION_SINGLE && self->current_panel_id)
+ {
+ GtkWidget *listbox;
+ RowData *data;
+
+ data = g_hash_table_lookup (self->id_to_data, self->current_panel_id);
+ listbox = gtk_widget_get_parent (data->row);
+ gtk_list_box_select_row (GTK_LIST_BOX (listbox), GTK_LIST_BOX_ROW (data->row));
+ }
+}
+