1 /* dzl-shortcut-manager.c
2  *
3  * Copyright (C) 2016 Christian Hergert <chergert@redhat.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #define G_LOG_DOMAIN "dzl-shortcut-manager.h"
20 
21 #include "config.h"
22 
23 #include <glib/gi18n.h>
24 
25 #include "dzl-debug.h"
26 
27 #include "shortcuts/dzl-shortcut-controller.h"
28 #include "shortcuts/dzl-shortcut-label.h"
29 #include "shortcuts/dzl-shortcut-manager.h"
30 #include "shortcuts/dzl-shortcut-private.h"
31 #include "shortcuts/dzl-shortcut-private.h"
32 #include "shortcuts/dzl-shortcuts-group.h"
33 #include "shortcuts/dzl-shortcuts-section.h"
34 #include "shortcuts/dzl-shortcuts-shortcut.h"
35 #include "util/dzl-gtk.h"
36 #include "util/dzl-util-private.h"
37 
38 typedef struct
39 {
40   /*
41    * This is the currently selected theme by the user (or default until
42    * a theme has been set). You can change this with the
43    * dzl_shortcut_manager_set_theme() function.
44    */
45   DzlShortcutTheme *theme;
46 
47   /*
48    * To avoid re-implementing lots of behavior, we use an internal theme
49    * to store all the built-in keybindings for shortcut controllers. Then,
50    * when loading themes (particularly default), we copy these into that
51    * theme to give the effect of inheritance.
52    */
53   DzlShortcutTheme *internal_theme;
54 
55   /*
56    * This is an array of all of the themes owned by the manager. It does
57    * not, however, contain the @internal_theme instance.
58    */
59   GPtrArray *themes;
60 
61   /*
62    * This is the user directory to save changes to the theme so they can
63    * be reloaded later.
64    */
65   gchar *user_dir;
66 
67   /*
68    * To simplify the process of registering entries, we allow them to be
69    * called from the instance init function. But we only want to see those
70    * entries once. If we did this from class_init(), we'd run into issues
71    * with gtk not being initialized yet (and we need access to keymaps).
72    *
73    * This allows us to keep a unique pointer to know if we've already
74    * dealt with some entries by discarding them up front.
75    */
76   GHashTable *seen_entries;
77 
78   /*
79    * We store a tree of various shortcut data so that we can build the
80    * shortcut window using the registered controller actions. This is
81    * done in dzl_shortcut_manager_add_shortcuts_to_window().
82    */
83   GNode *root;
84 
85   /*
86    * GHashTable to match command/action to a nodedata, useful to generate
87    * a tooltip-text string for a given widget.
88    */
89   GHashTable *command_id_to_node_data;
90 
91   /*
92    * We keep track of the search paths for loading themes here. Each element is
93    * a string containing the path to the file-system resource. If the path
94    * starts with 'resource://" it is assumed a resource embedded in the current
95    * process.
96    */
97   GQueue search_path;
98 
99   /*
100    * Upon making changes to @search path, we need to reload the themes. This
101    * is a GSource identifier to indicate our queued reload request.
102    */
103   guint reload_handler;
104 } DzlShortcutManagerPrivate;
105 
106 enum {
107   PROP_0,
108   PROP_THEME,
109   PROP_THEME_NAME,
110   PROP_USER_DIR,
111   N_PROPS
112 };
113 
114 enum {
115   CHANGED,
116   N_SIGNALS
117 };
118 
119 static void list_model_iface_init               (GListModelInterface *iface);
120 static void initable_iface_init                 (GInitableIface      *iface);
121 static void dzl_shortcut_manager_load_directory (DzlShortcutManager  *self,
122                                                  const gchar         *resource_dir,
123                                                  GCancellable        *cancellable);
124 static void dzl_shortcut_manager_load_resources (DzlShortcutManager  *self,
125                                                  const gchar         *resource_dir,
126                                                  GCancellable        *cancellable);
127 static void dzl_shortcut_manager_merge          (DzlShortcutManager  *self,
128                                                  DzlShortcutTheme    *theme);
129 
130 G_DEFINE_TYPE_WITH_CODE (DzlShortcutManager, dzl_shortcut_manager, G_TYPE_OBJECT,
131                          G_ADD_PRIVATE (DzlShortcutManager)
132                          G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
133                          G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
134 
135 static GParamSpec *properties [N_PROPS];
136 static guint signals [N_SIGNALS];
137 
138 static gboolean
free_node_data(GNode * node,gpointer user_data)139 free_node_data (GNode    *node,
140                 gpointer  user_data)
141 {
142   DzlShortcutNodeData *data = node->data;
143 
144   g_assert (data != NULL);
145   g_assert (DZL_IS_SHORTCUT_NODE_DATA (data));
146 
147   data->magic = 0xAAAAAAAA;
148 
149   g_slice_free (DzlShortcutNodeData, data);
150 
151   return FALSE;
152 }
153 
154 static void
destroy_theme(gpointer data)155 destroy_theme (gpointer data)
156 {
157   g_autoptr(DzlShortcutTheme) theme = data;
158 
159   g_assert (DZL_IS_SHORTCUT_THEME (theme));
160 
161   _dzl_shortcut_theme_set_manager (theme, NULL);
162 }
163 
164 void
dzl_shortcut_manager_reload(DzlShortcutManager * self,GCancellable * cancellable)165 dzl_shortcut_manager_reload (DzlShortcutManager *self,
166                              GCancellable       *cancellable)
167 {
168   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
169   g_autofree gchar *theme_name = NULL;
170   g_autofree gchar *parent_theme_name = NULL;
171   DzlShortcutTheme *theme = NULL;
172   guint previous_len;
173 
174   DZL_ENTRY;
175 
176   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
177   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
178 
179   DZL_TRACE_MSG ("reloading shortcuts, current theme is “%s”",
180                  priv->theme ? dzl_shortcut_theme_get_name (priv->theme) : "internal");
181 
182   /*
183    * If there is a queued reload when we get here, just remove it. When called
184    * from a queued callback, this will already be zeroed.
185    */
186   if (priv->reload_handler != 0)
187     {
188       g_source_remove (priv->reload_handler);
189       priv->reload_handler = 0;
190     }
191 
192   if (priv->theme != NULL)
193     {
194       /*
195        * Keep a copy of the current theme name so that we can return to the
196        * same theme if it is still available. If it has disappeared, then we
197        * will try to fallback to the parent theme.
198        */
199       theme_name = g_strdup (dzl_shortcut_theme_get_name (priv->theme));
200       parent_theme_name = g_strdup (dzl_shortcut_theme_get_parent_name (priv->theme));
201       _dzl_shortcut_theme_detach (priv->theme);
202       g_clear_object (&priv->theme);
203     }
204 
205   /*
206    * Now remove all of our old themes and notify listeners via the GListModel
207    * interface so things like preferences can update. We ensure that we place
208    * a "default" item in the list as we should always have one. We'll append to
209    * it when loading the default theme anyway.
210    *
211    * The default theme always inherits from internal so that we can store
212    * our widget/controller defined shortcuts separate from the mutable default
213    * theme which various applications might want to tweak in their overrides.
214    */
215   previous_len = priv->themes->len;
216   g_ptr_array_remove_range (priv->themes, 0, previous_len);
217   g_ptr_array_add (priv->themes, g_object_new (DZL_TYPE_SHORTCUT_THEME,
218                                                "name", "default",
219                                                "title", _("Default Shortcuts"),
220                                                "parent-name", "internal",
221                                                NULL));
222   _dzl_shortcut_theme_set_manager (g_ptr_array_index (priv->themes, 0), self);
223   g_list_model_items_changed (G_LIST_MODEL (self), 0, previous_len, 1);
224 
225   /*
226    * Okay, now we can go and load all the files in the search path. After
227    * loading a file, the loader code will call dzl_shortcut_manager_merge()
228    * to layer that theme into any base theme which matches the name. This
229    * allows application plugins to simply load a keytheme file to have it
230    * merged into the parent keytheme.
231    */
232   for (const GList *iter = priv->search_path.tail; iter != NULL; iter = iter->prev)
233     {
234       const gchar *directory = iter->data;
235 
236       if (g_str_has_prefix (directory, "resource://"))
237         dzl_shortcut_manager_load_resources (self, directory, cancellable);
238       else
239         dzl_shortcut_manager_load_directory (self, directory, cancellable);
240     }
241 
242   DZL_TRACE_MSG ("Attempting to reset theme to %s",
243                  theme_name ?: parent_theme_name ?: "internal");
244 
245   /* Now try to reapply the same theme if we can find it. */
246   if (theme_name != NULL)
247     {
248       theme = dzl_shortcut_manager_get_theme_by_name (self, theme_name);
249       if (theme != NULL)
250         dzl_shortcut_manager_set_theme (self, theme);
251     }
252 
253   if (priv->theme == NULL && parent_theme_name != NULL)
254     {
255       theme = dzl_shortcut_manager_get_theme_by_name (self, parent_theme_name);
256       if (theme != NULL)
257         dzl_shortcut_manager_set_theme (self, theme);
258     }
259 
260   /* Notify possibly changed properties */
261   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
262   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]);
263 
264   DZL_EXIT;
265 }
266 
267 static gboolean
dzl_shortcut_manager_do_reload(gpointer data)268 dzl_shortcut_manager_do_reload (gpointer data)
269 {
270   DzlShortcutManager *self = data;
271   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
272 
273   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
274 
275   priv->reload_handler = 0;
276   dzl_shortcut_manager_reload (self, NULL);
277   return G_SOURCE_REMOVE;
278 }
279 
280 void
dzl_shortcut_manager_queue_reload(DzlShortcutManager * self)281 dzl_shortcut_manager_queue_reload (DzlShortcutManager *self)
282 {
283   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
284 
285   DZL_ENTRY;
286 
287   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
288 
289   if (priv->reload_handler == 0)
290     {
291       /*
292        * Reload at a high priority to happen immediately, but defer
293        * until getting to the main loop.
294        */
295       priv->reload_handler =
296         gdk_threads_add_idle_full (G_PRIORITY_HIGH,
297                                    dzl_shortcut_manager_do_reload,
298                                    g_object_ref (self),
299                                    g_object_unref);
300     }
301 
302   DZL_EXIT;
303 }
304 
305 static void
dzl_shortcut_manager_finalize(GObject * object)306 dzl_shortcut_manager_finalize (GObject *object)
307 {
308   DzlShortcutManager *self = (DzlShortcutManager *)object;
309   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
310 
311   g_clear_pointer (&priv->command_id_to_node_data, g_hash_table_unref);
312 
313   if (priv->root != NULL)
314     {
315       g_node_traverse (priv->root, G_IN_ORDER, G_TRAVERSE_ALL, -1, free_node_data, NULL);
316       g_node_destroy (priv->root);
317       priv->root = NULL;
318     }
319 
320   if (priv->theme != NULL)
321     {
322       _dzl_shortcut_theme_detach (priv->theme);
323       g_clear_object (&priv->theme);
324     }
325 
326   g_clear_pointer (&priv->seen_entries, g_hash_table_unref);
327   g_clear_pointer (&priv->themes, g_ptr_array_unref);
328   g_clear_pointer (&priv->user_dir, g_free);
329   g_clear_object (&priv->internal_theme);
330 
331   G_OBJECT_CLASS (dzl_shortcut_manager_parent_class)->finalize (object);
332 }
333 
334 static void
dzl_shortcut_manager_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)335 dzl_shortcut_manager_get_property (GObject    *object,
336                                    guint       prop_id,
337                                    GValue     *value,
338                                    GParamSpec *pspec)
339 {
340   DzlShortcutManager *self = (DzlShortcutManager *)object;
341 
342   switch (prop_id)
343     {
344     case PROP_THEME:
345       g_value_set_object (value, dzl_shortcut_manager_get_theme (self));
346       break;
347 
348     case PROP_THEME_NAME:
349       g_value_set_string (value, dzl_shortcut_manager_get_theme_name (self));
350       break;
351 
352     case PROP_USER_DIR:
353       g_value_set_string (value, dzl_shortcut_manager_get_user_dir (self));
354       break;
355 
356     default:
357       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
358     }
359 }
360 
361 static void
dzl_shortcut_manager_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)362 dzl_shortcut_manager_set_property (GObject      *object,
363                                    guint         prop_id,
364                                    const GValue *value,
365                                    GParamSpec   *pspec)
366 {
367   DzlShortcutManager *self = (DzlShortcutManager *)object;
368 
369   switch (prop_id)
370     {
371     case PROP_THEME:
372       dzl_shortcut_manager_set_theme (self, g_value_get_object (value));
373       break;
374 
375     case PROP_THEME_NAME:
376       dzl_shortcut_manager_set_theme_name (self, g_value_get_string (value));
377       break;
378 
379     case PROP_USER_DIR:
380       dzl_shortcut_manager_set_user_dir (self, g_value_get_string (value));
381       break;
382 
383     default:
384       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
385     }
386 }
387 
388 static void
dzl_shortcut_manager_class_init(DzlShortcutManagerClass * klass)389 dzl_shortcut_manager_class_init (DzlShortcutManagerClass *klass)
390 {
391   GObjectClass *object_class = G_OBJECT_CLASS (klass);
392 
393   object_class->finalize = dzl_shortcut_manager_finalize;
394   object_class->get_property = dzl_shortcut_manager_get_property;
395   object_class->set_property = dzl_shortcut_manager_set_property;
396 
397   properties [PROP_THEME] =
398     g_param_spec_object ("theme",
399                          "Theme",
400                          "The current key theme.",
401                          DZL_TYPE_SHORTCUT_THEME,
402                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
403 
404   properties [PROP_THEME_NAME] =
405     g_param_spec_string ("theme-name",
406                          "Theme Name",
407                          "The name of the current theme",
408                          NULL,
409                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
410 
411   properties [PROP_USER_DIR] =
412     g_param_spec_string ("user-dir",
413                          "User Dir",
414                          "The directory for saved user modifications",
415                          NULL,
416                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
417 
418   g_object_class_install_properties (object_class, N_PROPS, properties);
419 
420   signals [CHANGED] =
421     g_signal_new ("changed",
422                   G_TYPE_FROM_CLASS (klass),
423                   G_SIGNAL_RUN_LAST,
424                   0, NULL, NULL, NULL, G_TYPE_NONE, 0);
425 }
426 
427 static guint
shortcut_entry_hash(gconstpointer key)428 shortcut_entry_hash (gconstpointer key)
429 {
430   DzlShortcutEntry *entry = (DzlShortcutEntry *)key;
431   guint command_hash = 0;
432   guint section_hash = 0;
433   guint group_hash = 0;
434   guint title_hash = 0;
435   guint subtitle_hash = 0;
436 
437   if (entry->command != NULL)
438     command_hash = g_str_hash (entry->command);
439 
440   if (entry->section != NULL)
441     section_hash = g_str_hash (entry->section);
442 
443   if (entry->group != NULL)
444     group_hash = g_str_hash (entry->group);
445 
446   if (entry->title != NULL)
447     title_hash = g_str_hash (entry->title);
448 
449   if (entry->subtitle != NULL)
450     subtitle_hash = g_str_hash (entry->subtitle);
451 
452   return (command_hash ^ section_hash ^ group_hash ^ title_hash ^ subtitle_hash);
453 }
454 
455 static void
dzl_shortcut_manager_init(DzlShortcutManager * self)456 dzl_shortcut_manager_init (DzlShortcutManager *self)
457 {
458   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
459 
460   priv->command_id_to_node_data = g_hash_table_new (g_str_hash, g_str_equal);
461   priv->seen_entries = g_hash_table_new (shortcut_entry_hash, NULL);
462   priv->themes = g_ptr_array_new_with_free_func (destroy_theme);
463   priv->root = g_node_new (NULL);
464   priv->internal_theme = g_object_new (DZL_TYPE_SHORTCUT_THEME,
465                                        "name", "internal",
466                                        NULL);
467 }
468 
469 static void
dzl_shortcut_manager_load_directory(DzlShortcutManager * self,const gchar * directory,GCancellable * cancellable)470 dzl_shortcut_manager_load_directory (DzlShortcutManager  *self,
471                                      const gchar         *directory,
472                                      GCancellable        *cancellable)
473 {
474   g_autoptr(GDir) dir = NULL;
475   const gchar *name;
476 
477   DZL_ENTRY;
478 
479   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
480   g_assert (directory != NULL);
481   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
482 
483   DZL_TRACE_MSG ("directory = %s", directory);
484 
485   if (!g_file_test (directory, G_FILE_TEST_IS_DIR))
486     DZL_EXIT;
487 
488   if (NULL == (dir = g_dir_open (directory, 0, NULL)))
489     DZL_EXIT;
490 
491   while (NULL != (name = g_dir_read_name (dir)))
492     {
493       g_autofree gchar *path = g_build_filename (directory, name, NULL);
494       g_autoptr(DzlShortcutTheme) theme = NULL;
495       g_autoptr(GError) local_error = NULL;
496 
497       theme = dzl_shortcut_theme_new (NULL);
498 
499       if (dzl_shortcut_theme_load_from_path (theme, path, cancellable, &local_error))
500         {
501           _dzl_shortcut_theme_set_manager (theme, self);
502           dzl_shortcut_manager_merge (self, theme);
503         }
504       else
505         g_warning ("%s", local_error->message);
506     }
507 
508   DZL_EXIT;
509 }
510 
511 static void
dzl_shortcut_manager_load_resources(DzlShortcutManager * self,const gchar * resource_dir,GCancellable * cancellable)512 dzl_shortcut_manager_load_resources (DzlShortcutManager *self,
513                                      const gchar        *resource_dir,
514                                      GCancellable       *cancellable)
515 {
516   g_auto(GStrv) children = NULL;
517 
518   DZL_ENTRY;
519 
520   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
521   g_assert (resource_dir != NULL);
522   g_assert (g_str_has_prefix (resource_dir, "resource://"));
523   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
524 
525   DZL_TRACE_MSG ("resource_dir = %s", resource_dir);
526 
527   if (g_str_has_prefix (resource_dir, "resource://"))
528     resource_dir += strlen ("resource://");
529 
530   children = g_resources_enumerate_children (resource_dir, 0, NULL);
531 
532   if (children != NULL)
533     {
534       for (guint i = 0; children[i] != NULL; i++)
535         {
536           g_autofree gchar *path = g_build_path ("/", resource_dir, children[i], NULL);
537           g_autoptr(DzlShortcutTheme) theme = NULL;
538           g_autoptr(GError) local_error = NULL;
539           g_autoptr(GBytes) bytes = NULL;
540           const gchar *data;
541           gsize len = 0;
542 
543           if (NULL == (bytes = g_resources_lookup_data (path, 0, NULL)))
544             continue;
545 
546           data = g_bytes_get_data (bytes, &len);
547           theme = dzl_shortcut_theme_new (NULL);
548 
549           if (dzl_shortcut_theme_load_from_data (theme, data, len, &local_error))
550             {
551               _dzl_shortcut_theme_set_manager (theme, self);
552               dzl_shortcut_manager_merge (self, theme);
553             }
554           else
555             g_warning ("%s", local_error->message);
556         }
557     }
558 
559   DZL_EXIT;
560 }
561 
562 static gboolean
dzl_shortcut_manager_initiable_init(GInitable * initable,GCancellable * cancellable,GError ** error)563 dzl_shortcut_manager_initiable_init (GInitable     *initable,
564                                      GCancellable  *cancellable,
565                                      GError       **error)
566 {
567   DzlShortcutManager *self = (DzlShortcutManager *)initable;
568 
569   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
570   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
571 
572   dzl_shortcut_manager_reload (self, cancellable);
573 
574   return TRUE;
575 }
576 
577 static void
initable_iface_init(GInitableIface * iface)578 initable_iface_init (GInitableIface *iface)
579 {
580   iface->init = dzl_shortcut_manager_initiable_init;
581 }
582 
583 /**
584  * dzl_shortcut_manager_get_default:
585  *
586  * Gets the singleton #DzlShortcutManager for the process.
587  *
588  * Returns: (transfer none) (not nullable): An #DzlShortcutManager.
589  */
590 DzlShortcutManager *
dzl_shortcut_manager_get_default(void)591 dzl_shortcut_manager_get_default (void)
592 {
593   static DzlShortcutManager *instance;
594 
595   if (instance == NULL)
596     {
597       instance = g_object_new (DZL_TYPE_SHORTCUT_MANAGER, NULL);
598       g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
599     }
600 
601   return instance;
602 }
603 
604 /**
605  * dzl_shortcut_manager_get_theme:
606  * @self: (nullable): A #DzlShortcutManager or %NULL
607  *
608  * Gets the "theme" property.
609  *
610  * Returns: (transfer none) (not nullable): An #DzlShortcutTheme.
611  */
612 DzlShortcutTheme *
dzl_shortcut_manager_get_theme(DzlShortcutManager * self)613 dzl_shortcut_manager_get_theme (DzlShortcutManager *self)
614 {
615   DzlShortcutManagerPrivate *priv;
616 
617   g_return_val_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self), NULL);
618 
619   if (self == NULL)
620     self = dzl_shortcut_manager_get_default ();
621 
622   priv = dzl_shortcut_manager_get_instance_private (self);
623 
624   if G_LIKELY (priv->theme != NULL)
625     return priv->theme;
626 
627   for (guint i = 0; i < priv->themes->len; i++)
628     {
629       DzlShortcutTheme *theme = g_ptr_array_index (priv->themes, i);
630 
631       if (g_strcmp0 (dzl_shortcut_theme_get_name (theme), "default") == 0)
632         {
633           priv->theme = g_object_ref (theme);
634           return priv->theme;
635         }
636     }
637 
638   return priv->internal_theme;
639 }
640 
641 /**
642  * dzl_shortcut_manager_set_theme:
643  * @self: An #DzlShortcutManager
644  * @theme: (not nullable): An #DzlShortcutTheme
645  *
646  * Sets the theme for the shortcut manager.
647  */
648 void
dzl_shortcut_manager_set_theme(DzlShortcutManager * self,DzlShortcutTheme * theme)649 dzl_shortcut_manager_set_theme (DzlShortcutManager *self,
650                                 DzlShortcutTheme   *theme)
651 {
652   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
653 
654   DZL_ENTRY;
655 
656   g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (self));
657   g_return_if_fail (DZL_IS_SHORTCUT_THEME (theme));
658 
659   /*
660    * It is important that DzlShortcutController instances watch for
661    * notify::theme so that they can reset their state. Otherwise, we
662    * could be transitioning between incorrect contexts.
663    */
664 
665   if (priv->theme != theme)
666     {
667       if (priv->theme != NULL)
668         {
669           _dzl_shortcut_theme_detach (priv->theme);
670           g_clear_object (&priv->theme);
671         }
672 
673       if (theme != NULL)
674         {
675           priv->theme = g_object_ref (theme);
676           _dzl_shortcut_theme_attach (priv->theme);
677         }
678 
679       DZL_TRACE_MSG ("theme set to “%s”",
680                      theme ? dzl_shortcut_theme_get_name (theme) : "internal");
681 
682       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
683       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]);
684     }
685 }
686 
687 /*
688  * dzl_shortcut_manager_run_phase:
689  * @self: a #DzlShortcutManager
690  * @event: the event in question
691  * @chord: the current chord for the toplevel
692  * @phase: the phase (capture, bubble)
693  * @widget: the widget the event was destined for
694  * @focus: the current focus widget
695  *
696  * Runs a particular phase of the event dispatch.
697  *
698  * A phase can be either capture or bubble. Capture tries to deliver the
699  * event starting from the root down to the given widget. Bubble tries to
700  * deliver the event starting from the widget up to the toplevel.
701  *
702  * These two phases allow stealing before or after, depending on the needs
703  * of the keybindings.
704  *
705  * Returns: A #DzlShortcutMatch
706  */
707 static DzlShortcutMatch
dzl_shortcut_manager_run_phase(DzlShortcutManager * self,const GdkEventKey * event,const DzlShortcutChord * chord,int phase,GtkWidget * widget,GtkWidget * focus)708 dzl_shortcut_manager_run_phase (DzlShortcutManager     *self,
709                                 const GdkEventKey      *event,
710                                 const DzlShortcutChord *chord,
711                                 int                     phase,
712                                 GtkWidget              *widget,
713                                 GtkWidget              *focus)
714 {
715   GtkWidget *ancestor = widget;
716   GQueue queue = G_QUEUE_INIT;
717   DzlShortcutMatch ret = DZL_SHORTCUT_MATCH_NONE;
718 
719   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
720   g_assert (event != NULL);
721   g_assert (chord != NULL);
722   g_assert ((phase & DZL_SHORTCUT_PHASE_GLOBAL) == 0);
723   g_assert (GTK_IS_WIDGET (widget));
724   g_assert (GTK_IS_WIDGET (focus));
725 
726   /*
727    * Collect all the widgets that might be needed for this phase and order them
728    * so that we can process from first-to-last. Capture phase is
729    * toplevel-to-widget, and bubble is widget-to-toplevel.  Dispatch only has
730    * the the widget itself.
731    */
732   do
733     {
734       if (phase == DZL_SHORTCUT_PHASE_CAPTURE)
735         g_queue_push_head (&queue, g_object_ref (ancestor));
736       else
737         g_queue_push_tail (&queue, g_object_ref (ancestor));
738       ancestor = gtk_widget_get_parent (ancestor);
739     }
740   while (phase != DZL_SHORTCUT_PHASE_DISPATCH && ancestor != NULL);
741 
742   /*
743    * Now look through our widget chain to find a match to activate.
744    */
745   for (const GList *iter = queue.head; iter; iter = iter->next)
746     {
747       GtkWidget *current = iter->data;
748       DzlShortcutController *controller;
749 
750       controller = dzl_shortcut_controller_try_find (current);
751 
752       if (controller != NULL)
753         {
754           /*
755            * Now try to activate the event using the controller. If we get
756            * any result other than DZL_SHORTCUT_MATCH_NONE, we need to stop
757            * processing and swallow the event.
758            *
759            * Multiple controllers can have a partial match, but if any hits
760            * a partial match, it's undefined behavior to also have a shortcut
761            * which would activate.
762            */
763           ret = _dzl_shortcut_controller_handle (controller, event, chord, phase, focus);
764           if (ret)
765             DZL_GOTO (cleanup);
766         }
767 
768       /*
769        * If we are in the dispatch phase, we will only see our target widget for
770        * the event delivery. Try to dispatch the event and if so we consider
771        * the event handled.
772        */
773       if (phase == DZL_SHORTCUT_PHASE_DISPATCH)
774         {
775           if (gtk_widget_event (current, (GdkEvent *)event))
776             {
777               ret = DZL_SHORTCUT_MATCH_EQUAL;
778               DZL_GOTO (cleanup);
779             }
780         }
781     }
782 
783 cleanup:
784   g_queue_foreach (&queue, (GFunc)g_object_unref, NULL);
785   g_queue_clear (&queue);
786 
787   DZL_RETURN (ret);
788 }
789 
790 static DzlShortcutMatch
dzl_shortcut_manager_run_global(DzlShortcutManager * self,const GdkEventKey * event,const DzlShortcutChord * chord,DzlShortcutPhase phase,DzlShortcutController * root,GtkWidget * widget)791 dzl_shortcut_manager_run_global (DzlShortcutManager     *self,
792                                  const GdkEventKey      *event,
793                                  const DzlShortcutChord *chord,
794                                  DzlShortcutPhase        phase,
795                                  DzlShortcutController  *root,
796                                  GtkWidget              *widget)
797 {
798   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
799   g_assert (event != NULL);
800   g_assert (chord != NULL);
801   g_assert (phase == DZL_SHORTCUT_PHASE_CAPTURE ||
802             phase == DZL_SHORTCUT_PHASE_BUBBLE);
803   g_assert (DZL_IS_SHORTCUT_CONTROLLER (root));
804   g_assert (GTK_WIDGET (widget));
805 
806   /*
807    * The goal of this function is to locate a shortcut within any
808    * controller registered with the root controller (or the root
809    * controller itself) that is registered as a "global shortcut".
810    */
811 
812   phase |= DZL_SHORTCUT_PHASE_GLOBAL;
813 
814   return _dzl_shortcut_controller_handle (root, event, chord, phase, widget);
815 }
816 
817 static gboolean
dzl_shortcut_manager_run_fallbacks(DzlShortcutManager * self,GtkWidget * widget,GtkWidget * toplevel,const DzlShortcutChord * chord)818 dzl_shortcut_manager_run_fallbacks (DzlShortcutManager     *self,
819                                     GtkWidget              *widget,
820                                     GtkWidget              *toplevel,
821                                     const DzlShortcutChord *chord)
822 {
823   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
824   static DzlShortcutChord *inspector_chord;
825 
826   DZL_ENTRY;
827 
828   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
829   g_assert (GTK_IS_WIDGET (widget));
830   g_assert (GTK_IS_WIDGET (toplevel));
831   g_assert (chord != NULL);
832 
833   if (dzl_shortcut_chord_get_length (chord) == 1)
834     {
835       GApplication *app = g_application_get_default ();
836       const gchar *action;
837       GdkModifierType state;
838       guint keyval;
839 
840       dzl_shortcut_chord_get_nth_key (chord, 0, &keyval, &state);
841 
842       /* Special case shift-tab, which is shown as ISO_Left_Tab when
843        * we converted into a Dazzle chord.
844        */
845       if (keyval == GDK_KEY_ISO_Left_Tab && state == 0)
846         {
847           if (gtk_bindings_activate (G_OBJECT (toplevel), keyval, GDK_SHIFT_MASK))
848             DZL_RETURN (TRUE);
849         }
850 
851       /* See if the toplevel activates this, like Tab, etc */
852       if (gtk_bindings_activate (G_OBJECT (toplevel), keyval, state))
853         DZL_RETURN (TRUE);
854 
855       /* See if there is a mnemonic active that should be activated */
856       if (GTK_IS_WINDOW (toplevel) &&
857           gtk_window_mnemonic_activate (GTK_WINDOW (toplevel), keyval, state))
858         DZL_RETURN (TRUE);
859 
860       /*
861        * See if we have something defined for this theme that
862        * can be activated directly.
863        */
864       action = _dzl_shortcut_theme_lookup_action (priv->internal_theme, chord);
865 
866       if (action != NULL)
867         {
868           g_autofree gchar *prefix = NULL;
869           g_autofree gchar *name = NULL;
870           g_autoptr(GVariant) target = NULL;
871 
872           dzl_g_action_name_parse_full (action, &prefix, &name, &target);
873 
874           if (dzl_gtk_widget_action (toplevel, prefix, name, target))
875             DZL_RETURN (TRUE);
876         }
877 
878       /*
879        * If we this is the ctrl+shift+d keybinding to activate the inspector,
880        * then try to see if we should handle that manually.
881        */
882       if G_UNLIKELY (inspector_chord == NULL)
883         inspector_chord = dzl_shortcut_chord_new_from_string ("<ctrl><shift>d");
884       if (dzl_shortcut_chord_equal (chord, inspector_chord))
885         {
886           g_autoptr(GSettings) settings = g_settings_new ("org.gtk.Settings.Debug");
887 
888           if (g_settings_get_boolean (settings, "enable-inspector-keybinding"))
889             {
890               gtk_window_set_interactive_debugging (TRUE);
891               DZL_RETURN (TRUE);
892             }
893         }
894 
895       /*
896        * Now fallback to trying to activate the action within GtkApplication
897        * as the legacy Gtk bindings would do.
898        */
899 
900       if (GTK_IS_APPLICATION (app))
901         {
902           g_autofree gchar *accel = dzl_shortcut_chord_to_string (chord);
903           g_auto(GStrv) actions = NULL;
904 
905           actions = gtk_application_get_actions_for_accel (GTK_APPLICATION (app), accel);
906 
907           if (actions != NULL)
908             {
909               for (guint i = 0; actions[i] != NULL; i++)
910                 {
911                   g_autofree gchar *prefix = NULL;
912                   g_autofree gchar *name = NULL;
913                   g_autoptr(GVariant) param = NULL;
914 
915                   action = actions[i];
916 
917                   if (!dzl_g_action_name_parse_full (action, &prefix, &name, &param))
918                     {
919                       g_warning ("Failed to parse: %s", action);
920                       continue;
921                     }
922 
923                   if (dzl_gtk_widget_action (widget, prefix, name, param))
924                     DZL_RETURN (TRUE);
925                 }
926             }
927         }
928     }
929 
930   DZL_RETURN (FALSE);
931 }
932 
933 /**
934  * dzl_shortcut_manager_handle_event:
935  * @self: (nullable): An #DzlShortcutManager
936  * @toplevel: A #GtkWidget or %NULL.
937  * @event: A #GdkEventKey event to handle.
938  *
939  * This function will try to dispatch @event to the proper widget and
940  * #DzlShortcutContext. If the event is handled, then %TRUE is returned.
941  *
942  * You should call this from #GtkWidget::key-press-event handler in your
943  * #GtkWindow toplevel.
944  *
945  * Returns: %TRUE if the event was handled.
946  */
947 gboolean
dzl_shortcut_manager_handle_event(DzlShortcutManager * self,const GdkEventKey * event,GtkWidget * toplevel)948 dzl_shortcut_manager_handle_event (DzlShortcutManager *self,
949                                    const GdkEventKey  *event,
950                                    GtkWidget          *toplevel)
951 {
952   g_autoptr(DzlShortcutChord) chord = NULL;
953   DzlShortcutController *root;
954   DzlShortcutMatch match;
955   GtkWidget *widget;
956   GtkWidget *focus;
957   gboolean ret = GDK_EVENT_PROPAGATE;
958 
959   DZL_ENTRY;
960 
961   g_return_val_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self), FALSE);
962   g_return_val_if_fail (!toplevel || GTK_IS_WINDOW (toplevel), FALSE);
963   g_return_val_if_fail (event != NULL, FALSE);
964 
965   if (self == NULL)
966     self = dzl_shortcut_manager_get_default ();
967 
968   /* We don't support anything but key-press */
969   if (event->type != GDK_KEY_PRESS)
970     DZL_RETURN (GDK_EVENT_PROPAGATE);
971 
972   /* We might need to discover our toplevel from the event */
973   if (toplevel == NULL)
974     {
975       gpointer user_data;
976 
977       gdk_window_get_user_data (event->window, &user_data);
978       g_return_val_if_fail (GTK_IS_WIDGET (user_data), FALSE);
979 
980       toplevel = gtk_widget_get_toplevel (user_data);
981       g_return_val_if_fail (GTK_IS_WINDOW (toplevel), FALSE);
982     }
983 
984   /* Sanitiy checks */
985   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
986   g_assert (GTK_IS_WINDOW (toplevel));
987   g_assert (event != NULL);
988 
989   /* Synthesize focus as the toplevel if there is none */
990   widget = focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
991   if (widget == NULL)
992     widget = focus = toplevel;
993 
994   /*
995    * We want to push this event into the toplevel controller. If it
996    * gives us back a chord, then we can try to dispatch that up/down
997    * the controller tree.
998    */
999   root = dzl_shortcut_controller_find (toplevel);
1000   chord = _dzl_shortcut_controller_push (root, event);
1001   if (chord == NULL)
1002     DZL_RETURN (GDK_EVENT_PROPAGATE);
1003 
1004 #ifdef DZL_ENABLE_TRACE
1005   {
1006     g_autofree gchar *str = dzl_shortcut_chord_to_string (chord);
1007     DZL_TRACE_MSG ("current chord: %s", str);
1008   }
1009 #endif
1010 
1011   /*
1012    * Now we have our chord/event to dispatch to the individual controllers
1013    * on widgets. We can run through the phases to capture/dispatch/bubble.
1014    */
1015   if ((match = dzl_shortcut_manager_run_global (self, event, chord, DZL_SHORTCUT_PHASE_CAPTURE, root, widget)) ||
1016       (match = dzl_shortcut_manager_run_phase (self, event, chord, DZL_SHORTCUT_PHASE_CAPTURE, widget, focus)) ||
1017       (match = dzl_shortcut_manager_run_phase (self, event, chord, DZL_SHORTCUT_PHASE_DISPATCH, widget, focus)) ||
1018       (match = dzl_shortcut_manager_run_phase (self, event, chord, DZL_SHORTCUT_PHASE_BUBBLE, widget, focus)) ||
1019       (match = dzl_shortcut_manager_run_global (self, event, chord, DZL_SHORTCUT_PHASE_BUBBLE, root, widget)) ||
1020       (match = dzl_shortcut_manager_run_fallbacks (self, widget, toplevel, chord)))
1021     ret = GDK_EVENT_STOP;
1022 
1023   DZL_TRACE_MSG ("match = %d", match);
1024 
1025   /* No match, clear our current chord */
1026   if (match != DZL_SHORTCUT_MATCH_PARTIAL)
1027     _dzl_shortcut_controller_clear (root);
1028 
1029   DZL_RETURN (ret);
1030 }
1031 
1032 const gchar *
dzl_shortcut_manager_get_theme_name(DzlShortcutManager * self)1033 dzl_shortcut_manager_get_theme_name (DzlShortcutManager *self)
1034 {
1035   DzlShortcutTheme *theme;
1036 
1037   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL);
1038 
1039   theme = dzl_shortcut_manager_get_theme (self);
1040 
1041   g_return_val_if_fail (DZL_IS_SHORTCUT_THEME (theme), NULL);
1042 
1043   return dzl_shortcut_theme_get_name (theme);
1044 }
1045 
1046 void
dzl_shortcut_manager_set_theme_name(DzlShortcutManager * self,const gchar * name)1047 dzl_shortcut_manager_set_theme_name (DzlShortcutManager *self,
1048                                      const gchar        *name)
1049 {
1050   DzlShortcutManagerPrivate *priv;
1051 
1052   if (self == NULL)
1053     self = dzl_shortcut_manager_get_default ();
1054 
1055   priv = dzl_shortcut_manager_get_instance_private (self);
1056 
1057   if (name == NULL)
1058     name = "default";
1059 
1060   for (guint i = 0; i < priv->themes->len; i++)
1061     {
1062       DzlShortcutTheme *theme = g_ptr_array_index (priv->themes, i);
1063       const gchar *theme_name = dzl_shortcut_theme_get_name (theme);
1064 
1065       if (g_strcmp0 (name, theme_name) == 0)
1066         {
1067           dzl_shortcut_manager_set_theme (self, theme);
1068           return;
1069         }
1070     }
1071 
1072   g_warning ("No such shortcut theme “%s”", name);
1073 }
1074 
1075 static guint
dzl_shortcut_manager_get_n_items(GListModel * model)1076 dzl_shortcut_manager_get_n_items (GListModel *model)
1077 {
1078   DzlShortcutManager *self = (DzlShortcutManager *)model;
1079   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1080 
1081   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), 0);
1082 
1083   return priv->themes->len;
1084 }
1085 
1086 static GType
dzl_shortcut_manager_get_item_type(GListModel * model)1087 dzl_shortcut_manager_get_item_type (GListModel *model)
1088 {
1089   return DZL_TYPE_SHORTCUT_THEME;
1090 }
1091 
1092 static gpointer
dzl_shortcut_manager_get_item(GListModel * model,guint position)1093 dzl_shortcut_manager_get_item (GListModel *model,
1094                                guint       position)
1095 {
1096   DzlShortcutManager *self = (DzlShortcutManager *)model;
1097   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1098 
1099   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL);
1100   g_return_val_if_fail (position < priv->themes->len, NULL);
1101 
1102   return g_object_ref (g_ptr_array_index (priv->themes, position));
1103 }
1104 
1105 static void
list_model_iface_init(GListModelInterface * iface)1106 list_model_iface_init (GListModelInterface *iface)
1107 {
1108   iface->get_n_items = dzl_shortcut_manager_get_n_items;
1109   iface->get_item_type = dzl_shortcut_manager_get_item_type;
1110   iface->get_item = dzl_shortcut_manager_get_item;
1111 }
1112 
1113 const gchar *
dzl_shortcut_manager_get_user_dir(DzlShortcutManager * self)1114 dzl_shortcut_manager_get_user_dir (DzlShortcutManager *self)
1115 {
1116   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1117 
1118   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL);
1119 
1120   if (priv->user_dir == NULL)
1121     {
1122       priv->user_dir = g_build_filename (g_get_user_data_dir (),
1123                                          g_get_prgname (),
1124                                          NULL);
1125     }
1126 
1127   return priv->user_dir;
1128 }
1129 
1130 void
dzl_shortcut_manager_set_user_dir(DzlShortcutManager * self,const gchar * user_dir)1131 dzl_shortcut_manager_set_user_dir (DzlShortcutManager *self,
1132                                    const gchar        *user_dir)
1133 {
1134   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1135 
1136   g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (self));
1137 
1138   if (g_strcmp0 (user_dir, priv->user_dir) != 0)
1139     {
1140       g_free (priv->user_dir);
1141       priv->user_dir = g_strdup (user_dir);
1142       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USER_DIR]);
1143     }
1144 }
1145 
1146 void
dzl_shortcut_manager_remove_search_path(DzlShortcutManager * self,const gchar * directory)1147 dzl_shortcut_manager_remove_search_path (DzlShortcutManager *self,
1148                                          const gchar        *directory)
1149 {
1150   DzlShortcutManagerPrivate *priv;
1151 
1152   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1153   g_return_if_fail (directory != NULL);
1154 
1155   if (self == NULL)
1156     self = dzl_shortcut_manager_get_default ();
1157 
1158   priv = dzl_shortcut_manager_get_instance_private (self);
1159 
1160   for (GList *iter = priv->search_path.head; iter != NULL; iter = iter->next)
1161     {
1162       gchar *path = iter->data;
1163 
1164       if (g_strcmp0 (path, directory) == 0)
1165         {
1166           /* TODO: Remove any merged keybindings */
1167 
1168           g_queue_delete_link (&priv->search_path, iter);
1169           g_free (path);
1170 
1171           dzl_shortcut_manager_queue_reload (self);
1172 
1173           break;
1174         }
1175     }
1176 }
1177 
1178 void
dzl_shortcut_manager_append_search_path(DzlShortcutManager * self,const gchar * directory)1179 dzl_shortcut_manager_append_search_path (DzlShortcutManager *self,
1180                                          const gchar        *directory)
1181 {
1182   DzlShortcutManagerPrivate *priv;
1183 
1184   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1185   g_return_if_fail (directory != NULL);
1186 
1187   if (self == NULL)
1188     self = dzl_shortcut_manager_get_default ();
1189 
1190   priv = dzl_shortcut_manager_get_instance_private (self);
1191 
1192   g_queue_push_tail (&priv->search_path, g_strdup (directory));
1193 
1194   dzl_shortcut_manager_queue_reload (self);
1195 }
1196 
1197 void
dzl_shortcut_manager_prepend_search_path(DzlShortcutManager * self,const gchar * directory)1198 dzl_shortcut_manager_prepend_search_path (DzlShortcutManager *self,
1199                                           const gchar        *directory)
1200 {
1201   DzlShortcutManagerPrivate *priv;
1202 
1203   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1204   g_return_if_fail (directory != NULL);
1205 
1206   if (self == NULL)
1207     self = dzl_shortcut_manager_get_default ();
1208 
1209   priv = dzl_shortcut_manager_get_instance_private (self);
1210 
1211   g_queue_push_head (&priv->search_path, g_strdup (directory));
1212 
1213   dzl_shortcut_manager_queue_reload (self);
1214 }
1215 
1216 /**
1217  * dzl_shortcut_manager_get_search_path:
1218  * @self: A #DzlShortcutManager
1219  *
1220  * This function will get the list of search path entries. These are used to
1221  * load themes for the application. You should set this search path for
1222  * themes before calling g_initable_init() on the search manager.
1223  *
1224  * Returns: (transfer none) (element-type utf8): A #GList containing each of
1225  *   the search path items used to load shortcut themes.
1226  */
1227 const GList *
dzl_shortcut_manager_get_search_path(DzlShortcutManager * self)1228 dzl_shortcut_manager_get_search_path (DzlShortcutManager *self)
1229 {
1230   DzlShortcutManagerPrivate *priv;
1231 
1232   if (self == NULL)
1233     self = dzl_shortcut_manager_get_default ();
1234 
1235   priv = dzl_shortcut_manager_get_instance_private (self);
1236 
1237   return priv->search_path.head;
1238 }
1239 
1240 static GNode *
dzl_shortcut_manager_find_child(DzlShortcutManager * self,GNode * parent,DzlShortcutNodeType type,const gchar * name)1241 dzl_shortcut_manager_find_child (DzlShortcutManager  *self,
1242                                  GNode               *parent,
1243                                  DzlShortcutNodeType  type,
1244                                  const gchar         *name)
1245 {
1246   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
1247   g_assert (parent != NULL);
1248   g_assert (type != 0);
1249   g_assert (name != NULL);
1250 
1251   for (GNode *iter = parent->children; iter != NULL; iter = iter->next)
1252     {
1253       DzlShortcutNodeData *data = iter->data;
1254 
1255       g_assert (DZL_IS_SHORTCUT_NODE_DATA (data));
1256 
1257       if (data->type == type && data->name == name)
1258         return iter;
1259     }
1260 
1261   return NULL;
1262 }
1263 
1264 static GNode *
dzl_shortcut_manager_get_group(DzlShortcutManager * self,const gchar * section,const gchar * group)1265 dzl_shortcut_manager_get_group (DzlShortcutManager *self,
1266                                 const gchar        *section,
1267                                 const gchar        *group)
1268 {
1269   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1270   DzlShortcutNodeData *data;
1271   GNode *parent;
1272   GNode *node;
1273 
1274   g_assert (DZL_IS_SHORTCUT_MANAGER (self));
1275   g_assert (section != NULL);
1276   g_assert (group != NULL);
1277 
1278   node = dzl_shortcut_manager_find_child (self, priv->root, DZL_SHORTCUT_NODE_SECTION, section);
1279 
1280   if (node == NULL)
1281     {
1282       data = g_slice_new0 (DzlShortcutNodeData);
1283       data->magic = DZL_SHORTCUT_NODE_DATA_MAGIC;
1284       data->type = DZL_SHORTCUT_NODE_SECTION;
1285       data->name = g_intern_string (section);
1286       data->title = g_intern_string (section);
1287       data->subtitle = NULL;
1288 
1289       node = g_node_append_data (priv->root, data);
1290     }
1291 
1292   parent = node;
1293 
1294   node = dzl_shortcut_manager_find_child (self, parent, DZL_SHORTCUT_NODE_GROUP, group);
1295 
1296   if (node == NULL)
1297     {
1298       data = g_slice_new0 (DzlShortcutNodeData);
1299       data->magic = DZL_SHORTCUT_NODE_DATA_MAGIC;
1300       data->type = DZL_SHORTCUT_NODE_GROUP;
1301       data->name = g_intern_string (group);
1302       data->title = g_intern_string (group);
1303       data->subtitle = NULL;
1304 
1305       node = g_node_append_data (parent, data);
1306     }
1307 
1308   g_assert (node != NULL);
1309   g_assert (DZL_IS_SHORTCUT_NODE_DATA (node->data));
1310 
1311   return node;
1312 }
1313 
1314 void
dzl_shortcut_manager_add_action(DzlShortcutManager * self,const gchar * detailed_action_name,const gchar * section,const gchar * group,const gchar * title,const gchar * subtitle)1315 dzl_shortcut_manager_add_action (DzlShortcutManager *self,
1316                                  const gchar        *detailed_action_name,
1317                                  const gchar        *section,
1318                                  const gchar        *group,
1319                                  const gchar        *title,
1320                                  const gchar        *subtitle)
1321 {
1322   DzlShortcutManagerPrivate *priv;
1323   DzlShortcutNodeData *data;
1324   GNode *parent;
1325 
1326   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1327   g_return_if_fail (detailed_action_name != NULL);
1328   g_return_if_fail (title != NULL);
1329 
1330   if (self == NULL)
1331     self = dzl_shortcut_manager_get_default ();
1332 
1333   priv = dzl_shortcut_manager_get_instance_private (self);
1334 
1335   section = g_intern_string (section);
1336   group = g_intern_string (group);
1337   title = g_intern_string (title);
1338   subtitle = g_intern_string (subtitle);
1339 
1340   parent = dzl_shortcut_manager_get_group (self, section, group);
1341 
1342   g_assert (parent != NULL);
1343 
1344   data = g_slice_new0 (DzlShortcutNodeData);
1345   data->magic = DZL_SHORTCUT_NODE_DATA_MAGIC;
1346   data->type = DZL_SHORTCUT_NODE_ACTION;
1347   data->name = g_intern_string (detailed_action_name);
1348   data->title = title;
1349   data->subtitle = subtitle;
1350 
1351   g_node_append_data (parent, data);
1352 
1353   g_hash_table_insert (priv->command_id_to_node_data, (gpointer)data->name, data);
1354 
1355   g_signal_emit (self, signals [CHANGED], 0);
1356 }
1357 
1358 void
dzl_shortcut_manager_add_command(DzlShortcutManager * self,const gchar * command,const gchar * section,const gchar * group,const gchar * title,const gchar * subtitle)1359 dzl_shortcut_manager_add_command (DzlShortcutManager *self,
1360                                   const gchar        *command,
1361                                   const gchar        *section,
1362                                   const gchar        *group,
1363                                   const gchar        *title,
1364                                   const gchar        *subtitle)
1365 {
1366   DzlShortcutManagerPrivate *priv;
1367   DzlShortcutNodeData *data;
1368   GNode *parent;
1369 
1370   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1371   g_return_if_fail (command != NULL);
1372   g_return_if_fail (title != NULL);
1373 
1374   if (self == NULL)
1375     self = dzl_shortcut_manager_get_default ();
1376 
1377   priv = dzl_shortcut_manager_get_instance_private (self);
1378 
1379   section = g_intern_string (section);
1380   group = g_intern_string (group);
1381   title = g_intern_string (title);
1382   subtitle = g_intern_string (subtitle);
1383 
1384   parent = dzl_shortcut_manager_get_group (self, section, group);
1385 
1386   g_assert (parent != NULL);
1387 
1388   data = g_slice_new0 (DzlShortcutNodeData);
1389   data->magic = DZL_SHORTCUT_NODE_DATA_MAGIC;
1390   data->type = DZL_SHORTCUT_NODE_COMMAND;
1391   data->name = g_intern_string (command);
1392   data->title = title;
1393   data->subtitle = subtitle;
1394 
1395   g_node_append_data (parent, data);
1396 
1397   g_hash_table_insert (priv->command_id_to_node_data, (gpointer)data->name, data);
1398 
1399   g_signal_emit (self, signals [CHANGED], 0);
1400 }
1401 
1402 static DzlShortcutsShortcut *
create_shortcut(const DzlShortcutChord * chord,const gchar * title,const gchar * subtitle)1403 create_shortcut (const DzlShortcutChord *chord,
1404                  const gchar            *title,
1405                  const gchar            *subtitle)
1406 {
1407   g_autofree gchar *accel = dzl_shortcut_chord_to_string (chord);
1408 
1409   return g_object_new (DZL_TYPE_SHORTCUTS_SHORTCUT,
1410                        "accelerator", accel,
1411                        "subtitle", subtitle,
1412                        "title", title,
1413                        "visible", TRUE,
1414                        NULL);
1415 }
1416 
1417 /**
1418  * dzl_shortcut_manager_add_shortcuts_to_window:
1419  * @self: A #DzlShortcutManager
1420  * @window: A #DzlShortcutsWindow
1421  *
1422  * Adds shortcuts registered with the #DzlShortcutManager to the
1423  * #DzlShortcutsWindow.
1424  */
1425 void
dzl_shortcut_manager_add_shortcuts_to_window(DzlShortcutManager * self,DzlShortcutsWindow * window)1426 dzl_shortcut_manager_add_shortcuts_to_window (DzlShortcutManager *self,
1427                                               DzlShortcutsWindow *window)
1428 {
1429   DzlShortcutManagerPrivate *priv;
1430   DzlShortcutTheme *theme;
1431   GNode *parent;
1432 
1433   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1434   g_return_if_fail (DZL_IS_SHORTCUTS_WINDOW (window));
1435 
1436   if (self == NULL)
1437     self = dzl_shortcut_manager_get_default ();
1438 
1439   priv = dzl_shortcut_manager_get_instance_private (self);
1440 
1441   theme = dzl_shortcut_manager_get_theme (self);
1442 
1443   /*
1444    * The GNode tree is in four levels. priv->root is the root of the tree and
1445    * contains no data items itself. It is just our stable root. The children
1446    * of priv->root are our section nodes. Each section node has group nodes
1447    * as children. Finally, the shortcut nodes are the leaves.
1448    */
1449 
1450   parent = priv->root;
1451 
1452   for (const GNode *sections = parent->children; sections != NULL; sections = sections->next)
1453     {
1454       DzlShortcutNodeData *section_data = sections->data;
1455       DzlShortcutsSection *section;
1456 
1457       g_assert (DZL_IS_SHORTCUT_NODE_DATA (section_data));
1458 
1459       section = g_object_new (DZL_TYPE_SHORTCUTS_SECTION,
1460                               "title", section_data->title,
1461                               "section-name", section_data->title,
1462                               "visible", TRUE,
1463                               NULL);
1464 
1465       for (const GNode *groups = sections->children; groups != NULL; groups = groups->next)
1466         {
1467           DzlShortcutNodeData *group_data = groups->data;
1468           DzlShortcutsGroup *group;
1469 
1470           g_assert (DZL_IS_SHORTCUT_NODE_DATA (group_data));
1471 
1472           group = g_object_new (DZL_TYPE_SHORTCUTS_GROUP,
1473                                 "title", group_data->title,
1474                                 "visible", TRUE,
1475                                 NULL);
1476 
1477           for (const GNode *iter = groups->children; iter != NULL; iter = iter->next)
1478             {
1479               DzlShortcutNodeData *data = iter->data;
1480               const DzlShortcutChord *chord = NULL;
1481               DzlShortcutsShortcut *shortcut;
1482 
1483               g_assert (DZL_IS_SHORTCUT_NODE_DATA (data));
1484 
1485               if (data->type == DZL_SHORTCUT_NODE_ACTION)
1486                 chord = dzl_shortcut_theme_get_chord_for_action (theme, data->name);
1487               else if (data->type == DZL_SHORTCUT_NODE_COMMAND)
1488                 chord = dzl_shortcut_theme_get_chord_for_command (theme, data->name);
1489 
1490               shortcut = create_shortcut (chord, data->title, data->subtitle);
1491               gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (shortcut));
1492             }
1493 
1494           gtk_container_add (GTK_CONTAINER (section), GTK_WIDGET (group));
1495         }
1496 
1497       gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (section));
1498     }
1499 }
1500 
1501 GNode *
_dzl_shortcut_manager_get_root(DzlShortcutManager * self)1502 _dzl_shortcut_manager_get_root (DzlShortcutManager *self)
1503 {
1504   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1505 
1506   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL);
1507 
1508   return priv->root;
1509 }
1510 
1511 /**
1512  * dzl_shortcut_manager_add_shortcut_entries:
1513  * @self: (nullable): a #DzlShortcutManager or %NULL for the default
1514  * @shortcuts: (array length=n_shortcuts): shortcuts to add
1515  * @n_shortcuts: the number of entries in @shortcuts
1516  * @translation_domain: (nullable): the gettext domain to use for translations
1517  *
1518  * This method will add @shortcuts to the #DzlShortcutManager.
1519  *
1520  * This provides a simple way for widgets to add their shortcuts to the manager
1521  * so that they may be overriden by themes or the end user.
1522  */
1523 void
dzl_shortcut_manager_add_shortcut_entries(DzlShortcutManager * self,const DzlShortcutEntry * shortcuts,guint n_shortcuts,const gchar * translation_domain)1524 dzl_shortcut_manager_add_shortcut_entries (DzlShortcutManager     *self,
1525                                            const DzlShortcutEntry *shortcuts,
1526                                            guint                   n_shortcuts,
1527                                            const gchar            *translation_domain)
1528 {
1529   DzlShortcutManagerPrivate *priv;
1530 
1531   g_return_if_fail (!self || DZL_IS_SHORTCUT_MANAGER (self));
1532   g_return_if_fail (shortcuts != NULL || n_shortcuts == 0);
1533 
1534   if (self == NULL)
1535     self = dzl_shortcut_manager_get_default ();
1536 
1537   priv = dzl_shortcut_manager_get_instance_private (self);
1538 
1539   /* Ignore duplicate calls with the same entries. This is out of convenience
1540    * to allow registering shortcuts from instance init (and thusly after the
1541    * GdkDisplay has been connected.
1542    */
1543   if (g_hash_table_contains (priv->seen_entries, shortcuts))
1544     return;
1545 
1546   g_hash_table_insert (priv->seen_entries, (gpointer)shortcuts, NULL);
1547 
1548   for (guint i = 0; i < n_shortcuts; i++)
1549     {
1550       const DzlShortcutEntry *entry = &shortcuts[i];
1551 
1552       if (entry->command == NULL)
1553         {
1554           g_warning ("Shortcut entry missing command id");
1555           continue;
1556         }
1557 
1558       if (entry->default_accel != NULL)
1559         dzl_shortcut_theme_set_accel_for_command (priv->internal_theme,
1560                                                   entry->command,
1561                                                   entry->default_accel,
1562                                                   entry->phase);
1563 
1564       dzl_shortcut_manager_add_command (self,
1565                                         entry->command,
1566                                         g_dgettext (translation_domain, entry->section),
1567                                         g_dgettext (translation_domain, entry->group),
1568                                         g_dgettext (translation_domain, entry->title),
1569                                         g_dgettext (translation_domain, entry->subtitle));
1570     }
1571 }
1572 
1573 /**
1574  * dzl_shortcut_manager_get_theme_by_name:
1575  * @self: a #DzlShortcutManager
1576  * @theme_name: (nullable): the name of a theme or %NULL of the internal theme
1577  *
1578  * Locates a theme by the name of the theme.
1579  *
1580  * If @theme_name is %NULL, then the internal theme is used. You probably dont
1581  * need to use that as it is used by various controllers to hook up their
1582  * default actions.
1583  *
1584  * Returns: (transfer none) (nullable): A #DzlShortcutTheme or %NULL.
1585  */
1586 DzlShortcutTheme *
dzl_shortcut_manager_get_theme_by_name(DzlShortcutManager * self,const gchar * theme_name)1587 dzl_shortcut_manager_get_theme_by_name (DzlShortcutManager *self,
1588                                         const gchar        *theme_name)
1589 {
1590   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1591 
1592   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL);
1593 
1594   if (theme_name == NULL || g_strcmp0 (theme_name, "internal") == 0)
1595     return priv->internal_theme;
1596 
1597   for (guint i = 0; i < priv->themes->len; i++)
1598     {
1599       DzlShortcutTheme *theme = g_ptr_array_index (priv->themes, i);
1600 
1601       g_assert (DZL_IS_SHORTCUT_THEME (theme));
1602 
1603       if (g_strcmp0 (theme_name, dzl_shortcut_theme_get_name (theme)) == 0)
1604         return theme;
1605     }
1606 
1607   return NULL;
1608 }
1609 
1610 DzlShortcutTheme *
_dzl_shortcut_manager_get_internal_theme(DzlShortcutManager * self)1611 _dzl_shortcut_manager_get_internal_theme (DzlShortcutManager *self)
1612 {
1613   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1614 
1615   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), NULL);
1616 
1617   return priv->internal_theme;
1618 }
1619 
1620 static void
dzl_shortcut_manager_merge(DzlShortcutManager * self,DzlShortcutTheme * theme)1621 dzl_shortcut_manager_merge (DzlShortcutManager *self,
1622                             DzlShortcutTheme   *theme)
1623 {
1624   DzlShortcutManagerPrivate *priv = dzl_shortcut_manager_get_instance_private (self);
1625   g_autoptr(DzlShortcutTheme) alloc_layer = NULL;
1626   DzlShortcutTheme *base_layer;
1627   const gchar *name;
1628 
1629   DZL_ENTRY;
1630 
1631   g_return_if_fail (DZL_IS_SHORTCUT_MANAGER (self));
1632   g_return_if_fail (DZL_IS_SHORTCUT_THEME (theme));
1633 
1634   /*
1635    * One thing we are trying to avoid here is having separate code paths for
1636    * adding the "first theme modification" from merging additional layers from
1637    * plugins and the like. Having the same merge path in all situations
1638    * hopefully will help us avoid some bugs.
1639    */
1640 
1641   name = dzl_shortcut_theme_get_name (theme);
1642 
1643   if (dzl_str_empty0 (name))
1644     {
1645       g_warning ("Attempt to merge theme with empty name");
1646       DZL_EXIT;
1647     }
1648 
1649   base_layer = dzl_shortcut_manager_get_theme_by_name (self, name);
1650 
1651   if (base_layer == NULL)
1652     {
1653       const gchar *parent_name;
1654       const gchar *title;
1655       const gchar *subtitle;
1656 
1657       parent_name = dzl_shortcut_theme_get_parent_name (theme);
1658       title = dzl_shortcut_theme_get_title (theme);
1659       subtitle = dzl_shortcut_theme_get_subtitle (theme);
1660 
1661       alloc_layer = g_object_new (DZL_TYPE_SHORTCUT_THEME,
1662                                   "name", name,
1663                                   "parent-name", parent_name,
1664                                   "subtitle", subtitle,
1665                                   "title", title,
1666                                   NULL);
1667 
1668       base_layer = alloc_layer;
1669 
1670       /*
1671        * Now notify the GListModel consumers that our internal theme list
1672        * has changed to include the newly created base layer.
1673        */
1674       g_ptr_array_add (priv->themes, g_object_ref (alloc_layer));
1675       _dzl_shortcut_theme_set_manager (alloc_layer, self);
1676       g_list_model_items_changed (G_LIST_MODEL (self), priv->themes->len - 1, 0, 1);
1677     }
1678 
1679   /*
1680    * Okay, now we need to go through all the custom contexts, and global
1681    * shortcuts in the theme and merge them into the base_layer. However, we
1682    * will defer that work to the DzlShortcutTheme module so it has access to
1683    * the internal structures.
1684    */
1685   _dzl_shortcut_theme_merge (base_layer, theme);
1686 
1687   DZL_EXIT;
1688 }
1689 
1690 /**
1691  * _dzl_shortcut_manager_get_command_info:
1692  * @self: a #DzlShortcutManager
1693  * @command_id: the command-id
1694  * @title: (out) (optional): a location for the title
1695  * @subtitle: (out) (optional): a location for the subtitle
1696  *
1697  * Gets command information about command-id
1698  *
1699  * Returns: %TRUE if the command-id was found and out parameters were set.
1700  *
1701  * Since: 3.32
1702  */
1703 gboolean
_dzl_shortcut_manager_get_command_info(DzlShortcutManager * self,const gchar * command_id,const gchar ** title,const gchar ** subtitle)1704 _dzl_shortcut_manager_get_command_info (DzlShortcutManager  *self,
1705                                         const gchar         *command_id,
1706                                         const gchar        **title,
1707                                         const gchar        **subtitle)
1708 {
1709   DzlShortcutManagerPrivate *priv;
1710   DzlShortcutNodeData *node;
1711 
1712   if (self == NULL)
1713     self = dzl_shortcut_manager_get_default ();
1714 
1715   g_return_val_if_fail (DZL_IS_SHORTCUT_MANAGER (self), FALSE);
1716 
1717   priv = dzl_shortcut_manager_get_instance_private (self);
1718 
1719   if ((node = g_hash_table_lookup (priv->command_id_to_node_data, command_id)))
1720     {
1721       if (title != NULL)
1722         *title = node->title;
1723 
1724       if (subtitle != NULL)
1725         *subtitle = node->subtitle;
1726 
1727       return TRUE;
1728     }
1729 
1730   return FALSE;
1731 }
1732