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, ¶m))
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