gmenu exporter: put submenus in separate groups
[platform/upstream/glib.git] / gio / gmenuexporter.c
1 /*
2  * Copyright © 2011 Canonical Ltd.
3  *
4  *  This library is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU Lesser General Public License as
6  *  published by the Free Software Foundation; either version 2 of the
7  *  licence, or (at your option) any later version.
8  *
9  *  This library is distributed in the hope that it will be useful, but
10  *  WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  *  Lesser General Public License for more details.
13  *
14  *  You should have received a copy of the GNU Lesser General Public
15  *  License along with this library; if not, write to the Free Software
16  *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
17  *  USA.
18  *
19  * Author: Ryan Lortie <desrt@desrt.ca>
20  */
21
22 #include "gmenuexporter.h"
23
24 #include "gdbusmethodinvocation.h"
25 #include "gdbusintrospection.h"
26 #include "gdbusnamewatching.h"
27 #include "gdbuserror.h"
28
29 /**
30  * SECTION:gmenuexporter
31  * @title: GMenuModel exporter
32  * @short_description: Export GMenuModels on D-Bus
33  * @see_also: #GMenuModel, #GMenuProxy
34  *
35  * These functions support exporting a #GMenuModel on D-Bus.
36  * The D-Bus interface that is used is a private implementation
37  * detail.
38  *
39  * To access an exported #GMenuModel remotely, use
40  * g_menu_proxy_get() to obtain a #GMenuProxy.
41  */
42
43 /* {{{1 D-Bus Interface description */
44
45 /* The org.gtk.Menus interface
46  * ===========================
47  *
48  * The interface is primarily concerned with three things:
49  *
50  * - communicating menus to the client
51  * - establishing links between menus and other menus
52  * - notifying clients of changes
53  *
54  * As a basic principle, it is recognised that the menu structure
55  * of an application is often large. It is also recognised that some
56  * menus are liable to frequently change without the user ever having
57  * opened the menu. For both of these reasons, the individual menus are
58  * arranged into subscription groups. Each subscription group is specified
59  * by an unsigned integer. The assignment of integers need not be consecutive.
60  *
61  * Within a subscription group there are multiple menus. Each menu is
62  * identified with an unsigned integer, unique to its subscription group.
63  *
64  * By convention, the primary menu is numbered 0 without subscription group 0.
65  *
66  * Actionable menu items (ie: those that produce some effect in the
67  * application when they are activated) have a related action, specified by
68  * a string. This string specifies the name of the action, according to the
69  * org.gtk.Actions interface, at the same object path as the menu.
70  *
71  * Methods
72  * -------
73  *
74  * Start :: (au) → (a(uuaa{sv}))
75  *
76  *   The Start method is used to indicate that a client is interested in
77  *   tracking and displaying the content of the menus of a particular list
78  *   of subscription groups.
79  *
80  *   Most typically, the client will request subscription group 0 to start.
81  *
82  *   The call has two effects. First, it replies with all menus defined
83  *   within the requested subscription groups. The format of the reply is
84  *   an array of tuples, where the items in each tuple are:
85  *   - the subscription group of the menu
86  *   - the number of the menu within that group
87  *   - an array of menu items
88  *
89  *   Each menu item is a dictionary of attributes (a{sv}).
90  *
91  *   Secondly, this call has a side effect: it atomically requests that
92  *   the Changed signal start to be emitted for the requested subscription
93  *   group. Each group has a subscription count and only signals changes
94  *   on itself when this count is greater than zero.
95  *
96  *   If a group is specified multiple times then the result is that the
97  *   contents of that group is only returned once, but the subscription
98  *   count is increased multiple times.
99  *
100  *   If a client disconnects from the bus while holding subscriptions then
101  *   its subscriptions will be cancelled. This prevents "leaking" subscriptions
102  *   in the case of crashes and is also useful for applications that want
103  *   to exit without manually cleaning up.
104  *
105  * End :: (au)
106  *
107  *   The End method reverses the previous effects of a call to Start.
108  *
109  *   When clients are no longer interested in the contents of a subscription
110  *   group, they should call the End method.
111  *
112  *   The parameter lists the subscription groups. A subscription group
113  *   needs to be cancelled the same number of times as it was requested.
114  *   For this reason, it might make sense to specify the same subscription
115  *   group multiple times (if multiple Start calls were made for this group).
116  *
117  * Signals
118  * -------
119  *
120  * Changed :: (a(uuuuaa{sv}))
121  *
122  *   The changed signal indicates changes to a particular menu.
123  *
124  *   The changes come as an array of tuples where the items in each tuple are:
125  *   - the subscription group of the menu
126  *   - the number of the menu within that group
127  *   - the position in the menu at which to make the change
128  *   - the number of items to delete from that position
129  *   - a list of new items to insert at that position
130  *
131  *   Each new menu item is a dictionary of attributes (a{sv}).
132  *
133  * Attributes
134  * ----------
135  *
136  * label (string): the label to display
137  * action (string): the name of the action
138  * target (variant): the parameter to pass when activating the action
139  * :section ((uu)): the menu to use to populate that section, specified
140  *     as a pair of subscription group and menu within that group
141  * :submenu ((uu)): the menu to use as a submenu, specified
142  *     as a pair of subscription group and menu within that group
143  */
144
145 static GDBusInterfaceInfo *
146 org_gtk_Menus_get_interface (void)
147 {
148   static GDBusInterfaceInfo *interface_info;
149
150   if (interface_info == NULL)
151     {
152       GError *error = NULL;
153       GDBusNodeInfo *info;
154
155       info = g_dbus_node_info_new_for_xml ("<node>"
156                                            "  <interface name='org.gtk.Menus'>"
157                                            "    <method name='Start'>"
158                                            "      <arg type='au' name='groups' direction='in'/>"
159                                            "      <arg type='a(uuaa{sv})' name='content' direction='out'/>"
160                                            "    </method>"
161                                            "    <method name='End'>"
162                                            "      <arg type='au' name='groups' direction='in'/>"
163                                            "    </method>"
164                                            "    <signal name='Changed'>"
165                                            "      arg type='a(uuuuaa{sv})' name='changes'/>"
166                                            "    </signal>"
167                                            "  </interface>"
168                                            "</node>", &error);
169       if (info == NULL)
170         g_error ("%s\n", error->message);
171       interface_info = g_dbus_node_info_lookup_interface (info, "org.gtk.Menus");
172       g_assert (interface_info != NULL);
173       g_dbus_interface_info_ref (interface_info);
174       g_dbus_node_info_unref (info);
175     }
176
177   return interface_info;
178 }
179
180 /* {{{1 Forward declarations */
181 typedef struct _GMenuExporterMenu                           GMenuExporterMenu;
182 typedef struct _GMenuExporterLink                           GMenuExporterLink;
183 typedef struct _GMenuExporterGroup                          GMenuExporterGroup;
184 typedef struct _GMenuExporterRemote                         GMenuExporterRemote;
185 typedef struct _GMenuExporterWatch                          GMenuExporterWatch;
186 typedef struct _GMenuExporter                               GMenuExporter;
187
188 static gboolean                 g_menu_exporter_group_is_subscribed    (GMenuExporterGroup *group);
189 static guint                    g_menu_exporter_group_get_id           (GMenuExporterGroup *group);
190 static GMenuExporter *          g_menu_exporter_group_get_exporter     (GMenuExporterGroup *group);
191 static GMenuExporterMenu *      g_menu_exporter_group_add_menu         (GMenuExporterGroup *group,
192                                                                         GMenuModel         *model);
193 static void                     g_menu_exporter_group_remove_menu      (GMenuExporterGroup *group,
194                                                                         guint               id);
195
196 static GMenuExporterGroup *     g_menu_exporter_create_group           (GMenuExporter      *exporter);
197 static GMenuExporterGroup *     g_menu_exporter_lookup_group           (GMenuExporter      *exporter,
198                                                                         guint               group_id);
199 static void                     g_menu_exporter_report                 (GMenuExporter      *exporter,
200                                                                         GVariant           *report);
201 static void                     g_menu_exporter_remove_group           (GMenuExporter      *exporter,
202                                                                         guint               id);
203
204 /* {{{1 GMenuExporterLink, GMenuExporterMenu */
205
206 struct _GMenuExporterMenu
207 {
208   GMenuExporterGroup *group;
209   guint               id;
210
211   GMenuModel *model;
212   gulong      handler_id;
213   GSequence  *item_links;
214 };
215
216 struct _GMenuExporterLink
217 {
218   gchar             *name;
219   GMenuExporterMenu *menu;
220   GMenuExporterLink *next;
221 };
222
223 static void
224 g_menu_exporter_menu_free (GMenuExporterMenu *menu)
225 {
226   g_menu_exporter_group_remove_menu (menu->group, menu->id);
227
228   if (menu->handler_id != 0)
229     g_signal_handler_disconnect (menu->model, menu->handler_id);
230
231   if (menu->item_links != NULL)
232     g_sequence_free (menu->item_links);
233
234   g_object_unref (menu->model);
235
236   g_slice_free (GMenuExporterMenu, menu);
237 }
238
239 static void
240 g_menu_exporter_link_free (gpointer data)
241 {
242   GMenuExporterLink *link = data;
243
244   while (link != NULL)
245     {
246       GMenuExporterLink *tmp = link;
247       link = tmp->next;
248
249       g_menu_exporter_menu_free (tmp->menu);
250       g_free (tmp->name);
251
252       g_slice_free (GMenuExporterLink, tmp);
253     }
254 }
255
256 static GMenuExporterLink *
257 g_menu_exporter_menu_create_links (GMenuExporterMenu *menu,
258                                    gint               position)
259 {
260   GMenuExporterLink *list = NULL;
261   GMenuLinkIter *iter;
262   const char *name;
263   GMenuModel *model;
264
265   iter = g_menu_model_iterate_item_links (menu->model, position);
266
267   while (g_menu_link_iter_get_next (iter, &name, &model))
268     {
269       GMenuExporterGroup *group;
270       GMenuExporterLink *tmp;
271
272       /* keep sections in the same group, but create new groups
273        * otherwise
274        */
275       if (!g_str_equal (name, "section"))
276         group = g_menu_exporter_create_group (g_menu_exporter_group_get_exporter (menu->group));
277       else
278         group = menu->group;
279
280       tmp = g_slice_new (GMenuExporterLink);
281       tmp->name = g_strconcat (":", name, NULL);
282       tmp->menu = g_menu_exporter_group_add_menu (group, model);
283       tmp->next = list;
284       list = tmp;
285
286       g_object_unref (model);
287     }
288
289   g_object_unref (iter);
290
291   return list;
292 }
293
294 static GVariant *
295 g_menu_exporter_menu_describe_item (GMenuExporterMenu *menu,
296                                     gint               position)
297 {
298   GMenuAttributeIter *attr_iter;
299   GVariantBuilder builder;
300   GSequenceIter *iter;
301   GMenuExporterLink *link;
302   const char *name;
303   GVariant *value;
304
305   g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
306
307   attr_iter = g_menu_model_iterate_item_attributes (menu->model, position);
308   while (g_menu_attribute_iter_get_next (attr_iter, &name, &value))
309     {
310       g_variant_builder_add (&builder, "{sv}", name, value);
311       g_variant_unref (value);
312     }
313   g_object_unref (attr_iter);
314
315   iter = g_sequence_get_iter_at_pos (menu->item_links, position);
316   for (link = g_sequence_get (iter); link; link = link->next)
317     g_variant_builder_add (&builder, "{sv}", link->name,
318                            g_variant_new ("(uu)", g_menu_exporter_group_get_id (link->menu->group), link->menu->id));
319
320   return g_variant_builder_end (&builder);
321 }
322
323 static GVariant *
324 g_menu_exporter_menu_list (GMenuExporterMenu *menu)
325 {
326   GVariantBuilder builder;
327   gint i, n;
328
329   g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
330
331   n = g_sequence_get_length (menu->item_links);
332   for (i = 0; i < n; i++)
333     g_variant_builder_add_value (&builder, g_menu_exporter_menu_describe_item (menu, i));
334
335   return g_variant_builder_end (&builder);
336 }
337
338 static void
339 g_menu_exporter_menu_items_changed (GMenuModel *model,
340                                     gint        position,
341                                     gint        removed,
342                                     gint        added,
343                                     gpointer    user_data)
344 {
345   GMenuExporterMenu *menu = user_data;
346   GSequenceIter *point;
347   gint i;
348
349   g_assert (menu->model == model);
350   g_assert (menu->item_links != NULL);
351   g_assert (position + removed <= g_sequence_get_length (menu->item_links));
352
353   point = g_sequence_get_iter_at_pos (menu->item_links, position + removed);
354   g_sequence_remove_range (g_sequence_get_iter_at_pos (menu->item_links, position), point);
355
356   for (i = position; i < position + added; i++)
357     g_sequence_insert_before (point, g_menu_exporter_menu_create_links (menu, i));
358
359   if (g_menu_exporter_group_is_subscribed (menu->group))
360     {
361       GVariantBuilder builder;
362
363       g_variant_builder_init (&builder, G_VARIANT_TYPE ("(uuuuaa{sv})"));
364       g_variant_builder_add (&builder, "u", g_menu_exporter_group_get_id (menu->group));
365       g_variant_builder_add (&builder, "u", menu->id);
366       g_variant_builder_add (&builder, "u", position);
367       g_variant_builder_add (&builder, "u", removed);
368
369       g_variant_builder_open (&builder, G_VARIANT_TYPE ("aa{sv}"));
370       for (i = position; i < position + added; i++)
371         g_variant_builder_add_value (&builder, g_menu_exporter_menu_describe_item (menu, i));
372       g_variant_builder_close (&builder);
373
374       g_menu_exporter_report (g_menu_exporter_group_get_exporter (menu->group), g_variant_builder_end (&builder));
375     }
376 }
377
378 static void
379 g_menu_exporter_menu_prepare (GMenuExporterMenu *menu)
380 {
381   gint n_items;
382
383   g_assert (menu->item_links == NULL);
384
385   if (g_menu_model_is_mutable (menu->model))
386     menu->handler_id = g_signal_connect (menu->model, "items-changed",
387                                          G_CALLBACK (g_menu_exporter_menu_items_changed), menu);
388
389   menu->item_links = g_sequence_new (g_menu_exporter_link_free);
390
391   n_items = g_menu_model_get_n_items (menu->model);
392   if (n_items)
393     g_menu_exporter_menu_items_changed (menu->model, 0, 0, n_items, menu);
394 }
395
396 static GMenuExporterMenu *
397 g_menu_exporter_menu_new (GMenuExporterGroup *group,
398                           guint               id,
399                           GMenuModel         *model)
400 {
401   GMenuExporterMenu *menu;
402
403   menu = g_slice_new0 (GMenuExporterMenu);
404   menu->group = group;
405   menu->id = id;
406   menu->model = g_object_ref (model);
407
408   return menu;
409 }
410
411 /* {{{1 GMenuExporterGroup */
412
413 struct _GMenuExporterGroup
414 {
415   GMenuExporter *exporter;
416   guint          id;
417
418   GHashTable *menus;
419   guint       next_menu_id;
420   gboolean    prepared;
421
422   gint subscribed;
423 };
424
425 static void
426 g_menu_exporter_group_check_if_useless (GMenuExporterGroup *group)
427 {
428   if (g_hash_table_size (group->menus) == 0 && group->subscribed == 0)
429     {
430       g_menu_exporter_remove_group (group->exporter, group->id);
431
432       g_hash_table_unref (group->menus);
433
434       g_slice_free (GMenuExporterGroup, group);
435     }
436 }
437
438 static void
439 g_menu_exporter_group_subscribe (GMenuExporterGroup *group,
440                                  GVariantBuilder    *builder)
441 {
442   GHashTableIter iter;
443   gpointer key, val;
444
445   if (!group->prepared)
446     {
447       GMenuExporterMenu *menu;
448
449       /* set this first, so that any menus created during the
450        * preparation of the first menu also end up in the prepared
451        * state.
452        * */
453       group->prepared = TRUE;
454
455       menu = g_hash_table_lookup (group->menus, 0);
456       g_menu_exporter_menu_prepare (menu);
457     }
458
459   group->subscribed++;
460
461   g_hash_table_iter_init (&iter, group->menus);
462   while (g_hash_table_iter_next (&iter, &key, &val))
463     {
464       guint id = GPOINTER_TO_INT (key);
465       GMenuExporterMenu *menu = val;
466
467       if (g_sequence_get_length (menu->item_links))
468         {
469           g_variant_builder_open (builder, G_VARIANT_TYPE ("(uuaa{sv})"));
470           g_variant_builder_add (builder, "u", group->id);
471           g_variant_builder_add (builder, "u", id);
472           g_variant_builder_add_value (builder, g_menu_exporter_menu_list (menu));
473           g_variant_builder_close (builder);
474         }
475     }
476 }
477
478 static void
479 g_menu_exporter_group_unsubscribe (GMenuExporterGroup *group,
480                                    gint                count)
481 {
482   g_assert (group->subscribed >= count);
483
484   group->subscribed -= count;
485
486   g_menu_exporter_group_check_if_useless (group);
487 }
488
489 static GMenuExporter *
490 g_menu_exporter_group_get_exporter (GMenuExporterGroup *group)
491 {
492   return group->exporter;
493 }
494
495 static gboolean
496 g_menu_exporter_group_is_subscribed (GMenuExporterGroup *group)
497 {
498   return group->subscribed > 0;
499 }
500
501 static guint
502 g_menu_exporter_group_get_id (GMenuExporterGroup *group)
503 {
504   return group->id;
505 }
506
507 static void
508 g_menu_exporter_group_remove_menu (GMenuExporterGroup *group,
509                                    guint               id)
510 {
511   g_hash_table_remove (group->menus, GINT_TO_POINTER (id));
512
513   g_menu_exporter_group_check_if_useless (group);
514 }
515
516 static GMenuExporterMenu *
517 g_menu_exporter_group_add_menu (GMenuExporterGroup *group,
518                                 GMenuModel         *model)
519 {
520   GMenuExporterMenu *menu;
521   guint id;
522
523   id = group->next_menu_id++;
524   menu = g_menu_exporter_menu_new (group, id, model);
525   g_hash_table_insert (group->menus, GINT_TO_POINTER (id), menu);
526
527   if (group->prepared)
528     g_menu_exporter_menu_prepare (menu);
529
530   return menu;
531 }
532
533 static GMenuExporterGroup *
534 g_menu_exporter_group_new (GMenuExporter *exporter,
535                            guint          id)
536 {
537   GMenuExporterGroup *group;
538
539   group = g_slice_new0 (GMenuExporterGroup);
540   group->menus = g_hash_table_new (NULL, NULL);
541   group->exporter = exporter;
542   group->id = id;
543
544   return group;
545 }
546
547 /* {{{1 GMenuExporterRemote */
548
549 struct _GMenuExporterRemote
550 {
551   GMenuExporter *exporter;
552   GHashTable    *watches;
553   guint          watch_id;
554 };
555
556 static void
557 g_menu_exporter_remote_subscribe (GMenuExporterRemote *remote,
558                                   guint                group_id,
559                                   GVariantBuilder     *builder)
560 {
561   GMenuExporterGroup *group;
562   guint count;
563
564   count = (gsize) g_hash_table_lookup (remote->watches, GINT_TO_POINTER (group_id));
565   g_hash_table_insert (remote->watches, GINT_TO_POINTER (group_id), GINT_TO_POINTER (count + 1));
566
567   group = g_menu_exporter_lookup_group (remote->exporter, group_id);
568   g_menu_exporter_group_subscribe (group, builder);
569 }
570
571 static void
572 g_menu_exporter_remote_unsubscribe (GMenuExporterRemote *remote,
573                                     guint                group_id)
574 {
575   GMenuExporterGroup *group;
576   guint count;
577
578   count = (gsize) g_hash_table_lookup (remote->watches, GINT_TO_POINTER (group_id));
579
580   if (count == 0)
581     return;
582
583   if (count != 1)
584     g_hash_table_insert (remote->watches, GINT_TO_POINTER (group_id), GINT_TO_POINTER (count - 1));
585   else
586     g_hash_table_remove (remote->watches, GINT_TO_POINTER (group_id));
587
588   group = g_menu_exporter_lookup_group (remote->exporter, group_id);
589   g_menu_exporter_group_unsubscribe (group, 1);
590 }
591
592 static gboolean
593 g_menu_exporter_remote_has_subscriptions (GMenuExporterRemote *remote)
594 {
595   return g_hash_table_size (remote->watches) != 0;
596 }
597
598 static void
599 g_menu_exporter_remote_free (gpointer data)
600 {
601   GMenuExporterRemote *remote = data;
602   GHashTableIter iter;
603   gpointer key, val;
604
605   g_hash_table_iter_init (&iter, remote->watches);
606   while (g_hash_table_iter_next (&iter, &key, &val))
607     {
608       GMenuExporterGroup *group;
609
610       group = g_menu_exporter_lookup_group (remote->exporter, GPOINTER_TO_INT (key));
611       g_menu_exporter_group_unsubscribe (group, GPOINTER_TO_INT (val));
612     }
613
614   g_bus_unwatch_name (remote->watch_id);
615   g_hash_table_unref (remote->watches);
616
617   g_slice_free (GMenuExporterRemote, remote);
618 }
619
620 static GMenuExporterRemote *
621 g_menu_exporter_remote_new (GMenuExporter *exporter,
622                             guint          watch_id)
623 {
624   GMenuExporterRemote *remote;
625
626   remote = g_slice_new0 (GMenuExporterRemote);
627   remote->exporter = exporter;
628   remote->watches = g_hash_table_new (NULL, NULL);
629   remote->watch_id = watch_id;
630
631   return remote;
632 }
633
634 /* {{{1 GMenuExporter */
635
636 struct _GMenuExporter
637 {
638   GDBusConnection *connection;
639   gchar *object_path;
640   guint registration_id;
641   GHashTable *groups;
642   guint next_group_id;
643
644   GMenuExporterMenu *root;
645   GHashTable *remotes;
646 };
647
648 static void
649 g_menu_exporter_name_vanished (GDBusConnection *connection,
650                                const gchar     *name,
651                                gpointer         user_data)
652 {
653   GMenuExporter *exporter = user_data;
654
655   g_assert (exporter->connection == connection);
656
657   g_hash_table_remove (exporter->remotes, name);
658 }
659
660 static GVariant *
661 g_menu_exporter_subscribe (GMenuExporter *exporter,
662                            const gchar   *sender,
663                            GVariant      *group_ids)
664 {
665   GMenuExporterRemote *remote;
666   GVariantBuilder builder;
667   GVariantIter iter;
668   guint32 id;
669
670   remote = g_hash_table_lookup (exporter->remotes, sender);
671
672   if (remote == NULL)
673     {
674       guint watch_id;
675
676       watch_id = g_bus_watch_name_on_connection (exporter->connection, sender, G_BUS_NAME_WATCHER_FLAGS_NONE,
677                                                  NULL, g_menu_exporter_name_vanished, exporter, NULL);
678       remote = g_menu_exporter_remote_new (exporter, watch_id);
679       g_hash_table_insert (exporter->remotes, g_strdup (sender), remote);
680     }
681
682   g_variant_builder_init (&builder, G_VARIANT_TYPE ("(a(uuaa{sv}))"));
683
684   g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(uuaa{sv})"));
685
686   g_variant_iter_init (&iter, group_ids);
687   while (g_variant_iter_next (&iter, "u", &id))
688     g_menu_exporter_remote_subscribe (remote, id, &builder);
689
690   g_variant_builder_close (&builder);
691
692   return g_variant_builder_end (&builder);
693 }
694
695 static void
696 g_menu_exporter_unsubscribe (GMenuExporter *exporter,
697                              const gchar   *sender,
698                              GVariant      *group_ids)
699 {
700   GMenuExporterRemote *remote;
701   GVariantIter iter;
702   guint32 id;
703
704   remote = g_hash_table_lookup (exporter->remotes, sender);
705
706   if (remote == NULL)
707     return;
708
709   g_variant_iter_init (&iter, group_ids);
710   while (g_variant_iter_next (&iter, "u", &id))
711     g_menu_exporter_remote_unsubscribe (remote, id);
712
713   if (!g_menu_exporter_remote_has_subscriptions (remote))
714     g_hash_table_remove (exporter->remotes, sender);
715 }
716
717 static void
718 g_menu_exporter_report (GMenuExporter *exporter,
719                         GVariant      *report)
720 {
721   GVariantBuilder builder;
722
723   g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE);
724   g_variant_builder_open (&builder, G_VARIANT_TYPE_ARRAY);
725   g_variant_builder_add_value (&builder, report);
726   g_variant_builder_close (&builder);
727
728   g_dbus_connection_emit_signal (exporter->connection,
729                                  NULL,
730                                  exporter->object_path,
731                                  "org.gtk.Menus", "Changed",
732                                  g_variant_builder_end (&builder),
733                                  NULL);
734 }
735
736 static void
737 g_menu_exporter_remove_group (GMenuExporter *exporter,
738                               guint          id)
739 {
740   g_hash_table_remove (exporter->groups, GINT_TO_POINTER (id));
741 }
742
743 static GMenuExporterGroup *
744 g_menu_exporter_lookup_group (GMenuExporter *exporter,
745                               guint          group_id)
746 {
747   GMenuExporterGroup *group;
748
749   group = g_hash_table_lookup (exporter->groups, GINT_TO_POINTER (group_id));
750
751   if (group == NULL)
752     {
753       group = g_menu_exporter_group_new (exporter, group_id);
754       g_hash_table_insert (exporter->groups, GINT_TO_POINTER (group_id), group);
755     }
756
757   return group;
758 }
759
760 static GMenuExporterGroup *
761 g_menu_exporter_create_group (GMenuExporter *exporter)
762 {
763   GMenuExporterGroup *group;
764   guint id;
765
766   id = exporter->next_group_id++;
767   group = g_menu_exporter_group_new (exporter, id);
768   g_hash_table_insert (exporter->groups, GINT_TO_POINTER (id), group);
769
770   return group;
771 }
772
773 static void
774 g_menu_exporter_free (GMenuExporter *exporter)
775 {
776   g_dbus_connection_unregister_object (exporter->connection, exporter->registration_id);
777   g_menu_exporter_menu_free (exporter->root);
778   g_hash_table_unref (exporter->remotes);
779   g_hash_table_unref (exporter->groups);
780   g_object_unref (exporter->connection);
781   g_free (exporter->object_path);
782
783   g_slice_free (GMenuExporter, exporter);
784 }
785
786 static void
787 g_menu_exporter_method_call (GDBusConnection       *connection,
788                              const gchar           *sender,
789                              const gchar           *object_path,
790                              const gchar           *interface_name,
791                              const gchar           *method_name,
792                              GVariant              *parameters,
793                              GDBusMethodInvocation *invocation,
794                              gpointer               user_data)
795 {
796   GMenuExporter *exporter = user_data;
797   GVariant *group_ids;
798
799   group_ids = g_variant_get_child_value (parameters, 0);
800
801   if (g_str_equal (method_name, "Start"))
802     g_dbus_method_invocation_return_value (invocation, g_menu_exporter_subscribe (exporter, sender, group_ids));
803
804   else if (g_str_equal (method_name, "End"))
805     {
806       g_menu_exporter_unsubscribe (exporter, sender, group_ids);
807       g_dbus_method_invocation_return_value (invocation, NULL);
808     }
809
810   else
811     g_assert_not_reached ();
812
813   g_variant_unref (group_ids);
814 }
815
816 static GDBusConnection *
817 g_menu_exporter_get_connection (GMenuExporter *exporter)
818 {
819   return exporter->connection;
820 }
821
822 static const gchar *
823 g_menu_exporter_get_object_path (GMenuExporter *exporter)
824 {
825   return exporter->object_path;
826 }
827
828 static GMenuExporter *
829 g_menu_exporter_new (GDBusConnection  *connection,
830                      const gchar      *object_path,
831                      GMenuModel       *model,
832                      GError          **error)
833 {
834   const GDBusInterfaceVTable vtable = {
835     g_menu_exporter_method_call,
836   };
837   GMenuExporter *exporter;
838   guint id;
839
840   exporter = g_slice_new0 (GMenuExporter);
841
842   id = g_dbus_connection_register_object (connection, object_path, org_gtk_Menus_get_interface (),
843                                           &vtable, exporter, NULL, error);
844
845   if (id == 0)
846     {
847       g_slice_free (GMenuExporter, exporter);
848       return NULL;
849     }
850
851   exporter->connection = g_object_ref (connection);
852   exporter->object_path = g_strdup (object_path);
853   exporter->registration_id = id;
854   exporter->groups = g_hash_table_new (NULL, NULL);
855   exporter->remotes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_menu_exporter_remote_free);
856   exporter->root = g_menu_exporter_group_add_menu (g_menu_exporter_create_group (exporter), model);
857
858   return exporter;
859 }
860
861 /* {{{1 Public API */
862
863 static GHashTable *exported_menus;
864
865 /**
866  * g_menu_model_dbus_export_start:
867  * @connection: a #GDBusConnection
868  * @object_path: a D-Bus object path
869  * @menu: a #GMenuModel
870  * @error: return location for an error, or %NULL
871  *
872  * Exports @menu on @connection at @object_path.
873  *
874  * The implemented D-Bus API should be considered private.
875  * It is subject to change in the future.
876  *
877  * A given menu model can only be exported on one object path
878  * and an object path can only have one action group exported
879  * on it. If either constraint is violated, the export will
880  * fail and %FALSE will be returned (with @error set accordingly).
881  *
882  * Use g_menu_model_dbus_export_stop() to stop exporting @menu
883  * or g_menu_model_dbus_export_query() to find out if and where
884  * a given menu model is exported.
885  *
886  * Returns: %TRUE if the export is successful, or %FALSE (with
887  *     @error set) in the event of a failure.
888  */
889 gboolean
890 g_menu_model_dbus_export_start (GDBusConnection  *connection,
891                                 const gchar      *object_path,
892                                 GMenuModel       *menu,
893                                 GError          **error)
894 {
895   GMenuExporter *exporter;
896
897   if G_UNLIKELY (exported_menus == NULL)
898     exported_menus = g_hash_table_new (NULL, NULL);
899
900   if G_UNLIKELY (g_hash_table_lookup (exported_menus, menu))
901     {
902       g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FILE_EXISTS,
903                    "The given GMenuModel has already been exported");
904       return FALSE;
905     }
906
907   exporter = g_menu_exporter_new (connection, object_path, menu, error);
908
909   if (exporter == NULL)
910     return FALSE;
911
912   g_hash_table_insert (exported_menus, menu, exporter);
913
914   return TRUE;
915 }
916
917 /**
918  * g_menu_model_dbus_export_stop:
919  * @menu: a #GMenuModel
920  *
921  * Stops the export of @menu.
922  *
923  * This reverses the effect of a previous call to
924  * g_menu_model_dbus_export_start() for @menu.
925  *
926  * Returns: %TRUE if an export was stopped or %FALSE
927  *     if @menu was not exported in the first place
928  */
929 gboolean
930 g_menu_model_dbus_export_stop (GMenuModel *menu)
931 {
932   GMenuExporter *exporter;
933
934   if G_UNLIKELY (exported_menus == NULL)
935     return FALSE;
936
937   exporter = g_hash_table_lookup (exported_menus, menu);
938   if G_UNLIKELY (exporter == NULL)
939     return FALSE;
940
941   g_hash_table_remove (exported_menus, menu);
942   g_menu_exporter_free (exporter);
943
944   return TRUE;
945 }
946
947 /**
948  * g_menu_model_dbus_export_query:
949  * @menu: a #GMenuModel
950  * @connection: (out): the #GDBusConnection used for exporting
951  * @object_path: (out): the object path used for exporting
952  *
953  * Queries if and where @menu is exported.
954  *
955  * If @menu is exported, %TRUE is returned. If @connection is
956  * non-%NULL then it is set to the #GDBusConnection used for
957  * the export. If @object_path is non-%NULL then it is set to
958  * the object path.
959  *
960  * If the @menu is not exported, %FALSE is returned and
961  * @connection and @object_path remain unmodified.
962  *
963  * Returns: %TRUE if @menu was exported, else %FALSE
964  */
965 gboolean
966 g_menu_model_dbus_export_query (GMenuModel       *menu,
967                                 GDBusConnection **connection,
968                                 const gchar     **object_path)
969 {
970   GMenuExporter *exporter;
971
972   if (exported_menus == NULL)
973     return FALSE;
974
975   exporter = g_hash_table_lookup (exported_menus, menu);
976   if (exporter == NULL)
977     return FALSE;
978
979   if (connection)
980     *connection = g_menu_exporter_get_connection (exporter);
981
982   if (object_path)
983     *object_path = g_menu_exporter_get_object_path (exporter);
984
985   return TRUE;
986 }
987
988 /* {{{1 Epilogue */
989 /* vim:set foldmethod=marker: */