# HG changeset patch
# Node ID 7068dd5697cb
# Parent  53e4815ef254
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -57,6 +57,7 @@
 	'purpleimconversation.c',
 	'purplekeyvaluepair.c',
 	'purplemarkup.c',
+	'purplemenu.c',
 	'purplemessage.c',
 	'purplenoopcredentialprovider.c',
 	'purpleoptions.c',
@@ -150,6 +151,7 @@
 	'purpleattachment.h',
 	'purplekeyvaluepair.h',
 	'purplemarkup.h',
+	'purplemenu.h',
 	'purplemessage.h',
 	'purplenoopcredentialprovider.h',
 	'purpleoptions.h',
diff --git a/libpurple/purplemenu.h b/libpurple/purplemenu.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplemenu.h
@@ -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
+ * source distribution.
+ *
+ * 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"
+#endif
+
+#ifndef PURPLE_MENU_H
+#define PURPLE_MENU_H
+
+#include <glib.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET "dynamic-target"
+
+/**
+ * PurpleMenuWalkFunc:
+ * @model: The current [class@Gio.MenuModel] being walked.
+ * @index: The index of the item.
+ * @data: User data.
+ *
+ * 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
+ * children.
+ *
+ * Since: 3.0.0
+ */
+typedef void (*PurpleMenuWalkFunc)(GMenuModel *model, gint index, gpointer data);
+
+/**
+ * purple_menu_walk:
+ * @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.
+ *
+ * Since: 3.0.0
+ */
+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
+ *                  replace.
+ * @...: 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
+ * parameters.
+ *
+ * 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].
+ *
+ * Since: 3.0.0
+ */
+void purple_menu_populate_dynamic_targets(GMenu *menu, const gchar *first_property, ...) G_GNUC_NULL_TERMINATED;
+
+G_END_DECLS
+
+#endif /* PURPLE_MENU_H */
diff --git a/libpurple/purplemenu.c b/libpurple/purplemenu.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplemenu.c
@@ -0,0 +1,124 @@
+/*
+ * 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
+ * source distribution.
+ *
+ * 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/>.
+ */
+
+#include "purplemenu.h"
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_menu_populate_dynamic_targets_func(GMenuModel *model, gint index,
+                                          gpointer data)
+{
+	GHashTable *table = data;
+	gchar *property = NULL;
+
+	if(!G_IS_MENU(model)) {
+		g_warning("not a GMenu");
+
+		return;
+	}
+
+	if(g_menu_model_get_item_attribute(model, index,
+	                                   PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET,
+	                                   "s", &property))
+	{
+		const gchar *value;
+
+		value = g_hash_table_lookup(table, property);
+		g_free(property);
+
+		if(value != NULL) {
+			GMenuItem *item = NULL;
+
+			item = g_menu_item_new_from_model(model, index);
+			g_menu_item_set_attribute(item, G_MENU_ATTRIBUTE_TARGET, "s",
+			                          value);
+
+			g_menu_remove(G_MENU(model), index);
+			g_menu_insert_item(G_MENU(model), index, item);
+			g_object_unref(item);
+		}
+	}
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+purple_menu_walk(GMenuModel *model, PurpleMenuWalkFunc func, gpointer data) {
+	gint index = 0;
+
+	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);
+
+			g_clear_object(&link);
+		}
+
+		g_clear_object(&iter);
+	}
+}
+
+void
+purple_menu_populate_dynamic_targets(GMenu *menu, const gchar *first_property,
+                                     ...)
+{
+	GHashTable *table = NULL;
+	const gchar *property = NULL, *value = NULL;
+	va_list vargs;
+
+	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
+	 * NULL.
+	 */
+	do {
+		value = va_arg(vargs, const gchar *);
+		if(value != NULL) {
+			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);
+
+	va_end(vargs);
+
+	purple_menu_walk(G_MENU_MODEL(menu),
+	                 purple_menu_populate_dynamic_targets_func, table);
+
+	g_hash_table_unref(table);
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -9,6 +9,7 @@
     'image',
     'keyvaluepair',
     'markup',
+    'menu',
     'protocol_action',
     'protocol_xfer',
     'purplepath',
diff --git a/libpurple/tests/test_menu.c b/libpurple/tests/test_menu.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_menu.c
@@ -0,0 +1,233 @@
+/*
+ * 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/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+typedef struct {
+	const gchar *dynamic_target;
+	const gchar *target;
+} TestPurpleMenuItemData;
+
+typedef struct {
+	GList *items;
+} TestPurpleMenuWalkData;
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+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;
+	data->target = target;
+
+	return data;
+}
+
+static void
+test_purple_menu_items_func(GMenuModel *model, gint index, gpointer data) {
+	TestPurpleMenuItemData *item = NULL;
+	TestPurpleMenuWalkData *walk_data = data;
+	gchar *actual = NULL;
+
+	/* 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",
+	                                &actual);
+
+	g_assert_cmpstr(item->dynamic_target, ==, actual);
+	g_free(actual);
+
+	/* Check that the target value matches our expectations. */
+	g_menu_model_get_item_attribute(model, index,
+	                                G_MENU_ATTRIBUTE_TARGET, "s", &actual);
+
+	g_assert_cmpstr(item->target, ==, actual);
+	g_free(actual);
+
+	/* Free our data and move to the next item. */
+	g_free(walk_data->items->data);
+	walk_data->items = g_list_delete_link(walk_data->items, walk_data->items);
+}
+
+static void
+test_purple_menu_items(GMenu *menu, GList *items) {
+	TestPurpleMenuWalkData data = {
+		.items = items,
+	};
+
+	purple_menu_walk(G_MENU_MODEL(menu), test_purple_menu_items_func, &data);
+
+	g_assert_null(data.items);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+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);
+
+	g_object_unref(menu);
+}
+
+static void
+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",
+	                          "foo");
+	g_menu_append_item(menu, item);
+	g_object_unref(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);
+
+	g_object_unref(menu);
+}
+
+static void
+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",
+	                          "foo");
+	g_menu_append_item(section, item);
+	g_object_unref(item);
+	expected = g_list_append(expected, test_purple_menu_item_new("foo", "bar"));
+
+	/* Finally add our section to our menu. */
+	menu = g_menu_new();
+	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);
+
+	g_object_unref(menu);
+}
+
+static void
+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",
+	                          "foo");
+	g_menu_append_item(menu, item);
+	g_object_unref(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",
+	                          "abc");
+	g_menu_append_item(menu, item);
+	g_object_unref(item);
+	expected = g_list_append(expected, test_purple_menu_item_new("abc", "123"));
+
+	purple_menu_populate_dynamic_targets(menu, "foo", "bar", "abc", "123",
+	                                     NULL);
+
+	test_purple_menu_items(menu, expected);
+
+	g_object_unref(menu);
+}
+
+static void
+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",
+	                          "foo");
+	g_menu_append_item(menu, item);
+	g_object_unref(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",
+	                          "abc");
+	g_menu_append_item(menu, item);
+	g_object_unref(item);
+	expected = g_list_append(expected, test_purple_menu_item_new("abc", "123"));
+
+	purple_menu_populate_dynamic_targets(menu, "foo", "bar", "abc", "123",
+	                                     NULL);
+
+	test_purple_menu_items(menu, expected);
+
+	g_object_unref(menu);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+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/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);
+
+	return g_test_run();
+}
\ No newline at end of file
diff --git a/pidgin/pidginaccountsenabledmenu.c b/pidgin/pidginaccountsenabledmenu.c
--- a/pidgin/pidginaccountsenabledmenu.c
+++ b/pidgin/pidginaccountsenabledmenu.c
@@ -22,6 +22,8 @@
 
 #include <glib/gi18n.h>
 
