blob: 909780cb2cfd6ab42b73a4635ef6e9dbdb32391e [file] [log] [blame]
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Author: Ryan Lortie <desrt@desrt.ca>
*/
#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 ("<node>"
" <interface name='org.gtk.Menus'>"
" <method name='Start'>"
" <arg type='au' name='groups' direction='in'/>"
" <arg type='a(uuaa{sv})' name='content' direction='out'/>"
" </method>"
" <method name='End'>"
" <arg type='au' name='groups' direction='in'/>"
" </method>"
" <signal name='Changed'>"
" arg type='a(uuuuaa{sv})' name='changes'/>"
" </signal>"
" </interface>"
"</node>", &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 (&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 (&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 (&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 (&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 (&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 (gpointer user_data)
{
GMenuExporter *exporter = user_data;
g_menu_exporter_menu_free (exporter->root);
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);
id = g_dbus_connection_register_object (connection, object_path, org_gtk_Menus_get_interface (),
&vtable, exporter, g_menu_exporter_free, error);
if (id == 0)
{
g_slice_free (GMenuExporter, exporter);
return 0;
}
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);
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: */