Currently setting's panel list is over-populated. We can fix this by moving some of the settings inside other panels as sub-panels. Created a new row in applications panel for removable media and default apps. On selecting the row we show the respective settings which is same as before. Removed default app and removable media from sidebar Closes: #1092 #1096 Related to: #1090
1079 lines
30 KiB
C
1079 lines
30 KiB
C
/* 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-log.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
|
|
{
|
|
AdwBin parent;
|
|
|
|
GtkWidget *main_listbox;
|
|
GtkWidget *search_listbox;
|
|
GtkStack *stack;
|
|
|
|
/* When clicking on Details or Devices row, show it
|
|
* automatically select the first panel of the list.
|
|
*/
|
|
gboolean autoselect_panel : 1;
|
|
|
|
gchar *current_panel_id;
|
|
gchar *search_query;
|
|
|
|
CcPanelListView previous_view;
|
|
CcPanelListView view;
|
|
GHashTable *id_to_data;
|
|
GHashTable *id_to_search_data;
|
|
|
|
/* When true, the next row being activated will be vertically centered on
|
|
* the visible part of panel list. Currently we do that for panels activated
|
|
* from Search or from the set_active_panel_from_id() CcShell iface */
|
|
gboolean center_activated_row;
|
|
};
|
|
|
|
G_DEFINE_TYPE (CcPanelList, cc_panel_list, ADW_TYPE_BIN)
|
|
|
|
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_SEARCH:
|
|
return self->search_listbox;
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static void
|
|
activate_row_below (CcPanelList *self,
|
|
RowData *data)
|
|
{
|
|
GtkListBoxRow *next_row;
|
|
guint row_index;
|
|
|
|
row_index = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (data->row));
|
|
next_row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->main_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 (GTK_LIST_BOX (self->main_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;
|
|
|
|
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 (self->stack,
|
|
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 (self->stack, 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)
|
|
{
|
|
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)
|
|
{
|
|
/* Don't autoselect first panel when going back from search view */
|
|
self->autoselect_panel = FALSE;
|
|
|
|
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 = 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 = gtk_grid_new ();
|
|
gtk_widget_set_hexpand (grid, TRUE);
|
|
gtk_widget_set_margin_top (grid, 12);
|
|
gtk_widget_set_margin_bottom (grid, 12);
|
|
gtk_widget_set_margin_start (grid, 6);
|
|
gtk_widget_set_margin_end (grid, 6);
|
|
gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
|
|
|
|
/* Icon */
|
|
image = gtk_image_new_from_icon_name (icon);
|
|
|
|
gtk_grid_attach (GTK_GRID (grid), image, 0, 0, 1, 1);
|
|
|
|
/* Name label */
|
|
label = gtk_label_new (name);
|
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
|
|
gtk_widget_set_hexpand (label, TRUE);
|
|
gtk_grid_attach (GTK_GRID (grid), label, 1, 0, 1, 1);
|
|
gtk_accessible_update_relation (GTK_ACCESSIBLE (data->row),
|
|
GTK_ACCESSIBLE_RELATION_LABELLED_BY,
|
|
label,
|
|
NULL,
|
|
-1);
|
|
|
|
/* Description label */
|
|
label = gtk_label_new (description);
|
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
|
|
gtk_widget_set_hexpand (label, TRUE);
|
|
gtk_label_set_max_width_chars (GTK_LABEL (label), 25);
|
|
gtk_label_set_wrap (GTK_LABEL (label), TRUE);
|
|
gtk_widget_set_visible (label, FALSE);
|
|
gtk_accessible_update_relation (GTK_ACCESSIBLE (data->row),
|
|
GTK_ACCESSIBLE_RELATION_DESCRIBED_BY,
|
|
label,
|
|
NULL,
|
|
-1);
|
|
|
|
if (has_sidebar)
|
|
{
|
|
image = gtk_image_new_from_icon_name ("go-next-symbolic");
|
|
gtk_grid_attach (GTK_GRID (grid), image, 2, 0, 1, 1);
|
|
}
|
|
|
|
gtk_widget_add_css_class (label, "dim-label");
|
|
gtk_grid_attach (GTK_GRID (grid), label, 1, 1, 1, 1);
|
|
|
|
data->description_label = label;
|
|
|
|
gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (data->row), grid);
|
|
|
|
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",
|
|
"wwan",
|
|
"mobile-broadband",
|
|
"bluetooth",
|
|
"background",
|
|
"notifications",
|
|
"search",
|
|
"multitasking",
|
|
"applications",
|
|
"privacy",
|
|
"online-accounts",
|
|
"sharing",
|
|
|
|
/* Devices page */
|
|
"sound",
|
|
"power",
|
|
"display",
|
|
"mouse",
|
|
"keyboard",
|
|
"printers",
|
|
"wacom",
|
|
"color",
|
|
|
|
/* Details page */
|
|
"region",
|
|
"universal-access",
|
|
"user-accounts",
|
|
"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)
|
|
{
|
|
RowData *row_data, *before_data;
|
|
|
|
if (!before)
|
|
return;
|
|
|
|
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_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;
|
|
|
|
/*
|
|
* 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");
|
|
|
|
g_signal_emit (self, signals[SHOW_PANEL], 0, data->id);
|
|
|
|
/* 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 *child;
|
|
RowData *data;
|
|
|
|
CC_ENTRY;
|
|
|
|
data = g_object_get_data (G_OBJECT (row), "data");
|
|
|
|
/* Select the correct row */
|
|
for (child = gtk_widget_get_first_child (self->main_listbox);
|
|
child != NULL;
|
|
child = gtk_widget_get_next_sibling (child))
|
|
{
|
|
RowData *real_row_data;
|
|
|
|
real_row_data = g_object_get_data (G_OBJECT (child), "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 (self->main_listbox), real_row);
|
|
gtk_widget_grab_focus (GTK_WIDGET (real_row));
|
|
|
|
/* Don't autoselect first panel because we are already
|
|
* activating a panel from search result */
|
|
self->autoselect_panel = FALSE;
|
|
|
|
/* center Search activated row on panel list */
|
|
self->center_activated_row = TRUE;
|
|
g_signal_emit_by_name (real_row, "activate");
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 gboolean
|
|
search_list_keynav_failed_cb (CcPanelList *self,
|
|
GtkDirectionType direction)
|
|
{
|
|
GtkWidget *toplevel;
|
|
|
|
/* We are in the first result of search list and pressing Arrow Up,
|
|
* so then we move focus back to search text entry */
|
|
if (direction == GTK_DIR_UP)
|
|
{
|
|
toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
|
|
|
|
if (!toplevel)
|
|
return FALSE;
|
|
|
|
return gtk_widget_child_focus (toplevel, GTK_DIR_TAB_BACKWARD);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
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/Settings/gtk/cc-panel-list.ui");
|
|
|
|
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_child (widget_class, CcPanelList, stack);
|
|
|
|
gtk_widget_class_bind_template_callback (widget_class, row_activated_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, search_row_activated_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, search_list_keynav_failed_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_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);
|
|
}
|
|
|
|
void
|
|
cc_panel_list_center_activated_row (CcPanelList *self,
|
|
gboolean val)
|
|
{
|
|
g_return_if_fail (CC_IS_PANEL_LIST (self));
|
|
|
|
if (self->center_activated_row != val)
|
|
self->center_activated_row = val;
|
|
}
|
|
|
|
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)) &&
|
|
gtk_widget_get_child_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;
|
|
}
|
|
|
|
/**
|
|
* cc_panel_list_get_current_panel:
|
|
* @self: a #CcPanelList
|
|
*
|
|
* Returns: (allow-none): id string of current active panel on @self, or %NULL when there's none yet.
|
|
*/
|
|
const gchar*
|
|
cc_panel_list_get_current_panel (CcPanelList *self)
|
|
{
|
|
g_return_val_if_fail (CC_IS_PANEL_LIST (self), NULL);
|
|
|
|
return self->current_panel_id;
|
|
}
|
|
|
|
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)
|
|
{
|
|
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);
|
|
|
|
gtk_list_box_append (GTK_LIST_BOX (self->main_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_list_box_append (GTK_LIST_BOX (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);
|
|
}
|
|
|
|
/* Scrolls sibebar so that @row is at middle of the visible part of list */
|
|
static void
|
|
cc_panel_list_scroll_to_center_row (CcPanelList *self,
|
|
GtkWidget *row)
|
|
{
|
|
double target_value;
|
|
graphene_point_t p;
|
|
GtkAdjustment *adj;
|
|
GtkWidget *scrolled_window;
|
|
|
|
g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
|
|
|
|
scrolled_window = gtk_widget_get_ancestor (row, GTK_TYPE_SCROLLED_WINDOW);
|
|
adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled_window));
|
|
if (!adj)
|
|
return;
|
|
|
|
if (!gtk_widget_compute_point (row, GTK_WIDGET (self), &GRAPHENE_POINT_INIT (0, 0), &p))
|
|
return;
|
|
|
|
target_value = p.y + gtk_widget_get_height (row) / 2;
|
|
|
|
gtk_adjustment_set_value (adj, target_value - gtk_adjustment_get_page_size (adj) / 2);
|
|
}
|
|
|
|
typedef struct {
|
|
CcPanelList *panel_list;
|
|
GtkWidget *row;
|
|
} ScrollData;
|
|
|
|
static gboolean
|
|
scroll_to_idle_cb (ScrollData *data)
|
|
{
|
|
cc_panel_list_scroll_to_center_row (data->panel_list, data->row);
|
|
|
|
g_object_unref (data->panel_list);
|
|
g_object_unref (data->row);
|
|
g_free (data);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
ScrollData *sdata;
|
|
gboolean scroll_to_center = FALSE;
|
|
|
|
g_return_if_fail (CC_IS_PANEL_LIST (self));
|
|
|
|
data = g_hash_table_lookup (self->id_to_data, id);
|
|
|
|
g_assert (data != NULL);
|
|
|
|
if (self->center_activated_row)
|
|
{
|
|
scroll_to_center = TRUE;
|
|
self->center_activated_row = FALSE;
|
|
}
|
|
|
|
/* 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_set_visible (data->row, TRUE);
|
|
|
|
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;
|
|
|
|
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);
|
|
|
|
/* This centering is currently set for panels activated from Search
|
|
* or from set_active_panel_from_id() CcShell iface */
|
|
if (scroll_to_center)
|
|
{
|
|
/* Scroll the sidebar to the selected panel row, as that row may be
|
|
* out of view when panel is launched from a search or from cli */
|
|
sdata = g_new (ScrollData, 1);
|
|
sdata->panel_list = g_object_ref (self);
|
|
sdata->row = g_object_ref (data->row);
|
|
|
|
g_idle_add (G_SOURCE_FUNC (scroll_to_idle_cb), sdata);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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_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));
|
|
}
|
|
}
|
|
|