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