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 "gtkshortcutswindow.h"
22 #include "gtkscrolledwindow.h"
23 #include "gtkshortcutssection.h"
24 #include "gtkshortcutsgroup.h"
25 #include "gtkshortcutsshortcutprivate.h"
26 #include "gtksearchbar.h"
27 #include "gtksearchentry.h"
28 #include "gtkwidgetprivate.h"
29 #include "gtkprivate.h"
30 #include "gtkintl.h"
31 
32 /**
33  * SECTION:gtkshortcutswindow
34  * @Title: GtkShortcutsWindow
35  * @Short_description: Toplevel which shows help for shortcuts
36  *
37  * A GtkShortcutsWindow shows brief information about the keyboard shortcuts
38  * and gestures of an application. The shortcuts can be grouped, and you can
39  * have multiple sections in this window, corresponding to the major modes of
40  * your application.
41  *
42  * Additionally, the shortcuts can be filtered by the current view, to avoid
43  * showing information that is not relevant in the current application context.
44  *
45  * The recommended way to construct a GtkShortcutsWindow is with GtkBuilder,
46  * by populating a #GtkShortcutsWindow with one or more #GtkShortcutsSection
47  * objects, which contain #GtkShortcutsGroups that in turn contain objects of
48  * class #GtkShortcutsShortcut.
49  *
50  * # A simple example:
51  *
52  * ![](gedit-shortcuts.png)
53  *
54  * This example has as single section. As you can see, the shortcut groups
55  * are arranged in columns, and spread across several pages if there are too
56  * many to find on a single page.
57  *
58  * The .ui file for this example can be found [here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-gedit.ui).
59  *
60  * # An example with multiple views:
61  *
62  * ![](clocks-shortcuts.png)
63  *
64  * This example shows a #GtkShortcutsWindow that has been configured to show only
65  * the shortcuts relevant to the "stopwatch" view.
66  *
67  * The .ui file for this example can be found [here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-clocks.ui).
68  *
69  * # An example with multiple sections:
70  *
71  * ![](builder-shortcuts.png)
72  *
73  * This example shows a #GtkShortcutsWindow with two sections, "Editor Shortcuts"
74  * and "Terminal Shortcuts".
75  *
76  * The .ui file for this example can be found [here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-builder.ui).
77  */
78 
79 typedef struct
80 {
81   GHashTable     *keywords;
82   gchar          *initial_section;
83   gchar          *last_section_name;
84   gchar          *view_name;
85   GtkSizeGroup   *search_text_group;
86   GtkSizeGroup   *search_image_group;
87   GHashTable     *search_items_hash;
88 
89   GtkStack       *stack;
90   GtkStack       *title_stack;
91   GtkMenuButton  *menu_button;
92   GtkLabel       *menu_label;
93   GtkSearchBar   *search_bar;
94   GtkSearchEntry *search_entry;
95   GtkHeaderBar   *header_bar;
96   GtkWidget      *main_box;
97   GtkPopover     *popover;
98   GtkListBox     *list_box;
99   GtkBox         *search_gestures;
100   GtkBox         *search_shortcuts;
101 
102   GtkWindow      *window;
103   gulong          keys_changed_id;
104 } GtkShortcutsWindowPrivate;
105 
106 typedef struct
107 {
108   GtkShortcutsWindow *self;
109   GtkBuilder        *builder;
110   GQueue            *stack;
111   gchar             *property_name;
112   guint              translatable : 1;
113 } ViewsParserData;
114 
115 
116 G_DEFINE_TYPE_WITH_PRIVATE (GtkShortcutsWindow, gtk_shortcuts_window, GTK_TYPE_WINDOW)
117 
118 
119 enum {
120   CLOSE,
121   SEARCH,
122   LAST_SIGNAL
123 };
124 
125 enum {
126   PROP_0,
127   PROP_SECTION_NAME,
128   PROP_VIEW_NAME,
129   LAST_PROP
130 };
131 
132 static GParamSpec *properties[LAST_PROP];
133 static guint signals[LAST_SIGNAL];
134 
135 
136 static gint
number_of_children(GtkContainer * container)137 number_of_children (GtkContainer *container)
138 {
139   GList *children;
140   gint n;
141 
142   children = gtk_container_get_children (container);
143   n = g_list_length (children);
144   g_list_free (children);
145 
146   return n;
147 }
148 
149 static void
update_title_stack(GtkShortcutsWindow * self)150 update_title_stack (GtkShortcutsWindow *self)
151 {
152   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
153   GtkWidget *visible_child;
154 
155   visible_child = gtk_stack_get_visible_child (priv->stack);
156 
157   if (GTK_IS_SHORTCUTS_SECTION (visible_child))
158     {
159       if (number_of_children (GTK_CONTAINER (priv->stack)) > 3)
160         {
161           gchar *title;
162 
163           gtk_stack_set_visible_child_name (priv->title_stack, "sections");
164           g_object_get (visible_child, "title", &title, NULL);
165           gtk_label_set_label (priv->menu_label, title);
166           g_free (title);
167         }
168       else
169         {
170           gtk_stack_set_visible_child_name (priv->title_stack, "title");
171         }
172     }
173   else if (visible_child != NULL)
174     {
175       gtk_stack_set_visible_child_name (priv->title_stack, "search");
176     }
177 }
178 
179 static void
gtk_shortcuts_window_add_search_item(GtkWidget * child,gpointer data)180 gtk_shortcuts_window_add_search_item (GtkWidget *child, gpointer data)
181 {
182   GtkShortcutsWindow *self = data;
183   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
184   GtkWidget *item;
185   gchar *accelerator = NULL;
186   gchar *title = NULL;
187   gchar *hash_key = NULL;
188   GIcon *icon = NULL;
189   gboolean icon_set = FALSE;
190   gboolean subtitle_set = FALSE;
191   GtkTextDirection direction;
192   GtkShortcutType shortcut_type;
193   gchar *action_name = NULL;
194   gchar *subtitle;
195   gchar *str;
196   gchar *keywords;
197 
198   if (GTK_IS_SHORTCUTS_SHORTCUT (child))
199     {
200       GEnumClass *class;
201       GEnumValue *value;
202 
203       g_object_get (child,
204                     "accelerator", &accelerator,
205                     "title", &title,
206                     "direction", &direction,
207                     "icon-set", &icon_set,
208                     "subtitle-set", &subtitle_set,
209                     "shortcut-type", &shortcut_type,
210                     "action-name", &action_name,
211                     NULL);
212 
213       class = G_ENUM_CLASS (g_type_class_ref (GTK_TYPE_SHORTCUT_TYPE));
214       value = g_enum_get_value (class, shortcut_type);
215 
216       hash_key = g_strdup_printf ("%s-%s-%s", title, value->value_nick, accelerator);
217 
218       g_type_class_unref (class);
219 
220       if (g_hash_table_contains (priv->search_items_hash, hash_key))
221         {
222           g_free (hash_key);
223           g_free (title);
224           g_free (accelerator);
225           return;
226         }
227 
228       g_hash_table_insert (priv->search_items_hash, hash_key, GINT_TO_POINTER (1));
229 
230       item = g_object_new (GTK_TYPE_SHORTCUTS_SHORTCUT,
231                            "accelerator", accelerator,
232                            "title", title,
233                            "direction", direction,
234                            "shortcut-type", shortcut_type,
235                            "accel-size-group", priv->search_image_group,
236                            "title-size-group", priv->search_text_group,
237                            "action-name", action_name,
238                            NULL);
239       if (icon_set)
240         {
241           g_object_get (child, "icon", &icon, NULL);
242           g_object_set (item, "icon", icon, NULL);
243           g_clear_object (&icon);
244         }
245       if (subtitle_set)
246         {
247           g_object_get (child, "subtitle", &subtitle, NULL);
248           g_object_set (item, "subtitle", subtitle, NULL);
249           g_free (subtitle);
250         }
251       str = g_strdup_printf ("%s %s", accelerator, title);
252       keywords = g_utf8_strdown (str, -1);
253 
254       g_hash_table_insert (priv->keywords, item, keywords);
255       if (shortcut_type == GTK_SHORTCUT_ACCELERATOR)
256         gtk_container_add (GTK_CONTAINER (priv->search_shortcuts), item);
257       else
258         gtk_container_add (GTK_CONTAINER (priv->search_gestures), item);
259 
260       g_free (title);
261       g_free (accelerator);
262       g_free (str);
263       g_free (action_name);
264     }
265   else if (GTK_IS_CONTAINER (child))
266     {
267       gtk_container_foreach (GTK_CONTAINER (child), gtk_shortcuts_window_add_search_item, self);
268     }
269 }
270 
271 static void
section_notify_cb(GObject * section,GParamSpec * pspec,gpointer data)272 section_notify_cb (GObject    *section,
273                    GParamSpec *pspec,
274                    gpointer    data)
275 {
276   GtkShortcutsWindow *self = data;
277   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
278 
279   if (strcmp (pspec->name, "section-name") == 0)
280     {
281       gchar *name;
282 
283       g_object_get (section, "section-name", &name, NULL);
284       gtk_container_child_set (GTK_CONTAINER (priv->stack), GTK_WIDGET (section), "name", name, NULL);
285       g_free (name);
286     }
287   else if (strcmp (pspec->name, "title") == 0)
288     {
289       gchar *title;
290       GtkWidget *label;
291 
292       label = g_object_get_data (section, "gtk-shortcuts-title");
293       g_object_get (section, "title", &title, NULL);
294       gtk_label_set_label (GTK_LABEL (label), title);
295       g_free (title);
296     }
297 }
298 
299 static void
gtk_shortcuts_window_add_section(GtkShortcutsWindow * self,GtkShortcutsSection * section)300 gtk_shortcuts_window_add_section (GtkShortcutsWindow  *self,
301                                   GtkShortcutsSection *section)
302 {
303   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
304   GtkListBoxRow *row;
305   gchar *title;
306   gchar *name;
307   const gchar *visible_section;
308   GtkWidget *label;
309 
310   gtk_container_foreach (GTK_CONTAINER (section), gtk_shortcuts_window_add_search_item, self);
311 
312   g_object_get (section,
313                 "section-name", &name,
314                 "title", &title,
315                 NULL);
316 
317   g_signal_connect (section, "notify", G_CALLBACK (section_notify_cb), self);
318 
319   if (name == NULL)
320     name = g_strdup ("shortcuts");
321 
322   gtk_stack_add_titled (priv->stack, GTK_WIDGET (section), name, title);
323 
324   visible_section = gtk_stack_get_visible_child_name (priv->stack);
325   if (strcmp (visible_section, "internal-search") == 0 ||
326       (priv->initial_section && strcmp (priv->initial_section, visible_section) == 0))
327     gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (section));
328 
329   row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
330                       "visible", TRUE,
331                       NULL);
332   g_object_set_data (G_OBJECT (row), "gtk-shortcuts-section", section);
333   label = g_object_new (GTK_TYPE_LABEL,
334                         "margin", 6,
335                         "label", title,
336                         "xalign", 0.5f,
337                         "visible", TRUE,
338                         NULL);
339   g_object_set_data (G_OBJECT (section), "gtk-shortcuts-title", label);
340   gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (label));
341   gtk_container_add (GTK_CONTAINER (priv->list_box), GTK_WIDGET (row));
342 
343   update_title_stack (self);
344 
345   g_free (name);
346   g_free (title);
347 }
348 
349 static void
gtk_shortcuts_window_add(GtkContainer * container,GtkWidget * widget)350 gtk_shortcuts_window_add (GtkContainer *container,
351                           GtkWidget    *widget)
352 {
353   GtkShortcutsWindow *self = (GtkShortcutsWindow *)container;
354 
355   if (GTK_IS_SHORTCUTS_SECTION (widget))
356     gtk_shortcuts_window_add_section (self, GTK_SHORTCUTS_SECTION (widget));
357   else
358     g_warning ("Can't add children of type %s to %s",
359                G_OBJECT_TYPE_NAME (widget),
360                G_OBJECT_TYPE_NAME (container));
361 }
362 
363 static void
gtk_shortcuts_window_remove(GtkContainer * container,GtkWidget * widget)364 gtk_shortcuts_window_remove (GtkContainer *container,
365                              GtkWidget    *widget)
366 {
367   GtkShortcutsWindow *self = (GtkShortcutsWindow *)container;
368   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
369 
370   g_signal_handlers_disconnect_by_func (widget, section_notify_cb, self);
371 
372   if (widget == (GtkWidget *)priv->header_bar ||
373       widget == (GtkWidget *)priv->main_box)
374     GTK_CONTAINER_CLASS (gtk_shortcuts_window_parent_class)->remove (container, widget);
375   else
376     gtk_container_remove (GTK_CONTAINER (priv->stack), widget);
377 }
378 
379 static void
gtk_shortcuts_window_forall(GtkContainer * container,gboolean include_internal,GtkCallback callback,gpointer callback_data)380 gtk_shortcuts_window_forall (GtkContainer *container,
381                              gboolean      include_internal,
382                              GtkCallback   callback,
383                              gpointer      callback_data)
384 {
385   GtkShortcutsWindow *self = (GtkShortcutsWindow *)container;
386   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
387 
388   if (include_internal)
389     {
390       GTK_CONTAINER_CLASS (gtk_shortcuts_window_parent_class)->forall (container, include_internal, callback, callback_data);
391     }
392   else
393     {
394       if (priv->stack)
395         {
396           GList *children, *l;
397           GtkWidget *search;
398           GtkWidget *empty;
399 
400           search = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "internal-search");
401           empty = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "no-search-results");
402           children = gtk_container_get_children (GTK_CONTAINER (priv->stack));
403           for (l = children; l; l = l->next)
404             {
405               GtkWidget *child = l->data;
406 
407               if (include_internal ||
408                   (child != search && child != empty))
409                 callback (child, callback_data);
410             }
411           g_list_free (children);
412         }
413     }
414 }
415 
416 static void
gtk_shortcuts_window_set_view_name(GtkShortcutsWindow * self,const gchar * view_name)417 gtk_shortcuts_window_set_view_name (GtkShortcutsWindow *self,
418                                     const gchar        *view_name)
419 {
420   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
421   GList *sections, *l;
422 
423   g_free (priv->view_name);
424   priv->view_name = g_strdup (view_name);
425 
426   sections = gtk_container_get_children (GTK_CONTAINER (priv->stack));
427   for (l = sections; l; l = l->next)
428     {
429       GtkShortcutsSection *section = l->data;
430 
431       if (GTK_IS_SHORTCUTS_SECTION (section))
432         g_object_set (section, "view-name", priv->view_name, NULL);
433     }
434   g_list_free (sections);
435 }
436 
437 static void
gtk_shortcuts_window_set_section_name(GtkShortcutsWindow * self,const gchar * section_name)438 gtk_shortcuts_window_set_section_name (GtkShortcutsWindow *self,
439                                        const gchar        *section_name)
440 {
441   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
442   GtkWidget *section = NULL;
443 
444   g_free (priv->initial_section);
445   priv->initial_section = g_strdup (section_name);
446 
447   if (section_name)
448     section = gtk_stack_get_child_by_name (priv->stack, section_name);
449   if (section)
450     gtk_stack_set_visible_child (priv->stack, section);
451 }
452 
453 static void
update_accels_cb(GtkWidget * widget,gpointer data)454 update_accels_cb (GtkWidget *widget,
455                   gpointer   data)
456 {
457   GtkShortcutsWindow *self = data;
458   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
459 
460   if (GTK_IS_SHORTCUTS_SHORTCUT (widget))
461     gtk_shortcuts_shortcut_update_accel (GTK_SHORTCUTS_SHORTCUT (widget), priv->window);
462   else if (GTK_IS_CONTAINER (widget))
463     gtk_container_foreach (GTK_CONTAINER (widget), update_accels_cb, self);
464 }
465 
466 static void
update_accels_for_actions(GtkShortcutsWindow * self)467 update_accels_for_actions (GtkShortcutsWindow *self)
468 {
469   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
470 
471   if (priv->window)
472     gtk_container_forall (GTK_CONTAINER (self), update_accels_cb, self);
473 }
474 
475 static void
keys_changed_handler(GtkWindow * window,GtkShortcutsWindow * self)476 keys_changed_handler (GtkWindow          *window,
477                       GtkShortcutsWindow *self)
478 {
479   update_accels_for_actions (self);
480 }
481 
482 void
gtk_shortcuts_window_set_window(GtkShortcutsWindow * self,GtkWindow * window)483 gtk_shortcuts_window_set_window (GtkShortcutsWindow *self,
484                                  GtkWindow          *window)
485 {
486   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
487 
488   if (priv->keys_changed_id)
489     {
490       g_signal_handler_disconnect (priv->window, priv->keys_changed_id);
491       priv->keys_changed_id = 0;
492     }
493 
494   priv->window = window;
495 
496   if (priv->window)
497     priv->keys_changed_id = g_signal_connect (window, "keys-changed",
498                                               G_CALLBACK (keys_changed_handler),
499                                               self);
500 
501   update_accels_for_actions (self);
502 }
503 
504 static void
gtk_shortcuts_window__list_box__row_activated(GtkShortcutsWindow * self,GtkListBoxRow * row,GtkListBox * list_box)505 gtk_shortcuts_window__list_box__row_activated (GtkShortcutsWindow *self,
506                                                GtkListBoxRow      *row,
507                                                GtkListBox         *list_box)
508 {
509   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
510   GtkWidget *section;
511 
512   section = g_object_get_data (G_OBJECT (row), "gtk-shortcuts-section");
513   gtk_stack_set_visible_child (priv->stack, section);
514   gtk_popover_popdown (priv->popover);
515 }
516 
517 static gboolean
hidden_by_direction(GtkWidget * widget)518 hidden_by_direction (GtkWidget *widget)
519 {
520   if (GTK_IS_SHORTCUTS_SHORTCUT (widget))
521     {
522       GtkTextDirection dir;
523 
524       g_object_get (widget, "direction", &dir, NULL);
525       if (dir != GTK_TEXT_DIR_NONE &&
526           dir != gtk_widget_get_direction (widget))
527         return TRUE;
528     }
529 
530   return FALSE;
531 }
532 
533 static void
gtk_shortcuts_window__entry__changed(GtkShortcutsWindow * self,GtkSearchEntry * search_entry)534 gtk_shortcuts_window__entry__changed (GtkShortcutsWindow *self,
535                                      GtkSearchEntry      *search_entry)
536 {
537   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
538   gchar *downcase = NULL;
539   GHashTableIter iter;
540   const gchar *text;
541   const gchar *last_section_name;
542   gpointer key;
543   gpointer value;
544   gboolean has_result;
545 
546   text = gtk_entry_get_text (GTK_ENTRY (search_entry));
547 
548   if (!text || !*text)
549     {
550       if (priv->last_section_name != NULL)
551         {
552           gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name);
553           return;
554 
555         }
556     }
557 
558   last_section_name = gtk_stack_get_visible_child_name (priv->stack);
559 
560   if (g_strcmp0 (last_section_name, "internal-search") != 0 &&
561       g_strcmp0 (last_section_name, "no-search-results") != 0)
562     {
563       g_free (priv->last_section_name);
564       priv->last_section_name = g_strdup (last_section_name);
565     }
566 
567   downcase = g_utf8_strdown (text, -1);
568 
569   g_hash_table_iter_init (&iter, priv->keywords);
570 
571   has_result = FALSE;
572   while (g_hash_table_iter_next (&iter, &key, &value))
573     {
574       GtkWidget *widget = key;
575       const gchar *keywords = value;
576       gboolean match;
577 
578       if (hidden_by_direction (widget))
579         match = FALSE;
580       else
581         match = strstr (keywords, downcase) != NULL;
582 
583       gtk_widget_set_visible (widget, match);
584       has_result |= match;
585     }
586 
587   g_free (downcase);
588 
589   if (has_result)
590     gtk_stack_set_visible_child_name (priv->stack, "internal-search");
591   else
592     gtk_stack_set_visible_child_name (priv->stack, "no-search-results");
593 }
594 
595 static void
gtk_shortcuts_window__search_mode__changed(GtkShortcutsWindow * self)596 gtk_shortcuts_window__search_mode__changed (GtkShortcutsWindow *self)
597 {
598   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
599 
600   if (!gtk_search_bar_get_search_mode (priv->search_bar))
601     {
602       if (priv->last_section_name != NULL)
603         gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name);
604     }
605 }
606 
607 static void
gtk_shortcuts_window_close(GtkShortcutsWindow * self)608 gtk_shortcuts_window_close (GtkShortcutsWindow *self)
609 {
610   gtk_window_close (GTK_WINDOW (self));
611 }
612 
613 static void
gtk_shortcuts_window_search(GtkShortcutsWindow * self)614 gtk_shortcuts_window_search (GtkShortcutsWindow *self)
615 {
616   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
617 
618   gtk_search_bar_set_search_mode (priv->search_bar, TRUE);
619 }
620 
621 static void
gtk_shortcuts_window_constructed(GObject * object)622 gtk_shortcuts_window_constructed (GObject *object)
623 {
624   GtkShortcutsWindow *self = (GtkShortcutsWindow *)object;
625   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
626 
627   G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->constructed (object);
628 
629   if (priv->initial_section != NULL)
630     gtk_stack_set_visible_child_name (priv->stack, priv->initial_section);
631 }
632 
633 static void
gtk_shortcuts_window_finalize(GObject * object)634 gtk_shortcuts_window_finalize (GObject *object)
635 {
636   GtkShortcutsWindow *self = (GtkShortcutsWindow *)object;
637   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
638 
639   g_clear_pointer (&priv->keywords, g_hash_table_unref);
640   g_clear_pointer (&priv->initial_section, g_free);
641   g_clear_pointer (&priv->view_name, g_free);
642   g_clear_pointer (&priv->last_section_name, g_free);
643   g_clear_pointer (&priv->search_items_hash, g_hash_table_unref);
644 
645   g_clear_object (&priv->search_image_group);
646   g_clear_object (&priv->search_text_group);
647 
648   G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->finalize (object);
649 }
650 
651 static void
gtk_shortcuts_window_dispose(GObject * object)652 gtk_shortcuts_window_dispose (GObject *object)
653 {
654   GtkShortcutsWindow *self = (GtkShortcutsWindow *)object;
655   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
656 
657   g_signal_handlers_disconnect_by_func (priv->stack, G_CALLBACK (update_title_stack), self);
658 
659   gtk_shortcuts_window_set_window (self, NULL);
660 
661   if (priv->header_bar)
662     {
663       gtk_widget_destroy (GTK_WIDGET (priv->header_bar));
664       priv->header_bar = NULL;
665       priv->popover = NULL;
666     }
667 
668   G_OBJECT_CLASS (gtk_shortcuts_window_parent_class)->dispose (object);
669 
670 #if 0
671   if (priv->main_box)
672     {
673       gtk_widget_destroy (GTK_WIDGET (priv->main_box));
674       priv->main_box = NULL;
675     }
676 #endif
677 }
678 
679 static void
gtk_shortcuts_window_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)680 gtk_shortcuts_window_get_property (GObject    *object,
681                                   guint       prop_id,
682                                   GValue     *value,
683                                   GParamSpec *pspec)
684 {
685   GtkShortcutsWindow *self = (GtkShortcutsWindow *)object;
686   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
687 
688   switch (prop_id)
689     {
690     case PROP_SECTION_NAME:
691       {
692         GtkWidget *child = gtk_stack_get_visible_child (priv->stack);
693 
694         if (child != NULL)
695           {
696             gchar *name = NULL;
697 
698             gtk_container_child_get (GTK_CONTAINER (priv->stack), child,
699                                      "name", &name,
700                                      NULL);
701             g_value_take_string (value, name);
702           }
703       }
704       break;
705 
706     case PROP_VIEW_NAME:
707       g_value_set_string (value, priv->view_name);
708       break;
709 
710     default:
711       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
712     }
713 }
714 
715 static void
gtk_shortcuts_window_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)716 gtk_shortcuts_window_set_property (GObject      *object,
717                                   guint         prop_id,
718                                   const GValue *value,
719                                   GParamSpec   *pspec)
720 {
721   GtkShortcutsWindow *self = (GtkShortcutsWindow *)object;
722 
723   switch (prop_id)
724     {
725     case PROP_SECTION_NAME:
726       gtk_shortcuts_window_set_section_name (self, g_value_get_string (value));
727       break;
728 
729     case PROP_VIEW_NAME:
730       gtk_shortcuts_window_set_view_name (self, g_value_get_string (value));
731       break;
732 
733     default:
734       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
735     }
736 }
737 
738 static void
gtk_shortcuts_window_unmap(GtkWidget * widget)739 gtk_shortcuts_window_unmap (GtkWidget *widget)
740 {
741   GtkShortcutsWindow *self = (GtkShortcutsWindow *)widget;
742   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
743 
744   gtk_search_bar_set_search_mode (priv->search_bar, FALSE);
745 
746   GTK_WIDGET_CLASS (gtk_shortcuts_window_parent_class)->unmap (widget);
747 }
748 
749 static GType
gtk_shortcuts_window_child_type(GtkContainer * container)750 gtk_shortcuts_window_child_type (GtkContainer *container)
751 {
752   return GTK_TYPE_SHORTCUTS_SECTION;
753 }
754 
755 static void
gtk_shortcuts_window_class_init(GtkShortcutsWindowClass * klass)756 gtk_shortcuts_window_class_init (GtkShortcutsWindowClass *klass)
757 {
758   GObjectClass *object_class = G_OBJECT_CLASS (klass);
759   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
760   GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
761   GtkBindingSet *binding_set = gtk_binding_set_by_class (klass);
762 
763   object_class->constructed = gtk_shortcuts_window_constructed;
764   object_class->finalize = gtk_shortcuts_window_finalize;
765   object_class->get_property = gtk_shortcuts_window_get_property;
766   object_class->set_property = gtk_shortcuts_window_set_property;
767   object_class->dispose = gtk_shortcuts_window_dispose;
768 
769   widget_class->unmap = gtk_shortcuts_window_unmap;
770   container_class->add = gtk_shortcuts_window_add;
771   container_class->remove = gtk_shortcuts_window_remove;
772   container_class->child_type = gtk_shortcuts_window_child_type;
773   container_class->forall = gtk_shortcuts_window_forall;
774 
775   klass->close = gtk_shortcuts_window_close;
776   klass->search = gtk_shortcuts_window_search;
777 
778   /**
779    * GtkShortcutsWindow:section-name:
780    *
781    * The name of the section to show.
782    *
783    * This should be the section-name of one of the #GtkShortcutsSection
784    * objects that are in this shortcuts window.
785    */
786   properties[PROP_SECTION_NAME] =
787     g_param_spec_string ("section-name", P_("Section Name"), P_("Section Name"),
788                          "internal-search",
789                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
790 
791   /**
792    * GtkShortcutsWindow:view-name:
793    *
794    * The view name by which to filter the contents.
795    *
796    * This should correspond to the #GtkShortcutsGroup:view property of some of
797    * the #GtkShortcutsGroup objects that are inside this shortcuts window.
798    *
799    * Set this to %NULL to show all groups.
800    */
801   properties[PROP_VIEW_NAME] =
802     g_param_spec_string ("view-name", P_("View Name"), P_("View Name"),
803                          NULL,
804                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
805 
806   g_object_class_install_properties (object_class, LAST_PROP, properties);
807 
808   /**
809    * GtkShortcutsWindow::close:
810    *
811    * The ::close signal is a
812    * [keybinding signal][GtkBindingSignal]
813    * which gets emitted when the user uses a keybinding to close
814    * the window.
815    *
816    * The default binding for this signal is the Escape key.
817    */
818   signals[CLOSE] = g_signal_new (I_("close"),
819                                  G_TYPE_FROM_CLASS (klass),
820                                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
821                                  G_STRUCT_OFFSET (GtkShortcutsWindowClass, close),
822                                  NULL, NULL, NULL,
823                                  G_TYPE_NONE,
824                                  0);
825 
826   /**
827    * GtkShortcutsWindow::search:
828    *
829    * The ::search signal is a
830    * [keybinding signal][GtkBindingSignal]
831    * which gets emitted when the user uses a keybinding to start a search.
832    *
833    * The default binding for this signal is Control-F.
834    */
835   signals[SEARCH] = g_signal_new (I_("search"),
836                                  G_TYPE_FROM_CLASS (klass),
837                                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
838                                  G_STRUCT_OFFSET (GtkShortcutsWindowClass, search),
839                                  NULL, NULL, NULL,
840                                  G_TYPE_NONE,
841                                  0);
842 
843   gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0);
844   gtk_binding_entry_add_signal (binding_set, GDK_KEY_f, GDK_CONTROL_MASK, "search", 0);
845 
846   g_type_ensure (GTK_TYPE_SHORTCUTS_GROUP);
847   g_type_ensure (GTK_TYPE_SHORTCUTS_SHORTCUT);
848 }
849 
850 static gboolean
window_key_press_event_cb(GtkWidget * window,GdkEvent * event,gpointer data)851 window_key_press_event_cb (GtkWidget *window,
852                            GdkEvent  *event,
853                            gpointer   data)
854 {
855   GtkShortcutsWindow *self = GTK_SHORTCUTS_WINDOW (window);
856   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
857 
858   return gtk_search_bar_handle_event (priv->search_bar, event);
859 }
860 
861 static void
gtk_shortcuts_window_init(GtkShortcutsWindow * self)862 gtk_shortcuts_window_init (GtkShortcutsWindow *self)
863 {
864   GtkShortcutsWindowPrivate *priv = gtk_shortcuts_window_get_instance_private (self);
865   GtkToggleButton *search_button;
866   GtkBox *menu_box;
867   GtkBox *box;
868   GtkArrow *arrow;
869   GtkWidget *scroller;
870   GtkWidget *label;
871   GtkWidget *empty;
872   PangoAttrList *attributes;
873 
874   gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
875   gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_DIALOG);
876 
877   g_signal_connect (self, "key-press-event",
878                     G_CALLBACK (window_key_press_event_cb), NULL);
879 
880   priv->keywords = g_hash_table_new_full (NULL, NULL, NULL, g_free);
881   priv->search_items_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
882 
883   priv->search_text_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
884   priv->search_image_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
885 
886   priv->header_bar = g_object_new (GTK_TYPE_HEADER_BAR,
887                                    "show-close-button", TRUE,
888                                    "visible", TRUE,
889                                    NULL);
890   gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (priv->header_bar));
891 
892   search_button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
893                                 "child", g_object_new (GTK_TYPE_IMAGE,
894                                                        "visible", TRUE,
895                                                        "icon-name", "edit-find-symbolic",
896                                                        NULL),
897                                 "visible", TRUE,
898                                 NULL);
899   gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (search_button)), "image-button");
900   gtk_container_add (GTK_CONTAINER (priv->header_bar), GTK_WIDGET (search_button));
901 
902   priv->main_box = g_object_new (GTK_TYPE_BOX,
903                            "orientation", GTK_ORIENTATION_VERTICAL,
904                            "visible", TRUE,
905                            NULL);
906   GTK_CONTAINER_CLASS (gtk_shortcuts_window_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET (priv->main_box));
907 
908   priv->search_bar = g_object_new (GTK_TYPE_SEARCH_BAR,
909                                    "visible", TRUE,
910                                    NULL);
911   g_object_bind_property (priv->search_bar, "search-mode-enabled",
912                           search_button, "active",
913                           G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
914   gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->search_bar));
915 
916   priv->stack = g_object_new (GTK_TYPE_STACK,
917                               "expand", TRUE,
918                               "homogeneous", TRUE,
919                               "transition-type", GTK_STACK_TRANSITION_TYPE_CROSSFADE,
920                               "visible", TRUE,
921                               NULL);
922   gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->stack));
923 
924   priv->title_stack = g_object_new (GTK_TYPE_STACK,
925                                     "visible", TRUE,
926                                     NULL);
927   gtk_header_bar_set_custom_title (priv->header_bar, GTK_WIDGET (priv->title_stack));
928 
929   label = gtk_label_new (_("Shortcuts"));
930   gtk_widget_show (label);
931   gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE);
932   gtk_stack_add_named (priv->title_stack, label, "title");
933 
934   label = gtk_label_new (_("Search Results"));
935   gtk_widget_show (label);
936   gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE);
937   gtk_stack_add_named (priv->title_stack, label, "search");
938 
939   priv->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON,
940                                     "focus-on-click", FALSE,
941                                     "visible", TRUE,
942                                     "relief", GTK_RELIEF_NONE,
943                                     NULL);
944   gtk_stack_add_named (priv->title_stack, GTK_WIDGET (priv->menu_button), "sections");
945 
946   menu_box = g_object_new (GTK_TYPE_BOX,
947                            "orientation", GTK_ORIENTATION_HORIZONTAL,
948                            "spacing", 6,
949                            "visible", TRUE,
950                            NULL);
951   gtk_container_add (GTK_CONTAINER (priv->menu_button), GTK_WIDGET (menu_box));
952 
953   priv->menu_label = g_object_new (GTK_TYPE_LABEL,
954                                    "visible", TRUE,
955                                    NULL);
956   gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (priv->menu_label));
957 
958   G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
959   arrow = g_object_new (GTK_TYPE_ARROW,
960                         "arrow-type", GTK_ARROW_DOWN,
961                         "visible", TRUE,
962                         NULL);
963   gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (arrow));
964   G_GNUC_END_IGNORE_DEPRECATIONS;
965 
966   priv->popover = g_object_new (GTK_TYPE_POPOVER,
967                                 "border-width", 6,
968                                 "relative-to", priv->menu_button,
969                                 "position", GTK_POS_BOTTOM,
970                                 NULL);
971   gtk_menu_button_set_popover (priv->menu_button, GTK_WIDGET (priv->popover));
972 
973   priv->list_box = g_object_new (GTK_TYPE_LIST_BOX,
974                                  "selection-mode", GTK_SELECTION_NONE,
975                                  "visible", TRUE,
976                                  NULL);
977   g_signal_connect_object (priv->list_box,
978                            "row-activated",
979                            G_CALLBACK (gtk_shortcuts_window__list_box__row_activated),
980                            self,
981                            G_CONNECT_SWAPPED);
982   gtk_container_add (GTK_CONTAINER (priv->popover), GTK_WIDGET (priv->list_box));
983 
984   priv->search_entry = GTK_SEARCH_ENTRY (gtk_search_entry_new ());
985   gtk_widget_show (GTK_WIDGET (priv->search_entry));
986   gtk_container_add (GTK_CONTAINER (priv->search_bar), GTK_WIDGET (priv->search_entry));
987   g_object_set (priv->search_entry,
988                 "placeholder-text", _("Search Shortcuts"),
989                 "width-chars", 40,
990                 NULL);
991   g_signal_connect_object (priv->search_entry,
992                            "search-changed",
993                            G_CALLBACK (gtk_shortcuts_window__entry__changed),
994                            self,
995                            G_CONNECT_SWAPPED);
996   g_signal_connect_object (priv->search_bar,
997                            "notify::search-mode-enabled",
998                            G_CALLBACK (gtk_shortcuts_window__search_mode__changed),
999                            self,
1000                            G_CONNECT_SWAPPED);
1001 
1002   scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
1003                            "visible", TRUE,
1004                            NULL);
1005   box = g_object_new (GTK_TYPE_BOX,
1006                       "border-width", 24,
1007                       "halign", GTK_ALIGN_CENTER,
1008                       "spacing", 24,
1009                       "orientation", GTK_ORIENTATION_VERTICAL,
1010                       "visible", TRUE,
1011                       NULL);
1012   gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (box));
1013   gtk_stack_add_named (priv->stack, scroller, "internal-search");
1014 
1015   priv->search_shortcuts = g_object_new (GTK_TYPE_BOX,
1016                                          "halign", GTK_ALIGN_CENTER,
1017                                          "spacing", 6,
1018                                          "orientation", GTK_ORIENTATION_VERTICAL,
1019                                          "visible", TRUE,
1020                                          NULL);
1021   gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_shortcuts));
1022 
1023   priv->search_gestures = g_object_new (GTK_TYPE_BOX,
1024                                         "halign", GTK_ALIGN_CENTER,
1025                                         "spacing", 6,
1026                                         "orientation", GTK_ORIENTATION_VERTICAL,
1027                                         "visible", TRUE,
1028                                         NULL);
1029   gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_gestures));
1030 
1031   empty = g_object_new (GTK_TYPE_GRID,
1032                         "visible", TRUE,
1033                         "row-spacing", 12,
1034                         "margin", 12,
1035                         "hexpand", TRUE,
1036                         "vexpand", TRUE,
1037                         "halign", GTK_ALIGN_CENTER,
1038                         "valign", GTK_ALIGN_CENTER,
1039                         NULL);
1040   gtk_style_context_add_class (gtk_widget_get_style_context (empty), GTK_STYLE_CLASS_DIM_LABEL);
1041   gtk_grid_attach (GTK_GRID (empty),
1042                    g_object_new (GTK_TYPE_IMAGE,
1043                                  "visible", TRUE,
1044                                  "icon-name", "edit-find-symbolic",
1045                                  "pixel-size", 72,
1046                                  NULL),
1047                    0, 0, 1, 1);
1048   attributes = pango_attr_list_new ();
1049   pango_attr_list_insert (attributes, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
1050   pango_attr_list_insert (attributes, pango_attr_scale_new (1.44));
1051   label = g_object_new (GTK_TYPE_LABEL,
1052                         "visible", TRUE,
1053                         "label", _("No Results Found"),
1054                         "attributes", attributes,
1055                         NULL);
1056   pango_attr_list_unref (attributes);
1057   gtk_grid_attach (GTK_GRID (empty), label, 0, 1, 1, 1);
1058   label = g_object_new (GTK_TYPE_LABEL,
1059                         "visible", TRUE,
1060                         "label", _("Try a different search"),
1061                         NULL);
1062   gtk_grid_attach (GTK_GRID (empty), label, 0, 2, 1, 1);
1063 
1064   gtk_stack_add_named (priv->stack, empty, "no-search-results");
1065 
1066   g_signal_connect_object (priv->stack, "notify::visible-child",
1067                            G_CALLBACK (update_title_stack), self, G_CONNECT_SWAPPED);
1068 
1069 }
1070