diff options
Diffstat (limited to 'libdbusmenu-gtk/parser.c')
-rw-r--r-- | libdbusmenu-gtk/parser.c | 854 |
1 files changed, 693 insertions, 161 deletions
diff --git a/libdbusmenu-gtk/parser.c b/libdbusmenu-gtk/parser.c index 5d71585..3243c81 100644 --- a/libdbusmenu-gtk/parser.c +++ b/libdbusmenu-gtk/parser.c @@ -28,9 +28,19 @@ License version 3 and version 2.1 along with this program. If not, see #include "parser.h" #include "menuitem.h" -#include "serializablemenuitem.h" +#include "client.h" #define CACHED_MENUITEM "dbusmenu-gtk-parser-cached-item" +#define PARSER_DATA "dbusmenu-gtk-parser-data" + +typedef struct _ParserData +{ + GtkWidget *label; + GtkAction *action; + GtkWidget *widget; + GtkWidget *shell; + GtkWidget *image; +} ParserData; typedef struct _RecurseContext { @@ -42,83 +52,289 @@ static void parse_menu_structure_helper (GtkWidget * widget, RecurseContext * re static DbusmenuMenuitem * construct_dbusmenu_for_widget (GtkWidget * widget); static void accel_changed (GtkWidget * widget, gpointer data); -static gboolean update_stock_item (DbusmenuMenuitem * menuitem, - GtkWidget * widget); static void checkbox_toggled (GtkWidget * widget, DbusmenuMenuitem * mi); -static void update_icon_name (DbusmenuMenuitem * menuitem, - GtkWidget * widget); +static void update_icon (DbusmenuMenuitem * menuitem, + GtkImage * image); static GtkWidget * find_menu_label (GtkWidget * widget); static void label_notify_cb (GtkWidget * widget, GParamSpec * pspec, gpointer data); +static void image_notify_cb (GtkWidget * widget, + GParamSpec * pspec, + gpointer data); static void action_notify_cb (GtkAction * action, GParamSpec * pspec, gpointer data); +static void child_added_cb (GtkContainer * menu, + GtkWidget * widget, + gpointer data); +static void child_removed_cb (GtkContainer * menu, + GtkWidget * widget, + gpointer data); +static void theme_changed_cb (GtkIconTheme * theme, + gpointer data); static void item_activated (DbusmenuMenuitem * item, guint timestamp, gpointer user_data); static gboolean item_about_to_show (DbusmenuMenuitem * item, gpointer user_data); +static gboolean item_handle_event (DbusmenuMenuitem * item, + const gchar * name, + GVariant * variant, + guint timestamp, + GtkWidget * widget); static void widget_notify_cb (GtkWidget * widget, GParamSpec * pspec, gpointer data); +static void widget_add_cb (GtkWidget * widget, + GtkWidget * child, + gpointer data); static gboolean should_show_image (GtkImage * image); static void menuitem_notify_cb (GtkWidget * widget, GParamSpec * pspec, gpointer data); /** - dbusmenu_gtk_parse_menu_structure: - @widget: A #GtkMenuItem or #GtkMenuShell to turn into a #DbusmenuMenuitem + * dbusmenu_gtk_parse_menu_structure: + * @widget: A #GtkMenuItem or #GtkMenuShell to turn into a #DbusmenuMenuitem + * + * Goes through the GTK structures and turns them into the appropraite + * Dbusmenu structures along with setting up all the relationships + * between the objects. It also stores the dbusmenu items as a cache + * on the GTK items so that they'll be reused if necissary. + * + * Return value: (transfer full): A dbusmenu item representing the menu structure + */ +DbusmenuMenuitem * +dbusmenu_gtk_parse_menu_structure (GtkWidget * widget) +{ + g_return_val_if_fail(GTK_IS_MENU_ITEM(widget) || GTK_IS_MENU_SHELL(widget), NULL); - Goes through the GTK structures and turns them into the appropraite - Dbusmenu structures along with setting up all the relationships - between the objects. It also stores the dbusmenu items as a cache - on the GTK items so that they'll be reused if necissary. + DbusmenuMenuitem * returnval = NULL; + gpointer data = g_object_get_data(G_OBJECT(widget), CACHED_MENUITEM); - Return value: A dbusmenu item representing the menu structure -*/ + if (data == NULL) { + RecurseContext recurse = {0}; + + recurse.toplevel = gtk_widget_get_toplevel(widget); + + parse_menu_structure_helper(widget, &recurse); + + returnval = recurse.parent; + } else { + returnval = DBUSMENU_MENUITEM(data); + g_object_ref(G_OBJECT(returnval)); + } + + return returnval; +} + +/** + * dbusmenu_gtk_parse_get_cached_item: + * @widget: A #GtkMenuItem that may have a cached #DbusmenuMenuitem from the parser + * + * The Dbusmenu GTK parser adds cached items on the various + * menu items throughout the tree. Sometimes it can be useful + * to get that cached item to use directly. This function + * will retrieve it for you. + * + * Return value: (transfer none): A pointer to the cached item + * or NULL if it isn't there. + */ DbusmenuMenuitem * -dbusmenu_gtk_parse_menu_structure (GtkWidget * widget) +dbusmenu_gtk_parse_get_cached_item (GtkWidget * widget) { - g_return_val_if_fail(GTK_IS_MENU_ITEM(widget) || GTK_IS_MENU_SHELL(widget), NULL); + if (!GTK_IS_MENU_ITEM(widget)) { + return NULL; + } + return DBUSMENU_MENUITEM(g_object_get_data(G_OBJECT(widget), CACHED_MENUITEM)); +} - RecurseContext recurse = {0}; +static void +parse_data_free (gpointer data) +{ + ParserData *pdata = (ParserData *)data; + + if (pdata != NULL && pdata->label != NULL) { + g_signal_handlers_disconnect_matched(pdata->label, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(label_notify_cb), NULL); + g_object_remove_weak_pointer(G_OBJECT(pdata->label), (gpointer*)&pdata->label); + } + + if (pdata != NULL && pdata->action != NULL) { + g_signal_handlers_disconnect_matched(pdata->action, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(action_notify_cb), NULL); + g_object_remove_weak_pointer(G_OBJECT(pdata->action), (gpointer*)&pdata->action); + } - recurse.toplevel = gtk_widget_get_toplevel(widget); + if (pdata != NULL && pdata->widget != NULL) { + g_signal_handlers_disconnect_matched(pdata->widget, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(widget_notify_cb), NULL); + g_signal_handlers_disconnect_matched(pdata->widget, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(widget_add_cb), NULL); + g_signal_handlers_disconnect_matched(pdata->widget, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(accel_changed), NULL); + g_signal_handlers_disconnect_matched(pdata->widget, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(checkbox_toggled), NULL); + g_signal_handlers_disconnect_matched(pdata->widget, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(menuitem_notify_cb), NULL); + g_object_remove_weak_pointer(G_OBJECT(pdata->widget), (gpointer*)&pdata->widget); + } + + if (pdata != NULL && pdata->shell != NULL) { + g_signal_handlers_disconnect_matched(pdata->shell, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(child_added_cb), NULL); + g_signal_handlers_disconnect_matched(pdata->shell, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(child_removed_cb), NULL); + g_object_remove_weak_pointer(G_OBJECT(pdata->shell), (gpointer*)&pdata->shell); + } + + if (pdata != NULL && pdata->image != NULL) { + g_signal_handlers_disconnect_matched(pdata->image, (GSignalMatchType)G_SIGNAL_MATCH_FUNC, + 0, 0, NULL, G_CALLBACK(image_notify_cb), NULL); + g_object_remove_weak_pointer(G_OBJECT(pdata->image), (gpointer*)&pdata->image); + } - parse_menu_structure_helper(widget, &recurse); + g_free(pdata); - return recurse.parent; + return; +} + +static void +widget_freed (gpointer data, GObject * obj) +{ + g_signal_handlers_disconnect_by_func(gtk_icon_theme_get_default(), G_CALLBACK(theme_changed_cb), obj); + + return; } /* Called when the dbusmenu item that we're keeping around is finalized */ static void -dbusmenu_cache_freed (gpointer data, GObject * obj) +dbusmenu_item_freed (gpointer data, GObject * obj) { - /* If the dbusmenu item is killed we don't need to remove - the weak ref as well. */ - g_object_steal_data(G_OBJECT(data), CACHED_MENUITEM); - g_signal_handlers_disconnect_by_func(data, G_CALLBACK(widget_notify_cb), obj); - return; + ParserData *pdata = (ParserData *)g_object_get_data(G_OBJECT(obj), PARSER_DATA); + + if (pdata != NULL && pdata->widget != NULL) { + g_signal_handlers_disconnect_by_func(gtk_icon_theme_get_default(), G_CALLBACK(theme_changed_cb), pdata->widget); + g_object_steal_data(G_OBJECT(pdata->widget), CACHED_MENUITEM); + g_object_weak_unref(G_OBJECT(pdata->widget), widget_freed, NULL); + } +} + +/* Gets the positon of the child with its' parent if it has one. + Returns -1 if the position is unable to be calculated. */ +static gint +get_child_position (GtkWidget * child) +{ + GtkWidget * parent = gtk_widget_get_parent (child); + if (parent == NULL || !GTK_IS_CONTAINER (parent)) + return -1; + + GList * children = gtk_container_get_children (GTK_CONTAINER (parent)); + GList * iter; + gint position = 0; + + for (iter = children; iter != NULL; iter = iter->next) { + if (iter->data == child) + break; + ++position; + } + + g_list_free (children); + + if (iter == NULL) + return -1; + else + return position; +} + +/* Creates a new menu item that is attached to the widget and has + the data linkages hooked up. Also allocates the ParserData */ +static DbusmenuMenuitem * +new_menuitem (GtkWidget * widget) +{ + DbusmenuMenuitem * item = dbusmenu_menuitem_new(); + + ParserData *pdata = g_new0 (ParserData, 1); + g_object_set_data_full(G_OBJECT(item), PARSER_DATA, pdata, parse_data_free); + + g_object_weak_ref(G_OBJECT(item), dbusmenu_item_freed, NULL); + g_object_weak_ref(G_OBJECT(widget), widget_freed, NULL); + + pdata->widget = widget; + g_object_add_weak_pointer(G_OBJECT (widget), (gpointer*)&pdata->widget); + g_object_set_data(G_OBJECT(widget), CACHED_MENUITEM, item); + + return item; +} + +static gboolean +toggle_widget_visibility (GtkWidget * widget) +{ + gboolean vis = gtk_widget_get_visible (widget); + gtk_widget_set_visible (widget, !vis); + gtk_widget_set_visible (widget, vis); + g_object_unref (G_OBJECT (widget)); + return FALSE; } -/* Called if we replace the cache on the object with a new - dbusmenu menuitem */ static void -object_cache_freed (gpointer data) +watch_submenu(DbusmenuMenuitem * mi, GtkWidget * menu) { - if (!G_IS_OBJECT(data)) return; - g_object_weak_unref(G_OBJECT(data), dbusmenu_cache_freed, data); - return; + ParserData *pdata = (ParserData *)g_object_get_data(G_OBJECT(mi), PARSER_DATA); + + pdata->shell = menu; + g_signal_connect (G_OBJECT (menu), + "child-added", + G_CALLBACK (child_added_cb), + mi); + g_signal_connect (G_OBJECT (menu), + "child-removed", + G_CALLBACK (child_removed_cb), + mi); + g_object_add_weak_pointer(G_OBJECT (menu), (gpointer*)&pdata->shell); + + /* Some apps (notably Eclipse RCP apps) don't fill contents of submenus + until the menu is shown. So we fake that by toggling the visibility of + any submenus we come across. Further, these apps need it done with a + delay while they finish initializing, so we put the call in the idle + queue. */ + g_idle_add((GSourceFunc)toggle_widget_visibility, + g_object_ref (G_OBJECT (menu))); } static void -parse_menu_structure_helper (GtkWidget * widget, RecurseContext * recurse) +activate_toplevel_item (GtkWidget * item) { + /* Make sure that we have a menu item before we start calling + functions that depend on it. This should almost always be + the case. */ + if (!GTK_IS_MENU_ITEM(item)) { + return; + } + + /* If the item is not opening a submenu we don't want to activate + it as that'd cause an action. Like opening a preferences dialog + to the user. That's not a good idea. */ + if (gtk_menu_item_get_submenu(GTK_MENU_ITEM(item)) == NULL) { + return; + } + + GtkWidget * shell = gtk_widget_get_parent (item); + if (!GTK_IS_MENU_BAR (shell)) { + return; + } + gtk_menu_shell_activate_item (GTK_MENU_SHELL (shell), + item, + TRUE); +} + +static void +parse_menu_structure_helper (GtkWidget * widget, RecurseContext * recurse) +{ /* If this is a shell, then let's handle the items in it. */ if (GTK_IS_MENU_SHELL (widget)) { /* Okay, this is a little janky and all.. but some applications update some @@ -132,19 +348,14 @@ parse_menu_structure_helper (GtkWidget * widget, RecurseContext * recurse) * Note that this will not force menuitems in submenus to be updated as well. */ if (recurse->parent == NULL && GTK_IS_MENU_BAR(widget)) { - GList *children = gtk_container_get_children (GTK_CONTAINER (widget)); - - for (; children != NULL; children = children->next) { - gtk_menu_shell_activate_item (GTK_MENU_SHELL (widget), - children->data, - TRUE); - } - - g_list_free (children); + gtk_container_foreach (GTK_CONTAINER (widget), + (GtkCallback)activate_toplevel_item, + NULL); } if (recurse->parent == NULL) { - recurse->parent = dbusmenu_menuitem_new(); + recurse->parent = new_menuitem(widget); + watch_submenu(recurse->parent, widget); } gtk_container_foreach (GTK_CONTAINER (widget), @@ -166,8 +377,6 @@ parse_menu_structure_helper (GtkWidget * widget, RecurseContext * recurse) /* We don't have one, so we'll need to build it */ if (thisitem == NULL) { thisitem = construct_dbusmenu_for_widget (widget); - g_object_set_data_full(G_OBJECT(widget), CACHED_MENUITEM, thisitem, object_cache_freed); - g_object_weak_ref(G_OBJECT(thisitem), dbusmenu_cache_freed, widget); if (!gtk_widget_get_visible (widget)) { g_signal_connect (G_OBJECT (widget), @@ -195,12 +404,18 @@ parse_menu_structure_helper (GtkWidget * widget, RecurseContext * recurse) /* Oops, let's tell our parents about us */ if (peek == NULL) { - /* TODO: Should we set a weak ref on the parent? */ - g_object_set_data (G_OBJECT (thisitem), - "dbusmenu-parent", - recurse->parent); - dbusmenu_menuitem_child_append (recurse->parent, - thisitem); + if (dbusmenu_menuitem_get_parent(thisitem) != NULL) { + dbusmenu_menuitem_unparent(thisitem); + } + + gint pos = get_child_position (widget); + if (pos >= 0) + dbusmenu_menuitem_child_add_position (recurse->parent, + thisitem, + pos); + else + dbusmenu_menuitem_child_append (recurse->parent, + thisitem); } } @@ -222,38 +437,80 @@ parse_menu_structure_helper (GtkWidget * widget, RecurseContext * recurse) return; } +static gchar * +sanitize_label_text (const gchar * label) +{ + /* Label contains underscores, which we like, and pango markup, + which we don't. */ + gchar * sanitized = NULL; + GError * error = NULL; + + if (label == NULL) { + return NULL; + } + + if (pango_parse_markup (label, -1, 0, NULL, &sanitized, NULL, &error)) { + return sanitized; + } + + if (error != NULL) { + g_warning ("Could not parse '%s': %s", label, error->message); + g_error_free (error); + } + return g_strdup (label); +} + +static gchar * +sanitize_label (GtkLabel * label) +{ + gchar * text; + + if (gtk_label_get_use_markup (label)) { + text = sanitize_label_text (gtk_label_get_label (label)); + } + else { + text = g_strdup (gtk_label_get_label (label)); + } + + if (!gtk_label_get_use_underline (label)) { + /* Insert extra underscores */ + GRegex * regex = g_regex_new ("_", 0, 0, NULL); + gchar * escaped = g_regex_replace_literal (regex, text, -1, 0, "__", 0, NULL); + + g_regex_unref (regex); + g_free (text); + + text = escaped; + } + + return text; +} + /* Turn a widget into a dbusmenu item depending on the type of GTK object that it is. */ static DbusmenuMenuitem * construct_dbusmenu_for_widget (GtkWidget * widget) { - /* If it's a subclass of our serializable menu item then we can - use its own build function */ - if (DBUSMENU_IS_GTK_SERIALIZABLE_MENU_ITEM(widget)) { - DbusmenuGtkSerializableMenuItem * smi = DBUSMENU_GTK_SERIALIZABLE_MENU_ITEM(widget); - return dbusmenu_gtk_serializable_menu_item_build_menuitem(smi); - } - /* If it's a standard GTK Menu Item we need to do some of our own work */ if (GTK_IS_MENU_ITEM (widget)) { - DbusmenuMenuitem *mi = dbusmenu_menuitem_new (); + DbusmenuMenuitem *mi = new_menuitem(widget); + + ParserData *pdata = (ParserData *)g_object_get_data(G_OBJECT(mi), PARSER_DATA); gboolean visible = FALSE; gboolean sensitive = FALSE; - if (GTK_IS_SEPARATOR_MENU_ITEM (widget)) + if (GTK_IS_SEPARATOR_MENU_ITEM (widget) || !find_menu_label (widget)) { dbusmenu_menuitem_property_set (mi, - "type", - "separator"); + DBUSMENU_MENUITEM_PROP_TYPE, + DBUSMENU_CLIENT_TYPES_SEPARATOR); visible = gtk_widget_get_visible (widget); sensitive = gtk_widget_get_sensitive (widget); } else { - gboolean label_set = FALSE; - g_signal_connect (widget, "accel-closures-changed", G_CALLBACK (accel_changed), @@ -278,46 +535,41 @@ construct_dbusmenu_for_widget (GtkWidget * widget) if (GTK_IS_IMAGE_MENU_ITEM (widget)) { GtkWidget *image; - GtkImageType image_type; image = gtk_image_menu_item_get_image (GTK_IMAGE_MENU_ITEM (widget)); if (GTK_IS_IMAGE (image)) { - image_type = gtk_image_get_storage_type (GTK_IMAGE (image)); - - if (image_type == GTK_IMAGE_STOCK) - { - label_set = update_stock_item (mi, image); - } - else if (image_type == GTK_IMAGE_ICON_NAME) - { - update_icon_name (mi, image); - } - else if (image_type == GTK_IMAGE_PIXBUF) - { - dbusmenu_menuitem_property_set_image (mi, - DBUSMENU_MENUITEM_PROP_ICON_DATA, - gtk_image_get_pixbuf (GTK_IMAGE (image))); - } + update_icon (mi, GTK_IMAGE (image)); + + /* Watch for theme changes because if gicon changes, we want to send a + different pixbuf. */ + g_signal_connect(G_OBJECT(gtk_icon_theme_get_default()), + "changed", G_CALLBACK(theme_changed_cb), widget); + + pdata->image = image; + g_signal_connect (G_OBJECT (image), + "notify", + G_CALLBACK (image_notify_cb), + mi); + g_object_add_weak_pointer(G_OBJECT (image), (gpointer*)&pdata->image); } } GtkWidget *label = find_menu_label (widget); - dbusmenu_menuitem_property_set (mi, - "label", - label ? gtk_label_get_text (GTK_LABEL (label)) : NULL); + // Sometimes, an app will directly find and modify the label + // (like empathy), so watch the label especially for that. + gchar * text = sanitize_label (GTK_LABEL (label)); + dbusmenu_menuitem_property_set (mi, "label", text); + g_free (text); - if (label) - { - // Sometimes, an app will directly find and modify the label - // (like empathy), so watch the label especially for that. - g_signal_connect (G_OBJECT (label), - "notify", - G_CALLBACK (label_notify_cb), - mi); - } + pdata->label = label; + g_signal_connect (G_OBJECT (label), + "notify", + G_CALLBACK (label_notify_cb), + mi); + g_object_add_weak_pointer(G_OBJECT (label), (gpointer*)&pdata->label); if (GTK_IS_ACTIVATABLE (widget)) { @@ -332,14 +584,22 @@ construct_dbusmenu_for_widget (GtkWidget * widget) visible = gtk_action_is_visible (action); sensitive = gtk_action_is_sensitive (action); + pdata->action = action; g_signal_connect_object (action, "notify", G_CALLBACK (action_notify_cb), mi, G_CONNECT_AFTER); + g_object_add_weak_pointer(G_OBJECT (action), (gpointer*)&pdata->action); } } } + GtkWidget *submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(widget)); + if (submenu) + { + watch_submenu(mi, submenu); + } + if (!g_object_get_data (G_OBJECT (widget), "gtk-empty-menu-item") && !GTK_IS_TEAROFF_MENU_ITEM (widget)) { visible = gtk_widget_get_visible (widget); @@ -357,6 +617,11 @@ construct_dbusmenu_for_widget (GtkWidget * widget) DBUSMENU_MENUITEM_SIGNAL_ABOUT_TO_SHOW, G_CALLBACK (item_about_to_show), widget); + + g_signal_connect (G_OBJECT (mi), + DBUSMENU_MENUITEM_SIGNAL_EVENT, + G_CALLBACK (item_handle_event), + widget); } dbusmenu_menuitem_property_set_bool (mi, @@ -371,12 +636,18 @@ construct_dbusmenu_for_widget (GtkWidget * widget) "notify", G_CALLBACK (widget_notify_cb), mi); + + g_signal_connect (widget, + "add", + G_CALLBACK (widget_add_cb), + mi); + return mi; } /* If it's none of those we're going to just create a generic menuitem as a place holder for it. */ - return dbusmenu_menuitem_new(); + return new_menuitem(widget); } static void @@ -408,48 +679,6 @@ accel_changed (GtkWidget *widget, dbusmenu_menuitem_property_set_shortcut_menuitem (mi, GTK_MENU_ITEM (widget)); } -static gboolean -update_stock_item (DbusmenuMenuitem *menuitem, - GtkWidget *widget) -{ - GtkStockItem stock; - GtkImage *image; - - g_return_val_if_fail (GTK_IS_IMAGE (widget), FALSE); - - image = GTK_IMAGE (widget); - - if (gtk_image_get_storage_type (image) != GTK_IMAGE_STOCK) - return FALSE; - - gchar * stock_id = NULL; - gtk_image_get_stock(image, &stock_id, NULL); - - gtk_stock_lookup (stock_id, &stock); - - if (should_show_image (image)) - dbusmenu_menuitem_property_set (menuitem, - DBUSMENU_MENUITEM_PROP_ICON_NAME, - stock_id); - else - dbusmenu_menuitem_property_remove (menuitem, - DBUSMENU_MENUITEM_PROP_ICON_NAME); - - const gchar *label = dbusmenu_menuitem_property_get (menuitem, - DBUSMENU_MENUITEM_PROP_LABEL); - - if (stock.label != NULL && label != NULL) - { - dbusmenu_menuitem_property_set (menuitem, - DBUSMENU_MENUITEM_PROP_LABEL, - stock.label); - - return TRUE; - } - - return FALSE; -} - static void checkbox_toggled (GtkWidget *widget, DbusmenuMenuitem *mi) { @@ -459,27 +688,89 @@ checkbox_toggled (GtkWidget *widget, DbusmenuMenuitem *mi) } static void -update_icon_name (DbusmenuMenuitem *menuitem, - GtkWidget *widget) +update_icon (DbusmenuMenuitem *menuitem, GtkImage *image) { - GtkImage *image; - - g_return_if_fail (GTK_IS_IMAGE (widget)); - - image = GTK_IMAGE (widget); + GdkPixbuf * pixbuf = NULL; + const gchar * icon_name = NULL; + GtkStockItem stock; + GIcon * gicon; + GtkIconInfo * info; + gint width; + + if (image != NULL && should_show_image (image)) { + switch (gtk_image_get_storage_type (image)) { + case GTK_IMAGE_EMPTY: + break; + + case GTK_IMAGE_PIXBUF: + pixbuf = g_object_ref (gtk_image_get_pixbuf (image)); + break; + + case GTK_IMAGE_ICON_NAME: + gtk_image_get_icon_name (image, &icon_name, NULL); + break; + + case GTK_IMAGE_STOCK: + gtk_image_get_stock (image, (gchar **) &icon_name, NULL); + if (gtk_stock_lookup (icon_name, &stock)) { + /* Now set label too */ + const gchar * label = NULL; + label = dbusmenu_menuitem_property_get (menuitem, + DBUSMENU_MENUITEM_PROP_LABEL); + if (stock.label != NULL && label != NULL) { + dbusmenu_menuitem_property_set (menuitem, + DBUSMENU_MENUITEM_PROP_LABEL, + stock.label); + } + } + break; + + case GTK_IMAGE_GICON: + /* Load up a pixbuf and send that over. We don't bother differentiating + between icon-name gicons and pixbuf gicons because even when given a + icon-name gicon, there's no easy way to lookup which icon-name among + its set is present and should be used among the icon themes available. + So instead, we render to a pixbuf and watch icon theme changes. */ + gtk_image_get_gicon (image, &gicon, NULL); + gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, NULL); + info = gtk_icon_theme_lookup_by_gicon (gtk_icon_theme_get_default (), + gicon, width, + GTK_ICON_LOOKUP_FORCE_SIZE); + if (info != NULL) { + pixbuf = gtk_icon_info_load_icon (info, NULL); + gtk_icon_info_free (info); + } + break; - if (gtk_image_get_storage_type (image) != GTK_IMAGE_ICON_NAME) - return; + default: + g_debug ("Could not handle image type %i\n", gtk_image_get_storage_type (image)); + break; + } + } - if (should_show_image (image)) { - const gchar * icon_name = NULL; - gtk_image_get_icon_name(image, &icon_name, NULL); + if (icon_name != NULL) { dbusmenu_menuitem_property_set (menuitem, DBUSMENU_MENUITEM_PROP_ICON_NAME, icon_name); - } else { + dbusmenu_menuitem_property_remove (menuitem, + DBUSMENU_MENUITEM_PROP_ICON_DATA); + } + else if (pixbuf != NULL) { dbusmenu_menuitem_property_remove (menuitem, DBUSMENU_MENUITEM_PROP_ICON_NAME); + dbusmenu_menuitem_property_set_image (menuitem, + DBUSMENU_MENUITEM_PROP_ICON_DATA, + pixbuf); + } + else { + dbusmenu_menuitem_property_remove (menuitem, + DBUSMENU_MENUITEM_PROP_ICON_NAME); + dbusmenu_menuitem_property_remove (menuitem, + DBUSMENU_MENUITEM_PROP_ICON_DATA); + } + + if (pixbuf != NULL) { + g_object_unref (pixbuf); } } @@ -513,17 +804,100 @@ find_menu_label (GtkWidget *widget) } static void +recreate_menu_item (DbusmenuMenuitem * parent, DbusmenuMenuitem * child) +{ + if (parent == NULL) + { + /* We need a parent */ + return; + } + ParserData * pdata = g_object_get_data (G_OBJECT (child), PARSER_DATA); + /* Keep a pointer to the GtkMenuItem, as pdata->widget might be + * invalidated when we delete the DbusmenuMenuitem + */ + GtkWidget * menuitem = pdata->widget; + + dbusmenu_menuitem_child_delete (parent, child); + + RecurseContext recurse = {0}; + recurse.toplevel = gtk_widget_get_toplevel(menuitem); + recurse.parent = parent; + + parse_menu_structure_helper(menuitem, &recurse); +} + +static gboolean +recreate_menu_item_in_idle_cb (gpointer data) +{ + DbusmenuMenuitem * child = (DbusmenuMenuitem *)data; + DbusmenuMenuitem * parent = dbusmenu_menuitem_get_parent (child); + g_object_unref (child); + recreate_menu_item (parent, child); + return FALSE; +} + +static void label_notify_cb (GtkWidget *widget, GParamSpec *pspec, gpointer data) { DbusmenuMenuitem *child = (DbusmenuMenuitem *)data; + GValue prop_value = {0}; + + g_value_init (&prop_value, pspec->value_type); + g_object_get_property (G_OBJECT (widget), pspec->name, &prop_value); if (pspec->name == g_intern_static_string ("label")) { + gchar * text = sanitize_label (GTK_LABEL (widget)); dbusmenu_menuitem_property_set (child, DBUSMENU_MENUITEM_PROP_LABEL, - gtk_label_get_text (GTK_LABEL (widget))); + text); + g_free (text); + } + else if (pspec->name == g_intern_static_string ("parent")) + { + if (GTK_WIDGET (g_value_get_object (&prop_value)) == NULL) + { + /* This label is being removed from its GtkMenuItem. The + * menuitem becomes a separator now. As the client doesn't handle + * changing types so well, we remove the current DbusmenuMenuitem + * and add a new one. + * + * Note, we have to defer this to idle, as we are called before + * bin->child member of our old parent is invalidated. If we go ahead + * and call parse_menu_structure_helper now, the GtkMenuItem will + * still appear to have a label and we never convert it to a separator + */ + g_object_ref (child); + g_idle_add ((GSourceFunc)recreate_menu_item_in_idle_cb, child); + } + } + + g_value_unset(&prop_value); + return; +} + +static void +image_notify_cb (GtkWidget *widget, + GParamSpec *pspec, + gpointer data) +{ + DbusmenuMenuitem *mi = (DbusmenuMenuitem *)data; + + if (pspec->name == g_intern_static_string ("file") || + pspec->name == g_intern_static_string ("gicon") || + pspec->name == g_intern_static_string ("icon-name") || + pspec->name == g_intern_static_string ("icon-set") || + pspec->name == g_intern_static_string ("image") || + pspec->name == g_intern_static_string ("mask") || + pspec->name == g_intern_static_string ("pixbuf") || + pspec->name == g_intern_static_string ("pixbuf-animation") || + pspec->name == g_intern_static_string ("pixmap") || + pspec->name == g_intern_static_string ("stock") || + pspec->name == g_intern_static_string ("storage-type")) + { + update_icon (mi, GTK_IMAGE (widget)); } } @@ -554,9 +928,11 @@ action_notify_cb (GtkAction *action, } else if (pspec->name == g_intern_static_string ("label")) { + gchar * text = sanitize_label_text (gtk_action_get_label (action)); dbusmenu_menuitem_property_set (mi, DBUSMENU_MENUITEM_PROP_LABEL, - gtk_action_get_label (action)); + text); + g_free (text); } } @@ -571,7 +947,9 @@ item_activated (DbusmenuMenuitem *item, guint timestamp, gpointer user_data) if (GTK_IS_MENU_ITEM (child)) { + gdk_threads_enter (); gtk_menu_item_activate (GTK_MENU_ITEM (child)); + gdk_threads_leave (); } } } @@ -596,38 +974,94 @@ item_about_to_show (DbusmenuMenuitem *item, gpointer user_data) return TRUE; } +static gboolean +item_handle_event (DbusmenuMenuitem *item, const gchar *name, + GVariant *variant, guint timestamp, GtkWidget *widget) +{ + if (g_strcmp0 (name, DBUSMENU_MENUITEM_EVENT_OPENED) == 0) + { + GtkWidget *submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget)); + if (submenu != NULL) + { + // Show the submenu so the app can notice and futz with the menus as + // desired (empathy and geany do this) + gtk_widget_show (submenu); + } + } + else if (g_strcmp0 (name, DBUSMENU_MENUITEM_EVENT_CLOSED) == 0) + { + GtkWidget *submenu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (widget)); + if (submenu != NULL) + { + // Hide the submenu so the app can notice and futz with the menus as + // desired (empathy and geany do this) + gtk_widget_hide (submenu); + } + } + + return FALSE; // just pass through on everything +} + +static gboolean +handle_first_label (DbusmenuMenuitem *mi) +{ + ParserData *pdata = g_object_get_data (G_OBJECT (mi), PARSER_DATA); + if (!pdata->label) + { + /* GtkMenuItem's can start life as a separator if they have no child + * GtkLabel. In this case, we need to convert the DbusmenuMenuitem from + * a separator to a normal menuitem if the application adds a label. + * As changing types isn't handled too well by the client, we delete + * this menuitem for now and then recreate it + */ + DbusmenuMenuitem * parent = dbusmenu_menuitem_get_parent (mi); + recreate_menu_item (parent, mi); + return TRUE; + } + + return FALSE; +} + static void widget_notify_cb (GtkWidget *widget, GParamSpec *pspec, gpointer data) { DbusmenuMenuitem *child = (DbusmenuMenuitem *)data; + GValue prop_value = {0}; + + g_value_init (&prop_value, pspec->value_type); + g_object_get_property (G_OBJECT (widget), pspec->name, &prop_value); if (pspec->name == g_intern_static_string ("sensitive")) { dbusmenu_menuitem_property_set_bool (child, DBUSMENU_MENUITEM_PROP_ENABLED, - gtk_widget_get_sensitive (widget)); + g_value_get_boolean (&prop_value)); } else if (pspec->name == g_intern_static_string ("label")) { + if (handle_first_label (child)) + { + return; + } + dbusmenu_menuitem_property_set (child, DBUSMENU_MENUITEM_PROP_LABEL, - gtk_menu_item_get_label (GTK_MENU_ITEM (widget))); + g_value_get_string (&prop_value)); } else if (pspec->name == g_intern_static_string ("visible")) { dbusmenu_menuitem_property_set_bool (child, DBUSMENU_MENUITEM_PROP_VISIBLE, - gtk_widget_get_visible (widget)); + g_value_get_boolean (&prop_value)); } - else if (pspec->name == g_intern_static_string ("stock")) + else if (pspec->name == g_intern_static_string ("image") || + pspec->name == g_intern_static_string ("always-show-image")) { - update_stock_item (child, widget); - } - else if (pspec->name == g_intern_static_string ("icon-name")) - { - update_icon_name (child, widget); + GtkWidget *image; + image = GTK_WIDGET (g_value_get_object (&prop_value)); + update_icon (child, GTK_IMAGE (image)); } else if (pspec->name == g_intern_static_string ("parent")) { @@ -635,13 +1069,13 @@ widget_notify_cb (GtkWidget *widget, * We probably should have added a 'remove' method to the * UbuntuMenuProxy early on, but it's late in the cycle now. */ - if (gtk_widget_get_parent (widget) == NULL) + if (GTK_WIDGET (g_value_get_object (&prop_value)) == NULL) { g_signal_handlers_disconnect_by_func (widget, G_CALLBACK (widget_notify_cb), child); - DbusmenuMenuitem *parent = g_object_get_data (G_OBJECT (child), "dbusmenu-parent"); + DbusmenuMenuitem *parent = dbusmenu_menuitem_get_parent (child); if (DBUSMENU_IS_MENUITEM (parent) && DBUSMENU_IS_MENUITEM (child)) { @@ -649,6 +1083,104 @@ widget_notify_cb (GtkWidget *widget, } } } + else if (pspec->name == g_intern_static_string ("submenu")) + { + /* The underlying submenu got swapped out. Let's see what it is now. */ + /* First, delete any children that may exist currently. */ + DbusmenuMenuitem * item = DBUSMENU_MENUITEM(g_object_get_data(G_OBJECT(widget), CACHED_MENUITEM)); + if (item != NULL) + { + GList * children = dbusmenu_menuitem_take_children (item); + GList * child = children; + while (child != NULL) { + g_object_unref (G_OBJECT(child->data)); + child = child->next; + } + g_list_free(children); + } + + /* Now parse new submenu. */ + RecurseContext recurse = {0}; + recurse.toplevel = gtk_widget_get_toplevel(widget); + recurse.parent = item; + + if (item != NULL) { + GtkWidget * menu = GTK_WIDGET (g_value_get_object (&prop_value)); + parse_menu_structure_helper(menu, &recurse); + watch_submenu(item, menu); + } else { + /* Note: it would be really odd that we wouldn't have a cached + item, but we should handle that appropriately. */ + parse_menu_structure_helper(widget, &recurse); + g_object_unref(G_OBJECT(recurse.parent)); + } + } + g_value_unset (&prop_value); +} + +static void +widget_add_cb (GtkWidget *widget, + GtkWidget *child, + gpointer data) +{ + if (find_menu_label (child) != NULL) + handle_first_label (data); +} + +/* A child item was added to a menu we're watching. Let's try to integrate it. */ +static void +child_added_cb (GtkContainer *menu, GtkWidget *widget, gpointer data) +{ + DbusmenuMenuitem *menuitem = (DbusmenuMenuitem *)data; + + RecurseContext recurse = {0}; + recurse.toplevel = gtk_widget_get_toplevel(GTK_WIDGET(menu)); + recurse.parent = menuitem; + + if (GTK_IS_MENU_BAR(menu)) { + activate_toplevel_item (widget); + } + + parse_menu_structure_helper(widget, &recurse); +} + +/* A child item was added to a menu we're watching. Let's try to integrate it. */ +static void +child_removed_cb (GtkContainer *menu, GtkWidget *widget, gpointer data) +{ + gpointer pmi = g_object_get_data(G_OBJECT(widget), CACHED_MENUITEM); + if (pmi == NULL) { + return; + } + + DbusmenuMenuitem * child = DBUSMENU_MENUITEM(pmi); + + pmi = g_object_get_data(G_OBJECT(menu), CACHED_MENUITEM); + if (pmi == NULL) { + return; + } + + DbusmenuMenuitem * parent = DBUSMENU_MENUITEM(pmi); + + dbusmenu_menuitem_child_delete(parent, child); + return; +} + +static void +theme_changed_cb (GtkIconTheme *theme, gpointer data) +{ + GtkWidget *image; + + image = gtk_image_menu_item_get_image (GTK_IMAGE_MENU_ITEM (data)); + + gpointer pmi = g_object_get_data(G_OBJECT(data), CACHED_MENUITEM); + if (pmi != NULL) { + update_icon(DBUSMENU_MENUITEM(pmi), GTK_IMAGE(image)); + } + + /* Switch signal to new theme */ + g_signal_handlers_disconnect_by_func(theme, G_CALLBACK(theme_changed_cb), data); + g_signal_connect(gtk_icon_theme_get_default(), "changed", G_CALLBACK(theme_changed_cb), data); } static gboolean |