/*
* Copyright 2010 Canonical, Ltd.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License version 3, as published by the Free Software Foundation.
*
* 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 .
*
* Authors:
* Cody Russell
*/
#include "config.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "bridge.h"
#include "gen-application-menu-registrar.xml.h"
#define APP_MENU_DBUS_NAME "com.canonical.AppMenu.Registrar"
#define APP_MENU_DBUS_OBJECT "/com/canonical/AppMenu/Registrar"
#define APP_MENU_INTERFACE "com.canonical.AppMenu.Registrar"
#define APP_MENU_PATH "/this/is/a/long/object/path"
static void app_menu_bridge_insert (UbuntuMenuProxy *proxy,
GtkWidget *shell,
GtkWidget *child,
guint position);
static gboolean app_menu_bridge_show_local (UbuntuMenuProxy *proxy);
static void toplevel_realized (GtkWidget *widget,
gpointer user_data);
static void toplevel_notify_cb (GtkWidget *widget,
GParamSpec *pspec,
UbuntuMenuProxy *proxy);
static void rebuild_window_items (AppMenuBridge *bridge,
GtkWidget *toplevel);
static void rebuild (AppMenuBridge *bridge,
GtkWidget *toplevel);
static void app_menu_bridge_proxy_vanished (AppMenuBridge *bridge);
static void app_menu_bridge_proxy_appeared (AppMenuBridge *bridge);
static void appmenuproxy_created_cb (GObject * object,
GAsyncResult * res,
gpointer user_data);
typedef struct _AppWindowContext AppWindowContext;
struct _AppWindowContext
{
GtkWidget *window;
DbusmenuServer *server;
gchar *path;
gboolean registered;
AppMenuBridge *bridge;
GHashTable *lookup;
};
struct _AppMenuBridgePrivate
{
GList *windows;
GDBusProxy *appmenuproxy;
gboolean online;
};
typedef struct _RecurseContext
{
AppMenuBridge *bridge;
AppWindowContext *context;
gint count;
gboolean previous;
DbusmenuMenuitem *stack[30];
} RecurseContext;
G_DEFINE_DYNAMIC_TYPE(AppMenuBridge, app_menu_bridge, UBUNTU_TYPE_MENU_PROXY)
static GHashTable *rebuild_ids = NULL;
static GDBusNodeInfo * registrar_node_info = NULL;
static GDBusInterfaceInfo * registrar_interface_info = NULL;
static gboolean after_startup = FALSE;
static void
activate_menu (AppMenuBridge *bridge,
GtkWidget *widget,
gpointer user_data)
{
GList *list;
for (list = bridge->priv->windows; list != NULL; list = list->next)
{
AppWindowContext *context = list->data;
if (context->lookup)
{
DbusmenuMenuitem *mi = g_hash_table_lookup (context->lookup, widget);
if (mi != NULL)
{
dbusmenu_menuitem_show_to_user (mi, 0);
return;
}
}
}
}
static void
app_menu_bridge_init (AppMenuBridge *bridge)
{
bridge->priv = G_TYPE_INSTANCE_GET_PRIVATE (bridge, APP_MENU_TYPE_BRIDGE, AppMenuBridgePrivate);
bridge->priv->windows = NULL;
bridge->priv->appmenuproxy = NULL;
g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION,
G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
registrar_interface_info,
APP_MENU_DBUS_NAME,
APP_MENU_DBUS_OBJECT,
APP_MENU_INTERFACE,
NULL, /* TODO: cancelable */
appmenuproxy_created_cb,
bridge);
bridge->priv->online = FALSE;
g_signal_connect (bridge,
"activate-menu",
G_CALLBACK (activate_menu),
NULL);
return;
}
static void
app_menu_bridge_dispose (GObject *object)
{
AppMenuBridge *bridge = APP_MENU_BRIDGE (object);
if (bridge->priv->appmenuproxy)
{
g_object_unref (bridge->priv->appmenuproxy);
bridge->priv->appmenuproxy = NULL;
}
}
static void
free_context_contents (AppWindowContext *context)
{
if (context->path != NULL)
{
g_free (context->path);
context->path = NULL;
}
if (context->server != NULL)
{
g_object_unref (context->server);
context->server = NULL;
}
if (context->lookup)
{
g_hash_table_unref (context->lookup);
}
}
static void
app_menu_bridge_finalize (GObject *object)
{
AppMenuBridge *bridge = APP_MENU_BRIDGE (object);
GList *tmp = NULL;
for (tmp = bridge->priv->windows; tmp != NULL; tmp = tmp->next)
{
AppWindowContext *context = tmp->data;
free_context_contents (context);
g_free (context);
}
g_list_free (bridge->priv->windows);
bridge->priv->windows = NULL;
G_OBJECT_CLASS (app_menu_bridge_parent_class)->finalize (object);
}
static void
toplevel_destroyed (GtkWidget *widget,
gpointer user_data)
{
AppWindowContext *context = (AppWindowContext *)user_data;
if (context)
{
free_context_contents (context);
context->bridge->priv->windows = g_list_remove (context->bridge->priv->windows, context);
g_free (context);
}
}
static void
app_menu_bridge_class_init (AppMenuBridgeClass *class)
{
UbuntuMenuProxyClass *proxy_class;
GObjectClass *object_class;
app_menu_bridge_parent_class = g_type_class_peek_parent (class);
proxy_class = UBUNTU_MENU_PROXY_CLASS (class);
object_class = G_OBJECT_CLASS (class);
proxy_class->insert = app_menu_bridge_insert;
proxy_class->show_local = app_menu_bridge_show_local;
object_class->dispose = app_menu_bridge_dispose;
object_class->finalize = app_menu_bridge_finalize;
g_type_class_add_private (class, sizeof (AppMenuBridgePrivate));
if (registrar_node_info == NULL) {
GError * error = NULL;
registrar_node_info = g_dbus_node_info_new_for_xml(_application_menu_registrar, &error);
if (error != NULL) {
g_error("Unable to parse app menu registrar Interface description: %s", error->message);
g_error_free(error);
}
}
if (registrar_interface_info == NULL) {
registrar_interface_info = g_dbus_node_info_lookup_interface(registrar_node_info, APP_MENU_INTERFACE);
if (registrar_interface_info == NULL) {
g_error("Unable to find interface '" APP_MENU_INTERFACE "'");
}
}
return;
}
static void
app_menu_bridge_class_finalize (AppMenuBridgeClass *class)
{
}
static void
app_menu_bridge_set_show_local (AppMenuBridge *bridge,
gboolean local)
{
const gchar *env = g_getenv ("APPMENU_DISPLAY_BOTH");
if (g_strcmp0 (env, "1") == 0)
local = TRUE;
g_object_set (bridge,
"show-local", local,
NULL);
}
static void
register_application_window_cb (GObject *object,
GAsyncResult *res,
void *user_data)
{
GError * error = NULL;
AppWindowContext *context = (AppWindowContext *)user_data;
GVariant * variants = g_dbus_proxy_call_finish(G_DBUS_PROXY(object), res, &error);
g_variant_unref(variants);
if (error != NULL)
{
g_warning("Unable to register window with path '%s': %s", context->path, error->message);
g_error_free(error);
if (context->bridge != NULL)
{
context->registered = FALSE;
app_menu_bridge_set_show_local (context->bridge, TRUE);
return;
}
}
context->registered = TRUE;
if (context->bridge != NULL)
app_menu_bridge_set_show_local (context->bridge, FALSE);
}
static void
register_application_windows (AppMenuBridge *bridge)
{
GList *tmp = NULL;
for (tmp = bridge->priv->windows; tmp != NULL; tmp = tmp->next)
{
AppWindowContext *context = tmp->data;
GtkWidget *widget = context->window;
if (bridge->priv->appmenuproxy == NULL || bridge->priv->online == FALSE)
{
if (context->bridge != NULL)
{
app_menu_bridge_set_show_local (context->bridge, TRUE);
}
}
if (!context->registered && context->server != NULL && GTK_IS_WINDOW (widget) && bridge->priv->appmenuproxy != NULL)
{
g_dbus_proxy_call(bridge->priv->appmenuproxy,
"RegisterWindow",
g_variant_new("(uo)",
GDK_WINDOW_XID(gtk_widget_get_window(widget)),
context->path),
G_DBUS_CALL_FLAGS_NONE,
-1, /* timeout */
NULL, /* cancelable */
register_application_window_cb,
context);
}
}
}
static void
unregister_application_windows (AppMenuBridge *bridge)
{
GList *tmp = NULL;
for (tmp = bridge->priv->windows; tmp != NULL; tmp = tmp->next)
{
AppWindowContext *context = tmp->data;
context->registered = FALSE;
}
app_menu_bridge_set_show_local (bridge, TRUE);
}
static void
app_menu_bridge_proxy_vanished (AppMenuBridge *bridge)
{
bridge->priv->online = FALSE;
unregister_application_windows (bridge);
}
static void
app_menu_bridge_proxy_appeared (AppMenuBridge *bridge)
{
bridge->priv->online = TRUE;
register_application_windows (bridge);
}
/* Gets called anytime the name owner changes, this is typically
when the indicator crashes or gets removed. */
static void
appmenuproxy_owner_changed (GObject * object, GParamSpec * pspec, gpointer user_data)
{
AppMenuBridge * bridge = APP_MENU_BRIDGE(user_data);
g_return_if_fail(bridge != NULL);
gchar * name = g_dbus_proxy_get_name_owner(bridge->priv->appmenuproxy);
if (name != NULL) {
g_free(name);
app_menu_bridge_proxy_appeared(bridge);
} else {
app_menu_bridge_proxy_vanished(bridge);
}
return;
}
/* Callback for the asyncronous creation of the proxy object.
If it is created successfully we act like it just appeared, otherwise
we error as if it vanished */
static void
appmenuproxy_created_cb (GObject * object, GAsyncResult * res, gpointer user_data)
{
GError * error = NULL;
GDBusProxy * proxy = NULL;
proxy = g_dbus_proxy_new_for_bus_finish(res, &error);
if (error != NULL) {
g_warning("Unable to create Ubuntu Menu Proxy: %s", error->message);
g_error_free(error);
/* No return, we want to still call vanished */
}
AppMenuBridge * bridge = APP_MENU_BRIDGE(user_data);
g_return_if_fail(bridge != NULL);
bridge->priv->appmenuproxy = proxy;
gchar * name = NULL;
if (proxy != NULL) {
name = g_dbus_proxy_get_name_owner(proxy);
g_signal_connect(G_OBJECT(proxy),
"notify::g-name-owner",
G_CALLBACK(appmenuproxy_owner_changed),
bridge);
}
/* Note: name will be NULL if proxy was NULL */
if (name != NULL) {
g_free(name);
app_menu_bridge_proxy_appeared(bridge);
} else {
app_menu_bridge_proxy_vanished(bridge);
}
return;
}
typedef struct _RebuildData {
AppMenuBridge *bridge;
GtkWidget *widget;
} RebuildData;
static gboolean
do_rebuild (RebuildData *data)
{
if (gtk_widget_is_toplevel (data->widget))
{
rebuild_window_items (data->bridge,
data->widget);
g_hash_table_remove (rebuild_ids, data->widget);
}
g_free (data);
return FALSE;
}
static void
rebuild (AppMenuBridge *bridge,
GtkWidget *toplevel)
{
guint id = 0;
if (rebuild_ids != NULL)
{
id = GPOINTER_TO_UINT (g_hash_table_lookup (rebuild_ids, toplevel));
if (id > 0)
{
g_source_remove (id);
g_hash_table_remove (rebuild_ids, toplevel);
id = 0;
}
}
RebuildData *data = g_new0 (RebuildData, 1);
data->bridge = bridge;
data->widget = toplevel;
id = g_timeout_add (100,
(GSourceFunc)do_rebuild,
data);
if (rebuild_ids == NULL)
{
rebuild_ids = g_hash_table_new (g_direct_hash, g_direct_equal);
}
g_hash_table_insert (rebuild_ids, toplevel, GUINT_TO_POINTER (id));
}
static DbusmenuMenuitem * find_menu_bar (GtkWidget * widget);
static void
find_menu_bar_helper (GtkWidget * widget, gpointer data)
{
DbusmenuMenuitem ** mi = (DbusmenuMenuitem **)data;
/* We've already found a menu, let's get through the
foreach as quickly as possible */
if (*mi != NULL) {
return;
}
*mi = find_menu_bar(widget);
return;
}
static DbusmenuMenuitem *
find_menu_bar (GtkWidget * widget)
{
if (GTK_IS_MENU_BAR(widget) || GTK_IS_MENU_ITEM(widget)) {
return dbusmenu_gtk_parse_menu_structure(widget);
}
if (GTK_IS_CONTAINER(widget)) {
DbusmenuMenuitem * mi = NULL;
gtk_container_foreach(GTK_CONTAINER(widget), find_menu_bar_helper, &mi);
return mi;
}
return NULL;
}
static void
rebuild_window_items (AppMenuBridge *bridge,
GtkWidget *toplevel)
{
XID xid;
AppWindowContext *context = NULL;
if (!GTK_IS_WINDOW (toplevel))
{
g_signal_connect (G_OBJECT (toplevel),
"notify",
G_CALLBACK (toplevel_notify_cb),
bridge);
return;
}
else if (g_object_class_find_property (G_OBJECT_GET_CLASS (toplevel),
"ubuntu-no-proxy") != NULL)
{
gboolean no_proxy;
g_object_get (G_OBJECT (toplevel),
"ubuntu-no-proxy", &no_proxy,
NULL);
if (no_proxy)
{
return;
}
}
if (!gtk_widget_get_realized (toplevel))
{
g_signal_connect (toplevel, "realize",
G_CALLBACK (toplevel_realized),
bridge);
return;
}
xid = GDK_WINDOW_XID (gtk_widget_get_window (toplevel));
GList *tmp;
gboolean found = FALSE;
for (tmp = bridge->priv->windows; tmp != NULL; tmp = tmp->next)
{
context = tmp->data;
if (context && GTK_IS_WIDGET (context->window))
{
XID xid2 = GDK_WINDOW_XID (gtk_widget_get_window (context->window));
if (xid == xid2)
{
found = TRUE;
break;
}
}
}
if (!found)
{
context = g_new0 (AppWindowContext, 1);
context->bridge = bridge;
context->lookup = g_hash_table_new (g_direct_hash, g_direct_equal);
bridge->priv->windows = g_list_prepend (bridge->priv->windows, context);
}
if (context->window)
{
if (context->window != toplevel)
{
g_signal_handlers_disconnect_by_func (context->window,
G_CALLBACK (toplevel_destroyed),
context);
}
}
if (context->window != toplevel)
{
context->window = toplevel;
g_signal_connect (toplevel,
"destroy",
G_CALLBACK (toplevel_destroyed),
context);
}
if (!context->path)
context->path = g_strdup_printf ("/com/canonical/menu/%X", (guint)xid);
if (!context->server)
context->server = dbusmenu_server_new (context->path);
DbusmenuMenuitem * mi = find_menu_bar(toplevel);
dbusmenu_server_set_root(context->server, mi);
if (mi != NULL) {
g_object_unref(G_OBJECT(mi));
}
register_application_windows (bridge);
}
static void
toplevel_realized (GtkWidget *widget,
gpointer user_data)
{
AppMenuBridge *bridge = APP_MENU_BRIDGE (user_data);
if (GTK_IS_WINDOW (widget))
{
rebuild (bridge, widget);
//register_application_windows (bridge);
return;
}
}
static void
toplevel_notify_cb (GtkWidget *widget,
GParamSpec *pspec,
UbuntuMenuProxy *proxy)
{
if (pspec->name == g_intern_static_string ("parent"))
{
GtkWidget *toplevel = gtk_widget_get_toplevel (widget);
AppMenuBridge *bridge = APP_MENU_BRIDGE (proxy);
if (gtk_widget_get_parent (widget) == NULL)
return;
if (GTK_IS_WINDOW (toplevel))
{
g_signal_handlers_disconnect_by_func (widget,
G_CALLBACK (toplevel_notify_cb),
proxy);
rebuild (bridge, toplevel);
}
else
{
g_signal_connect (G_OBJECT (toplevel),
"notify",
G_CALLBACK (toplevel_notify_cb),
proxy);
}
}
}
static void
attach_notify_cb (GtkWidget *widget,
GParamSpec *pspec,
AppMenuBridge *bridge)
{
if (pspec->name == g_intern_static_string ("attach-widget"))
{
GtkWidget *attach = NULL;
g_object_get (widget, "attach-widget", &attach, NULL);
rebuild (bridge, attach);
}
}
gboolean
startup_timeout (gpointer data)
{
after_startup = TRUE;
return FALSE;
}
static void
app_menu_bridge_insert (UbuntuMenuProxy *proxy,
GtkWidget *parent,
GtkWidget *child,
guint position)
{
AppMenuBridge *bridge;
AppMenuBridgePrivate *priv;
GtkWidget *toplevel = NULL;
gboolean append = FALSE;
g_idle_add ((GSourceFunc)startup_timeout, NULL);
if (GTK_IS_TEAROFF_MENU_ITEM (child))
return;
bridge = APP_MENU_BRIDGE (proxy);
priv = bridge->priv;
toplevel = gtk_widget_get_toplevel (parent);
if (GTK_IS_MENU_BAR (parent))
{
g_signal_connect (G_OBJECT (toplevel),
"notify",
G_CALLBACK (toplevel_notify_cb),
proxy);
append = TRUE;
}
else if (GTK_IS_MENU (parent))
{
GtkWidget *attach = NULL;
GList *tmp = NULL;
AppWindowContext *context = NULL;
g_object_get (parent, "attach-widget", &attach, NULL);
/* First attempt to setup the menuitem immediately if possible. */
for (tmp = bridge->priv->windows; tmp != NULL; tmp = tmp->next)
{
context = (AppWindowContext *)tmp->data;
DbusmenuMenuitem *mi = (DbusmenuMenuitem *)g_hash_table_lookup (context->lookup, attach);
if (mi != NULL)
{
DbusmenuMenuitem *child_dmi = dbusmenu_gtk_parse_menu_structure (child);
g_object_set_data (G_OBJECT (child_dmi), "dbusmenu-parent", mi);
dbusmenu_menuitem_child_add_position (mi,
child_dmi,
position);
return;
}
}
if (attach == NULL)
{
g_signal_connect (G_OBJECT (parent),
"notify",
G_CALLBACK (attach_notify_cb),
bridge);
return;
}
else
{
rebuild (bridge, toplevel);
}
}
if (GTK_IS_WINDOW (toplevel))
{
g_signal_connect (toplevel, "realize",
G_CALLBACK (toplevel_realized),
bridge);
}
}
static gboolean
app_menu_bridge_show_local (UbuntuMenuProxy *proxy)
{
gboolean local;
g_object_get (proxy,
"show-local", &local,
NULL);
return local;
}
/* crude blacklist to avoid patching innoncent apps */
static gboolean
app_menu_brige_shouldnt_load (void)
{
const char *prg = g_get_prgname ();
if ((g_strrstr (prg, "indicator-applet") != NULL)
|| (g_strcmp0 (prg, "indicator-loader") == 0)
|| (g_strcmp0 (prg, "mutter") == 0)
|| (g_strcmp0 (prg, "midori") == 0)
|| (g_strcmp0 (prg, "firefox-bin") == 0)
|| (g_strcmp0 (prg, "thunderbird-bin") == 0)
|| (g_strcmp0 (prg, "shotwell") == 0)
|| (g_strcmp0 (prg, "gnome-panel") == 0))
{
return TRUE;
}
return FALSE;
}
G_MODULE_EXPORT void
menu_proxy_module_load (UbuntuMenuProxyModule *module)
{
static gboolean registered = FALSE;
/* Prevent well-known applications to re-export
their own dbusmenus */
if (app_menu_brige_shouldnt_load ())
return;
if (!registered)
{
app_menu_bridge_register_type (G_TYPE_MODULE (module));
registered = TRUE;
}
}
G_MODULE_EXPORT void
menu_proxy_module_unload (UbuntuMenuProxyModule *module)
{
if (rebuild_ids)
{
g_hash_table_destroy (rebuild_ids);
rebuild_ids = NULL;
}
}