/* * Copyright © 2011 Canonical Ltd. * * SPDX-License-Identifier: LGPL-2.1-or-later * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, see . * * Author: Ryan Lortie */ #include "config.h" #include "gmenuexporter.h" #include "gdbusmethodinvocation.h" #include "gdbusintrospection.h" #include "gdbusnamewatching.h" #include "gdbuserror.h" /* {{{1 D-Bus Interface description */ /* For documentation of this interface, see * https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI */ static GDBusInterfaceInfo * org_gtk_Menus_get_interface (void) { static GDBusInterfaceInfo *interface_info; static gsize interface_info_initialized = 0; if (g_once_init_enter (&interface_info_initialized)) { GError *error = NULL; GDBusNodeInfo *info; info = g_dbus_node_info_new_for_xml ("" " " " " " " " " " " " " " " " " " " " arg type='a(uuuuaa{sv})' name='changes'/>" " " " " "", &error); if (info == NULL) g_error ("%s", error->message); interface_info = g_dbus_node_info_lookup_interface (info, "org.gtk.Menus"); g_assert (interface_info != NULL); g_dbus_interface_info_ref (interface_info); g_dbus_node_info_unref (info); g_once_init_leave (&interface_info_initialized, 1); } return interface_info; } /* {{{1 Forward declarations */ typedef struct _GMenuExporterMenu GMenuExporterMenu; typedef struct _GMenuExporterLink GMenuExporterLink; typedef struct _GMenuExporterGroup GMenuExporterGroup; typedef struct _GMenuExporterRemote GMenuExporterRemote; typedef struct _GMenuExporterWatch GMenuExporterWatch; typedef struct _GMenuExporter GMenuExporter; static gboolean g_menu_exporter_group_is_subscribed (GMenuExporterGroup *group); static guint g_menu_exporter_group_get_id (GMenuExporterGroup *group); static GMenuExporter * g_menu_exporter_group_get_exporter (GMenuExporterGroup *group); static GMenuExporterMenu * g_menu_exporter_group_add_menu (GMenuExporterGroup *group, GMenuModel *model); static void g_menu_exporter_group_remove_menu (GMenuExporterGroup *group, guint id); static GMenuExporterGroup * g_menu_exporter_create_group (GMenuExporter *exporter); static GMenuExporterGroup * g_menu_exporter_lookup_group (GMenuExporter *exporter, guint group_id); static void g_menu_exporter_report (GMenuExporter *exporter, GVariant *report); static void g_menu_exporter_remove_group (GMenuExporter *exporter, guint id); /* {{{1 GMenuExporterLink, GMenuExporterMenu */ struct _GMenuExporterMenu { GMenuExporterGroup *group; guint id; GMenuModel *model; gulong handler_id; GSequence *item_links; }; struct _GMenuExporterLink { gchar *name; GMenuExporterMenu *menu; GMenuExporterLink *next; }; static void g_menu_exporter_menu_free (GMenuExporterMenu *menu) { g_menu_exporter_group_remove_menu (menu->group, menu->id); if (menu->handler_id != 0) g_signal_handler_disconnect (menu->model, menu->handler_id); if (menu->item_links != NULL) g_sequence_free (menu->item_links); g_object_unref (menu->model); g_slice_free (GMenuExporterMenu, menu); } static void g_menu_exporter_link_free (gpointer data) { GMenuExporterLink *link = data; while (link != NULL) { GMenuExporterLink *tmp = link; link = tmp->next; g_menu_exporter_menu_free (tmp->menu); g_free (tmp->name); g_slice_free (GMenuExporterLink, tmp); } } static GMenuExporterLink * g_menu_exporter_menu_create_links (GMenuExporterMenu *menu, gint position) { GMenuExporterLink *list = NULL; GMenuLinkIter *iter; const char *name; GMenuModel *model; iter = g_menu_model_iterate_item_links (menu->model, position); while (g_menu_link_iter_get_next (iter, &name, &model)) { GMenuExporterGroup *group; GMenuExporterLink *tmp; /* keep sections in the same group, but create new groups * otherwise */ if (!g_str_equal (name, "section")) group = g_menu_exporter_create_group (g_menu_exporter_group_get_exporter (menu->group)); else group = menu->group; tmp = g_slice_new (GMenuExporterLink); tmp->name = g_strconcat (":", name, NULL); tmp->menu = g_menu_exporter_group_add_menu (group, model); tmp->next = list; list = tmp; g_object_unref (model); } g_object_unref (iter); return list; } static GVariant * g_menu_exporter_menu_describe_item (GMenuExporterMenu *menu, gint position) { GMenuAttributeIter *attr_iter; GVariantBuilder builder; GSequenceIter *iter; GMenuExporterLink *link; const char *name; GVariant *value; g_variant_builder_init_static (&builder, G_VARIANT_TYPE_VARDICT); attr_iter = g_menu_model_iterate_item_attributes (menu->model, position); while (g_menu_attribute_iter_get_next (attr_iter, &name, &value)) { g_variant_builder_add (&builder, "{sv}", name, value); g_variant_unref (value); } g_object_unref (attr_iter); iter = g_sequence_get_iter_at_pos (menu->item_links, position); for (link = g_sequence_get (iter); link; link = link->next) g_variant_builder_add (&builder, "{sv}", link->name, g_variant_new ("(uu)", g_menu_exporter_group_get_id (link->menu->group), link->menu->id)); return g_variant_builder_end (&builder); } static GVariant * g_menu_exporter_menu_list (GMenuExporterMenu *menu) { GVariantBuilder builder; gint i, n; g_variant_builder_init_static (&builder, G_VARIANT_TYPE ("aa{sv}")); n = g_sequence_get_length (menu->item_links); for (i = 0; i < n; i++) g_variant_builder_add_value (&builder, g_menu_exporter_menu_describe_item (menu, i)); return g_variant_builder_end (&builder); } static void g_menu_exporter_menu_items_changed (GMenuModel *model, gint position, gint removed, gint added, gpointer user_data) { GMenuExporterMenu *menu = user_data; GSequenceIter *point; gint i; #ifndef G_DISABLE_ASSERT gint n_items; #endif g_assert (menu->model == model); g_assert (menu->item_links != NULL); #ifndef G_DISABLE_ASSERT n_items = g_sequence_get_length (menu->item_links); #endif g_assert (position >= 0 && position < G_MENU_EXPORTER_MAX_SECTION_SIZE); g_assert (removed >= 0 && removed < G_MENU_EXPORTER_MAX_SECTION_SIZE); g_assert (added < G_MENU_EXPORTER_MAX_SECTION_SIZE); g_assert (position + removed <= n_items); g_assert (n_items - removed + added < G_MENU_EXPORTER_MAX_SECTION_SIZE); point = g_sequence_get_iter_at_pos (menu->item_links, position + removed); g_sequence_remove_range (g_sequence_get_iter_at_pos (menu->item_links, position), point); for (i = position; i < position + added; i++) g_sequence_insert_before (point, g_menu_exporter_menu_create_links (menu, i)); if (g_menu_exporter_group_is_subscribed (menu->group)) { GVariantBuilder builder; g_variant_builder_init_static (&builder, G_VARIANT_TYPE ("(uuuuaa{sv})")); g_variant_builder_add (&builder, "u", g_menu_exporter_group_get_id (menu->group)); g_variant_builder_add (&builder, "u", menu->id); g_variant_builder_add (&builder, "u", position); g_variant_builder_add (&builder, "u", removed); g_variant_builder_open (&builder, G_VARIANT_TYPE ("aa{sv}")); for (i = position; i < position + added; i++) g_variant_builder_add_value (&builder, g_menu_exporter_menu_describe_item (menu, i)); g_variant_builder_close (&builder); g_menu_exporter_report (g_menu_exporter_group_get_exporter (menu->group), g_variant_builder_end (&builder)); } } static void g_menu_exporter_menu_prepare (GMenuExporterMenu *menu) { gint n_items; g_assert (menu->item_links == NULL); if (g_menu_model_is_mutable (menu->model)) menu->handler_id = g_signal_connect (menu->model, "items-changed", G_CALLBACK (g_menu_exporter_menu_items_changed), menu); menu->item_links = g_sequence_new (g_menu_exporter_link_free); n_items = g_menu_model_get_n_items (menu->model); if (n_items) g_menu_exporter_menu_items_changed (menu->model, 0, 0, n_items, menu); } static GMenuExporterMenu * g_menu_exporter_menu_new (GMenuExporterGroup *group, guint id, GMenuModel *model) { GMenuExporterMenu *menu; menu = g_slice_new0 (GMenuExporterMenu); menu->group = group; menu->id = id; menu->model = g_object_ref (model); return menu; } /* {{{1 GMenuExporterGroup */ struct _GMenuExporterGroup { GMenuExporter *exporter; guint id; GHashTable *menus; guint next_menu_id; gboolean prepared; gint subscribed; }; static void g_menu_exporter_group_check_if_useless (GMenuExporterGroup *group) { if (g_hash_table_size (group->menus) == 0 && group->subscribed == 0) { g_menu_exporter_remove_group (group->exporter, group->id); g_hash_table_unref (group->menus); g_slice_free (GMenuExporterGroup, group); } } static void g_menu_exporter_group_subscribe (GMenuExporterGroup *group, GVariantBuilder *builder) { GHashTableIter iter; gpointer key, val; if (!group->prepared) { GMenuExporterMenu *menu; /* set this first, so that any menus created during the * preparation of the first menu also end up in the prepared * state. * */ group->prepared = TRUE; menu = g_hash_table_lookup (group->menus, 0); /* If the group was created by a subscription and does not yet * exist, it won't have a root menu... * * That menu will be prepared if it is ever added (due to * group->prepared == TRUE). */ if (menu) g_menu_exporter_menu_prepare (menu); } group->subscribed++; g_hash_table_iter_init (&iter, group->menus); while (g_hash_table_iter_next (&iter, &key, &val)) { guint id = GPOINTER_TO_INT (key); GMenuExporterMenu *menu = val; if (!g_sequence_is_empty (menu->item_links)) { g_variant_builder_open (builder, G_VARIANT_TYPE ("(uuaa{sv})")); g_variant_builder_add (builder, "u", group->id); g_variant_builder_add (builder, "u", id); g_variant_builder_add_value (builder, g_menu_exporter_menu_list (menu)); g_variant_builder_close (builder); } } } static void g_menu_exporter_group_unsubscribe (GMenuExporterGroup *group, gint count) { g_assert (group->subscribed >= count); group->subscribed -= count; g_menu_exporter_group_check_if_useless (group); } static GMenuExporter * g_menu_exporter_group_get_exporter (GMenuExporterGroup *group) { return group->exporter; } static gboolean g_menu_exporter_group_is_subscribed (GMenuExporterGroup *group) { return group->subscribed > 0; } static guint g_menu_exporter_group_get_id (GMenuExporterGroup *group) { return group->id; } static void g_menu_exporter_group_remove_menu (GMenuExporterGroup *group, guint id) { g_hash_table_remove (group->menus, GINT_TO_POINTER (id)); g_menu_exporter_group_check_if_useless (group); } static GMenuExporterMenu * g_menu_exporter_group_add_menu (GMenuExporterGroup *group, GMenuModel *model) { GMenuExporterMenu *menu; guint id; id = group->next_menu_id++; menu = g_menu_exporter_menu_new (group, id, model); g_hash_table_insert (group->menus, GINT_TO_POINTER (id), menu); if (group->prepared) g_menu_exporter_menu_prepare (menu); return menu; } static GMenuExporterGroup * g_menu_exporter_group_new (GMenuExporter *exporter, guint id) { GMenuExporterGroup *group; group = g_slice_new0 (GMenuExporterGroup); group->menus = g_hash_table_new (NULL, NULL); group->exporter = exporter; group->id = id; return group; } /* {{{1 GMenuExporterRemote */ struct _GMenuExporterRemote { GMenuExporter *exporter; GHashTable *watches; guint watch_id; }; static void g_menu_exporter_remote_subscribe (GMenuExporterRemote *remote, guint group_id, GVariantBuilder *builder) { GMenuExporterGroup *group; guint count; count = (gsize) g_hash_table_lookup (remote->watches, GINT_TO_POINTER (group_id)); g_hash_table_insert (remote->watches, GINT_TO_POINTER (group_id), GINT_TO_POINTER (count + 1)); /* Group will be created (as empty/unsubscribed if it does not exist) */ group = g_menu_exporter_lookup_group (remote->exporter, group_id); g_menu_exporter_group_subscribe (group, builder); } static void g_menu_exporter_remote_unsubscribe (GMenuExporterRemote *remote, guint group_id) { GMenuExporterGroup *group; guint count; count = (gsize) g_hash_table_lookup (remote->watches, GINT_TO_POINTER (group_id)); if (count == 0) return; if (count != 1) g_hash_table_insert (remote->watches, GINT_TO_POINTER (group_id), GINT_TO_POINTER (count - 1)); else g_hash_table_remove (remote->watches, GINT_TO_POINTER (group_id)); group = g_menu_exporter_lookup_group (remote->exporter, group_id); g_menu_exporter_group_unsubscribe (group, 1); } static gboolean g_menu_exporter_remote_has_subscriptions (GMenuExporterRemote *remote) { return g_hash_table_size (remote->watches) != 0; } static void g_menu_exporter_remote_free (gpointer data) { GMenuExporterRemote *remote = data; GHashTableIter iter; gpointer key, val; g_hash_table_iter_init (&iter, remote->watches); while (g_hash_table_iter_next (&iter, &key, &val)) { GMenuExporterGroup *group; group = g_menu_exporter_lookup_group (remote->exporter, GPOINTER_TO_INT (key)); g_menu_exporter_group_unsubscribe (group, GPOINTER_TO_INT (val)); } if (remote->watch_id > 0) g_bus_unwatch_name (remote->watch_id); g_hash_table_unref (remote->watches); g_slice_free (GMenuExporterRemote, remote); } static GMenuExporterRemote * g_menu_exporter_remote_new (GMenuExporter *exporter, guint watch_id) { GMenuExporterRemote *remote; remote = g_slice_new0 (GMenuExporterRemote); remote->exporter = exporter; remote->watches = g_hash_table_new (NULL, NULL); remote->watch_id = watch_id; return remote; } /* {{{1 GMenuExporter */ struct _GMenuExporter { GDBusConnection *connection; gchar *object_path; guint registration_id; GHashTable *groups; guint next_group_id; GMenuExporterMenu *root; GMenuExporterRemote *peer_remote; GHashTable *remotes; }; static void g_menu_exporter_name_vanished (GDBusConnection *connection, const gchar *name, gpointer user_data) { GMenuExporter *exporter = user_data; /* connection == NULL when we get called because the connection closed */ g_assert (exporter->connection == connection || connection == NULL); g_hash_table_remove (exporter->remotes, name); } static GVariant * g_menu_exporter_subscribe (GMenuExporter *exporter, const gchar *sender, GVariant *group_ids) { GMenuExporterRemote *remote; GVariantBuilder builder; GVariantIter iter; guint32 id; if (sender != NULL) remote = g_hash_table_lookup (exporter->remotes, sender); else remote = exporter->peer_remote; if (remote == NULL) { if (sender != NULL) { guint watch_id; watch_id = g_bus_watch_name_on_connection (exporter->connection, sender, G_BUS_NAME_WATCHER_FLAGS_NONE, NULL, g_menu_exporter_name_vanished, exporter, NULL); remote = g_menu_exporter_remote_new (exporter, watch_id); g_hash_table_insert (exporter->remotes, g_strdup (sender), remote); } else remote = exporter->peer_remote = g_menu_exporter_remote_new (exporter, 0); } g_variant_builder_init_static (&builder, G_VARIANT_TYPE ("(a(uuaa{sv}))")); g_variant_builder_open (&builder, G_VARIANT_TYPE ("a(uuaa{sv})")); g_variant_iter_init (&iter, group_ids); while (g_variant_iter_next (&iter, "u", &id)) g_menu_exporter_remote_subscribe (remote, id, &builder); g_variant_builder_close (&builder); return g_variant_builder_end (&builder); } static void g_menu_exporter_unsubscribe (GMenuExporter *exporter, const gchar *sender, GVariant *group_ids) { GMenuExporterRemote *remote; GVariantIter iter; guint32 id; if (sender != NULL) remote = g_hash_table_lookup (exporter->remotes, sender); else remote = exporter->peer_remote; if (remote == NULL) return; g_variant_iter_init (&iter, group_ids); while (g_variant_iter_next (&iter, "u", &id)) g_menu_exporter_remote_unsubscribe (remote, id); if (!g_menu_exporter_remote_has_subscriptions (remote)) { if (sender != NULL) g_hash_table_remove (exporter->remotes, sender); else g_clear_pointer (&exporter->peer_remote, g_menu_exporter_remote_free); } } static void g_menu_exporter_report (GMenuExporter *exporter, GVariant *report) { GVariantBuilder builder; g_variant_builder_init_static (&builder, G_VARIANT_TYPE_TUPLE); g_variant_builder_open (&builder, G_VARIANT_TYPE_ARRAY); g_variant_builder_add_value (&builder, report); g_variant_builder_close (&builder); g_dbus_connection_emit_signal (exporter->connection, NULL, exporter->object_path, "org.gtk.Menus", "Changed", g_variant_builder_end (&builder), NULL); } static void g_menu_exporter_remove_group (GMenuExporter *exporter, guint id) { g_hash_table_remove (exporter->groups, GINT_TO_POINTER (id)); } static GMenuExporterGroup * g_menu_exporter_lookup_group (GMenuExporter *exporter, guint group_id) { GMenuExporterGroup *group; group = g_hash_table_lookup (exporter->groups, GINT_TO_POINTER (group_id)); if (group == NULL) { group = g_menu_exporter_group_new (exporter, group_id); g_hash_table_insert (exporter->groups, GINT_TO_POINTER (group_id), group); } return group; } static GMenuExporterGroup * g_menu_exporter_create_group (GMenuExporter *exporter) { GMenuExporterGroup *group; guint id; id = exporter->next_group_id++; group = g_menu_exporter_group_new (exporter, id); g_hash_table_insert (exporter->groups, GINT_TO_POINTER (id), group); return group; } static void g_menu_exporter_free (GMenuExporter *exporter) { g_clear_pointer (&exporter->root, g_menu_exporter_menu_free); g_clear_pointer (&exporter->peer_remote, g_menu_exporter_remote_free); g_hash_table_unref (exporter->remotes); g_hash_table_unref (exporter->groups); g_object_unref (exporter->connection); g_free (exporter->object_path); g_slice_free (GMenuExporter, exporter); } static void g_menu_exporter_method_call (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { GMenuExporter *exporter = user_data; GVariant *group_ids; group_ids = g_variant_get_child_value (parameters, 0); if (g_str_equal (method_name, "Start")) g_dbus_method_invocation_return_value (invocation, g_menu_exporter_subscribe (exporter, sender, group_ids)); else if (g_str_equal (method_name, "End")) { g_menu_exporter_unsubscribe (exporter, sender, group_ids); g_dbus_method_invocation_return_value (invocation, NULL); } else g_assert_not_reached (); g_variant_unref (group_ids); } /* {{{1 Public API */ /** * g_dbus_connection_export_menu_model: * @connection: a #GDBusConnection * @object_path: a D-Bus object path * @menu: a #GMenuModel * @error: return location for an error, or %NULL * * Exports @menu on @connection at @object_path. * * The implemented D-Bus API should be considered private. * It is subject to change in the future. * * An object path can only have one menu model exported on it. If this * constraint is violated, the export will fail and 0 will be * returned (with @error set accordingly). * * Exporting menus with sections containing more than * %G_MENU_EXPORTER_MAX_SECTION_SIZE items is not supported and results in * undefined behavior. * * You can unexport the menu model using * g_dbus_connection_unexport_menu_model() with the return value of * this function. * * Returns: the ID of the export (never zero), or 0 in case of failure * * Since: 2.32 */ guint g_dbus_connection_export_menu_model (GDBusConnection *connection, const gchar *object_path, GMenuModel *menu, GError **error) { const GDBusInterfaceVTable vtable = { g_menu_exporter_method_call, NULL, NULL, { 0 } }; GMenuExporter *exporter; guint id; exporter = g_slice_new0 (GMenuExporter); exporter->connection = g_object_ref (connection); exporter->object_path = g_strdup (object_path); exporter->groups = g_hash_table_new (NULL, NULL); exporter->remotes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_menu_exporter_remote_free); id = g_dbus_connection_register_object (connection, object_path, org_gtk_Menus_get_interface (), &vtable, exporter, (GDestroyNotify) g_menu_exporter_free, error); if (id != 0) exporter->root = g_menu_exporter_group_add_menu (g_menu_exporter_create_group (exporter), menu); return id; } /** * g_dbus_connection_unexport_menu_model: * @connection: a #GDBusConnection * @export_id: the ID from g_dbus_connection_export_menu_model() * * Reverses the effect of a previous call to * g_dbus_connection_export_menu_model(). * * It is an error to call this function with an ID that wasn't returned * from g_dbus_connection_export_menu_model() or to call it with the * same ID more than once. * * Since: 2.32 */ void g_dbus_connection_unexport_menu_model (GDBusConnection *connection, guint export_id) { g_dbus_connection_unregister_object (connection, export_id); } /* {{{1 Epilogue */ /* vim:set foldmethod=marker: */