cleanup
[platform/upstream/glib.git] / gio / gdbusmenumodel.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, see <http://www.gnu.org/licenses/>.
16  *
17  * Author: Ryan Lortie <desrt@desrt.ca>
18  */
19
20 #include "config.h"
21
22 #include "gdbusmenumodel.h"
23
24 #include "gmenumodel.h"
25
26 /* Prelude {{{1 */
27
28 /**
29  * SECTION:gdbusmenumodel
30  * @title: GDBusMenuModel
31  * @short_description: A D-Bus GMenuModel implementation
32  * @include: gio/gio.h
33  * @see_also: [GMenuModel Exporter][gio-GMenuModel-exporter]
34  *
35  * #GDBusMenuModel is an implementation of #GMenuModel that can be used
36  * as a proxy for a menu model that is exported over D-Bus with
37  * g_dbus_connection_export_menu_model().
38  */
39
40 /*
41  * There are 3 main (quasi-)classes involved here:
42  *
43  *   - GDBusMenuPath
44  *   - GDBusMenuGroup
45  *   - GDBusMenuModel
46  *
47  * Each of these classes exists as a parameterised singleton keyed to a
48  * particular thing:
49  *
50  *   - GDBusMenuPath represents a D-Bus object path on a particular
51  *     unique bus name on a particular GDBusConnection and in a
52  *     particular GMainContext.
53  *
54  *   - GDBusMenuGroup represents a particular group on a particular
55  *     GDBusMenuPath.
56  *
57  *   - GDBusMenuModel represents a particular menu within a particular
58  *     GDBusMenuGroup.
59  *
60  * There are also two (and a half) utility structs:
61  *
62  *  - PathIdentifier and ConstPathIdentifier
63  *  - GDBusMenuModelItem
64  *
65  * PathIdentifier is the 4-tuple of (GMainContext, GDBusConnection,
66  * unique name, object path) that uniquely identifies a particular
67  * GDBusMenuPath.  It holds ownership on each of these things, so we
68  * have a ConstPathIdentifier variant that does not.
69  *
70  * We have a 3-level hierarchy of hashtables:
71  *
72  *   - a global hashtable (g_dbus_menu_paths) maps from PathIdentifier
73  *     to GDBusMenuPath
74  *
75  *   - each GDBusMenuPath has a hashtable mapping from guint (group
76  *     number) to GDBusMenuGroup
77  *
78  *   - each GDBusMenuGroup has a hashtable mapping from guint (menu
79  *     number) to GDBusMenuModel.
80  *
81  * In this way, each quintuplet of (connection, bus name, object path,
82  * group id, menu id) maps to a single GDBusMenuModel instance that can be
83  * located via 3 hashtable lookups.
84  *
85  * All of the 3 classes are refcounted (GDBusMenuPath and
86  * GDBusMenuGroup manually, and GDBusMenuModel by virtue of being a
87  * GObject).  The hashtables do not hold references -- rather, when the
88  * last reference is dropped, the object is removed from the hashtable.
89  *
90  * The hard references go in the other direction: GDBusMenuModel is created
91  * as the user requests it and only exists as long as the user holds a
92  * reference on it.  GDBusMenuModel holds a reference on the GDBusMenuGroup
93  * from which it came. GDBusMenuGroup holds a reference on
94  * GDBusMenuPath.
95  *
96  * In addition to refcounts, each object has an 'active' variable (ints
97  * for GDBusMenuPath and GDBusMenuGroup, boolean for GDBusMenuModel).
98  *
99  *   - GDBusMenuModel is inactive when created and becomes active only when
100  *     first queried for information.  This prevents extra work from
101  *     happening just by someone acquiring a GDBusMenuModel (and not
102  *     actually trying to display it yet).
103  *
104  *   - The active count on GDBusMenuGroup is equal to the number of
105  *     GDBusMenuModel instances in that group that are active.  When the
106  *     active count transitions from 0 to 1, the group calls the 'Start'
107  *     method on the service to begin monitoring that group.  When it
108  *     drops from 1 to 0, the group calls the 'End' method to stop
109  *     monitoring.
110  *
111  *   - The active count on GDBusMenuPath is equal to the number of
112  *     GDBusMenuGroup instances on that path with a non-zero active
113  *     count.  When the active count transitions from 0 to 1, the path
114  *     sets up a signal subscription to monitor any changes.  The signal
115  *     subscription is taken down when the active count transitions from
116  *     1 to 0.
117  *
118  * When active, GDBusMenuPath gets incoming signals when changes occur.
119  * If the change signal mentions a group for which we currently have an
120  * active GDBusMenuGroup, the change signal is passed along to that
121  * group.  If the group is inactive, the change signal is ignored.
122  *
123  * Most of the "work" occurs in GDBusMenuGroup.  In addition to the
124  * hashtable of GDBusMenuModel instances, it keeps a hashtable of the actual
125  * menu contents, each encoded as GSequence of GDBusMenuModelItem.  It
126  * initially populates this table with the results of the "Start" method
127  * call and then updates it according to incoming change signals.  If
128  * the change signal mentions a menu for which we current have an active
129  * GDBusMenuModel, the change signal is passed along to that model.  If the
130  * model is inactive, the change signal is ignored.
131  *
132  * GDBusMenuModelItem is just a pair of hashtables, one for the attributes
133  * and one for the links of the item.  Both map strings to GVariant
134  * instances.  In the case of links, the GVariant has type '(uu)' and is
135  * turned into a GDBusMenuModel at the point that the user pulls it through
136  * the API.
137  *
138  * Following the "empty is the same as non-existent" rule, the hashtable
139  * of GSequence of GDBusMenuModelItem holds NULL for empty menus.
140  *
141  * GDBusMenuModel contains very little functionality of its own.  It holds a
142  * (weak) reference to the GSequence of GDBusMenuModelItem contained in the
143  * GDBusMenuGroup.  It uses this GSequence to implement the GMenuModel
144  * interface.  It also emits the "items-changed" signal if it is active
145  * and it was told that the contents of the GSequence changed.
146  */
147
148 typedef struct _GDBusMenuGroup GDBusMenuGroup;
149 typedef struct _GDBusMenuPath GDBusMenuPath;
150
151 static void                     g_dbus_menu_group_changed               (GDBusMenuGroup  *group,
152                                                                          guint            menu_id,
153                                                                          gint             position,
154                                                                          gint             removed,
155                                                                          GVariant        *added);
156 static void                     g_dbus_menu_model_changed               (GDBusMenuModel  *proxy,
157                                                                          GSequence       *items,
158                                                                          gint             position,
159                                                                          gint             removed,
160                                                                          gint             added);
161 static GDBusMenuGroup *         g_dbus_menu_group_get_from_path         (GDBusMenuPath   *path,
162                                                                          guint            group_id);
163 static GDBusMenuModel *         g_dbus_menu_model_get_from_group        (GDBusMenuGroup  *group,
164                                                                          guint            menu_id);
165
166 /* PathIdentifier {{{1 */
167 typedef struct
168 {
169   GMainContext *context;
170   GDBusConnection *connection;
171   gchar *bus_name;
172   gchar *object_path;
173 } PathIdentifier;
174
175 typedef const struct
176 {
177   GMainContext *context;
178   GDBusConnection *connection;
179   const gchar *bus_name;
180   const gchar *object_path;
181 } ConstPathIdentifier;
182
183 static guint
184 path_identifier_hash (gconstpointer data)
185 {
186   ConstPathIdentifier *id = data;
187
188   return g_str_hash (id->object_path);
189 }
190
191 static gboolean
192 path_identifier_equal (gconstpointer a,
193                        gconstpointer b)
194 {
195   ConstPathIdentifier *id_a = a;
196   ConstPathIdentifier *id_b = b;
197
198   return id_a->connection == id_b->connection &&
199          g_str_equal (id_a->bus_name, id_b->bus_name) &&
200          g_str_equal (id_a->object_path, id_b->object_path);
201 }
202
203 static void
204 path_identifier_free (PathIdentifier *id)
205 {
206   g_main_context_unref (id->context);
207   g_object_unref (id->connection);
208   g_free (id->bus_name);
209   g_free (id->object_path);
210
211   g_slice_free (PathIdentifier, id);
212 }
213
214 static PathIdentifier *
215 path_identifier_new (ConstPathIdentifier *cid)
216 {
217   PathIdentifier *id;
218
219   id = g_slice_new (PathIdentifier);
220   id->context = g_main_context_ref (cid->context);
221   id->connection = g_object_ref (cid->connection);
222   id->bus_name = g_strdup (cid->bus_name);
223   id->object_path = g_strdup (cid->object_path);
224
225   return id;
226 }
227
228 /* GDBusMenuPath {{{1 */
229
230 struct _GDBusMenuPath
231 {
232   PathIdentifier *id;
233   gint ref_count;
234
235   GHashTable *groups;
236   gint active;
237   guint watch_id;
238 };
239
240 static GHashTable *g_dbus_menu_paths;
241
242 static GDBusMenuPath *
243 g_dbus_menu_path_ref (GDBusMenuPath *path)
244 {
245   path->ref_count++;
246
247   return path;
248 }
249
250 static void
251 g_dbus_menu_path_unref (GDBusMenuPath *path)
252 {
253   if (--path->ref_count == 0)
254     {
255       g_hash_table_remove (g_dbus_menu_paths, path->id);
256       g_hash_table_unref (path->groups);
257       path_identifier_free (path->id);
258
259       g_slice_free (GDBusMenuPath, path);
260     }
261 }
262
263 static void
264 g_dbus_menu_path_signal (GDBusConnection *connection,
265                          const gchar     *sender_name,
266                          const gchar     *object_path,
267                          const gchar     *interface_name,
268                          const gchar     *signal_name,
269                          GVariant        *parameters,
270                          gpointer         user_data)
271 {
272   GDBusMenuPath *path = user_data;
273   GVariantIter *iter;
274   guint group_id;
275   guint menu_id;
276   guint position;
277   guint removes;
278   GVariant *adds;
279
280   if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(a(uuuuaa{sv}))")))
281     return;
282
283   g_variant_get (parameters, "(a(uuuuaa{sv}))", &iter);
284   while (g_variant_iter_loop (iter, "(uuuu@aa{sv})", &group_id, &menu_id, &position, &removes, &adds))
285     {
286       GDBusMenuGroup *group;
287
288       group = g_hash_table_lookup (path->groups, GINT_TO_POINTER (group_id));
289
290       if (group != NULL)
291         g_dbus_menu_group_changed (group, menu_id, position, removes, adds);
292     }
293   g_variant_iter_free (iter);
294 }
295
296 static void
297 g_dbus_menu_path_activate (GDBusMenuPath *path)
298 {
299   if (path->active++ == 0)
300     path->watch_id = g_dbus_connection_signal_subscribe (path->id->connection, path->id->bus_name,
301                                                          "org.gtk.Menus", "Changed", path->id->object_path,
302                                                          NULL, G_DBUS_SIGNAL_FLAGS_NONE,
303                                                          g_dbus_menu_path_signal, path, NULL);
304 }
305
306 static void
307 g_dbus_menu_path_deactivate (GDBusMenuPath *path)
308 {
309   if (--path->active == 0)
310     g_dbus_connection_signal_unsubscribe (path->id->connection, path->watch_id);
311 }
312
313 static GDBusMenuPath *
314 g_dbus_menu_path_get (GMainContext    *context,
315                       GDBusConnection *connection,
316                       const gchar     *bus_name,
317                       const gchar     *object_path)
318 {
319   ConstPathIdentifier cid = { context, connection, bus_name, object_path };
320   GDBusMenuPath *path;
321
322   if (g_dbus_menu_paths == NULL)
323     g_dbus_menu_paths = g_hash_table_new (path_identifier_hash, path_identifier_equal);
324
325   path = g_hash_table_lookup (g_dbus_menu_paths, &cid);
326
327   if (path == NULL)
328     {
329       path = g_slice_new (GDBusMenuPath);
330       path->id = path_identifier_new (&cid);
331       path->groups = g_hash_table_new (NULL, NULL);
332       path->ref_count = 0;
333       path->active = 0;
334
335       g_hash_table_insert (g_dbus_menu_paths, path->id, path);
336     }
337
338   return g_dbus_menu_path_ref (path);
339 }
340
341 /* GDBusMenuGroup, GDBusMenuModelItem {{{1 */
342 typedef enum
343 {
344   GROUP_OFFLINE,
345   GROUP_PENDING,
346   GROUP_ONLINE
347 } GroupStatus;
348
349 struct _GDBusMenuGroup
350 {
351   GDBusMenuPath *path;
352   guint id;
353
354   GHashTable *proxies; /* uint -> unowned GDBusMenuModel */
355   GHashTable *menus;   /* uint -> owned GSequence */
356   gint ref_count;
357   GroupStatus state;
358   gint active;
359 };
360
361 typedef struct
362 {
363   GHashTable *attributes;
364   GHashTable *links;
365 } GDBusMenuModelItem;
366
367 static GDBusMenuGroup *
368 g_dbus_menu_group_ref (GDBusMenuGroup *group)
369 {
370   group->ref_count++;
371
372   return group;
373 }
374
375 static void
376 g_dbus_menu_group_unref (GDBusMenuGroup *group)
377 {
378   if (--group->ref_count == 0)
379     {
380       g_assert (group->state == GROUP_OFFLINE);
381       g_assert (group->active == 0);
382
383       g_hash_table_remove (group->path->groups, GINT_TO_POINTER (group->id));
384       g_hash_table_unref (group->proxies);
385       g_hash_table_unref (group->menus);
386
387       g_dbus_menu_path_unref (group->path);
388
389       g_slice_free (GDBusMenuGroup, group);
390     }
391 }
392
393 static void
394 g_dbus_menu_model_item_free (gpointer data)
395 {
396   GDBusMenuModelItem *item = data;
397
398   g_hash_table_unref (item->attributes);
399   g_hash_table_unref (item->links);
400
401   g_slice_free (GDBusMenuModelItem, item);
402 }
403
404 static GDBusMenuModelItem *
405 g_dbus_menu_group_create_item (GVariant *description)
406 {
407   GDBusMenuModelItem *item;
408   GVariantIter iter;
409   const gchar *key;
410   GVariant *value;
411
412   item = g_slice_new (GDBusMenuModelItem);
413   item->attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
414   item->links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
415
416   g_variant_iter_init (&iter, description);
417   while (g_variant_iter_loop (&iter, "{&sv}", &key, &value))
418     if (key[0] == ':')
419       /* key + 1 to skip the ':' */
420       g_hash_table_insert (item->links, g_strdup (key + 1), g_variant_ref (value));
421     else
422       g_hash_table_insert (item->attributes, g_strdup (key), g_variant_ref (value));
423
424   return item;
425 }
426
427 /*
428  * GDBusMenuGroup can be in three states:
429  *
430  * OFFLINE: not subscribed to this group
431  * PENDING: we made the call to subscribe to this group, but the result
432  *          has not come back yet
433  * ONLINE: we are fully subscribed
434  *
435  * We can get into some nasty situations where we make a call due to an
436  * activation request but receive a deactivation request before the call
437  * returns.  If another activation request occurs then we could risk
438  * sending a Start request even though one is already in progress.  For
439  * this reason, we have to carefully consider what to do in each of the
440  * three states for each of the following situations:
441  *
442  *  - activation requested
443  *  - deactivation requested
444  *  - Start call finishes
445  *
446  * To simplify things a bit, we do not have a callback for the Stop
447  * call.  We just send it and assume that it takes effect immediately.
448  *
449  * Activation requested:
450  *   OFFLINE: make the Start call and transition to PENDING
451  *   PENDING: do nothing -- call is already in progress.
452  *   ONLINE: this should not be possible
453  *
454  * Deactivation requested:
455  *   OFFLINE: this should not be possible
456  *   PENDING: do nothing -- handle it when the Start call finishes
457  *   ONLINE: send the Stop call and move to OFFLINE immediately
458  *
459  * Start call finishes:
460  *   OFFLINE: this should not be possible
461  *   PENDING:
462  *     If we should be active (ie: active count > 0): move to ONLINE
463  *     If not: send Stop call and move to OFFLINE immediately
464  *   ONLINE: this should not be possible
465  *
466  * We have to take care with regards to signal subscriptions (ie:
467  * activation of the GDBusMenuPath).  The signal subscription is always
468  * established when transitioning from OFFLINE to PENDING and taken down
469  * when transitioning to OFFLINE (from either PENDING or ONLINE).
470  *
471  * Since there are two places where we transition to OFFLINE, we split
472  * that code out into a separate function.
473  */
474 static void
475 g_dbus_menu_group_go_offline (GDBusMenuGroup *group)
476 {
477   g_dbus_menu_path_deactivate (group->path);
478   g_dbus_connection_call (group->path->id->connection,
479                           group->path->id->bus_name,
480                           group->path->id->object_path,
481                           "org.gtk.Menus", "End",
482                           g_variant_new_parsed ("([ %u ],)", group->id),
483                           NULL, G_DBUS_CALL_FLAGS_NONE, -1,
484                           NULL, NULL, NULL);
485   group->state = GROUP_OFFLINE;
486 }
487
488
489 static void
490 g_dbus_menu_group_start_ready (GObject      *source_object,
491                                GAsyncResult *result,
492                                gpointer      user_data)
493 {
494   GDBusConnection *connection = G_DBUS_CONNECTION (source_object);
495   GDBusMenuGroup *group = user_data;
496   GVariant *reply;
497
498   g_assert (group->state == GROUP_PENDING);
499
500   reply = g_dbus_connection_call_finish (connection, result, NULL);
501
502   if (group->active)
503     {
504       group->state = GROUP_ONLINE;
505
506       /* If we receive no reply, just act like we got an empty reply. */
507       if (reply)
508         {
509           GVariantIter *iter;
510           GVariant *items;
511           guint group_id;
512           guint menu_id;
513
514           g_variant_get (reply, "(a(uuaa{sv}))", &iter);
515           while (g_variant_iter_loop (iter, "(uu@aa{sv})", &group_id, &menu_id, &items))
516             if (group_id == group->id)
517               g_dbus_menu_group_changed (group, menu_id, 0, 0, items);
518           g_variant_iter_free (iter);
519         }
520     }
521   else
522     g_dbus_menu_group_go_offline (group);
523
524   if (reply)
525     g_variant_unref (reply);
526
527   g_dbus_menu_group_unref (group);
528 }
529
530 static void
531 g_dbus_menu_group_activate (GDBusMenuGroup *group)
532 {
533   if (group->active++ == 0)
534     {
535       g_assert (group->state != GROUP_ONLINE);
536
537       if (group->state == GROUP_OFFLINE)
538         {
539           g_dbus_menu_path_activate (group->path);
540
541           g_dbus_connection_call (group->path->id->connection,
542                                   group->path->id->bus_name,
543                                   group->path->id->object_path,
544                                   "org.gtk.Menus", "Start",
545                                   g_variant_new_parsed ("([ %u ],)", group->id),
546                                   G_VARIANT_TYPE ("(a(uuaa{sv}))"),
547                                   G_DBUS_CALL_FLAGS_NONE, -1, NULL,
548                                   g_dbus_menu_group_start_ready,
549                                   g_dbus_menu_group_ref (group));
550           group->state = GROUP_PENDING;
551         }
552     }
553 }
554
555 static void
556 g_dbus_menu_group_deactivate (GDBusMenuGroup *group)
557 {
558   if (--group->active == 0)
559     {
560       g_assert (group->state != GROUP_OFFLINE);
561
562       if (group->state == GROUP_ONLINE)
563         {
564           /* We are here because nobody is watching, so just free
565            * everything and don't bother with the notifications.
566            */
567           g_hash_table_remove_all (group->menus);
568
569           g_dbus_menu_group_go_offline (group);
570         }
571     }
572 }
573
574 static void
575 g_dbus_menu_group_changed (GDBusMenuGroup *group,
576                            guint           menu_id,
577                            gint            position,
578                            gint            removed,
579                            GVariant       *added)
580 {
581   GSequenceIter *point;
582   GVariantIter iter;
583   GDBusMenuModel *proxy;
584   GSequence *items;
585   GVariant *item;
586   gint n_added;
587
588   /* We could have signals coming to us when we're not active (due to
589    * some other process having subscribed to this group) or when we're
590    * pending.  In both of those cases, we want to ignore the signal
591    * since we'll get our own information when we call "Start" for
592    * ourselves.
593    */
594   if (group->state != GROUP_ONLINE)
595     return;
596
597   items = g_hash_table_lookup (group->menus, GINT_TO_POINTER (menu_id));
598
599   if (items == NULL)
600     {
601       items = g_sequence_new (g_dbus_menu_model_item_free);
602       g_hash_table_insert (group->menus, GINT_TO_POINTER (menu_id), items);
603     }
604
605   point = g_sequence_get_iter_at_pos (items, position + removed);
606
607   g_return_if_fail (point != NULL);
608
609   if (removed)
610     {
611       GSequenceIter *start;
612
613       start = g_sequence_get_iter_at_pos (items, position);
614       g_sequence_remove_range (start, point);
615     }
616
617   n_added = g_variant_iter_init (&iter, added);
618   while (g_variant_iter_loop (&iter, "@a{sv}", &item))
619     g_sequence_insert_before (point, g_dbus_menu_group_create_item (item));
620
621   if (g_sequence_get_length (items) == 0)
622     {
623       g_hash_table_remove (group->menus, GINT_TO_POINTER (menu_id));
624       items = NULL;
625     }
626
627   if ((proxy = g_hash_table_lookup (group->proxies, GINT_TO_POINTER (menu_id))))
628     g_dbus_menu_model_changed (proxy, items, position, removed, n_added);
629 }
630
631 static GDBusMenuGroup *
632 g_dbus_menu_group_get_from_path (GDBusMenuPath *path,
633                                  guint          group_id)
634 {
635   GDBusMenuGroup *group;
636
637   group = g_hash_table_lookup (path->groups, GINT_TO_POINTER (group_id));
638
639   if (group == NULL)
640     {
641       group = g_slice_new (GDBusMenuGroup);
642       group->path = g_dbus_menu_path_ref (path);
643       group->id = group_id;
644       group->proxies = g_hash_table_new (NULL, NULL);
645       group->menus = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_sequence_free);
646       group->state = GROUP_OFFLINE;
647       group->active = 0;
648       group->ref_count = 0;
649
650       g_hash_table_insert (path->groups, GINT_TO_POINTER (group->id), group);
651     }
652
653   return g_dbus_menu_group_ref (group);
654 }
655
656 static GDBusMenuGroup *
657 g_dbus_menu_group_get (GMainContext    *context,
658                        GDBusConnection *connection,
659                        const gchar     *bus_name,
660                        const gchar     *object_path,
661                        guint            group_id)
662 {
663   GDBusMenuGroup *group;
664   GDBusMenuPath *path;
665
666   path = g_dbus_menu_path_get (context, connection, bus_name, object_path);
667   group = g_dbus_menu_group_get_from_path (path, group_id);
668   g_dbus_menu_path_unref (path);
669
670   return group;
671 }
672
673 /* GDBusMenuModel {{{1 */
674
675 typedef GMenuModelClass GDBusMenuModelClass;
676 struct _GDBusMenuModel
677 {
678   GMenuModel parent;
679
680   GDBusMenuGroup *group;
681   guint id;
682
683   GSequence *items; /* unowned */
684   gboolean active;
685 };
686
687 G_DEFINE_TYPE (GDBusMenuModel, g_dbus_menu_model, G_TYPE_MENU_MODEL)
688
689 static gboolean
690 g_dbus_menu_model_is_mutable (GMenuModel *model)
691 {
692   return TRUE;
693 }
694
695 static gint
696 g_dbus_menu_model_get_n_items (GMenuModel *model)
697 {
698   GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (model);
699
700   if (!proxy->active)
701     {
702       g_dbus_menu_group_activate (proxy->group);
703       proxy->active = TRUE;
704     }
705
706   return proxy->items ? g_sequence_get_length (proxy->items) : 0;
707 }
708
709 static void
710 g_dbus_menu_model_get_item_attributes (GMenuModel  *model,
711                                        gint         item_index,
712                                        GHashTable **table)
713 {
714   GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (model);
715   GDBusMenuModelItem *item;
716   GSequenceIter *iter;
717
718   g_return_if_fail (proxy->active);
719   g_return_if_fail (proxy->items);
720
721   iter = g_sequence_get_iter_at_pos (proxy->items, item_index);
722   g_return_if_fail (iter);
723
724   item = g_sequence_get (iter);
725   g_return_if_fail (item);
726
727   *table = g_hash_table_ref (item->attributes);
728 }
729
730 static void
731 g_dbus_menu_model_get_item_links (GMenuModel  *model,
732                                   gint         item_index,
733                                   GHashTable **table)
734 {
735   GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (model);
736   GDBusMenuModelItem *item;
737   GSequenceIter *iter;
738
739   g_return_if_fail (proxy->active);
740   g_return_if_fail (proxy->items);
741
742   iter = g_sequence_get_iter_at_pos (proxy->items, item_index);
743   g_return_if_fail (iter);
744
745   item = g_sequence_get (iter);
746   g_return_if_fail (item);
747
748   *table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
749
750   {
751     GHashTableIter tmp;
752     gpointer key;
753     gpointer value;
754
755     g_hash_table_iter_init (&tmp, item->links);
756     while (g_hash_table_iter_next (&tmp, &key, &value))
757       {
758         if (g_variant_is_of_type (value, G_VARIANT_TYPE ("(uu)")))
759           {
760             guint group_id, menu_id;
761             GDBusMenuGroup *group;
762             GDBusMenuModel *link;
763
764             g_variant_get (value, "(uu)", &group_id, &menu_id);
765
766             /* save the hash lookup in a relatively common case */
767             if (proxy->group->id != group_id)
768               group = g_dbus_menu_group_get_from_path (proxy->group->path, group_id);
769             else
770               group = g_dbus_menu_group_ref (proxy->group);
771
772             link = g_dbus_menu_model_get_from_group (group, menu_id);
773
774             g_hash_table_insert (*table, g_strdup (key), link);
775
776             g_dbus_menu_group_unref (group);
777           }
778       }
779   }
780 }
781
782 static void
783 g_dbus_menu_model_finalize (GObject *object)
784 {
785   GDBusMenuModel *proxy = G_DBUS_MENU_MODEL (object);
786
787   if (proxy->active)
788     g_dbus_menu_group_deactivate (proxy->group);
789
790   g_hash_table_remove (proxy->group->proxies, GINT_TO_POINTER (proxy->id));
791   g_dbus_menu_group_unref (proxy->group);
792
793   G_OBJECT_CLASS (g_dbus_menu_model_parent_class)
794     ->finalize (object);
795 }
796
797 static void
798 g_dbus_menu_model_init (GDBusMenuModel *proxy)
799 {
800 }
801
802 static void
803 g_dbus_menu_model_class_init (GDBusMenuModelClass *class)
804 {
805   GObjectClass *object_class = G_OBJECT_CLASS (class);
806
807   class->is_mutable = g_dbus_menu_model_is_mutable;
808   class->get_n_items = g_dbus_menu_model_get_n_items;
809   class->get_item_attributes = g_dbus_menu_model_get_item_attributes;
810   class->get_item_links = g_dbus_menu_model_get_item_links;
811
812   object_class->finalize = g_dbus_menu_model_finalize;
813 }
814
815 static void
816 g_dbus_menu_model_changed (GDBusMenuModel *proxy,
817                            GSequence      *items,
818                            gint            position,
819                            gint            removed,
820                            gint            added)
821 {
822   proxy->items = items;
823
824   if (proxy->active && (removed || added))
825     g_menu_model_items_changed (G_MENU_MODEL (proxy), position, removed, added);
826 }
827
828 static GDBusMenuModel *
829 g_dbus_menu_model_get_from_group (GDBusMenuGroup *group,
830                                   guint           menu_id)
831 {
832   GDBusMenuModel *proxy;
833
834   proxy = g_hash_table_lookup (group->proxies, GINT_TO_POINTER (menu_id));
835   if (proxy)
836     g_object_ref (proxy);
837
838   if (proxy == NULL)
839     {
840       proxy = g_object_new (G_TYPE_DBUS_MENU_MODEL, NULL);
841       proxy->items = g_hash_table_lookup (group->menus, GINT_TO_POINTER (menu_id));
842       g_hash_table_insert (group->proxies, GINT_TO_POINTER (menu_id), proxy);
843       proxy->group = g_dbus_menu_group_ref (group);
844       proxy->id = menu_id;
845     }
846
847   return proxy;
848 }
849
850 /**
851  * g_dbus_menu_model_get:
852  * @connection: a #GDBusConnection
853  * @bus_name: the bus name which exports the menu model
854  * @object_path: the object path at which the menu model is exported
855  *
856  * Obtains a #GDBusMenuModel for the menu model which is exported
857  * at the given @bus_name and @object_path.
858  *
859  * The thread default main context is taken at the time of this call.
860  * All signals on the menu model (and any linked models) are reported
861  * with respect to this context.  All calls on the returned menu model
862  * (and linked models) must also originate from this same context, with
863  * the thread default main context unchanged.
864  *
865  * Returns: (transfer full): a #GDBusMenuModel object. Free with
866  *     g_object_unref().
867  *
868  * Since: 2.32
869  */
870 GDBusMenuModel *
871 g_dbus_menu_model_get (GDBusConnection *connection,
872                        const gchar     *bus_name,
873                        const gchar     *object_path)
874 {
875   GDBusMenuGroup *group;
876   GDBusMenuModel *proxy;
877   GMainContext *context;
878
879   context = g_main_context_get_thread_default ();
880   if (context == NULL)
881     context = g_main_context_default ();
882
883   group = g_dbus_menu_group_get (context, connection, bus_name, object_path, 0);
884   proxy = g_dbus_menu_model_get_from_group (group, 0);
885   g_dbus_menu_group_unref (group);
886
887   return proxy;
888 }
889
890 #if 0
891 static void
892 dump_proxy (gpointer key, gpointer value, gpointer data)
893 {
894   GDBusMenuModel *proxy = value;
895
896   g_print ("    menu %d refcount %d active %d\n",
897            proxy->id, G_OBJECT (proxy)->ref_count, proxy->active);
898 }
899
900 static void
901 dump_group (gpointer key, gpointer value, gpointer data)
902 {
903   GDBusMenuGroup *group = value;
904
905   g_print ("  group %d refcount %d state %d active %d\n",
906            group->id, group->ref_count, group->state, group->active);
907
908   g_hash_table_foreach (group->proxies, dump_proxy, NULL);
909 }
910
911 static void
912 dump_path (gpointer key, gpointer value, gpointer data)
913 {
914   PathIdentifier *pid = key;
915   GDBusMenuPath *path = value;
916
917   g_print ("%s active %d\n", pid->object_path, path->active);
918   g_hash_table_foreach (path->groups, dump_group, NULL);
919 }
920
921 void
922 g_dbus_menu_model_dump (void)
923 {
924   g_hash_table_foreach (g_dbus_menu_paths, dump_path, NULL);
925 }
926
927 #endif
928
929 /* Epilogue {{{1 */
930 /* vim:set foldmethod=marker: */