1 | /* gtkshortcutswindow.c |
2 | * |
3 | * Copyright (C) 2015 Christian Hergert <christian@hergert.me> |
4 | * |
5 | * This library is free software; you can redistribute it and/or |
6 | * modify it under the terms of the GNU Library General Public License as |
7 | * published by the Free Software Foundation; either version 2 of the |
8 | * License, or (at your option) any later version. |
9 | * |
10 | * This library 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 GNU |
13 | * Library General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU Library General Public |
16 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
17 | */ |
18 | |
19 | #include "config.h" |
20 | |
21 | #include "gtkshortcutswindowprivate.h" |
22 | |
23 | #include "gtkbox.h" |
24 | #include "gtkbuildable.h" |
25 | #include "gtkgrid.h" |
26 | #include "gtkheaderbar.h" |
27 | #include "gtkintl.h" |
28 | #include "gtklabel.h" |
29 | #include "gtklistbox.h" |
30 | #include "gtkmain.h" |
31 | #include "gtkmenubutton.h" |
32 | #include "gtkpopover.h" |
33 | #include "gtkprivate.h" |
34 | #include "gtkscrolledwindow.h" |
35 | #include "gtksearchbar.h" |
36 | #include "gtksearchentry.h" |
37 | #include "gtkshortcutssection.h" |
38 | #include "gtkshortcutsgroup.h" |
39 | #include "gtkshortcutsshortcutprivate.h" |
40 | #include "gtksizegroup.h" |
41 | #include "gtkstack.h" |
42 | #include "gtktogglebutton.h" |
43 | #include "gtktypebuiltins.h" |
44 | #include "gtkwidgetprivate.h" |
45 | |
46 | /** |
47 | * GtkShortcutsWindow: |
48 | * |
49 | * A `GtkShortcutsWindow` shows information about the keyboard shortcuts |
50 | * and gestures of an application. |
51 | * |
52 | * The shortcuts can be grouped, and you can have multiple sections in this |
53 | * window, corresponding to the major modes of your application. |
54 | * |
55 | * Additionally, the shortcuts can be filtered by the current view, to avoid |
56 | * showing information that is not relevant in the current application context. |
57 | * |
58 | * The recommended way to construct a `GtkShortcutsWindow` is with |
59 | * [class@Gtk.Builder], by populating a `GtkShortcutsWindow` with one or |
60 | * more `GtkShortcutsSection` objects, which contain `GtkShortcutsGroups` |
61 | * that in turn contain objects of class `GtkShortcutsShortcut`. |
62 | * |
63 | * # A simple example: |
64 | * |
65 | * ![](gedit-shortcuts.png) |
66 | * |
67 | * This example has as single section. As you can see, the shortcut groups |
68 | * are arranged in columns, and spread across several pages if there are too |
69 | * many to find on a single page. |
70 | * |
71 | * The .ui file for this example can be found [here](https://gitlab.gnome.org/GNOME/gtk/tree/main/demos/gtk-demo/shortcuts-gedit.ui). |
72 | * |
73 | * # An example with multiple views: |
74 | * |
75 | * ![](clocks-shortcuts.png) |
76 | * |
77 | * This example shows a `GtkShortcutsWindow` that has been configured to show only |
78 | * the shortcuts relevant to the "stopwatch" view. |
79 | * |
80 | * The .ui file for this example can be found [here](https://gitlab.gnome.org/GNOME/gtk/tree/main/demos/gtk-demo/shortcuts-clocks.ui). |
81 | * |
82 | * # An example with multiple sections: |
83 | * |
84 | * ![](builder-shortcuts.png) |
85 | * |
86 | * This example shows a `GtkShortcutsWindow` with two sections, "Editor Shortcuts" |
87 | * and "Terminal Shortcuts". |
88 | * |
89 | * The .ui file for this example can be found [here](https://gitlab.gnome.org/GNOME/gtk/tree/main/demos/gtk-demo/shortcuts-builder.ui). |
90 | */ |
91 | |
92 | struct _GtkShortcutsWindow |
93 | { |
94 | GtkWindow parent_instance; |
95 | |
96 | GHashTable *keywords; |
97 | char *initial_section; |
98 | char *last_section_name; |
99 | char *view_name; |
100 | GtkSizeGroup *search_text_group; |
101 | GtkSizeGroup *search_image_group; |
102 | GHashTable *search_items_hash; |
103 | |
104 | GtkStack *stack; |
105 | GtkStack *title_stack; |
106 | GtkMenuButton *; |
107 | GtkSearchBar *search_bar; |
108 | GtkSearchEntry *search_entry; |
109 | GtkHeaderBar *; |
110 | GtkWidget *main_box; |
111 | GtkPopover *popover; |
112 | GtkListBox *list_box; |
113 | GtkBox *search_gestures; |
114 | GtkBox *search_shortcuts; |
115 | |
116 | GtkWindow *window; |
117 | gulong keys_changed_id; |
118 | }; |
119 | |
120 | typedef struct |
121 | { |
122 | GtkWindowClass parent_class; |
123 | |
124 | void (*close) (GtkShortcutsWindow *self); |
125 | void (*search) (GtkShortcutsWindow *self); |
126 | } GtkShortcutsWindowClass; |
127 | |
128 | typedef struct |
129 | { |
130 | GtkShortcutsWindow *self; |
131 | GtkBuilder *builder; |
132 | GQueue *stack; |
133 | char *property_name; |
134 | guint translatable : 1; |
135 | } ViewsParserData; |
136 | |
137 | static void gtk_shortcuts_window_buildable_iface_init (GtkBuildableIface *iface); |
138 | |
139 | |
140 | G_DEFINE_TYPE_WITH_CODE (GtkShortcutsWindow, gtk_shortcuts_window, GTK_TYPE_WINDOW, |
141 | G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, |
142 | gtk_shortcuts_window_buildable_iface_init)) |
143 | |
144 | |
145 | enum { |
146 | CLOSE, |
147 | SEARCH, |
148 | LAST_SIGNAL |
149 | }; |
150 | |
151 | enum { |
152 | PROP_0, |
153 | PROP_SECTION_NAME, |
154 | PROP_VIEW_NAME, |
155 | LAST_PROP |
156 | }; |
157 | |
158 | static GParamSpec *properties[LAST_PROP]; |
159 | static guint signals[LAST_SIGNAL]; |
160 | |
161 | |
162 | static gboolean |
163 | more_than_three_children (GtkWidget *widget) |
164 | { |
165 | GtkWidget *child; |
166 | int i; |
167 | |
168 | for (child = gtk_widget_get_first_child (widget), i = 0; |
169 | child != NULL; |
170 | child = gtk_widget_get_next_sibling (widget: child), i++) |
171 | { |
172 | if (i == 3) |
173 | return TRUE; |
174 | } |
175 | |
176 | return FALSE; |
177 | } |
178 | |
179 | static void |
180 | update_title_stack (GtkShortcutsWindow *self) |
181 | { |
182 | GtkWidget *visible_child; |
183 | |
184 | visible_child = gtk_stack_get_visible_child (stack: self->stack); |
185 | |
186 | if (GTK_IS_SHORTCUTS_SECTION (visible_child)) |
187 | { |
188 | if (more_than_three_children (GTK_WIDGET (self->stack))) |
189 | { |
190 | char *title; |
191 | |
192 | gtk_stack_set_visible_child_name (stack: self->title_stack, name: "sections" ); |
193 | g_object_get (object: visible_child, first_property_name: "title" , &title, NULL); |
194 | gtk_menu_button_set_label (menu_button: self->menu_button, label: title); |
195 | g_free (mem: title); |
196 | } |
197 | else |
198 | { |
199 | gtk_stack_set_visible_child_name (stack: self->title_stack, name: "title" ); |
200 | } |
201 | } |
202 | else if (visible_child != NULL) |
203 | { |
204 | gtk_stack_set_visible_child_name (stack: self->title_stack, name: "search" ); |
205 | } |
206 | } |
207 | |
208 | static void |
209 | gtk_shortcuts_window_add_search_item (GtkWidget *child, gpointer data) |
210 | { |
211 | GtkShortcutsWindow *self = data; |
212 | GtkWidget *item; |
213 | char *accelerator = NULL; |
214 | char *title = NULL; |
215 | char *hash_key = NULL; |
216 | GIcon *icon = NULL; |
217 | gboolean icon_set = FALSE; |
218 | gboolean subtitle_set = FALSE; |
219 | GtkTextDirection direction; |
220 | GtkShortcutType shortcut_type; |
221 | char *action_name = NULL; |
222 | char *subtitle; |
223 | char *str; |
224 | char *keywords; |
225 | |
226 | if (GTK_IS_SHORTCUTS_SHORTCUT (child)) |
227 | { |
228 | GEnumClass *class; |
229 | GEnumValue *value; |
230 | |
231 | g_object_get (object: child, |
232 | first_property_name: "accelerator" , &accelerator, |
233 | "title" , &title, |
234 | "direction" , &direction, |
235 | "icon-set" , &icon_set, |
236 | "subtitle-set" , &subtitle_set, |
237 | "shortcut-type" , &shortcut_type, |
238 | "action-name" , &action_name, |
239 | NULL); |
240 | |
241 | class = G_ENUM_CLASS (g_type_class_ref (GTK_TYPE_SHORTCUT_TYPE)); |
242 | value = g_enum_get_value (enum_class: class, value: shortcut_type); |
243 | |
244 | hash_key = g_strdup_printf (format: "%s-%s-%s" , title, value->value_nick, accelerator); |
245 | |
246 | g_type_class_unref (g_class: class); |
247 | |
248 | if (g_hash_table_contains (hash_table: self->search_items_hash, key: hash_key)) |
249 | { |
250 | g_free (mem: hash_key); |
251 | g_free (mem: title); |
252 | g_free (mem: accelerator); |
253 | return; |
254 | } |
255 | |
256 | g_hash_table_insert (hash_table: self->search_items_hash, key: hash_key, GINT_TO_POINTER (1)); |
257 | |
258 | item = g_object_new (GTK_TYPE_SHORTCUTS_SHORTCUT, |
259 | first_property_name: "accelerator" , accelerator, |
260 | "title" , title, |
261 | "direction" , direction, |
262 | "shortcut-type" , shortcut_type, |
263 | "accel-size-group" , self->search_image_group, |
264 | "title-size-group" , self->search_text_group, |
265 | "action-name" , action_name, |
266 | NULL); |
267 | if (icon_set) |
268 | { |
269 | g_object_get (object: child, first_property_name: "icon" , &icon, NULL); |
270 | g_object_set (object: item, first_property_name: "icon" , icon, NULL); |
271 | g_clear_object (&icon); |
272 | } |
273 | if (subtitle_set) |
274 | { |
275 | g_object_get (object: child, first_property_name: "subtitle" , &subtitle, NULL); |
276 | g_object_set (object: item, first_property_name: "subtitle" , subtitle, NULL); |
277 | g_free (mem: subtitle); |
278 | } |
279 | str = g_strdup_printf (format: "%s %s" , accelerator, title); |
280 | keywords = g_utf8_strdown (str, len: -1); |
281 | |
282 | g_hash_table_insert (hash_table: self->keywords, key: item, value: keywords); |
283 | if (shortcut_type == GTK_SHORTCUT_ACCELERATOR) |
284 | gtk_box_append (GTK_BOX (self->search_shortcuts), child: item); |
285 | else |
286 | gtk_box_append (GTK_BOX (self->search_gestures), child: item); |
287 | |
288 | g_free (mem: title); |
289 | g_free (mem: accelerator); |
290 | g_free (mem: str); |
291 | g_free (mem: action_name); |
292 | } |
293 | else |
294 | { |
295 | GtkWidget *widget; |
296 | |
297 | for (widget = gtk_widget_get_first_child (widget: child); |
298 | widget != NULL; |
299 | widget = gtk_widget_get_next_sibling (widget)) |
300 | gtk_shortcuts_window_add_search_item (child: widget, data: self); |
301 | } |
302 | } |
303 | |
304 | static void |
305 | section_notify_cb (GObject *section, |
306 | GParamSpec *pspec, |
307 | gpointer data) |
308 | { |
309 | GtkShortcutsWindow *self = data; |
310 | |
311 | if (strcmp (s1: pspec->name, s2: "section-name" ) == 0) |
312 | { |
313 | char *name; |
314 | |
315 | g_object_get (object: section, first_property_name: "section-name" , &name, NULL); |
316 | g_object_set (object: gtk_stack_get_page (stack: self->stack, GTK_WIDGET (section)), first_property_name: "name" , name, NULL); |
317 | g_free (mem: name); |
318 | } |
319 | else if (strcmp (s1: pspec->name, s2: "title" ) == 0) |
320 | { |
321 | char *title; |
322 | GtkWidget *label; |
323 | |
324 | label = g_object_get_data (object: section, key: "gtk-shortcuts-title" ); |
325 | g_object_get (object: section, first_property_name: "title" , &title, NULL); |
326 | gtk_label_set_label (GTK_LABEL (label), str: title); |
327 | g_free (mem: title); |
328 | } |
329 | } |
330 | |
331 | static void |
332 | gtk_shortcuts_window_add_section (GtkShortcutsWindow *self, |
333 | GtkShortcutsSection *section) |
334 | { |
335 | GtkListBoxRow *row; |
336 | char *title; |
337 | char *name; |
338 | const char *visible_section; |
339 | GtkWidget *label; |
340 | GtkWidget *child; |
341 | |
342 | for (child = gtk_widget_get_first_child (GTK_WIDGET (section)); |
343 | child != NULL; |
344 | child = gtk_widget_get_next_sibling (widget: child)) |
345 | gtk_shortcuts_window_add_search_item (child, data: self); |
346 | |
347 | g_object_get (object: section, |
348 | first_property_name: "section-name" , &name, |
349 | "title" , &title, |
350 | NULL); |
351 | |
352 | g_signal_connect (section, "notify" , G_CALLBACK (section_notify_cb), self); |
353 | |
354 | if (name == NULL) |
355 | name = g_strdup (str: "shortcuts" ); |
356 | |
357 | gtk_stack_add_titled (stack: self->stack, GTK_WIDGET (section), name, title); |
358 | |
359 | visible_section = gtk_stack_get_visible_child_name (stack: self->stack); |
360 | if (strcmp (s1: visible_section, s2: "internal-search" ) == 0 || |
361 | (self->initial_section && strcmp (s1: self->initial_section, s2: visible_section) == 0)) |
362 | gtk_stack_set_visible_child (stack: self->stack, GTK_WIDGET (section)); |
363 | |
364 | row = g_object_new (GTK_TYPE_LIST_BOX_ROW, |
365 | NULL); |
366 | g_object_set_data (G_OBJECT (row), key: "gtk-shortcuts-section" , data: section); |
367 | label = g_object_new (GTK_TYPE_LABEL, |
368 | first_property_name: "margin-start" , 6, |
369 | "margin-end" , 6, |
370 | "margin-top" , 6, |
371 | "margin-bottom" , 6, |
372 | "label" , title, |
373 | "xalign" , 0.5f, |
374 | NULL); |
375 | g_object_set_data (G_OBJECT (section), key: "gtk-shortcuts-title" , data: label); |
376 | gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), GTK_WIDGET (label)); |
377 | gtk_list_box_insert (GTK_LIST_BOX (self->list_box), GTK_WIDGET (row), position: -1); |
378 | |
379 | update_title_stack (self); |
380 | |
381 | g_free (mem: name); |
382 | g_free (mem: title); |
383 | } |
384 | |
385 | static GtkBuildableIface *parent_buildable_iface; |
386 | |
387 | static void |
388 | gtk_shortcuts_window_buildable_add_child (GtkBuildable *buildable, |
389 | GtkBuilder *builder, |
390 | GObject *child, |
391 | const char *type) |
392 | { |
393 | if (GTK_IS_SHORTCUTS_SECTION (child)) |
394 | gtk_shortcuts_window_add_section (GTK_SHORTCUTS_WINDOW (buildable), |
395 | GTK_SHORTCUTS_SECTION (child)); |
396 | else |
397 | parent_buildable_iface->add_child (buildable, builder, child, type); |
398 | } |
399 | |
400 | static void |
401 | gtk_shortcuts_window_buildable_iface_init (GtkBuildableIface *iface) |
402 | { |
403 | parent_buildable_iface = g_type_interface_peek_parent (g_iface: iface); |
404 | |
405 | iface->add_child = gtk_shortcuts_window_buildable_add_child; |
406 | } |
407 | |
408 | static void |
409 | gtk_shortcuts_window_set_view_name (GtkShortcutsWindow *self, |
410 | const char *view_name) |
411 | { |
412 | GtkWidget *section; |
413 | |
414 | g_free (mem: self->view_name); |
415 | self->view_name = g_strdup (str: view_name); |
416 | |
417 | for (section = gtk_widget_get_first_child (GTK_WIDGET (self->stack)); |
418 | section != NULL; |
419 | section = gtk_widget_get_next_sibling (widget: section)) |
420 | { |
421 | if (GTK_IS_SHORTCUTS_SECTION (section)) |
422 | g_object_set (object: section, first_property_name: "view-name" , self->view_name, NULL); |
423 | } |
424 | } |
425 | |
426 | static void |
427 | gtk_shortcuts_window_set_section_name (GtkShortcutsWindow *self, |
428 | const char *section_name) |
429 | { |
430 | GtkWidget *section = NULL; |
431 | |
432 | g_free (mem: self->initial_section); |
433 | self->initial_section = g_strdup (str: section_name); |
434 | |
435 | if (section_name) |
436 | section = gtk_stack_get_child_by_name (stack: self->stack, name: section_name); |
437 | if (section) |
438 | gtk_stack_set_visible_child (stack: self->stack, child: section); |
439 | } |
440 | |
441 | static void |
442 | update_accels_cb (GtkWidget *widget, |
443 | gpointer data) |
444 | { |
445 | GtkShortcutsWindow *self = data; |
446 | |
447 | if (GTK_IS_SHORTCUTS_SHORTCUT (widget)) |
448 | gtk_shortcuts_shortcut_update_accel (GTK_SHORTCUTS_SHORTCUT (widget), window: self->window); |
449 | else |
450 | { |
451 | GtkWidget *child; |
452 | |
453 | for (child = gtk_widget_get_first_child (GTK_WIDGET (widget)); |
454 | child != NULL; |
455 | child = gtk_widget_get_next_sibling (widget: child )) |
456 | update_accels_cb (widget: child, data: self); |
457 | } |
458 | } |
459 | |
460 | static void |
461 | update_accels_for_actions (GtkShortcutsWindow *self) |
462 | { |
463 | if (self->window) |
464 | { |
465 | GtkWidget *child; |
466 | |
467 | for (child = gtk_widget_get_first_child (GTK_WIDGET (self)); |
468 | child != NULL; |
469 | child = gtk_widget_get_next_sibling (widget: child)) |
470 | update_accels_cb (widget: child, data: self); |
471 | } |
472 | } |
473 | |
474 | static void |
475 | keys_changed_handler (GtkWindow *window, |
476 | GtkShortcutsWindow *self) |
477 | { |
478 | update_accels_for_actions (self); |
479 | } |
480 | |
481 | void |
482 | gtk_shortcuts_window_set_window (GtkShortcutsWindow *self, |
483 | GtkWindow *window) |
484 | { |
485 | if (self->keys_changed_id) |
486 | { |
487 | g_signal_handler_disconnect (instance: self->window, handler_id: self->keys_changed_id); |
488 | self->keys_changed_id = 0; |
489 | } |
490 | |
491 | self->window = window; |
492 | |
493 | if (self->window) |
494 | self->keys_changed_id = g_signal_connect (window, "keys-changed" , |
495 | G_CALLBACK (keys_changed_handler), |
496 | self); |
497 | |
498 | update_accels_for_actions (self); |
499 | } |
500 | |
501 | static void |
502 | gtk_shortcuts_window__list_box__row_activated (GtkShortcutsWindow *self, |
503 | GtkListBoxRow *row, |
504 | GtkListBox *list_box) |
505 | { |
506 | GtkWidget *section; |
507 | |
508 | section = g_object_get_data (G_OBJECT (row), key: "gtk-shortcuts-section" ); |
509 | gtk_stack_set_visible_child (stack: self->stack, child: section); |
510 | gtk_popover_popdown (popover: self->popover); |
511 | } |
512 | |
513 | static gboolean |
514 | hidden_by_direction (GtkWidget *widget) |
515 | { |
516 | if (GTK_IS_SHORTCUTS_SHORTCUT (widget)) |
517 | { |
518 | GtkTextDirection dir; |
519 | |
520 | g_object_get (object: widget, first_property_name: "direction" , &dir, NULL); |
521 | if (dir != GTK_TEXT_DIR_NONE && |
522 | dir != gtk_widget_get_direction (widget)) |
523 | return TRUE; |
524 | } |
525 | |
526 | return FALSE; |
527 | } |
528 | |
529 | static void |
530 | gtk_shortcuts_window__entry__changed (GtkShortcutsWindow *self, |
531 | GtkSearchEntry *search_entry) |
532 | { |
533 | char *downcase = NULL; |
534 | GHashTableIter iter; |
535 | const char *text; |
536 | const char *last_section_name; |
537 | gpointer key; |
538 | gpointer value; |
539 | gboolean has_result; |
540 | |
541 | text = gtk_editable_get_text (GTK_EDITABLE (search_entry)); |
542 | |
543 | if (!text || !*text) |
544 | { |
545 | if (self->last_section_name != NULL) |
546 | { |
547 | gtk_stack_set_visible_child_name (stack: self->stack, name: self->last_section_name); |
548 | return; |
549 | |
550 | } |
551 | } |
552 | |
553 | last_section_name = gtk_stack_get_visible_child_name (stack: self->stack); |
554 | |
555 | if (g_strcmp0 (str1: last_section_name, str2: "internal-search" ) != 0 && |
556 | g_strcmp0 (str1: last_section_name, str2: "no-search-results" ) != 0) |
557 | { |
558 | g_free (mem: self->last_section_name); |
559 | self->last_section_name = g_strdup (str: last_section_name); |
560 | } |
561 | |
562 | downcase = g_utf8_strdown (str: text, len: -1); |
563 | |
564 | g_hash_table_iter_init (iter: &iter, hash_table: self->keywords); |
565 | |
566 | has_result = FALSE; |
567 | while (g_hash_table_iter_next (iter: &iter, key: &key, value: &value)) |
568 | { |
569 | GtkWidget *widget = key; |
570 | const char *keywords = value; |
571 | gboolean match; |
572 | |
573 | if (hidden_by_direction (widget)) |
574 | match = FALSE; |
575 | else |
576 | match = strstr (haystack: keywords, needle: downcase) != NULL; |
577 | |
578 | gtk_widget_set_visible (widget, visible: match); |
579 | has_result |= match; |
580 | } |
581 | |
582 | g_free (mem: downcase); |
583 | |
584 | if (has_result) |
585 | gtk_stack_set_visible_child_name (stack: self->stack, name: "internal-search" ); |
586 | else |
587 | gtk_stack_set_visible_child_name (stack: self->stack, name: "no-search-results" ); |
588 | } |
589 | |
590 | static void |
591 | gtk_shortcuts_window__search_mode__changed (GtkShortcutsWindow *self) |
592 | { |
593 | if (!gtk_search_bar_get_search_mode (bar: self->search_bar)) |
594 | { |
595 | if (self->last_section_name != NULL) |
596 | gtk_stack_set_visible_child_name (stack: self->stack, name: self->last_section_name); |
597 | } |
598 | } |
599 | |
600 | static void |
601 | gtk_shortcuts_window_close (GtkShortcutsWindow *self) |
602 | { |
603 | gtk_window_close (GTK_WINDOW (self)); |
604 | } |
605 | |
606 | static void |
607 | gtk_shortcuts_window_search (GtkShortcutsWindow *self) |
608 | { |
609 | gtk_search_bar_set_search_mode (bar: self->search_bar, TRUE); |
610 | } |
611 | |
612 | static void |
613 | gtk_shortcuts_window_constructed (GObject *object) |
614 | { |
615 | GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; |
616 | |
617 | G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->constructed (object); |
618 | |
619 | if (self->initial_section != NULL) |
620 | gtk_stack_set_visible_child_name (stack: self->stack, name: self->initial_section); |
621 | } |
622 | |
623 | static void |
624 | gtk_shortcuts_window_finalize (GObject *object) |
625 | { |
626 | GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; |
627 | |
628 | g_clear_pointer (&self->keywords, g_hash_table_unref); |
629 | g_clear_pointer (&self->initial_section, g_free); |
630 | g_clear_pointer (&self->view_name, g_free); |
631 | g_clear_pointer (&self->last_section_name, g_free); |
632 | g_clear_pointer (&self->search_items_hash, g_hash_table_unref); |
633 | |
634 | g_clear_object (&self->search_image_group); |
635 | g_clear_object (&self->search_text_group); |
636 | |
637 | G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->finalize (object); |
638 | } |
639 | |
640 | static void |
641 | gtk_shortcuts_window_dispose (GObject *object) |
642 | { |
643 | GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; |
644 | |
645 | if (self->stack) |
646 | g_signal_handlers_disconnect_by_func (self->stack, G_CALLBACK (update_title_stack), self); |
647 | |
648 | gtk_shortcuts_window_set_window (self, NULL); |
649 | |
650 | self->stack = NULL; |
651 | self->search_bar = NULL; |
652 | self->main_box = NULL; |
653 | |
654 | G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->dispose (object); |
655 | } |
656 | |
657 | static void |
658 | gtk_shortcuts_window_get_property (GObject *object, |
659 | guint prop_id, |
660 | GValue *value, |
661 | GParamSpec *pspec) |
662 | { |
663 | GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; |
664 | |
665 | switch (prop_id) |
666 | { |
667 | case PROP_SECTION_NAME: |
668 | { |
669 | GtkWidget *child = gtk_stack_get_visible_child (stack: self->stack); |
670 | |
671 | if (child != NULL) |
672 | { |
673 | char *name = NULL; |
674 | |
675 | g_object_get (object: gtk_stack_get_page (stack: self->stack, child), |
676 | first_property_name: "name" , &name, |
677 | NULL); |
678 | g_value_take_string (value, v_string: name); |
679 | } |
680 | } |
681 | break; |
682 | |
683 | case PROP_VIEW_NAME: |
684 | g_value_set_string (value, v_string: self->view_name); |
685 | break; |
686 | |
687 | default: |
688 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
689 | } |
690 | } |
691 | |
692 | static void |
693 | gtk_shortcuts_window_set_property (GObject *object, |
694 | guint prop_id, |
695 | const GValue *value, |
696 | GParamSpec *pspec) |
697 | { |
698 | GtkShortcutsWindow *self = (GtkShortcutsWindow *)object; |
699 | |
700 | switch (prop_id) |
701 | { |
702 | case PROP_SECTION_NAME: |
703 | gtk_shortcuts_window_set_section_name (self, section_name: g_value_get_string (value)); |
704 | break; |
705 | |
706 | case PROP_VIEW_NAME: |
707 | gtk_shortcuts_window_set_view_name (self, view_name: g_value_get_string (value)); |
708 | break; |
709 | |
710 | default: |
711 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
712 | } |
713 | } |
714 | |
715 | static void |
716 | gtk_shortcuts_window_unmap (GtkWidget *widget) |
717 | { |
718 | GtkShortcutsWindow *self = (GtkShortcutsWindow *)widget; |
719 | |
720 | gtk_search_bar_set_search_mode (bar: self->search_bar, FALSE); |
721 | gtk_editable_set_text (GTK_EDITABLE (self->search_entry), text: "" ); |
722 | |
723 | GTK_WIDGET_CLASS (gtk_shortcuts_window_parent_class)->unmap (widget); |
724 | } |
725 | |
726 | static void |
727 | gtk_shortcuts_window_class_init (GtkShortcutsWindowClass *klass) |
728 | { |
729 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
730 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
731 | |
732 | object_class->constructed = gtk_shortcuts_window_constructed; |
733 | object_class->finalize = gtk_shortcuts_window_finalize; |
734 | object_class->get_property = gtk_shortcuts_window_get_property; |
735 | object_class->set_property = gtk_shortcuts_window_set_property; |
736 | object_class->dispose = gtk_shortcuts_window_dispose; |
737 | |
738 | widget_class->unmap = gtk_shortcuts_window_unmap; |
739 | |
740 | klass->close = gtk_shortcuts_window_close; |
741 | klass->search = gtk_shortcuts_window_search; |
742 | |
743 | /** |
744 | * GtkShortcutsWindow:section-name: |
745 | * |
746 | * The name of the section to show. |
747 | * |
748 | * This should be the section-name of one of the `GtkShortcutsSection` |
749 | * objects that are in this shortcuts window. |
750 | */ |
751 | properties[PROP_SECTION_NAME] = |
752 | g_param_spec_string (name: "section-name" , P_("Section Name" ), P_("Section Name" ), |
753 | default_value: "internal-search" , |
754 | flags: (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
755 | |
756 | /** |
757 | * GtkShortcutsWindow:view-name: |
758 | * |
759 | * The view name by which to filter the contents. |
760 | * |
761 | * This should correspond to the [property@Gtk.ShortcutsGroup:view] |
762 | * property of some of the [class@Gtk.ShortcutsGroup] objects that |
763 | * are inside this shortcuts window. |
764 | * |
765 | * Set this to %NULL to show all groups. |
766 | */ |
767 | properties[PROP_VIEW_NAME] = |
768 | g_param_spec_string (name: "view-name" , P_("View Name" ), P_("View Name" ), |
769 | NULL, |
770 | flags: (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); |
771 | |
772 | g_object_class_install_properties (oclass: object_class, n_pspecs: LAST_PROP, pspecs: properties); |
773 | |
774 | /** |
775 | * GtkShortcutsWindow::close: |
776 | * |
777 | * Emitted when the user uses a keybinding to close the window. |
778 | * |
779 | * This is a [keybinding signal](class.SignalAction.html). |
780 | * |
781 | * The default binding for this signal is the Escape key. |
782 | */ |
783 | signals[CLOSE] = g_signal_new (I_("close" ), |
784 | G_TYPE_FROM_CLASS (klass), |
785 | signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, |
786 | G_STRUCT_OFFSET (GtkShortcutsWindowClass, close), |
787 | NULL, NULL, NULL, |
788 | G_TYPE_NONE, |
789 | n_params: 0); |
790 | |
791 | /** |
792 | * GtkShortcutsWindow::search: |
793 | * |
794 | * Emitted when the user uses a keybinding to start a search. |
795 | * |
796 | * This is a [keybinding signal](class.SignalAction.html). |
797 | * |
798 | * The default binding for this signal is Control-F. |
799 | */ |
800 | signals[SEARCH] = g_signal_new (I_("search" ), |
801 | G_TYPE_FROM_CLASS (klass), |
802 | signal_flags: G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, |
803 | G_STRUCT_OFFSET (GtkShortcutsWindowClass, search), |
804 | NULL, NULL, NULL, |
805 | G_TYPE_NONE, |
806 | n_params: 0); |
807 | |
808 | gtk_widget_class_add_binding_signal (widget_class, |
809 | GDK_KEY_Escape, mods: 0, |
810 | signal: "close" , |
811 | NULL); |
812 | gtk_widget_class_add_binding_signal (widget_class, |
813 | GDK_KEY_f, mods: GDK_CONTROL_MASK, |
814 | signal: "search" , |
815 | NULL); |
816 | |
817 | g_type_ensure (GTK_TYPE_SHORTCUTS_GROUP); |
818 | g_type_ensure (GTK_TYPE_SHORTCUTS_SHORTCUT); |
819 | } |
820 | |
821 | static void |
822 | gtk_shortcuts_window_init (GtkShortcutsWindow *self) |
823 | { |
824 | GtkWidget *search_button; |
825 | GtkBox *box; |
826 | GtkWidget *scroller; |
827 | GtkWidget *label; |
828 | GtkWidget *empty; |
829 | PangoAttrList *attributes; |
830 | |
831 | gtk_window_set_resizable (GTK_WINDOW (self), FALSE); |
832 | |
833 | self->keywords = g_hash_table_new_full (NULL, NULL, NULL, value_destroy_func: g_free); |
834 | self->search_items_hash = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, key_destroy_func: g_free, NULL); |
835 | |
836 | self->search_text_group = gtk_size_group_new (mode: GTK_SIZE_GROUP_HORIZONTAL); |
837 | self->search_image_group = gtk_size_group_new (mode: GTK_SIZE_GROUP_HORIZONTAL); |
838 | |
839 | self->header_bar = GTK_HEADER_BAR (gtk_header_bar_new ()); |
840 | gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (self->header_bar)); |
841 | |
842 | search_button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, |
843 | first_property_name: "icon-name" , "edit-find-symbolic" , |
844 | NULL); |
845 | gtk_header_bar_pack_start (GTK_HEADER_BAR (self->header_bar), child: search_button); |
846 | |
847 | self->main_box = g_object_new (GTK_TYPE_BOX, |
848 | first_property_name: "orientation" , GTK_ORIENTATION_VERTICAL, |
849 | NULL); |
850 | gtk_window_set_child (GTK_WINDOW (self), child: self->main_box); |
851 | |
852 | self->search_bar = g_object_new (GTK_TYPE_SEARCH_BAR, NULL); |
853 | g_object_bind_property (source: self->search_bar, source_property: "search-mode-enabled" , |
854 | target: search_button, target_property: "active" , |
855 | flags: G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); |
856 | gtk_box_append (GTK_BOX (self->main_box), GTK_WIDGET (self->search_bar)); |
857 | gtk_search_bar_set_key_capture_widget (GTK_SEARCH_BAR (self->search_bar), |
858 | GTK_WIDGET (self)); |
859 | |
860 | self->stack = g_object_new (GTK_TYPE_STACK, |
861 | first_property_name: "hexpand" , TRUE, |
862 | "vexpand" , TRUE, |
863 | "hhomogeneous" , TRUE, |
864 | "vhomogeneous" , TRUE, |
865 | "transition-type" , GTK_STACK_TRANSITION_TYPE_CROSSFADE, |
866 | NULL); |
867 | gtk_box_append (GTK_BOX (self->main_box), GTK_WIDGET (self->stack)); |
868 | |
869 | self->title_stack = g_object_new (GTK_TYPE_STACK, |
870 | NULL); |
871 | gtk_header_bar_set_title_widget (bar: self->header_bar, GTK_WIDGET (self->title_stack)); |
872 | |
873 | /* Translators: This is the window title for the shortcuts window in normal mode */ |
874 | label = gtk_label_new (_("Shortcuts" )); |
875 | gtk_widget_add_css_class (widget: label, css_class: "title" ); |
876 | gtk_stack_add_named (stack: self->title_stack, child: label, name: "title" ); |
877 | |
878 | /* Translators: This is the window title for the shortcuts window in search mode */ |
879 | label = gtk_label_new (_("Search Results" )); |
880 | gtk_widget_add_css_class (widget: label, css_class: "title" ); |
881 | gtk_stack_add_named (stack: self->title_stack, child: label, name: "search" ); |
882 | |
883 | self->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON, |
884 | first_property_name: "focus-on-click" , FALSE, |
885 | NULL); |
886 | gtk_widget_add_css_class (GTK_WIDGET (self->menu_button), css_class: "flat" ); |
887 | gtk_stack_add_named (stack: self->title_stack, GTK_WIDGET (self->menu_button), name: "sections" ); |
888 | |
889 | self->popover = g_object_new (GTK_TYPE_POPOVER, |
890 | first_property_name: "position" , GTK_POS_BOTTOM, |
891 | NULL); |
892 | gtk_menu_button_set_popover (menu_button: self->menu_button, GTK_WIDGET (self->popover)); |
893 | |
894 | self->list_box = g_object_new (GTK_TYPE_LIST_BOX, |
895 | first_property_name: "selection-mode" , GTK_SELECTION_NONE, |
896 | NULL); |
897 | g_signal_connect_object (instance: self->list_box, |
898 | detailed_signal: "row-activated" , |
899 | G_CALLBACK (gtk_shortcuts_window__list_box__row_activated), |
900 | gobject: self, |
901 | connect_flags: G_CONNECT_SWAPPED); |
902 | gtk_popover_set_child (GTK_POPOVER (self->popover), GTK_WIDGET (self->list_box)); |
903 | |
904 | self->search_entry = GTK_SEARCH_ENTRY (gtk_search_entry_new ()); |
905 | gtk_search_bar_set_child (GTK_SEARCH_BAR (self->search_bar), GTK_WIDGET (self->search_entry)); |
906 | |
907 | g_object_set (object: self->search_entry, |
908 | /* Translators: This is placeholder text for the search entry in the shortcuts window */ |
909 | first_property_name: "placeholder-text" , _("Search Shortcuts" ), |
910 | "width-chars" , 40, |
911 | NULL); |
912 | g_signal_connect_object (instance: self->search_entry, |
913 | detailed_signal: "search-changed" , |
914 | G_CALLBACK (gtk_shortcuts_window__entry__changed), |
915 | gobject: self, |
916 | connect_flags: G_CONNECT_SWAPPED); |
917 | g_signal_connect_object (instance: self->search_bar, |
918 | detailed_signal: "notify::search-mode-enabled" , |
919 | G_CALLBACK (gtk_shortcuts_window__search_mode__changed), |
920 | gobject: self, |
921 | connect_flags: G_CONNECT_SWAPPED); |
922 | |
923 | scroller = gtk_scrolled_window_new (); |
924 | box = g_object_new (GTK_TYPE_BOX, |
925 | first_property_name: "halign" , GTK_ALIGN_CENTER, |
926 | "orientation" , GTK_ORIENTATION_VERTICAL, |
927 | NULL); |
928 | gtk_widget_add_css_class (GTK_WIDGET (box), css_class: "shortcuts-search-results" ); |
929 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scroller), GTK_WIDGET (box)); |
930 | gtk_stack_add_named (stack: self->stack, child: scroller, name: "internal-search" ); |
931 | |
932 | self->search_shortcuts = g_object_new (GTK_TYPE_BOX, |
933 | first_property_name: "halign" , GTK_ALIGN_CENTER, |
934 | "spacing" , 6, |
935 | "orientation" , GTK_ORIENTATION_VERTICAL, |
936 | NULL); |
937 | gtk_box_append (GTK_BOX (box), GTK_WIDGET (self->search_shortcuts)); |
938 | |
939 | self->search_gestures = g_object_new (GTK_TYPE_BOX, |
940 | first_property_name: "halign" , GTK_ALIGN_CENTER, |
941 | "spacing" , 6, |
942 | "orientation" , GTK_ORIENTATION_VERTICAL, |
943 | NULL); |
944 | gtk_box_append (GTK_BOX (box), GTK_WIDGET (self->search_gestures)); |
945 | |
946 | empty = g_object_new (GTK_TYPE_GRID, |
947 | first_property_name: "row-spacing" , 12, |
948 | "margin-start" , 12, |
949 | "margin-end" , 12, |
950 | "margin-top" , 12, |
951 | "margin-bottom" , 12, |
952 | "hexpand" , TRUE, |
953 | "vexpand" , TRUE, |
954 | "halign" , GTK_ALIGN_CENTER, |
955 | "valign" , GTK_ALIGN_CENTER, |
956 | NULL); |
957 | gtk_widget_add_css_class (widget: empty, css_class: "dim-label" ); |
958 | gtk_grid_attach (GTK_GRID (empty), |
959 | child: g_object_new (GTK_TYPE_IMAGE, |
960 | first_property_name: "icon-name" , "edit-find-symbolic" , |
961 | "pixel-size" , 72, |
962 | NULL), |
963 | column: 0, row: 0, width: 1, height: 1); |
964 | attributes = pango_attr_list_new (); |
965 | pango_attr_list_insert (list: attributes, attr: pango_attr_weight_new (weight: PANGO_WEIGHT_BOLD)); |
966 | pango_attr_list_insert (list: attributes, attr: pango_attr_scale_new (scale_factor: 1.44)); |
967 | label = g_object_new (GTK_TYPE_LABEL, |
968 | first_property_name: "label" , _("No Results Found" ), |
969 | "attributes" , attributes, |
970 | NULL); |
971 | pango_attr_list_unref (list: attributes); |
972 | gtk_grid_attach (GTK_GRID (empty), child: label, column: 0, row: 1, width: 1, height: 1); |
973 | label = g_object_new (GTK_TYPE_LABEL, |
974 | first_property_name: "label" , _("Try a different search" ), |
975 | NULL); |
976 | gtk_grid_attach (GTK_GRID (empty), child: label, column: 0, row: 2, width: 1, height: 1); |
977 | |
978 | gtk_stack_add_named (stack: self->stack, child: empty, name: "no-search-results" ); |
979 | |
980 | g_signal_connect_object (instance: self->stack, detailed_signal: "notify::visible-child" , |
981 | G_CALLBACK (update_title_stack), gobject: self, connect_flags: G_CONNECT_SWAPPED); |
982 | |
983 | } |
984 | |