+#include <gtk/gtk.h>
+
 #include "pidginaccountsenabledmenu.h"
 
 #include "pidgincore.h"
@@ -29,47 +31,34 @@
 /******************************************************************************
  * Helpers
  *****************************************************************************/
-static GMenu *
-pidgin_accounts_enabled_menu_build_submenu(PurpleAccount *account) {
-	GMenu *menu = NULL, *section = NULL;
-	gchar *action_id = NULL;
-	const gchar *account_id = NULL;
-
-	menu = g_menu_new();
-	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);
-	g_free(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);
-	g_free(action_id);
-
-	return menu;
-}
-
 static void
 pidgin_accounts_enabled_menu_refresh_helper(PurpleAccount *account,
                                             gpointer data)
 {
+	GApplication *application = g_application_get_default();
 	GMenu *menu = data;
 
 	if(purple_account_get_enabled(account, PIDGIN_UI)) {
+		PurpleConnection *connection = purple_account_get_connection(account);
 		GMenu *submenu = NULL;
 		gchar *label = NULL;
 		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),
+		                                         "enabled-account");
+
+		if(PURPLE_IS_CONNECTION(connection)) {
+			connection_id = purple_connection_get_id(connection);
+		}
+
+		purple_menu_populate_dynamic_targets(submenu,
+		                                     "account", account_id,
+		                                     "connection", connection_id,
+		                                     NULL);
 
 		/* translators: This format string is intended to contain the account
 		 * name followed by the protocol name to uniquely identify a specific
diff --git a/pidgin/resources/gtk/menus.ui b/pidgin/resources/gtk/menus.ui
--- a/pidgin/resources/gtk/menus.ui
+++ b/pidgin/resources/gtk/menus.ui
@@ -258,4 +258,21 @@
       </section>
     </submenu>
   </menu>
+
+  <menu id="enabled-account">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Edit Account</attribute>
+        <attribute name="action">app.edit-account</attribute>
+        <attribute name="dynamic-target">account</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Disable</attribute>
+        <attribute name="action">app.disable-account</attribute>
+        <attribute name="dynamic-target">account</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
