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