GCC Code Coverage Report


Directory: ./
File: shell/cc-panel-list.c
Date: 2024-05-04 07:58:27
Exec Total Coverage
Lines: 0 387 0.0%
Functions: 0 37 0.0%
Branches: 0 196 0.0%

Line Branch Exec Source
1 /* cc-panel-list.c
2 *
3 * Copyright (C) 2016 Endless, Inc
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * Author: Georges Basile Stavracas Neto <gbsneto@gnome.org>
19 */
20
21 #define G_LOG_DOMAIN "cc-panel-list"
22
23 #include <string.h>
24
25 #include "cc-log.h"
26 #include "cc-panel-list.h"
27 #include "cc-util.h"
28
29 typedef struct
30 {
31 GtkWidget *row;
32 GtkWidget *description_label;
33 CcPanelCategory category;
34 gchar *id;
35 gchar *name;
36 gchar *description;
37 gchar **keywords;
38 CcPanelVisibility visibility;
39 } RowData;
40
41 struct _CcPanelList
42 {
43 AdwBin parent;
44
45 GtkWidget *main_listbox;
46 GtkWidget *search_listbox;
47 GtkStack *stack;
48
49 /* When clicking on Details or Devices row, show it
50 * automatically select the first panel of the list.
51 */
52 gboolean autoselect_panel : 1;
53
54 gchar *current_panel_id;
55 gchar *search_query;
56 gchar **search_words;
57
58 CcPanelListView previous_view;
59 CcPanelListView view;
60 GHashTable *id_to_data;
61 GHashTable *id_to_search_data;
62
63 /* When true, the next row being activated will be vertically centered on
64 * the visible part of panel list. Currently we do that for panels activated
65 * from Search or from the set_active_panel_from_id() CcShell iface */
66 gboolean center_activated_row;
67 };
68
69 G_DEFINE_TYPE (CcPanelList, cc_panel_list, ADW_TYPE_BIN)
70
71 enum
72 {
73 PROP_0,
74 PROP_SEARCH_MODE,
75 PROP_SEARCH_QUERY,
76 PROP_VIEW,
77 N_PROPS
78 };
79
80 enum
81 {
82 SHOW_PANEL,
83 LAST_SIGNAL
84 };
85
86 static GParamSpec *properties [N_PROPS] = { NULL, };
87 static gint signals [LAST_SIGNAL] = { 0, };
88
89 /*
90 * Auxiliary methods
91 */
92 static GtkWidget*
93 get_widget_from_view (CcPanelList *self,
94 CcPanelListView view)
95 {
96 switch (view)
97 {
98 case CC_PANEL_LIST_MAIN:
99 return self->main_listbox;
100
101 case CC_PANEL_LIST_SEARCH:
102 return self->search_listbox;
103
104 default:
105 return NULL;
106 }
107 }
108
109 static void
110 activate_row_below (CcPanelList *self,
111 RowData *data)
112 {
113 GtkListBoxRow *next_row;
114 guint row_index;
115
116 row_index = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (data->row));
117 next_row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->main_listbox),
118 row_index + 1);
119
120 /* Try the previous one if the current is invalid */
121 if (!next_row)
122 next_row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->main_listbox),
123 row_index - 1);
124
125 if (next_row)
126 g_signal_emit_by_name (next_row, "activate");
127 }
128
129 static CcPanelListView
130 get_view_from_listbox (CcPanelList *self,
131 GtkWidget *listbox)
132 {
133 if (listbox == self->main_listbox)
134 return CC_PANEL_LIST_MAIN;
135
136 return CC_PANEL_LIST_SEARCH;
137 }
138
139 static void
140 switch_to_view (CcPanelList *self,
141 CcPanelListView view)
142 {
143 GtkWidget *visible_child;
144 gboolean should_crossfade;
145
146 CC_ENTRY;
147
148 if (self->view == view)
149 CC_RETURN ();
150
151 CC_TRACE_MSG ("Switching to view: %d", view);
152
153 self->previous_view = self->view;
154 self->view = view;
155
156 /*
157 * When changing to or from the search view, the animation should
158 * be crossfade. Otherwise, it's the previous-forward movement.
159 */
160 should_crossfade = view == CC_PANEL_LIST_SEARCH ||
161 self->previous_view == CC_PANEL_LIST_SEARCH;
162
163 gtk_stack_set_transition_type (self->stack,
164 should_crossfade ? GTK_STACK_TRANSITION_TYPE_CROSSFADE :
165 GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
166
167 visible_child = get_widget_from_view (self, view);
168
169 gtk_stack_set_visible_child (self->stack, visible_child);
170
171 /* For non-search views, make sure the displayed panel matches the
172 * newly selected row
173 */
174 if (self->autoselect_panel &&
175 view != CC_PANEL_LIST_SEARCH)
176 {
177 cc_panel_list_activate (self);
178 }
179
180 g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VIEW]);
181 g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_MODE]);
182
183 CC_EXIT;
184 }
185
186 static void
187 update_search (CcPanelList *self)
188 {
189 /*
190 * Only change to the search view is there's a
191 * search query available.
192 */
193 if (self->search_query &&
194 g_utf8_strlen (self->search_query, -1) > 0)
195 {
196 if (self->view == CC_PANEL_LIST_MAIN)
197 switch_to_view (self, CC_PANEL_LIST_SEARCH);
198 }
199 else if (self->view == CC_PANEL_LIST_SEARCH)
200 {
201 /* Don't autoselect first panel when going back from search view */
202 self->autoselect_panel = FALSE;
203
204 switch_to_view (self, self->previous_view);
205 }
206
207 gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->search_listbox));
208 gtk_list_box_unselect_all (GTK_LIST_BOX (self->search_listbox));
209 }
210
211 static const gchar*
212 get_panel_id_from_row (CcPanelList *self,
213 GtkListBoxRow *row)
214 {
215
216 RowData *row_data = g_object_get_data (G_OBJECT (row), "data");
217
218 g_assert (row_data != NULL);
219 return row_data->id;
220 }
221
222 /*
223 * RowData functions
224 */
225 static void
226 row_data_free (RowData *data)
227 {
228 g_strfreev (data->keywords);
229 g_free (data->description);
230 g_free (data->name);
231 g_free (data->id);
232 g_free (data);
233 }
234
235 static RowData*
236 row_data_new (CcPanelCategory category,
237 const gchar *id,
238 const gchar *name,
239 const gchar *description,
240 const GStrv keywords,
241 const gchar *icon,
242 CcPanelVisibility visibility)
243 {
244 GtkWidget *label, *grid, *image;
245 RowData *data;
246
247 data = g_new0 (RowData, 1);
248 data->category = category;
249 data->row = gtk_list_box_row_new ();
250 data->id = g_strdup (id);
251 data->name = g_strdup (name);
252 data->description = g_strdup (description);
253 data->keywords = g_strdupv (keywords);
254
255 /* Setup the row */
256 grid = gtk_grid_new ();
257 gtk_widget_set_hexpand (grid, TRUE);
258 gtk_widget_set_margin_top (grid, 12);
259 gtk_widget_set_margin_bottom (grid, 12);
260 gtk_widget_set_margin_start (grid, 6);
261 gtk_widget_set_margin_end (grid, 6);
262 gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
263
264 /* Icon */
265 image = gtk_image_new_from_icon_name (icon);
266
267 gtk_grid_attach (GTK_GRID (grid), image, 0, 0, 1, 1);
268
269 /* Name label */
270 label = gtk_label_new (name);
271 gtk_label_set_xalign (GTK_LABEL (label), 0.0);
272 gtk_widget_set_hexpand (label, TRUE);
273 gtk_grid_attach (GTK_GRID (grid), label, 1, 0, 1, 1);
274 gtk_accessible_update_relation (GTK_ACCESSIBLE (data->row),
275 GTK_ACCESSIBLE_RELATION_LABELLED_BY,
276 label,
277 NULL,
278 -1);
279
280 /* Description label */
281 label = gtk_label_new (description);
282 gtk_label_set_xalign (GTK_LABEL (label), 0.0);
283 gtk_widget_set_hexpand (label, TRUE);
284 gtk_label_set_max_width_chars (GTK_LABEL (label), 25);
285 gtk_label_set_wrap (GTK_LABEL (label), TRUE);
286 gtk_widget_set_visible (label, FALSE);
287 gtk_accessible_update_relation (GTK_ACCESSIBLE (data->row),
288 GTK_ACCESSIBLE_RELATION_DESCRIBED_BY,
289 label,
290 NULL,
291 -1);
292
293 gtk_widget_add_css_class (label, "dim-label");
294 gtk_grid_attach (GTK_GRID (grid), label, 1, 1, 1, 1);
295
296 data->description_label = label;
297
298 gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (data->row), grid);
299
300 g_object_set_data_full (G_OBJECT (data->row), "data", data, (GDestroyNotify) row_data_free);
301
302 data->visibility = visibility;
303
304 return data;
305 }
306
307 /*
308 * GtkListBox functions
309 */
310 static gboolean
311 filter_func (GtkListBoxRow *row,
312 gpointer user_data)
313 {
314 CcPanelList *self;
315 RowData *data;
316 g_autofree gchar *panel_text = NULL;
317 g_autofree gchar *panel_description = NULL;
318 gboolean retval = TRUE;
319 gint i, j;
320
321 self = CC_PANEL_LIST (user_data);
322 data = g_object_get_data (G_OBJECT (row), "data");
323
324 if (!self->search_words)
325 return TRUE;
326
327 panel_text = cc_util_normalize_casefold_and_unaccent (data->name);
328 panel_description = cc_util_normalize_casefold_and_unaccent (data->description);
329
330 g_strstrip (panel_text);
331 g_strstrip (panel_description);
332
333 /*
334 * The description label is only visible when the search is
335 * happening.
336 */
337 gtk_widget_set_visible (data->description_label, self->view == CC_PANEL_LIST_SEARCH);
338
339 for (j = 0; retval && self->search_words[j] != NULL; j++) {
340 const gchar *search_word = self->search_words[j];
341 gboolean match = FALSE;
342
343 if (search_word[0] == '\0')
344 continue;
345
346 // Compare keywords
347 for (i = 0; !match && data->keywords[i] != NULL; i++)
348 match = (strstr (data->keywords[i], search_word) == data->keywords[i]);
349
350 // Compare panel title and description
351 match = match || (g_strstr_len (panel_text, -1, search_word) != NULL ||
352 g_strstr_len (panel_description, -1, search_word) != NULL);
353
354 // All search words must match
355 retval = retval && match;
356 }
357
358 return retval;
359 }
360
361 static const gchar * const panel_order[] = {
362 /* Main page */
363 "wifi",
364 "network",
365 "wwan",
366 "mobile-broadband",
367 "bluetooth",
368
369 "separator",
370
371 "display",
372 "sound",
373 "power",
374 "multitasking",
375 "background",
376
377 "separator",
378
379 "applications",
380 "notifications",
381 "search",
382 "online-accounts",
383 "sharing",
384
385 "separator",
386
387 "mouse",
388 "keyboard",
389 "color",
390 "printers",
391 "wacom",
392
393 "separator",
394
395 "universal-access",
396 "privacy",
397 "system",
398 "reset-settings",
399 };
400
401 static guint
402 get_panel_id_index (const gchar *panel_id)
403 {
404 guint i;
405
406 for (i = 0; i < G_N_ELEMENTS (panel_order); i++)
407 {
408 if (g_str_equal (panel_order[i], panel_id))
409 return i;
410 }
411
412 return 0;
413 }
414
415 static gint
416 sort_function (GtkListBoxRow *a,
417 GtkListBoxRow *b,
418 gpointer user_data)
419 {
420 CcPanelList *self = CC_PANEL_LIST (user_data);
421 const gchar *a_id, *b_id;
422
423 a_id = get_panel_id_from_row (self, a);
424 b_id = get_panel_id_from_row (self, b);
425
426 return get_panel_id_index (a_id) - get_panel_id_index (b_id);
427 }
428
429
430 /* FIXME: This is now different from the "match all words" search.
431 Maybe add a search score based on number of matches in filter_func()? */
432 static gint
433 search_sort_function (GtkListBoxRow *a,
434 GtkListBoxRow *b,
435 gpointer user_data)
436 {
437 CcPanelList *self;
438 RowData *a_data, *b_data;
439 g_autofree gchar *a_name = NULL;
440 g_autofree gchar *b_name = NULL;
441 g_autofree gchar *search = NULL;
442 gchar *a_strstr, *b_strstr;
443 gint a_distance, b_distance;
444
445 self = CC_PANEL_LIST (user_data);
446 search = NULL;
447 a_data = g_object_get_data (G_OBJECT (a), "data");
448 b_data = g_object_get_data (G_OBJECT (b), "data");
449
450 a_distance = b_distance = G_MAXINT;
451
452 a_name = cc_util_normalize_casefold_and_unaccent (a_data->name);
453 b_name = cc_util_normalize_casefold_and_unaccent (b_data->name);
454 g_strstrip (a_name);
455 g_strstrip (b_name);
456
457 if (self->search_query)
458 {
459 search = cc_util_normalize_casefold_and_unaccent (self->search_query);
460 g_strstrip (search);
461 }
462
463 /* Default result for empty search */
464 if (!search || g_utf8_strlen (search, -1) == 0)
465 return g_strcmp0 (a_name, b_name);
466
467 a_strstr = g_strstr_len (a_name, -1, search);
468 b_strstr = g_strstr_len (b_name, -1, search);
469
470 if (a_strstr)
471 a_distance = g_strstr_len (a_name, -1, search) - a_name;
472
473 if (b_strstr)
474 b_distance = g_strstr_len (b_name, -1, search) - b_name;
475
476 return a_distance - b_distance;
477 }
478
479 static void
480 header_func (GtkListBoxRow *row,
481 GtkListBoxRow *before,
482 gpointer user_data)
483 {
484 guint pid;
485
486 if (!before)
487 return;
488
489 pid = get_panel_id_index (get_panel_id_from_row (user_data, row));
490 if (pid > 0 && g_str_equal (panel_order[pid-1], "separator"))
491 {
492 GtkWidget *separator;
493
494 separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
495 gtk_widget_set_hexpand (separator, TRUE);
496
497 gtk_list_box_row_set_header (row, separator);
498 }
499 else
500 {
501 gtk_list_box_row_set_header (row, NULL);
502 }
503 }
504
505 /*
506 * Callbacks
507 */
508 static void
509 row_activated_cb (GtkWidget *listbox,
510 GtkListBoxRow *row,
511 CcPanelList *self)
512 {
513 RowData *data;
514 const gchar *parent_panel = 0;
515
516 /*
517 * Since we're not sure that the activated row is in the
518 * current view, set the view here.
519 */
520 switch_to_view (self, get_view_from_listbox (self, listbox));
521
522 data = g_object_get_data (G_OBJECT (row), "data");
523 if (data->category == CC_CATEGORY_SYSTEM)
524 parent_panel = "system";
525 else if (data->category == CC_CATEGORY_PRIVACY)
526 parent_panel = "privacy";
527
528 g_signal_emit (self, signals[SHOW_PANEL], 0, data->id, parent_panel);
529
530 /* After selecting the panel and eventually changing the view, reset the
531 * autoselect flag. If necessary, cc_panel_list_set_active_panel() will
532 * set it to FALSE again.
533 */
534 self->autoselect_panel = TRUE;
535 }
536
537 static void
538 search_row_activated_cb (GtkWidget *listbox,
539 GtkListBoxRow *row,
540 CcPanelList *self)
541 {
542 GtkWidget *child;
543 RowData *data;
544
545 CC_ENTRY;
546
547 data = g_object_get_data (G_OBJECT (row), "data");
548
549 /* Select the correct row */
550 for (child = gtk_widget_get_first_child (self->main_listbox);
551 child != NULL;
552 child = gtk_widget_get_next_sibling (child))
553 {
554 RowData *real_row_data;
555
556 real_row_data = g_object_get_data (G_OBJECT (child), "data");
557
558 /*
559 * The main listbox has the Details & Devices rows, and neither
560 * of them contains "data", so we have to ensure we have valid
561 * data before going on.
562 */
563 if (!real_row_data)
564 continue;
565
566 if (g_strcmp0 (real_row_data->id, data->id) == 0)
567 {
568 GtkListBoxRow *real_row;
569
570 real_row = GTK_LIST_BOX_ROW (real_row_data->row);
571
572 gtk_list_box_select_row (GTK_LIST_BOX (self->main_listbox), real_row);
573 gtk_widget_grab_focus (GTK_WIDGET (real_row));
574
575 /* Don't autoselect first panel because we are already
576 * activating a panel from search result */
577 self->autoselect_panel = FALSE;
578
579 /* center Search activated row on panel list */
580 self->center_activated_row = TRUE;
581 g_signal_emit_by_name (real_row, "activate");
582 break;
583 }
584 }
585
586 CC_EXIT;
587 }
588
589 static void
590 cc_panel_list_finalize (GObject *object)
591 {
592 CcPanelList *self = (CcPanelList *)object;
593
594 g_clear_pointer (&self->search_query, g_free);
595 g_clear_pointer (&self->search_words, g_strfreev);
596 g_clear_pointer (&self->current_panel_id, g_free);
597 g_clear_pointer (&self->id_to_data, g_hash_table_destroy);
598 g_clear_pointer (&self->id_to_search_data, g_hash_table_destroy);
599
600 G_OBJECT_CLASS (cc_panel_list_parent_class)->finalize (object);
601 }
602
603 static void
604 cc_panel_list_get_property (GObject *object,
605 guint prop_id,
606 GValue *value,
607 GParamSpec *pspec)
608 {
609 CcPanelList *self = CC_PANEL_LIST (object);
610
611 switch (prop_id)
612 {
613 case PROP_SEARCH_MODE:
614 g_value_set_boolean (value, self->view == CC_PANEL_LIST_SEARCH);
615 break;
616
617 case PROP_SEARCH_QUERY:
618 g_value_set_string (value, self->search_query);
619 break;
620
621 case PROP_VIEW:
622 g_value_set_int (value, self->view);
623 break;
624
625 default:
626 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
627 }
628 }
629
630 static void
631 cc_panel_list_set_property (GObject *object,
632 guint prop_id,
633 const GValue *value,
634 GParamSpec *pspec)
635 {
636 CcPanelList *self = CC_PANEL_LIST (object);
637
638 switch (prop_id)
639 {
640 case PROP_SEARCH_MODE:
641 update_search (self);
642 break;
643
644 case PROP_SEARCH_QUERY:
645 cc_panel_list_set_search_query (self, g_value_get_string (value));
646 break;
647
648 case PROP_VIEW:
649 switch_to_view (self, g_value_get_int (value));
650 break;
651
652 default:
653 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
654 }
655 }
656
657 static gboolean
658 search_list_keynav_failed_cb (CcPanelList *self,
659 GtkDirectionType direction)
660 {
661 GtkWidget *toplevel;
662
663 /* We are in the first result of search list and pressing Arrow Up,
664 * so then we move focus back to search text entry */
665 if (direction == GTK_DIR_UP)
666 {
667 toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
668
669 if (!toplevel)
670 return FALSE;
671
672 return gtk_widget_child_focus (toplevel, GTK_DIR_TAB_BACKWARD);
673 }
674
675 return FALSE;
676 }
677
678
679 static void
680 cc_panel_list_class_init (CcPanelListClass *klass)
681 {
682 GObjectClass *object_class = G_OBJECT_CLASS (klass);
683 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
684
685 object_class->finalize = cc_panel_list_finalize;
686 object_class->get_property = cc_panel_list_get_property;
687 object_class->set_property = cc_panel_list_set_property;
688
689 /**
690 * CcPanelList:show-panel:
691 *
692 * Emitted when a panel is selected.
693 */
694 signals[SHOW_PANEL] = g_signal_new ("show-panel",
695 CC_TYPE_PANEL_LIST,
696 G_SIGNAL_RUN_LAST,
697 0, NULL, NULL, NULL,
698 G_TYPE_NONE,
699 2,
700 G_TYPE_STRING,
701 G_TYPE_STRING);
702
703 /**
704 * CcPanelList:search-mode:
705 *
706 * Whether the search is visible or not.
707 */
708 properties[PROP_SEARCH_MODE] = g_param_spec_boolean ("search-mode",
709 "Search mode",
710 "Whether it's in search mode or not",
711 FALSE,
712 G_PARAM_READWRITE);
713
714 /**
715 * CcPanelList:search-query:
716 *
717 * The search that is being applied to sidelist.
718 */
719 properties[PROP_SEARCH_QUERY] = g_param_spec_string ("search-query",
720 "Search query",
721 "The current search query",
722 NULL,
723 G_PARAM_READWRITE);
724
725 /**
726 * CcPanelList:view:
727 *
728 * The current view of the sidelist.
729 */
730 properties[PROP_VIEW] = g_param_spec_int ("view",
731 "View",
732 "The current view of the sidelist",
733 CC_PANEL_LIST_MAIN,
734 CC_PANEL_LIST_SEARCH,
735 CC_PANEL_LIST_MAIN,
736 G_PARAM_READWRITE);
737
738 g_object_class_install_properties (object_class, N_PROPS, properties);
739
740 gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Settings/gtk/cc-panel-list.ui");
741
742 gtk_widget_class_bind_template_child (widget_class, CcPanelList, main_listbox);
743 gtk_widget_class_bind_template_child (widget_class, CcPanelList, search_listbox);
744 gtk_widget_class_bind_template_child (widget_class, CcPanelList, stack);
745
746 gtk_widget_class_bind_template_callback (widget_class, row_activated_cb);
747 gtk_widget_class_bind_template_callback (widget_class, search_row_activated_cb);
748 gtk_widget_class_bind_template_callback (widget_class, search_list_keynav_failed_cb);
749 }
750
751 static void
752 cc_panel_list_init (CcPanelList *self)
753 {
754 gtk_widget_init_template (GTK_WIDGET (self));
755
756 self->id_to_data = g_hash_table_new (g_str_hash, g_str_equal);
757 self->id_to_search_data = g_hash_table_new (g_str_hash, g_str_equal);
758 self->view = CC_PANEL_LIST_MAIN;
759
760 gtk_list_box_set_sort_func (GTK_LIST_BOX (self->main_listbox),
761 sort_function,
762 self,
763 NULL);
764
765 gtk_list_box_set_header_func (GTK_LIST_BOX (self->main_listbox),
766 header_func,
767 self,
768 NULL);
769
770 /* Search listbox */
771 gtk_list_box_set_sort_func (GTK_LIST_BOX (self->search_listbox),
772 search_sort_function,
773 self,
774 NULL);
775
776 gtk_list_box_set_filter_func (GTK_LIST_BOX (self->search_listbox),
777 filter_func,
778 self,
779 NULL);
780 }
781
782 GtkWidget*
783 cc_panel_list_new (void)
784 {
785 return g_object_new (CC_TYPE_PANEL_LIST, NULL);
786 }
787
788 void
789 cc_panel_list_center_activated_row (CcPanelList *self,
790 gboolean val)
791 {
792 g_return_if_fail (CC_IS_PANEL_LIST (self));
793
794 if (self->center_activated_row != val)
795 self->center_activated_row = val;
796 }
797
798 gboolean
799 cc_panel_list_activate (CcPanelList *self)
800 {
801 GtkListBoxRow *row;
802 GtkWidget *listbox;
803 guint i = 0;
804
805 CC_ENTRY;
806
807 g_return_val_if_fail (CC_IS_PANEL_LIST (self), FALSE);
808
809 listbox = get_widget_from_view (self, self->view);
810 if (!GTK_IS_LIST_BOX (listbox))
811 CC_RETURN (FALSE);
812
813 /* Select the first visible row */
814 do
815 row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (listbox), i++);
816 while (row && !(gtk_widget_get_visible (GTK_WIDGET (row)) &&
817 gtk_widget_get_child_visible (GTK_WIDGET (row))));
818
819 /* If the row is valid, activate it */
820 if (row)
821 {
822 gtk_list_box_select_row (GTK_LIST_BOX (listbox), row);
823 gtk_widget_grab_focus (GTK_WIDGET (row));
824
825 g_signal_emit_by_name (row, "activate");
826 }
827
828 CC_RETURN (row != NULL);
829 }
830
831 const gchar*
832 cc_panel_list_get_search_query (CcPanelList *self)
833 {
834 g_return_val_if_fail (CC_IS_PANEL_LIST (self), NULL);
835
836 return self->search_query;
837 }
838
839 void
840 cc_panel_list_set_search_query (CcPanelList *self,
841 const gchar *search)
842 {
843 g_return_if_fail (CC_IS_PANEL_LIST (self));
844
845 if (g_strcmp0 (self->search_query, search) != 0)
846 {
847 g_autofree gchar *search_query_normalized;
848
849 g_clear_pointer (&self->search_query, g_free);
850 g_clear_pointer (&self->search_words, g_strfreev);
851
852 self->search_query = g_strdup (search);
853
854 /* Split on spaces */
855 search_query_normalized = cc_util_normalize_casefold_and_unaccent (search);
856 self->search_words = g_strsplit (g_strstrip (search_query_normalized), " ", 0);
857
858 update_search (self);
859
860 g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH_QUERY]);
861
862 gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->search_listbox));
863 gtk_list_box_invalidate_sort (GTK_LIST_BOX (self->search_listbox));
864 }
865 }
866
867 CcPanelListView
868 cc_panel_list_get_view (CcPanelList *self)
869 {
870 g_return_val_if_fail (CC_IS_PANEL_LIST (self), -1);
871
872 return self->view;
873 }
874
875 /**
876 * cc_panel_list_get_current_panel:
877 * @self: a #CcPanelList
878 *
879 * Returns: (allow-none): id string of current active panel on @self, or %NULL when there's none yet.
880 */
881 const gchar*
882 cc_panel_list_get_current_panel (CcPanelList *self)
883 {
884 g_return_val_if_fail (CC_IS_PANEL_LIST (self), NULL);
885
886 return self->current_panel_id;
887 }
888
889 void
890 cc_panel_list_add_panel (CcPanelList *self,
891 CcPanelCategory category,
892 const gchar *id,
893 const gchar *title,
894 const gchar *description,
895 const GStrv keywords,
896 const gchar *icon,
897 CcPanelVisibility visibility)
898 {
899 RowData *data, *search_data;
900
901 g_return_if_fail (CC_IS_PANEL_LIST (self));
902
903 /* Add the panel to the proper listbox */
904 data = row_data_new (category, id, title, description, keywords, icon, visibility);
905 gtk_widget_set_visible (data->row, visibility == CC_PANEL_VISIBLE);
906
907 gtk_list_box_append (GTK_LIST_BOX (self->main_listbox), data->row);
908
909 /* And add to the search listbox too */
910 search_data = row_data_new (category, id, title, description, keywords, icon, visibility);
911 gtk_widget_set_visible (search_data->row, visibility != CC_PANEL_HIDDEN);
912
913 gtk_list_box_append (GTK_LIST_BOX (self->search_listbox), search_data->row);
914
915 g_hash_table_insert (self->id_to_data, data->id, data);
916 g_hash_table_insert (self->id_to_search_data, search_data->id, search_data);
917 }
918
919 /* Scrolls sibebar so that @row is at middle of the visible part of list */
920 static void
921 cc_panel_list_scroll_to_center_row (CcPanelList *self,
922 GtkWidget *row)
923 {
924 double target_value;
925 graphene_point_t p;
926 GtkAdjustment *adj;
927 GtkWidget *scrolled_window;
928
929 g_return_if_fail (GTK_IS_LIST_BOX_ROW (row));
930
931 scrolled_window = gtk_widget_get_ancestor (row, GTK_TYPE_SCROLLED_WINDOW);
932 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (scrolled_window));
933 if (!adj)
934 return;
935
936 if (!gtk_widget_compute_point (row, GTK_WIDGET (self), &GRAPHENE_POINT_INIT (0, 0), &p))
937 return;
938
939 target_value = p.y + gtk_widget_get_height (row) / 2;
940
941 gtk_adjustment_set_value (adj, target_value - gtk_adjustment_get_page_size (adj) / 2);
942 }
943
944 typedef struct {
945 CcPanelList *panel_list;
946 GtkWidget *row;
947 } ScrollData;
948
949 static gboolean
950 scroll_to_idle_cb (ScrollData *data)
951 {
952 cc_panel_list_scroll_to_center_row (data->panel_list, data->row);
953
954 g_object_unref (data->panel_list);
955 g_object_unref (data->row);
956 g_free (data);
957
958 return FALSE;
959 }
960
961 /**
962 * cc_panel_list_set_active_panel:
963 * @self: a #CcPanelList
964 * @id: the id of the panel to be activated
965 *
966 * Sets the current active panel.
967 */
968 void
969 cc_panel_list_set_active_panel (CcPanelList *self,
970 const gchar *id)
971 {
972 GtkWidget *listbox;
973 RowData *data;
974 ScrollData *sdata;
975 gboolean scroll_to_center = FALSE;
976
977 g_return_if_fail (CC_IS_PANEL_LIST (self));
978
979 data = g_hash_table_lookup (self->id_to_data, id);
980
981 g_assert (data != NULL);
982
983 if (self->center_activated_row)
984 {
985 scroll_to_center = TRUE;
986 self->center_activated_row = FALSE;
987 }
988
989 /* Stop if row is supposed to be always hidden */
990 if (data->visibility == CC_PANEL_HIDDEN)
991 {
992 g_debug ("Panel '%s' is always hidden, stopping.", id);
993 cc_panel_list_activate (self);
994 return;
995 }
996
997 /* If the currently selected panel is not always visible, for example when
998 * the panel is only visible on search and we're temporarily seeing it, make
999 * sure to hide it after the user moves out.
1000 */
1001 if (self->current_panel_id != NULL && g_strcmp0 (self->current_panel_id, id) != 0)
1002 {
1003 RowData *current_row_data;
1004
1005 current_row_data = g_hash_table_lookup (self->id_to_data, self->current_panel_id);
1006
1007 /* We cannot be showing a non-existent panel */
1008 g_assert (current_row_data != NULL);
1009
1010 gtk_widget_set_visible (current_row_data->row, current_row_data->visibility == CC_PANEL_VISIBLE);
1011 }
1012
1013 listbox = gtk_widget_get_parent (data->row);
1014
1015 /* The row might be hidden now, so make sure it's visible */
1016 gtk_widget_set_visible (data->row, TRUE);
1017
1018 gtk_list_box_select_row (GTK_LIST_BOX (listbox), GTK_LIST_BOX_ROW (data->row));
1019 gtk_widget_grab_focus (data->row);
1020
1021 /* When setting the active panel programatically, prevent from
1022 * autoselecting the first panel of the new view.
1023 */
1024 self->autoselect_panel = FALSE;
1025
1026 g_signal_emit_by_name (data->row, "activate");
1027
1028 /* Store the current panel id */
1029 g_clear_pointer (&self->current_panel_id, g_free);
1030 self->current_panel_id = g_strdup (id);
1031
1032 /* This centering is currently set for panels activated from Search
1033 * or from set_active_panel_from_id() CcShell iface */
1034 if (scroll_to_center)
1035 {
1036 /* Scroll the sidebar to the selected panel row, as that row may be
1037 * out of view when panel is launched from a search or from cli */
1038 sdata = g_new (ScrollData, 1);
1039 sdata->panel_list = g_object_ref (self);
1040 sdata->row = g_object_ref (data->row);
1041
1042 g_idle_add (G_SOURCE_FUNC (scroll_to_idle_cb), sdata);
1043 }
1044 }
1045
1046 /**
1047 * cc_panel_list_set_panel_visibility:
1048 * @self: a #CcPanelList
1049 * @id: the id of the panel
1050 * @visibility: visibility of panel with @id
1051 *
1052 * Sets the visibility of panel with @id. @id must be a valid
1053 * id with a corresponding panel.
1054 */
1055 void
1056 cc_panel_list_set_panel_visibility (CcPanelList *self,
1057 const gchar *id,
1058 CcPanelVisibility visibility)
1059 {
1060 RowData *data, *search_data;
1061
1062 g_return_if_fail (CC_IS_PANEL_LIST (self));
1063
1064 data = g_hash_table_lookup (self->id_to_data, id);
1065 search_data = g_hash_table_lookup (self->id_to_search_data, id);
1066
1067 g_assert (data != NULL);
1068 g_assert (search_data != NULL);
1069
1070 data->visibility = visibility;
1071
1072 /* If this is the currently selected row, and the panel can't be displayed
1073 * (i.e. visibility != VISIBLE), then select the next possible row */
1074 if (gtk_list_box_row_is_selected (GTK_LIST_BOX_ROW (data->row)) &&
1075 visibility != CC_PANEL_VISIBLE)
1076 {
1077 activate_row_below (self, data);
1078 }
1079
1080 gtk_widget_set_visible (data->row, visibility == CC_PANEL_VISIBLE);
1081 gtk_widget_set_visible (search_data->row, visibility =! CC_PANEL_HIDDEN);
1082 }
1083
1084 void
1085 cc_panel_list_set_selection_mode (CcPanelList *self,
1086 GtkSelectionMode selection_mode)
1087 {
1088 g_return_if_fail (CC_IS_PANEL_LIST (self));
1089
1090 gtk_list_box_set_selection_mode (GTK_LIST_BOX (self->main_listbox), selection_mode);
1091
1092 /* When selection mode changed, selection will be lost. So reselect */
1093 if (selection_mode == GTK_SELECTION_SINGLE && self->current_panel_id)
1094 {
1095 GtkWidget *listbox;
1096 RowData *data;
1097
1098 data = g_hash_table_lookup (self->id_to_data, self->current_panel_id);
1099 listbox = gtk_widget_get_parent (data->row);
1100 gtk_list_box_select_row (GTK_LIST_BOX (listbox), GTK_LIST_BOX_ROW (data->row));
1101 }
1102 }
1103
1104