--- a/libpurple/meson.build Mon May 23 22:00:29 2022 -0500
+++ b/libpurple/meson.build Tue May 24 00:13:00 2022 -0500
@@ -58,6 +58,7 @@
'purpleimconversation.c',
'purplenoopcredentialprovider.c',
@@ -154,6 +155,7 @@
'purplenoopcredentialprovider.h',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplemenu.c Tue May 24 00:13:00 2022 -0500
@@ -0,0 +1,120 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program 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 General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +/****************************************************************************** + *****************************************************************************/ +purple_menu_populate_dynamic_targets_func(GMenuModel *model, gint index, + GHashTable *table = data; + gchar *property = NULL; + g_return_if_fail(G_IS_MENU(model)); + if(g_menu_model_get_item_attribute(model, index, + PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, + value = g_hash_table_lookup(table, property); + GMenuItem *item = NULL; + item = g_menu_item_new_from_model(model, index); + g_menu_item_set_attribute(item, G_MENU_ATTRIBUTE_TARGET, "s", + g_menu_remove(G_MENU(model), index); + g_menu_insert_item(G_MENU(model), index, item); +/****************************************************************************** + *****************************************************************************/ +purple_menu_walk(GMenuModel *model, PurpleMenuWalkFunc func, gpointer data) { + for(index = 0; index < g_menu_model_get_n_items(model); index++) { + GMenuLinkIter *iter = NULL; + func(model, index, data); + iter = g_menu_model_iterate_item_links(model, index); + while(g_menu_link_iter_next(iter)) { + GMenuModel *link = g_menu_link_iter_get_value(iter); + purple_menu_walk(link, func, data); +purple_menu_populate_dynamic_targets(GMenu *menu, const gchar *first_property, + GHashTable *table = NULL; + const gchar *property = NULL, *value = NULL; + g_return_if_fail(G_IS_MENU(menu)); + g_return_if_fail(first_property != NULL); + table = g_hash_table_new(g_str_hash, g_str_equal); + property = first_property; + va_start(vargs, first_property); + /* Iterate through the vargs adding values when we find one that isn't + value = va_arg(vargs, const gchar *); + g_hash_table_insert(table, (gpointer)property, (gpointer)value); + /* After adding the value, see if we have another property. */ + property = va_arg(vargs, const gchar *); + } while(property != NULL); + purple_menu_walk(G_MENU_MODEL(menu), + purple_menu_populate_dynamic_targets_func, table); + g_hash_table_unref(table); --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplemenu.h Tue May 24 00:13:00 2022 -0500
@@ -0,0 +1,89 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * This program 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 General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses/>. +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#define PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET "dynamic-target" + * @model: The current [class@Gio.MenuModel] being walked. + * @index: The index of the item. + * Used as a parameter to [func@Purple.menu_walk]. While walking, @model will + * be updated to point to the current section or submenu and will only be the + * model that was passed to [func@Purple.menu_walk] for its immediate +typedef void (*PurpleMenuWalkFunc)(GMenuModel *model, gint index, gpointer data); + * @model: A [class@Gio.MenuModel] to walk. + * @func: (scope call): The function to call. + * @data: User data to pass for func. + * Recursively calls @func for each item in @model and all of its children. +void purple_menu_walk(GMenuModel *model, PurpleMenuWalkFunc func, gpointer data); + * purple_menu_populate_dynamic_targets: + * @menu: The menu instance to modify. + * @first_property: The name of the first property of dynamic targets to + * @...: The value of the first property, followed optionally by more + * name/value pairs, followed by %NULL. + * Updates @menu by adding a target property when an item with an attribute + * named "dynamic-target" is found. + * The value for the target is set to the matching value from the passed in + * For example, if you need to set the target to an account, you would set + * the "dynamic-target" attribute of your menu item to "account" and then + * call purple_menu_populate_dynamic_targets() with a property pair of + * "account" and [method@Account.get_id]. +void purple_menu_populate_dynamic_targets(GMenu *menu, const gchar *first_property, ...) G_GNUC_NULL_TERMINATED; +#endif /* PURPLE_MENU_H */ --- a/libpurple/tests/meson.build Mon May 23 22:00:29 2022 -0500
+++ b/libpurple/tests/meson.build Tue May 24 00:13:00 2022 -0500
@@ -9,6 +9,7 @@
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/tests/test_menu.c Tue May 24 00:13:00 2022 -0500
@@ -0,0 +1,263 @@
+ * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * 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 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 <https://www.gnu.org/licenses/>. + const gchar *dynamic_target; +} TestPurpleMenuItemData; +} TestPurpleMenuWalkData; +/****************************************************************************** + *****************************************************************************/ +static TestPurpleMenuItemData * +test_purple_menu_item_new(const gchar *dynamic_target, const gchar *target) { + TestPurpleMenuItemData *data = g_new(TestPurpleMenuItemData, 1); + data->dynamic_target = dynamic_target; +test_purple_menu_items_func(GMenuModel *model, gint index, gpointer data) { + TestPurpleMenuItemData *item = NULL; + TestPurpleMenuWalkData *walk_data = data; + gboolean found = FALSE; + /* Set item to the first item in the list. */ + item = walk_data->items->data; + /* Check that the dynamic-target value matches our expectations. */ + g_menu_model_get_item_attribute(model, index, + PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_assert_cmpstr(item->dynamic_target, ==, actual); + /* Check that the target value matches our expectations. */ + found = g_menu_model_get_item_attribute(model, index, + G_MENU_ATTRIBUTE_TARGET, "s", + if(item->target != NULL) { + g_assert_cmpstr(item->target, ==, actual); + /* Free our data and move to the next item. */ + walk_data->items = g_list_delete_link(walk_data->items, walk_data->items); +test_purple_menu_items(GMenu *menu, GList *items) { + TestPurpleMenuWalkData data = { + purple_menu_walk(G_MENU_MODEL(menu), test_purple_menu_items_func, &data); + g_assert_null(data.items); +/****************************************************************************** + *****************************************************************************/ +test_purple_menu_single_no_dynamic_target(void) { + GMenu *menu = g_menu_new(); + GList *expected = NULL; + g_menu_append(menu, "item1", NULL); + expected = g_list_append(expected, test_purple_menu_item_new(NULL, NULL)); + purple_menu_populate_dynamic_targets(menu, "property", "1", NULL); + test_purple_menu_items(menu, expected); +test_purple_menu_single_unset_dynamic_target(void) { + GMenu *menu = g_menu_new(); + GMenuItem *item = NULL; + GList *expected = NULL; + item = g_menu_item_new("item1", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(menu, item); + expected = g_list_append(expected, test_purple_menu_item_new("foo", NULL)); + purple_menu_populate_dynamic_targets(menu, "bar", "123", NULL); + test_purple_menu_items(menu, expected); +test_purple_menu_single_single_dynamic_target(void) { + GMenu *menu = g_menu_new(); + GMenuItem *item = NULL; + GList *expected = NULL; + item = g_menu_item_new("item1", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(menu, item); + expected = g_list_append(expected, test_purple_menu_item_new("foo", "bar")); + purple_menu_populate_dynamic_targets(menu, "foo", "bar", NULL); + test_purple_menu_items(menu, expected); +test_purple_menu_section_single(void) { + GMenu *menu = NULL, *section = NULL; + GMenuItem *item = NULL; + GList *expected = NULL; + /* Create our section. */ + section = g_menu_new(); + expected = g_list_append(expected, test_purple_menu_item_new(NULL, NULL)); + /* Create our item and add it to the list. */ + item = g_menu_item_new("item1", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(section, item); + expected = g_list_append(expected, test_purple_menu_item_new("foo", "bar")); + /* Finally add our section to our menu. */ + g_menu_append_section(menu, NULL, G_MENU_MODEL(section)); + g_object_unref(section); + purple_menu_populate_dynamic_targets(menu, "foo", "bar", NULL); + test_purple_menu_items(menu, expected); +test_purple_menu_multiple_multiple_dynamic_target(void) { + GMenu *menu = g_menu_new(); + GMenuItem *item = NULL; + GList *expected = NULL; + item = g_menu_item_new("item1", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(menu, item); + expected = g_list_append(expected, test_purple_menu_item_new("foo", "bar")); + item = g_menu_item_new("item2", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(menu, item); + expected = g_list_append(expected, test_purple_menu_item_new("abc", "123")); + purple_menu_populate_dynamic_targets(menu, "foo", "bar", "abc", "123", + test_purple_menu_items(menu, expected); +test_purple_menu_multiple_mixed(void) { + GMenu *menu = g_menu_new(); + GMenuItem *item = NULL; + GList *expected = NULL; + g_menu_append(menu, "item1", NULL); + expected = g_list_append(expected, test_purple_menu_item_new(NULL, NULL)); + item = g_menu_item_new("item2", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(menu, item); + expected = g_list_append(expected, test_purple_menu_item_new("foo", "bar")); + item = g_menu_item_new("item3", NULL); + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s", + g_menu_append_item(menu, item); + expected = g_list_append(expected, test_purple_menu_item_new("abc", "123")); + purple_menu_populate_dynamic_targets(menu, "foo", "bar", "abc", "123", + test_purple_menu_items(menu, expected); +/****************************************************************************** + *****************************************************************************/ +main(gint argc, gchar *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/menu/single/no-dynamic-target", + test_purple_menu_single_no_dynamic_target); + g_test_add_func("/menu/single/unset-dynamic-target", + test_purple_menu_single_unset_dynamic_target); + g_test_add_func("/menu/single/single_dynamic_target", + test_purple_menu_single_single_dynamic_target); + g_test_add_func("/menu/section/single", + test_purple_menu_section_single); + g_test_add_func("/menu/multiple/multiple_dynamic_target", + test_purple_menu_multiple_multiple_dynamic_target); + g_test_add_func("/menu/multiple/mixed", + test_purple_menu_multiple_mixed); \ No newline at end of file
--- a/pidgin/pidginaccountsenabledmenu.c Mon May 23 22:00:29 2022 -0500
+++ b/pidgin/pidginaccountsenabledmenu.c Tue May 24 00:13:00 2022 -0500
@@ -22,6 +22,8 @@
#include "pidginaccountsenabledmenu.h"
@@ -29,47 +31,33 @@
/******************************************************************************
*****************************************************************************/
-pidgin_accounts_enabled_menu_build_submenu(PurpleAccount *account) {
- GMenu *menu = NULL, *section = NULL;
- gchar *action_id = NULL;
- const gchar *account_id = NULL;
- account_id = purple_account_get_id(account);
- /* Add the "Edit Account" section. */
- section = g_menu_new();
- g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
- action_id = g_strdup_printf("app.edit-account::%s", account_id);
- g_menu_append(section, _("Edit Account"), action_id);
- /* Add the "Disable Account" section. */
- section = g_menu_new();
- g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
- action_id = g_strdup_printf("app.disable-account::%s", account_id);
- g_menu_append(section, _("Disable"), action_id);
pidgin_accounts_enabled_menu_refresh_helper(PurpleAccount *account,
+ GApplication *application = g_application_get_default(); if(purple_account_get_enabled(account)) {
+ PurpleConnection *connection = purple_account_get_connection(account); const gchar *account_name = purple_account_get_username(account);
const gchar *protocol_name = purple_account_get_protocol_name(account);
+ const gchar *account_id = purple_account_get_id(account); + const gchar *connection_id = NULL; - submenu = pidgin_accounts_enabled_menu_build_submenu(account);
+ submenu = gtk_application_get_menu_by_id(GTK_APPLICATION(application), + if(PURPLE_IS_CONNECTION(connection)) { + connection_id = purple_connection_get_id(connection); + purple_menu_populate_dynamic_targets(submenu, + "connection", connection_id, /* translators: This format string is intended to contain the account
* name followed by the protocol name to uniquely identify a specific
@@ -113,6 +101,20 @@
+pidgin_accounts_enabled_menu_connected_cb(G_GNUC_UNUSED PurpleAccount *account, + pidgin_accounts_enabled_menu_refresh(data); +pidgin_accounts_enabled_menu_disconnected_cb(G_GNUC_UNUSED PurpleAccount *account, + pidgin_accounts_enabled_menu_refresh(data); pidgin_accounts_enabled_menu_weak_notify_cb(G_GNUC_UNUSED gpointer data,
@@ -146,5 +148,15 @@
G_CALLBACK(pidgin_accounts_enabled_menu_disabled_cb),
+ /* For the account actions, we also need to know when an account is online + purple_signal_connect(handle, "account-signed-on", menu, + G_CALLBACK(pidgin_accounts_enabled_menu_connected_cb), + purple_signal_connect(handle, "account-signed-off", menu, + G_CALLBACK(pidgin_accounts_enabled_menu_disconnected_cb), --- a/pidgin/resources/gtk/menus.ui Mon May 23 22:00:29 2022 -0500
+++ b/pidgin/resources/gtk/menus.ui Tue May 24 00:13:00 2022 -0500
@@ -258,4 +258,21 @@
+ <menu id="enabled-account"> + <attribute name="label" translatable="yes">_Edit Account</attribute> + <attribute name="action">app.edit-account</attribute> + <attribute name="dynamic-target">account</attribute> + <attribute name="label" translatable="yes">_Disable</attribute> + <attribute name="action">app.disable-account</attribute> + <attribute name="dynamic-target">account</attribute>