GCC Code Coverage Report


Directory: ./
File: panels/keyboard/cc-keyboard-shortcut-dialog.c
Date: 2024-05-04 07:58:27
Exec Total Coverage
Lines: 0 241 0.0%
Functions: 0 23 0.0%
Branches: 0 105 0.0%

Line Branch Exec Source
1 /* cc-keyboard-shortcut-dialog.c
2 *
3 * Copyright (C) 2010 Intel, Inc
4 * Copyright (C) 2016 Endless, Inc
5 * Copyright (C) 2020 System76, Inc.
6 * Copyright (C) 2022 Purism SPC
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, see <http://www.gnu.org/licenses/>.
20 *
21 * Author: Thomas Wood <thomas.wood@intel.com>
22 * Georges Basile Stavracas Neto <gbsneto@gnome.org>
23 * Ian Douglas Scott <idscott@system76.com>
24 * Mohammed Sadiq <sadiq@sadiqpk.org>
25 *
26 * SPDX-License-Identifier: GPL-2.0-or-later
27 */
28
29 #include <config.h>
30 #include <glib/gi18n.h>
31 #include <adwaita.h>
32
33 #include "cc-keyboard-shortcut-dialog.h"
34 #include "cc-keyboard-item.h"
35 #include "cc-keyboard-manager.h"
36 #include "cc-keyboard-shortcut-editor.h"
37 #include "cc-keyboard-shortcut-group.h"
38 #include "cc-keyboard-shortcut-row.h"
39 #include "cc-list-row.h"
40 #include "cc-util.h"
41 #include "keyboard-shortcuts.h"
42
43 struct _CcKeyboardShortcutDialog
44 {
45 AdwWindow parent_instance;
46
47 AdwNavigationView *navigation_view;
48 AdwNavigationPage *main_page;
49 GtkButton *reset_all_button;
50 GtkSearchEntry *search_entry;
51 GtkStack *section_stack;
52 AdwPreferencesPage *section_list_page;
53 GtkListBox *section_list_box;
54 AdwPreferencesPage *search_result_page;
55 AdwStatusPage *empty_results_page;
56
57 AdwNavigationPage *subview_page;
58 GtkStack *subview_stack;
59 GtkStack *shortcut_list_stack;
60 AdwStatusPage *empty_custom_shortcut_page;
61 GtkSizeGroup *accelerator_size_group;
62
63 /* A GListStore of sections containing a GListStore of CcKeyboardItem */
64 GListStore *sections;
65 GListStore *visible_section;
66 GtkFlattenListModel *filtered_shortcuts;
67
68 CcKeyboardManager *manager;
69 GtkWidget *shortcut_editor;
70 GStrv search_terms;
71 };
72
73 G_DEFINE_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, ADW_TYPE_WINDOW)
74
75
76 static GListStore *
77 keyboard_shortcut_get_section_store (CcKeyboardShortcutDialog *self,
78 const char *section_id,
79 const char *section_title)
80 {
81 g_autoptr(GListStore) section = NULL;
82 GtkWidget *group;
83 guint n_items;
84
85 g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
86 g_assert (section_id && *section_id);
87
88 n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
89
90 for (guint i = 0; i < n_items; i++)
91 {
92 g_autoptr(GObject) item = NULL;
93 const char *item_section_id;
94
95 item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
96 item_section_id = g_object_get_data (item, "id");
97
98 if (g_str_equal (item_section_id, section_id))
99 return G_LIST_STORE (item);
100 }
101
102 /* Found no matching section, so create one */
103 section = g_list_store_new (CC_TYPE_KEYBOARD_ITEM);
104 g_object_set_data_full (G_OBJECT (section), "id", g_strdup (section_id), g_free);
105 g_object_set_data_full (G_OBJECT (section), "title", g_strdup (section_title), g_free);
106
107 /* This group shall be shown in the search results page */
108 group = cc_keyboard_shortcut_group_new (G_LIST_MODEL (section),
109 section_id, section_title,
110 self->manager,
111 CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
112 self->accelerator_size_group);
113 g_object_set_data (G_OBJECT (section), "search-group", group);
114
115 /* This group shall be shown when a section title row is activated */
116 group = cc_keyboard_shortcut_group_new (G_LIST_MODEL (section),
117 section_id, NULL,
118 self->manager,
119 CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
120 self->accelerator_size_group);
121 g_object_set_data (G_OBJECT (section), "group", group);
122
123 g_list_store_append (self->sections, section);
124
125 return section;
126 }
127
128 static void
129 shortcut_added_cb (CcKeyboardShortcutDialog *self,
130 CcKeyboardItem *item,
131 const char *section_id,
132 const char *section_title)
133 {
134 GListStore *section;
135
136 section = keyboard_shortcut_get_section_store (self, section_id, section_title);
137 g_object_set_data (G_OBJECT (item), "section", section);
138 g_list_store_append (section, item);
139 }
140
141 static void
142 shortcut_removed_cb (CcKeyboardShortcutDialog *self,
143 CcKeyboardItem *item)
144 {
145 GListStore *section;
146 guint position;
147
148 section = g_object_get_data (G_OBJECT (item), "section");
149 g_return_if_fail (section);
150
151 if (g_list_store_find (section, item, &position))
152 g_list_store_remove (section, position);
153 }
154
155 static void
156 shortcut_custom_items_changed (CcKeyboardShortcutDialog *self)
157 {
158 GListStore *section;
159 GtkWidget *page;
160
161 g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
162
163 section = keyboard_shortcut_get_section_store (self, "custom", "Custom Shortcuts");
164
165 if (self->visible_section == section)
166 {
167 guint n_items;
168
169 n_items = g_list_model_get_n_items (G_LIST_MODEL (section));
170
171 if (n_items)
172 page = GTK_WIDGET (self->shortcut_list_stack);
173 else
174 page = GTK_WIDGET (self->empty_custom_shortcut_page);
175 }
176 else
177 page = GTK_WIDGET (self->shortcut_list_stack);
178
179 gtk_stack_set_visible_child (self->subview_stack, page);
180 }
181
182 static int
183 compare_sections_title (gconstpointer a,
184 gconstpointer b,
185 gpointer user_data)
186 {
187 GObject *obj_a, *obj_b;
188
189 const char *title_a, *title_b, *id_a, *id_b;
190
191 obj_a = G_OBJECT (a);
192 obj_b = G_OBJECT (b);
193
194 id_a = g_object_get_data (obj_a, "id");
195 id_b = g_object_get_data (obj_b, "id");
196
197 /* Always place custom row as the last item */
198 if (g_str_equal (id_a, "custom"))
199 return 1;
200
201 if (g_str_equal (id_b, "custom"))
202 return -1;
203
204 title_a = _(g_object_get_data (obj_a, "title"));
205 title_b = _(g_object_get_data (obj_b, "title"));
206
207 return g_strcmp0 (title_a, title_b);
208 }
209
210 static void
211 shortcut_search_result_changed_cb (CcKeyboardShortcutDialog *self)
212 {
213 GListModel *model;
214 GtkWidget *page;
215 guint n_items;
216
217 g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
218
219 /* If a section is already shown, it is handled in search change callback */
220 if (self->visible_section)
221 return;
222
223 model = G_LIST_MODEL (self->filtered_shortcuts);
224 n_items = g_list_model_get_n_items (model);
225
226 if (n_items == 0)
227 page = GTK_WIDGET (self->empty_results_page);
228 else if (self->search_terms)
229 page = GTK_WIDGET (self->search_result_page);
230 else
231 page = GTK_WIDGET (self->section_list_page);
232
233 gtk_stack_set_visible_child (self->section_stack, page);
234 }
235
236 /* All items have loaded, now sort the groups and add them to the page */
237 static void
238 shortcuts_loaded_cb (CcKeyboardShortcutDialog *self)
239 {
240 g_autoptr(GPtrArray) filtered_items = NULL;
241 g_autoptr(GPtrArray) widgets = NULL;
242 GListStore *filtered_lists;
243 GListStore *custom_store;
244 guint n_items;
245
246 /* Ensure that custom shorcuts section exists */
247 custom_store = keyboard_shortcut_get_section_store (self, "custom", "Custom Shortcuts");
248 n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
249 widgets = g_ptr_array_new ();
250 filtered_items = g_ptr_array_new ();
251 filtered_lists = g_list_store_new (G_TYPE_LIST_MODEL);
252
253 g_signal_connect_object (custom_store, "items-changed",
254 G_CALLBACK (shortcut_custom_items_changed),
255 self, G_CONNECT_SWAPPED);
256
257 g_list_store_sort (self->sections, compare_sections_title, NULL);
258
259 for (guint i = 0; i < n_items; i++)
260 {
261 g_autoptr(GObject) item = NULL;
262 CcKeyboardShortcutGroup *group;
263 GListModel *model;
264 GtkWidget *page;
265
266 item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
267 group = g_object_get_data (item, "search-group");
268 g_ptr_array_add (widgets, group);
269
270 model = cc_keyboard_shortcut_group_get_model (group);
271 g_ptr_array_add (filtered_items, model);
272
273 /* Populate shortcut section page */
274 group = g_object_get_data (item, "group");
275 page = adw_preferences_page_new ();
276 g_object_set_data (item, "page", page);
277 adw_preferences_page_add (ADW_PREFERENCES_PAGE (page), ADW_PREFERENCES_GROUP (group));
278 gtk_stack_add_child (self->shortcut_list_stack, page);
279 }
280
281 /* Populate search results page */
282 for (guint i = 0; i < widgets->len; i++)
283 adw_preferences_page_add (self->search_result_page, widgets->pdata[i]);
284
285 /* Keep track of search results so as to update empty state */
286 g_list_store_splice (filtered_lists, 0, 0, filtered_items->pdata, filtered_items->len);
287 self->filtered_shortcuts = gtk_flatten_list_model_new (G_LIST_MODEL (filtered_lists));
288
289 g_signal_connect_object (self->filtered_shortcuts, "items-changed",
290 G_CALLBACK (shortcut_search_result_changed_cb),
291 self, G_CONNECT_SWAPPED);
292 }
293
294 static GtkWidget *
295 shortcut_dialog_row_new (gpointer item,
296 gpointer user_data)
297 {
298 GtkWidget *row, *group;
299 const char *title;
300
301 group = g_object_get_data (item, "search-group");
302 title = g_object_get_data (item, "title");
303 row = g_object_new (CC_TYPE_LIST_ROW,
304 "title", _(title),
305 "show-arrow", TRUE,
306 NULL);
307
308 g_object_set_data (G_OBJECT (row), "section", item);
309
310 g_object_bind_property (group, "modified-text",
311 row, "secondary-label",
312 G_BINDING_SYNC_CREATE);
313
314 return row;
315 }
316
317 static void
318 add_custom_shortcut_clicked_cb (CcKeyboardShortcutDialog *self)
319 {
320 CcKeyboardShortcutEditor *editor;
321
322 editor = CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor);
323
324 cc_keyboard_shortcut_editor_set_mode (editor, CC_SHORTCUT_EDITOR_CREATE);
325 cc_keyboard_shortcut_editor_set_item (editor, NULL);
326
327 gtk_widget_set_visible (self->shortcut_editor, TRUE);
328 }
329
330 static void
331 on_reset_all_dialog_response_cb (CcKeyboardShortcutDialog *self,
332 gchar *response,
333 AdwMessageDialog *dialog)
334 {
335 guint n_items, j_items;
336
337 gtk_window_destroy (GTK_WINDOW (dialog));
338
339 if (g_strcmp0 (response, "cancel") == 0)
340 return;
341
342 n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
343
344 for (guint i = 0; i < n_items; i++)
345 {
346 g_autoptr(GListModel) section = NULL;
347
348 section = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
349 j_items = g_list_model_get_n_items (section);
350
351 for (guint j = 0; j < j_items; j++)
352 {
353 g_autoptr(CcKeyboardItem) item = NULL;
354
355 item = g_list_model_get_item (section, j);
356
357 /* Don't reset custom shortcuts */
358 if (cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
359 return;
360
361 /* cc_keyboard_manager_reset_shortcut() already resets conflicting shortcuts,
362 * so no other check is needed here. */
363 cc_keyboard_manager_reset_shortcut (self->manager, item);
364 }
365 }
366 }
367
368 static void
369 reset_all_clicked_cb (CcKeyboardShortcutDialog *self)
370 {
371 GtkWidget *dialog;
372
373 dialog = adw_message_dialog_new (GTK_WINDOW (self),
374 _("Reset All Shortcuts?"),
375 NULL);
376
377 adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog),
378 _("All changes to keyboard shortcuts will be lost."));
379
380 adw_message_dialog_add_responses (ADW_MESSAGE_DIALOG (dialog),
381 "cancel", _("_Cancel"),
382 "reset_all", _("_Reset All"),
383 NULL);
384
385 adw_message_dialog_set_response_appearance (ADW_MESSAGE_DIALOG (dialog),
386 "reset_all",
387 ADW_RESPONSE_DESTRUCTIVE);
388
389 adw_message_dialog_set_default_response (ADW_MESSAGE_DIALOG (dialog),
390 "cancel");
391
392 adw_message_dialog_set_close_response (ADW_MESSAGE_DIALOG (dialog),
393 "cancel");
394
395 g_signal_connect_swapped (dialog,
396 "response",
397 G_CALLBACK (on_reset_all_dialog_response_cb),
398 self);
399
400 gtk_window_present (GTK_WINDOW (dialog));
401 }
402
403 static void
404 shortcut_dialog_visible_page_changed_cb (CcKeyboardShortcutDialog *self)
405 {
406 gpointer visible_page;
407 gboolean is_main_view;
408
409 visible_page = adw_navigation_view_get_visible_page (self->navigation_view);
410 is_main_view = visible_page == self->main_page;
411
412 if (is_main_view)
413 {
414 gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
415 gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
416
417 self->visible_section = NULL;
418 }
419 else if (self->visible_section)
420 {
421 const char *title;
422
423 title = g_object_get_data (G_OBJECT (self->visible_section), "title");
424 adw_navigation_page_set_title (self->subview_page, _(title) ?: "");
425 }
426 }
427
428 static void
429 shortcut_search_entry_changed_cb (CcKeyboardShortcutDialog *self)
430 {
431 g_autofree char *search = NULL;
432 const char *search_text;
433 guint n_items;
434
435 g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
436
437 /* Don't update search if we are in a subview */
438 if (self->visible_section)
439 return;
440
441 n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
442 search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
443 search = cc_util_normalize_casefold_and_unaccent (search_text);
444
445 g_clear_pointer (&self->search_terms, g_strfreev);
446 if (search && *search && *search != ' ')
447 self->search_terms = g_strsplit (search, " ", -1);
448
449 /* "Reset all..." button should be sensitive only if the search is not active */
450 gtk_widget_set_sensitive (GTK_WIDGET (self->reset_all_button), !self->search_terms);
451
452 for (guint i = 0; i < n_items; i++)
453 {
454 g_autoptr(GObject) item = NULL;
455 CcKeyboardShortcutGroup *group;
456
457 item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
458 group = g_object_get_data (item, "search-group");
459
460 cc_keyboard_shortcut_group_set_filter (group, self->search_terms);
461 }
462
463 shortcut_search_result_changed_cb (self);
464 }
465
466 static void
467 shortcut_search_entry_stopped_cb (CcKeyboardShortcutDialog *self)
468 {
469 const char *search_text;
470 search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
471
472 if (search_text && g_strcmp0 (search_text, "") != 0)
473 gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
474 else
475 gtk_window_close (GTK_WINDOW (self));
476 }
477
478 static void
479 shortcut_section_row_activated_cb (CcKeyboardShortcutDialog *self,
480 GtkListBoxRow *row)
481 {
482 GListStore *section;
483 GtkWidget *page;
484
485 g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
486 g_assert (GTK_IS_LIST_BOX_ROW (row));
487
488 section = g_object_get_data (G_OBJECT (row), "section");
489 self->visible_section = section;
490
491 page = g_object_get_data (G_OBJECT (section), "page");
492 gtk_stack_set_visible_child (self->shortcut_list_stack, page);
493 adw_navigation_view_push (self->navigation_view, self->subview_page);
494 shortcut_custom_items_changed (self);
495 }
496
497 static void
498 cc_keyboard_shortcut_dialog_constructed (GObject *object)
499 {
500 CcKeyboardShortcutDialog *self = CC_KEYBOARD_SHORTCUT_DIALOG (object);
501
502 G_OBJECT_CLASS (cc_keyboard_shortcut_dialog_parent_class)->constructed (object);
503
504 /* Setup the dialog's transient parent */
505 gtk_window_set_transient_for (GTK_WINDOW (self->shortcut_editor), GTK_WINDOW (self));
506 }
507
508 static void
509 cc_keyboard_shortcut_dialog_finalize (GObject *object)
510 {
511 CcKeyboardShortcutDialog *self = CC_KEYBOARD_SHORTCUT_DIALOG (object);
512
513 g_clear_object (&self->manager);
514 g_clear_object (&self->sections);
515 g_clear_pointer (&self->search_terms, g_strfreev);
516 g_clear_object (&self->sections);
517 g_clear_object (&self->filtered_shortcuts);
518 g_clear_pointer ((GtkWindow**)&self->shortcut_editor, gtk_window_destroy);
519
520 G_OBJECT_CLASS (cc_keyboard_shortcut_dialog_parent_class)->finalize (object);
521 }
522
523 static void
524 cc_keyboard_shortcut_dialog_class_init (CcKeyboardShortcutDialogClass *klass)
525 {
526 GObjectClass *object_class = G_OBJECT_CLASS (klass);
527 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
528
529 object_class->constructed = cc_keyboard_shortcut_dialog_constructed;
530 object_class->finalize = cc_keyboard_shortcut_dialog_finalize;
531
532 gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Escape, 0, "window.close", NULL);
533
534 gtk_widget_class_set_template_from_resource (widget_class,
535 "/org/gnome/control-center/"
536 "keyboard/cc-keyboard-shortcut-dialog.ui");
537
538 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, navigation_view);
539 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, main_page);
540 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, reset_all_button);
541 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, search_entry);
542 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_stack);
543 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_list_page);
544 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_list_box);
545 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, search_result_page);
546 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_results_page);
547
548 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_page);
549 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_stack);
550 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_list_stack);
551 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_custom_shortcut_page);
552 gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, accelerator_size_group);
553
554 gtk_widget_class_bind_template_callback (widget_class, add_custom_shortcut_clicked_cb);
555 gtk_widget_class_bind_template_callback (widget_class, reset_all_clicked_cb);
556 gtk_widget_class_bind_template_callback (widget_class, shortcut_dialog_visible_page_changed_cb);
557 gtk_widget_class_bind_template_callback (widget_class, shortcut_search_entry_changed_cb);
558 gtk_widget_class_bind_template_callback (widget_class, shortcut_search_entry_stopped_cb);
559 gtk_widget_class_bind_template_callback (widget_class, shortcut_section_row_activated_cb);
560 }
561
562 static void
563 cc_keyboard_shortcut_dialog_init (CcKeyboardShortcutDialog *self)
564 {
565 GtkWindow *toplevel;
566
567 gtk_widget_init_template (GTK_WIDGET (self));
568 shortcut_dialog_visible_page_changed_cb (self);
569
570 self->manager = cc_keyboard_manager_new ();
571
572 toplevel = GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self)));
573 self->shortcut_editor = cc_keyboard_shortcut_editor_new (toplevel, self->manager);
574 shortcut_dialog_visible_page_changed_cb (self);
575
576 self->sections = g_list_store_new (G_TYPE_LIST_STORE);
577
578 g_signal_connect_object (self->manager,
579 "shortcut-added",
580 G_CALLBACK (shortcut_added_cb),
581 self, G_CONNECT_SWAPPED);
582 g_signal_connect_object (self->manager,
583 "shortcut-removed",
584 G_CALLBACK (shortcut_removed_cb),
585 self, G_CONNECT_SWAPPED);
586 g_signal_connect_object (self->manager,
587 "shortcuts-loaded",
588 G_CALLBACK (shortcuts_loaded_cb),
589 self, G_CONNECT_SWAPPED);
590
591 cc_keyboard_manager_load_shortcuts (self->manager);
592
593 gtk_list_box_bind_model (self->section_list_box,
594 G_LIST_MODEL (self->sections),
595 shortcut_dialog_row_new,
596 self, NULL);
597 }
598
599 GtkWidget*
600 cc_keyboard_shortcut_dialog_new (void)
601 {
602 return g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_DIALOG, NULL);
603 }
604