1 /* dzl-menu-manager.c
2 *
3 * Copyright (C) 2015 Christian Hergert <chergert@redhat.com>
4 *
5 * This file is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser 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-menu-manager"
20
21 #include "config.h"
22
23 #include <string.h>
24
25 #include "menus/dzl-menu-manager.h"
26 #include "util/dzl-util-private.h"
27
28 struct _DzlMenuManager
29 {
30 GObject parent_instance;
31
32 guint last_merge_id;
33 GHashTable *models;
34 };
35
G_DEFINE_TYPE(DzlMenuManager,dzl_menu_manager,G_TYPE_OBJECT)36 G_DEFINE_TYPE (DzlMenuManager, dzl_menu_manager, G_TYPE_OBJECT)
37
38 #define DZL_MENU_ATTRIBUTE_BEFORE "before"
39 #define DZL_MENU_ATTRIBUTE_AFTER "after"
40 #define DZL_MENU_ATTRIBUTE_MERGE_ID "dazzle-merge-id"
41
42 /**
43 * DzlMenuManager:
44 *
45 * The goal of #DzlMenuManager is to simplify the process of merging multiple
46 * GtkBuilder .ui files containing menus into a single representation of the
47 * application menus. Additionally, it provides the ability to "unmerge"
48 * previously merged menus.
49 *
50 * This allows for an application to have plugins which seemlessly extends
51 * the core application menus.
52 *
53 * Implementation notes:
54 *
55 * To make this work, we don't use the GMenu instances created by a GtkBuilder
56 * instance. Instead, we create the menus ourself and recreate section and
57 * submenu links. This allows the #DzlMenuManager to be in full control of
58 * the generated menus.
59 *
60 * dzl_menu_manager_get_menu_by_id() will always return a #GMenu, however
61 * that menu may contain no children until something has extended it later
62 * on during the application process.
63 *
64 * Since: 3.26
65 */
66
67 static const gchar *
68 get_object_id (GObject *object)
69 {
70 g_assert (G_IS_OBJECT (object));
71
72 if (GTK_IS_BUILDABLE (object))
73 return gtk_buildable_get_name (GTK_BUILDABLE (object));
74 else
75 return g_object_get_data (object, "gtk-builder-name");
76 }
77
78 static void
dzl_menu_manager_dispose(GObject * object)79 dzl_menu_manager_dispose (GObject *object)
80 {
81 DzlMenuManager *self = (DzlMenuManager *)object;
82
83 g_clear_pointer (&self->models, g_hash_table_unref);
84
85 G_OBJECT_CLASS (dzl_menu_manager_parent_class)->dispose (object);
86 }
87
88 static void
dzl_menu_manager_class_init(DzlMenuManagerClass * klass)89 dzl_menu_manager_class_init (DzlMenuManagerClass *klass)
90 {
91 GObjectClass *object_class = G_OBJECT_CLASS (klass);
92
93 object_class->dispose = dzl_menu_manager_dispose;
94 }
95
96 static void
dzl_menu_manager_init(DzlMenuManager * self)97 dzl_menu_manager_init (DzlMenuManager *self)
98 {
99 self->models = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
100 }
101
102 static gint
find_with_attribute_string(GMenuModel * model,const gchar * attribute,const gchar * value)103 find_with_attribute_string (GMenuModel *model,
104 const gchar *attribute,
105 const gchar *value)
106 {
107 guint n_items;
108
109 g_assert (G_IS_MENU_MODEL (model));
110 g_assert (attribute != NULL);
111 g_assert (value != NULL);
112
113 n_items = g_menu_model_get_n_items (model);
114
115 for (guint i = 0; i < n_items; i++)
116 {
117 g_autofree gchar *item_value = NULL;
118
119 if (g_menu_model_get_item_attribute (model, i, attribute, "s", &item_value) &&
120 (g_strcmp0 (value, item_value) == 0))
121 return i;
122 }
123
124 return -1;
125 }
126
127 static gboolean
dzl_menu_manager_menu_contains(DzlMenuManager * self,GMenu * menu,GMenuItem * item)128 dzl_menu_manager_menu_contains (DzlMenuManager *self,
129 GMenu *menu,
130 GMenuItem *item)
131 {
132 const gchar *link_id;
133 const gchar *label;
134
135 g_assert (DZL_IS_MENU_MANAGER (self));
136 g_assert (G_IS_MENU (menu));
137 g_assert (G_IS_MENU_ITEM (item));
138
139 /* try to find match by item label */
140 if (g_menu_item_get_attribute (item, G_MENU_ATTRIBUTE_LABEL, "&s", &label) &&
141 (find_with_attribute_string (G_MENU_MODEL (menu), G_MENU_ATTRIBUTE_LABEL, label) >= 0))
142 return TRUE;
143
144 /* try to find match by item link */
145 if (g_menu_item_get_attribute (item, "dzl-link-id", "&s", &link_id) &&
146 (find_with_attribute_string (G_MENU_MODEL (menu), "dzl-link-id", link_id) >= 0))
147 return TRUE;
148
149 return FALSE;
150 }
151
152 static void
model_copy_attributes_to_item(GMenuModel * model,gint item_index,GMenuItem * item)153 model_copy_attributes_to_item (GMenuModel *model,
154 gint item_index,
155 GMenuItem *item)
156 {
157 g_autoptr(GMenuAttributeIter) iter = NULL;
158 const gchar *attr_name;
159 GVariant *attr_value;
160
161 g_assert (G_IS_MENU_MODEL (model));
162 g_assert (item_index >= 0);
163 g_assert (G_IS_MENU_ITEM (item));
164
165 if (!(iter = g_menu_model_iterate_item_attributes (model, item_index)))
166 return;
167
168 while (g_menu_attribute_iter_get_next (iter, &attr_name, &attr_value))
169 {
170 g_menu_item_set_attribute_value (item, attr_name, attr_value);
171 g_variant_unref (attr_value);
172 }
173 }
174
175 static void
model_copy_links_to_item(GMenuModel * model,guint position,GMenuItem * item)176 model_copy_links_to_item (GMenuModel *model,
177 guint position,
178 GMenuItem *item)
179 {
180 g_autoptr(GMenuLinkIter) link_iter = NULL;
181
182 g_assert (G_IS_MENU_MODEL (model));
183 g_assert (G_IS_MENU_ITEM (item));
184
185 link_iter = g_menu_model_iterate_item_links (model, position);
186
187 while (g_menu_link_iter_next (link_iter))
188 {
189 g_autoptr(GMenuModel) link_model = NULL;
190 const gchar *link_name;
191
192 link_name = g_menu_link_iter_get_name (link_iter);
193 link_model = g_menu_link_iter_get_value (link_iter);
194
195 g_menu_item_set_link (item, link_name, link_model);
196 }
197 }
198
199 static void
menu_move_item_to(GMenu * menu,guint position,guint new_position)200 menu_move_item_to (GMenu *menu,
201 guint position,
202 guint new_position)
203 {
204 g_autoptr(GMenuItem) item = NULL;
205
206 g_assert (G_IS_MENU (menu));
207
208 item = g_menu_item_new (NULL, NULL);
209 model_copy_attributes_to_item (G_MENU_MODEL (menu), position, item);
210 model_copy_links_to_item (G_MENU_MODEL (menu), position, item);
211
212 g_menu_remove (menu, position);
213 g_menu_insert_item (menu, new_position, item);
214 }
215
216 static void
dzl_menu_manager_resolve_constraints(GMenu * menu)217 dzl_menu_manager_resolve_constraints (GMenu *menu)
218 {
219 GMenuModel *model = (GMenuModel *)menu;
220 gint n_items;
221
222 g_assert (G_IS_MENU (menu));
223
224 n_items = (gint)g_menu_model_get_n_items (G_MENU_MODEL (menu));
225
226 /*
227 * We start iterating forwards. As we look at each row, we start
228 * again from the end working backwards to see if we need to be
229 * moved after that row.
230 *
231 * This way we know we see the furthest we might need to jump first.
232 */
233
234 for (gint i = 0; i < n_items; i++)
235 {
236 g_autofree gchar *i_after = NULL;
237
238 g_menu_model_get_item_attribute (model, i, DZL_MENU_ATTRIBUTE_AFTER, "s", &i_after);
239 if (i_after == NULL)
240 continue;
241
242 /* Work our way backwards from the end back to
243 * our current position (but not overlapping).
244 */
245 for (gint j = n_items - 1; j > i; j--)
246 {
247 g_autofree gchar *j_id = NULL;
248 g_autofree gchar *j_label = NULL;
249
250 g_menu_model_get_item_attribute (model, j, "id", "s", &j_id);
251 g_menu_model_get_item_attribute (model, j, "label", "s", &j_label);
252
253 if (dzl_str_equal0 (i_after, j_id) || dzl_str_equal0 (i_after, j_label))
254 {
255 /* You might think we need to place the item *AFTER*
256 * our position "j". But since we remove the row where
257 * "i" currently is, we get the proper location.
258 */
259 menu_move_item_to (menu, i, j);
260 i--;
261 break;
262 }
263 }
264 }
265
266 /*
267 * Now we need to apply the same thing but for the "before" links
268 * in our model. To do this, we also want to ensure we find the
269 * furthest jump first. So we start from the end and work our way
270 * towards the front and for each of those nodes, start from the
271 * front and work our way back.
272 */
273
274 for (gint i = n_items - 1; i >= 0; i--)
275 {
276 g_autofree gchar *i_before = NULL;
277
278 g_menu_model_get_item_attribute (model, i, DZL_MENU_ATTRIBUTE_BEFORE, "s", &i_before);
279 if (i_before == NULL)
280 continue;
281
282 /* Work our way from the front back towards our current position
283 * that would cause our position to jump.
284 */
285 for (gint j = 0; j < i; j++)
286 {
287 g_autofree gchar *j_id = NULL;
288 g_autofree gchar *j_label = NULL;
289
290 g_menu_model_get_item_attribute (model, j, "id", "s", &j_id);
291 g_menu_model_get_item_attribute (model, j, "label", "s", &j_label);
292
293 if (dzl_str_equal0 (i_before, j_id) || dzl_str_equal0 (i_before, j_label))
294 {
295 /*
296 * This item needs to be placed before this item we just found.
297 * Since that is the furthest we could jump, just stop
298 * afterwards.
299 */
300 menu_move_item_to (menu, i, j);
301 i++;
302 break;
303 }
304 }
305 }
306 }
307
308 static void
dzl_menu_manager_add_to_menu(DzlMenuManager * self,GMenu * menu,GMenuItem * item)309 dzl_menu_manager_add_to_menu (DzlMenuManager *self,
310 GMenu *menu,
311 GMenuItem *item)
312 {
313 g_assert (DZL_IS_MENU_MANAGER (self));
314 g_assert (G_IS_MENU (menu));
315 g_assert (G_IS_MENU_ITEM (item));
316
317 /*
318 * The proplem here is one that could end up being an infinite
319 * loop if we tried to resolve all the position requirements
320 * until no more position changes were required. So instead we
321 * simplify the problem into an append, and two-passes as trying
322 * to fix up the positions.
323 */
324 g_menu_append_item (menu, item);
325 dzl_menu_manager_resolve_constraints (menu);
326 dzl_menu_manager_resolve_constraints (menu);
327 }
328
329 static void
dzl_menu_manager_merge_model(DzlMenuManager * self,GMenu * menu,GMenuModel * model,guint merge_id)330 dzl_menu_manager_merge_model (DzlMenuManager *self,
331 GMenu *menu,
332 GMenuModel *model,
333 guint merge_id)
334 {
335 guint n_items;
336
337 g_assert (DZL_IS_MENU_MANAGER (self));
338 g_assert (G_IS_MENU (menu));
339 g_assert (G_IS_MENU_MODEL (model));
340 g_assert (merge_id > 0);
341
342 /*
343 * NOTES:
344 *
345 * Instead of using g_menu_item_new_from_model(), we create our own item
346 * and resolve section/submenu links. This allows us to be in full control
347 * of all of the menu items created.
348 *
349 * We move through each item in @model. If that item does not exist within
350 * @menu, we add it taking into account %DZL_MENU_ATTRIBUTE_BEFORE and
351 * %DZL_MENU_ATTRIBUTE_AFTER.
352 */
353
354 n_items = g_menu_model_get_n_items (model);
355
356 for (guint i = 0; i < n_items; i++)
357 {
358 g_autoptr(GMenuItem) item = NULL;
359 g_autoptr(GMenuLinkIter) link_iter = NULL;
360
361 item = g_menu_item_new (NULL, NULL);
362
363 /*
364 * Copy attributes from the model. This includes, label, action,
365 * target, before, after, etc. Also set our merge-id so that we
366 * can remove the item when we are unmerged.
367 */
368 model_copy_attributes_to_item (model, i, item);
369 g_menu_item_set_attribute (item, DZL_MENU_ATTRIBUTE_MERGE_ID, "u", merge_id);
370
371 /*
372 * If this is a link, resolve it from our already created GMenu.
373 * The menu might be empty now, but it will get filled in on a
374 * followup pass for that model.
375 */
376 link_iter = g_menu_model_iterate_item_links (model, i);
377 while (g_menu_link_iter_next (link_iter))
378 {
379 g_autoptr(GMenuModel) link_model = NULL;
380 const gchar *link_name;
381 const gchar *link_id;
382 GMenuModel *internal_menu;
383
384 link_name = g_menu_link_iter_get_name (link_iter);
385 link_model = g_menu_link_iter_get_value (link_iter);
386
387 g_assert (link_name != NULL);
388 g_assert (G_IS_MENU_MODEL (link_model));
389
390 link_id = get_object_id (G_OBJECT (link_model));
391
392 if (link_id == NULL)
393 {
394 g_warning ("Link of type \"%s\" missing \"id=\". "
395 "Merging will not be possible.",
396 link_name);
397 continue;
398 }
399
400 internal_menu = g_hash_table_lookup (self->models, link_id);
401
402 if (internal_menu == NULL)
403 {
404 g_warning ("linked menu %s has not been created", link_id);
405 continue;
406 }
407
408 /*
409 * Save the internal link reference-id to do merging of items
410 * later on. We need to know if an item matches when we might
411 * not have a "label" to work from.
412 */
413 g_menu_item_set_attribute (item, "dzl-link-id", "s", link_id);
414
415 g_menu_item_set_link (item, link_name, internal_menu);
416 }
417
418 /*
419 * If the menu already has this item, that's fine. We will populate
420 * the submenu/section links in followup merges of their GMenuModel.
421 */
422 if (dzl_menu_manager_menu_contains (self, menu, item))
423 continue;
424
425 dzl_menu_manager_add_to_menu (self, menu, item);
426 }
427 }
428
429 static void
dzl_menu_manager_merge_builder(DzlMenuManager * self,GtkBuilder * builder,guint merge_id)430 dzl_menu_manager_merge_builder (DzlMenuManager *self,
431 GtkBuilder *builder,
432 guint merge_id)
433 {
434 const GSList *iter;
435 GSList *list;
436
437 g_assert (DZL_IS_MENU_MANAGER (self));
438 g_assert (GTK_IS_BUILDER (builder));
439 g_assert (merge_id > 0);
440
441 /*
442 * NOTES:
443 *
444 * We cannot re-use any of the created GMenu from the builder as we need
445 * control over all the created GMenu. Primarily because manipulating
446 * existing GMenu is such a PITA. So instead, we create our own GMenu and
447 * resolve links manually.
448 *
449 * Since GtkBuilder requires that all menus have an "id" element, we can
450 * resolve the menu->id fairly easily. First we create our own GMenu
451 * instances so that we can always resolve them during the creation process.
452 * Then we can go through and manually resolve links as we create items.
453 *
454 * We don't need to recursively create the menus since we will come across
455 * additional GMenu instances while iterating the available objects from the
456 * GtkBuilder. This does require 2 iterations of the objects, but that is
457 * not an issue.
458 */
459
460 list = gtk_builder_get_objects (builder);
461
462 /*
463 * For every menu with an id, check to see if we already created our
464 * instance of that menu. If not, create it now so we can resolve them
465 * while building the menu links.
466 */
467 for (iter = list; iter != NULL; iter = iter->next)
468 {
469 GObject *object = iter->data;
470 const gchar *id;
471 GMenu *menu;
472
473 if (!G_IS_MENU (object))
474 continue;
475
476 if (!(id = get_object_id (object)))
477 {
478 g_warning ("menu without identifier, implausible");
479 continue;
480 }
481
482 if (!(menu = g_hash_table_lookup (self->models, id)))
483 g_hash_table_insert (self->models, g_strdup (id), g_menu_new ());
484 }
485
486 /*
487 * Now build each menu we discovered in the GtkBuilder. We do not need to
488 * build them recursively since we will pass the linked menus as we make
489 * forward progress on the GtkBuilder created objects.
490 */
491
492 for (iter = list; iter != NULL; iter = iter->next)
493 {
494 GObject *object = iter->data;
495 const gchar *id;
496 GMenu *menu;
497
498 if (!G_IS_MENU_MODEL (object))
499 continue;
500
501 if (!(id = get_object_id (object)))
502 continue;
503
504 menu = g_hash_table_lookup (self->models, id);
505
506 g_assert (G_IS_MENU (menu));
507
508 dzl_menu_manager_merge_model (self, menu, G_MENU_MODEL (object), merge_id);
509 }
510
511 g_slist_free (list);
512 }
513
514 DzlMenuManager *
dzl_menu_manager_new(void)515 dzl_menu_manager_new (void)
516 {
517 return g_object_new (DZL_TYPE_MENU_MANAGER, NULL);
518 }
519
520 guint
dzl_menu_manager_add_filename(DzlMenuManager * self,const gchar * filename,GError ** error)521 dzl_menu_manager_add_filename (DzlMenuManager *self,
522 const gchar *filename,
523 GError **error)
524 {
525 GtkBuilder *builder;
526 guint merge_id;
527
528 g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), 0);
529 g_return_val_if_fail (filename != NULL, 0);
530
531 builder = gtk_builder_new ();
532
533 if (!gtk_builder_add_from_file (builder, filename, error))
534 {
535 g_object_unref (builder);
536 return 0;
537 }
538
539 merge_id = ++self->last_merge_id;
540 dzl_menu_manager_merge_builder (self, builder, merge_id);
541 g_object_unref (builder);
542
543 return merge_id;
544 }
545
546 guint
dzl_menu_manager_merge(DzlMenuManager * self,const gchar * menu_id,GMenuModel * menu_model)547 dzl_menu_manager_merge (DzlMenuManager *self,
548 const gchar *menu_id,
549 GMenuModel *menu_model)
550 {
551 GMenu *menu;
552 guint merge_id;
553
554 g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), 0);
555 g_return_val_if_fail (menu_id != NULL, 0);
556 g_return_val_if_fail (G_IS_MENU_MODEL (menu_model), 0);
557
558 merge_id = ++self->last_merge_id;
559
560 if (!(menu = g_hash_table_lookup (self->models, menu_id)))
561 {
562 GMenu *new_model = g_menu_new ();
563 g_hash_table_insert (self->models, g_strdup (menu_id), new_model);
564 menu = new_model;
565 }
566
567 dzl_menu_manager_merge_model (self, menu, menu_model, merge_id);
568
569 return merge_id;
570 }
571
572 /**
573 * dzl_menu_manager_remove:
574 * @self: a #DzlMenuManager
575 * @merge_id: A previously registered merge id
576 *
577 * This removes items from menus that were added as part of a previous
578 * menu merge. Use the value returned from dzl_menu_manager_merge() as
579 * the @merge_id.
580 *
581 * Since: 3.26
582 */
583 void
dzl_menu_manager_remove(DzlMenuManager * self,guint merge_id)584 dzl_menu_manager_remove (DzlMenuManager *self,
585 guint merge_id)
586 {
587 GHashTableIter iter;
588 GMenu *menu;
589
590 g_return_if_fail (DZL_IS_MENU_MANAGER (self));
591 g_return_if_fail (merge_id != 0);
592
593 g_hash_table_iter_init (&iter, self->models);
594
595 while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&menu))
596 {
597 gint n_items;
598 gint i;
599
600 g_assert (G_IS_MENU (menu));
601
602 n_items = g_menu_model_get_n_items (G_MENU_MODEL (menu));
603
604 /* Iterate backward so we have a stable loop variable. */
605 for (i = n_items - 1; i >= 0; i--)
606 {
607 guint item_merge_id = 0;
608
609 if (g_menu_model_get_item_attribute (G_MENU_MODEL (menu),
610 i,
611 DZL_MENU_ATTRIBUTE_MERGE_ID,
612 "u", &item_merge_id))
613 {
614 if (item_merge_id == merge_id)
615 g_menu_remove (menu, i);
616 }
617 }
618 }
619 }
620
621 /**
622 * dzl_menu_manager_get_menu_by_id:
623 *
624 * Returns: (transfer none): A #GMenu.
625 */
626 GMenu *
dzl_menu_manager_get_menu_by_id(DzlMenuManager * self,const gchar * menu_id)627 dzl_menu_manager_get_menu_by_id (DzlMenuManager *self,
628 const gchar *menu_id)
629 {
630 GMenu *menu;
631
632 g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), NULL);
633 g_return_val_if_fail (menu_id != NULL, NULL);
634
635 menu = g_hash_table_lookup (self->models, menu_id);
636
637 if (menu == NULL)
638 {
639 menu = g_menu_new ();
640 g_hash_table_insert (self->models, g_strdup (menu_id), menu);
641 }
642
643 return menu;
644 }
645
646 guint
dzl_menu_manager_add_resource(DzlMenuManager * self,const gchar * resource,GError ** error)647 dzl_menu_manager_add_resource (DzlMenuManager *self,
648 const gchar *resource,
649 GError **error)
650 {
651 GtkBuilder *builder;
652 guint merge_id;
653
654 g_return_val_if_fail (DZL_IS_MENU_MANAGER (self), 0);
655 g_return_val_if_fail (resource != NULL, 0);
656
657 if (g_str_has_prefix (resource, "resource://"))
658 resource += strlen ("resource://");
659
660 builder = gtk_builder_new ();
661
662 if (!gtk_builder_add_from_resource (builder, resource, error))
663 {
664 g_object_unref (builder);
665 return 0;
666 }
667
668 merge_id = ++self->last_merge_id;
669 dzl_menu_manager_merge_builder (self, builder, merge_id);
670 g_object_unref (builder);
671
672 return merge_id;
673 }
674