Convert plugin actions to GMenu and GAction

Fri, 27 May 2022 14:52:34 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Fri, 27 May 2022 14:52:34 -0500
changeset 41414
b76bc2b4d7cc
parent 41413
911c16e255ff
child 41416
253e831b0a1e

Convert plugin actions to GMenu and GAction

Also ported the idle maker plugin to the new api.

Testing Done:
Made sure all of the actions for the idle maker plugin worked.

Reviewed at https://reviews.imfreedom.org/r/1408/

libpurple/plugins/idle.c file | annotate | diff | comparison | revisions
libpurple/purpleplugininfo.c file | annotate | diff | comparison | revisions
libpurple/purpleplugininfo.h file | annotate | diff | comparison | revisions
pidgin/glade/pidgin3.xml.in file | annotate | diff | comparison | revisions
pidgin/pidginapplication.c file | annotate | diff | comparison | revisions
pidgin/pidgincontactlist.c file | annotate | diff | comparison | revisions
pidgin/pidginpluginsmenu.c file | annotate | diff | comparison | revisions
pidgin/pidginpluginsmenu.h file | annotate | diff | comparison | revisions
pidgin/resources/BuddyList/window.ui file | annotate | diff | comparison | revisions
pidgin/resources/gtk/menus.ui file | annotate | diff | comparison | revisions
pidgin/resources/pidgin.gresource.xml file | annotate | diff | comparison | revisions
--- a/libpurple/plugins/idle.c	Fri May 27 04:11:45 2022 -0500
+++ b/libpurple/plugins/idle.c	Fri May 27 14:52:34 2022 -0500
@@ -132,9 +132,22 @@
 	idled_accts = g_list_remove(idled_accts, acct);
 }
 
-
 static void
-idle_action(PurplePluginAction *action)
+signing_off_cb(PurpleConnection *gc, void *data)
+{
+	PurpleAccount *account;
+
+	account = purple_connection_get_account(gc);
+	idled_accts = g_list_remove(idled_accts, account);
+}
+
+/******************************************************************************
+ * Actions
+ *****************************************************************************/
+static void
+purple_idle_set_account_idle_time(G_GNUC_UNUSED GSimpleAction *action,
+                                  G_GNUC_UNUSED GVariant *parameter,
+                                  gpointer data)
 {
 	/* Use the super fancy request API */
 
@@ -155,7 +168,7 @@
 	request = purple_request_fields_new();
 	purple_request_fields_add_group(request, group);
 
-	purple_request_fields(action->plugin,
+	purple_request_fields(data,
 			N_("I'dle Mak'er"),
 			_("Set Account Idle Time"),
 			NULL,
@@ -166,7 +179,9 @@
 }
 
 static void
-unidle_action(PurplePluginAction *action)
+purple_idle_unset_account_idle_time(G_GNUC_UNUSED GSimpleAction *action,
+                                    G_GNUC_UNUSED GVariant *parameter,
+                                    gpointer data)
 {
 	PurpleRequestFields *request;
 	PurpleRequestFieldGroup *group;
@@ -188,7 +203,7 @@
 	request = purple_request_fields_new();
 	purple_request_fields_add_group(request, group);
 
-	purple_request_fields(action->plugin,
+	purple_request_fields(data,
 			N_("I'dle Mak'er"),
 			_("Unset Account Idle Time"),
 			NULL,
@@ -199,7 +214,9 @@
 }
 
 static void
-idle_all_action(PurplePluginAction *action)
+purple_idle_set_all_accounts_idle_time(G_GNUC_UNUSED GSimpleAction *action,
+                                       G_GNUC_UNUSED GVariant *parameter,
+                                       gpointer data)
 {
 	PurpleRequestFields *request;
 	PurpleRequestFieldGroup *group;
@@ -213,7 +230,7 @@
 	request = purple_request_fields_new();
 	purple_request_fields_add_group(request, group);
 
-	purple_request_fields(action->plugin,
+	purple_request_fields(data,
 			N_("I'dle Mak'er"),
 			_("Set Idle Time for All Accounts"),
 			NULL,
@@ -224,7 +241,9 @@
 }
 
 static void
-unidle_all_action(PurplePluginAction *action)
+purple_idle_unset_all_accounts_idle_time(G_GNUC_UNUSED GSimpleAction *action,
+                                         G_GNUC_UNUSED GVariant *parameter,
+                                         G_GNUC_UNUSED gpointer data)
 {
 	/* freeing the list here will cause segfaults if the user idles an account
 	 * after the list is freed */
@@ -233,48 +252,47 @@
 	idled_accts = NULL;
 }
 
-static GList *
-actions(PurplePlugin *plugin)
-{
-	GList *l = NULL;
-	PurplePluginAction *act = NULL;
-
-	act = purple_plugin_action_new(_("Set Account Idle Time"),
-			idle_action);
-	l = g_list_append(l, act);
-
-	act = purple_plugin_action_new(_("Unset Account Idle Time"),
-			unidle_action);
-	l = g_list_append(l, act);
-
-	act = purple_plugin_action_new(_("Set Idle Time for All Accounts"),
-			idle_all_action);
-	l = g_list_append(l, act);
-
-	act = purple_plugin_action_new(
-			_("Unset Idle Time for All Idled Accounts"), unidle_all_action);
-	l = g_list_append(l, act);
-
-	return l;
-}
-
-static void
-signing_off_cb(PurpleConnection *gc, void *data)
-{
-	PurpleAccount *account;
-
-	account = purple_connection_get_account(gc);
-	idled_accts = g_list_remove(idled_accts, account);
-}
-
+/******************************************************************************
+ * GPlugin Exports
+ *****************************************************************************/
 static GPluginPluginInfo *
 idle_query(GError **error)
 {
+	GSimpleActionGroup *group = NULL;
+	GActionEntry entries[] = {
+		{
+			.name = "set-account-idle-time",
+			.activate = purple_idle_set_account_idle_time,
+		}, {
+			.name = "unset-account-idle-time",
+			.activate = purple_idle_unset_account_idle_time,
+		}, {
+			.name = "set-all-accounts-idle-time",
+			.activate = purple_idle_set_all_accounts_idle_time,
+		}, {
+			.name = "unset-all-accounts-idle-time",
+			.activate = purple_idle_unset_all_accounts_idle_time,
+		}
+	};
+	GMenu *menu = NULL;
 	const gchar * const authors[] = {
 		"Eric Warmenhoven <eric@warmenhoven.org>",
 		NULL
 	};
 
+	group = g_simple_action_group_new();
+	g_action_map_add_action_entries(G_ACTION_MAP(group), entries,
+	                                G_N_ELEMENTS(entries), NULL);
+
+	menu = g_menu_new();
+	g_menu_append(menu, _("Set Account Idle Time"), "set-account-idle-time");
+	g_menu_append(menu, _("Unset Account Idle Time"),
+	              "unset-account-idle-time");
+	g_menu_append(menu, _("Set Idle Time for All Accounts"),
+	              "set-all-accounts-idle-time");
+	g_menu_append(menu, _("Unset Idle Time for All Idled Accounts"),
+	              "unset-all-accounts-idle-time");
+
 	return purple_plugin_info_new(
 		"id",           IDLE_PLUGIN_ID,
 		/* This is a cultural reference.  Dy'er Mak'er is a song by Led Zeppelin.
@@ -287,7 +305,8 @@
 		"authors",      authors,
 		"website",      PURPLE_WEBSITE,
 		"abi-version",  PURPLE_ABI_VERSION,
-		"actions-cb",   actions,
+		"action-group", group,
+		"action-menu",  menu,
 		NULL
 	);
 }
@@ -305,7 +324,7 @@
 static gboolean
 idle_unload(GPluginPlugin *plugin, gboolean shutdown, GError **error)
 {
-	unidle_all_action(NULL);
+	purple_idle_unset_all_accounts_idle_time(NULL, NULL, NULL);
 
 	return TRUE;
 }
--- a/libpurple/purpleplugininfo.c	Fri May 27 04:11:45 2022 -0500
+++ b/libpurple/purpleplugininfo.c	Fri May 27 14:52:34 2022 -0500
@@ -46,6 +46,9 @@
 	/* TRUE if a plugin has been unloaded at least once. Auto-load
 	 * plugins that have been unloaded once will not be auto-loaded again. */
 	gboolean unloaded;
+
+	GActionGroup *action_group;
+	GMenuModel *menu_model;
 } PurplePluginInfoPrivate;
 
 enum {
@@ -55,6 +58,8 @@
 	PROP_PREF_FRAME_CB,
 	PROP_PREF_REQUEST_CB,
 	PROP_FLAGS,
+	PROP_ACTION_GROUP,
+	PROP_ACTION_MENU,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = { NULL, };
@@ -63,6 +68,39 @@
                            GPLUGIN_TYPE_PLUGIN_INFO);
 
 /**************************************************************************
+ * Helpers
+ **************************************************************************/
+static void
+purple_plugin_info_set_action_group(PurplePluginInfo *info,
+                                    GActionGroup *group)
+{
+	PurplePluginInfoPrivate *priv = NULL;
+
+	g_return_if_fail(PURPLE_IS_PLUGIN_INFO(info));
+
+	priv = purple_plugin_info_get_instance_private(info);
+
+	if(g_set_object(&priv->action_group, group)) {
+		g_object_notify_by_pspec(G_OBJECT(info), properties[PROP_ACTION_GROUP]);
+	}
+}
+
+static void
+purple_plugin_info_set_action_menu(PurplePluginInfo *info,
+                                   GMenuModel *menu_model)
+{
+	PurplePluginInfoPrivate *priv = NULL;
+
+	g_return_if_fail(PURPLE_IS_PLUGIN_INFO(info));
+
+	priv = purple_plugin_info_get_instance_private(info);
+
+	if(g_set_object(&priv->menu_model, menu_model)) {
+		g_object_notify_by_pspec(G_OBJECT(info), properties[PROP_ACTION_MENU]);
+	}
+}
+
+/**************************************************************************
  * GObject Implementation
  **************************************************************************/
 static void
@@ -94,6 +132,14 @@
 		case PROP_FLAGS:
 			priv->flags = g_value_get_flags(value);
 			break;
+		case PROP_ACTION_GROUP:
+			purple_plugin_info_set_action_group(info,
+			                                    g_value_get_object(value));
+			break;
+		case PROP_ACTION_MENU:
+			purple_plugin_info_set_action_menu(info,
+			                                   g_value_get_object(value));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 			break;
@@ -126,6 +172,14 @@
 		case PROP_FLAGS:
 			g_value_set_flags(value, purple_plugin_info_get_flags(info));
 			break;
+		case PROP_ACTION_GROUP:
+			g_value_take_object(value,
+			                    purple_plugin_info_get_action_group(info));
+			break;
+		case PROP_ACTION_MENU:
+			g_value_take_object(value,
+			                    purple_plugin_info_get_action_menu(info));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 			break;
@@ -215,6 +269,32 @@
 		0,
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurplePluginInfo::action-group:
+	 *
+	 * A [class@Gio.ActionGroup] of actions that this plugin provides.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ACTION_GROUP] = g_param_spec_object(
+		"action-group", "action-group",
+		"The action group for this plugin",
+		G_TYPE_ACTION_GROUP,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurplePluginInfo::action-menu:
+	 *
+	 * A [class@Gio.MenuModel] for activating actions.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ACTION_MENU] = g_param_spec_object(
+		"action-menu", "action-menu",
+		"The menu model for this plugin",
+		G_TYPE_MENU_MODEL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -326,3 +406,33 @@
 
 	priv->unloaded = unloaded;
 }
+
+GActionGroup *
+purple_plugin_info_get_action_group(PurplePluginInfo *info) {
+	PurplePluginInfoPrivate *priv = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PLUGIN_INFO(info), NULL);
+
+	priv = purple_plugin_info_get_instance_private(info);
+
+	if(G_IS_ACTION_GROUP(priv->action_group)) {
+		return g_object_ref(priv->action_group);
+	}
+
+	return NULL;
+}
+
+GMenuModel *
+purple_plugin_info_get_action_menu(PurplePluginInfo *info) {
+	PurplePluginInfoPrivate *priv = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PLUGIN_INFO(info), NULL);
+
+	priv = purple_plugin_info_get_instance_private(info);
+
+	if(G_IS_MENU_MODEL(priv->menu_model)) {
+		return g_object_ref(priv->menu_model);
+	}
+
+	return NULL;
+}
--- a/libpurple/purpleplugininfo.h	Fri May 27 04:11:45 2022 -0500
+++ b/libpurple/purpleplugininfo.h	Fri May 27 14:52:34 2022 -0500
@@ -25,6 +25,8 @@
 
 #include <glib.h>
 
+#include <gio/gio.h>
+
 #include <gplugin.h>
 #include <gplugin-native.h>
 
@@ -381,6 +383,30 @@
  */
 void purple_plugin_info_set_unloaded(PurplePluginInfo *info, gboolean unloaded);
 
+/**
+ * purple_plugin_info_get_action_group:
+ * @info: The instance.
+ *
+ * Gets the [class:Gio.ActionGroup] from @info if one is set.
+ *
+ * Returns: (transfer full): The action group.
+ *
+ * Since: 3.0.0
+ */
+GActionGroup *purple_plugin_info_get_action_group(PurplePluginInfo *info);
+
+/**
+ * purple_plugin_info_get_action_menu:
+ * @info: The instance.
+ *
+ * Gets the [class:Gio.MenuModel] from @info if one is set.
+ *
+ * Returns: (transfer full): The menu model.
+ *
+ * Since: 3.0.0
+ */
+GMenuModel *purple_plugin_info_get_action_menu(PurplePluginInfo *info);
+
 G_END_DECLS
 
 #endif /* PURPLE_PLUGIN_INFO_H */
--- a/pidgin/glade/pidgin3.xml.in	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/glade/pidgin3.xml.in	Fri May 27 14:52:34 2022 -0500
@@ -13,7 +13,6 @@
     <glade-widget-class name="PidginCredentialsPage" generic-name="credentials_page" title="CredentialsPage"/>
     <glade-widget-class name="PidginDialog" generic-name="dialog" title="Dialog"/>
     <glade-widget-class name="PidginInviteDialog" generic-name="invite_dialog" title="InviteDialog"/>
-    <glade-widget-class name="PidginPluginsMenu" generic-name="plugins_menu" title="PluginsMenu"/>
     <glade-widget-class name="PidginPresenceIcon" generic-name="presence_icon" title="PresenceIcon"/>
     <glade-widget-class name="PidginProtocolChooser" generic-name="protocol_chooser" title="ProtocolChooser"/>
     <glade-widget-class name="PidginProtocolStore" generic-name="protocol_store" title="ProtocolStore"/>
@@ -35,7 +34,6 @@
     <glade-widget-class-ref name="PidginCredentialsPage"/>
     <glade-widget-class-ref name="PidginDialog"/>
     <glade-widget-class-ref name="PidginInviteDialog"/>
-    <glade-widget-class-ref name="PidginPluginsMenu"/>
     <glade-widget-class-ref name="PidginPresenceIcon"/>
     <glade-widget-class-ref name="PidginProtocolChooser"/>
     <glade-widget-class-ref name="PidginProtocolStore"/>
--- a/pidgin/pidginapplication.c	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/pidginapplication.c	Fri May 27 14:52:34 2022 -0500
@@ -51,6 +51,7 @@
 #include "pidgininactiveaccountsmenu.h"
 #include "pidginmooddialog.h"
 #include "pidginpluginsdialog.h"
+#include "pidginpluginsmenu.h"
 #include "pidginstatusmanager.h"
 #include "pidginprefs.h"
 
@@ -133,6 +134,12 @@
 	target = gtk_application_get_menu_by_id(GTK_APPLICATION(application),
 	                                        "enabled-accounts");
 	g_menu_append_section(target, NULL, G_MENU_MODEL(source));
+
+	/* Link the PluginsMenu into its proper location. */
+	model = pidgin_plugins_menu_new();
+	target = gtk_application_get_menu_by_id(GTK_APPLICATION(application),
+	                                        "plugins-menu");
+	g_menu_append_section(target, NULL, model);
 }
 
 /******************************************************************************
--- a/pidgin/pidgincontactlist.c	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/pidgincontactlist.c	Fri May 27 14:52:34 2022 -0500
@@ -34,9 +34,6 @@
 
 	GtkWidget *accounts;
 	GtkWidget *accounts_menu;
-
-	GtkWidget *plugins;
-	GtkWidget *plugins_menu;
 };
 
 G_DEFINE_TYPE(PidginContactList, pidgin_contact_list,
@@ -60,8 +57,6 @@
 
 	gtk_menu_item_set_submenu(GTK_MENU_ITEM(contact_list->accounts),
 	                          contact_list->accounts_menu);
-	gtk_menu_item_set_submenu(GTK_MENU_ITEM(contact_list->plugins),
-	                          contact_list->plugins_menu);
 }
 
 static void
@@ -83,10 +78,6 @@
 	                                     accounts);
 	gtk_widget_class_bind_template_child(widget_class, PidginContactList,
 	                                     accounts_menu);
-	gtk_widget_class_bind_template_child(widget_class, PidginContactList,
-	                                     plugins);
-	gtk_widget_class_bind_template_child(widget_class, PidginContactList,
-	                                     plugins_menu);
 }
 
 /******************************************************************************
--- a/pidgin/pidginpluginsmenu.c	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/pidginpluginsmenu.c	Fri May 27 14:52:34 2022 -0500
@@ -1,5 +1,6 @@
 /*
- * pidgin
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
  *
  * Pidgin is the legal property of its developers, whose names are too numerous
  * to list here.  Please refer to the COPYRIGHT file distributed with this
@@ -16,243 +17,214 @@
  * 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, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
  */
 
 #include "pidginpluginsmenu.h"
 
+#include "pidginapplication.h"
+
 #include <gplugin.h>
 
 #include <purple.h>
 
 struct _PidginPluginsMenu {
-	GtkMenu parent;
-
-	GtkWidget *separator;
+	GMenuModel parent;
 
-	GSimpleActionGroup *action_group;
-
-	GHashTable *plugin_items;
+	GQueue *plugins;
 };
 
-#define PIDGIN_PLUGINS_MENU_ACTION_PREFIX "plugins-menu"
+G_DEFINE_TYPE(PidginPluginsMenu, pidgin_plugins_menu, G_TYPE_MENU_MODEL)
 
 /******************************************************************************
  * Helpers
  *****************************************************************************/
 static void
-pidgin_plugins_menu_action_activated(GSimpleAction *simple, GVariant *parameter,
-                                     gpointer data)
-{
-	PurplePluginAction *action = (PurplePluginAction *)data;
+pidgin_plugins_menu_add_item(gpointer data, gpointer user_data) {
+	PidginPluginsMenu *menu = user_data;
+	GPluginPlugin *plugin = data;
+	GPluginPluginInfo *info = NULL;
+
+	info = gplugin_plugin_get_info(plugin);
+	if(PURPLE_IS_PLUGIN_INFO(info)) {
+		GActionGroup *group = NULL;
 
-	if(action != NULL && action->callback != NULL) {
-		action->callback(action);
+		group = purple_plugin_info_get_action_group(PURPLE_PLUGIN_INFO(info));
+		if(G_IS_ACTION_GROUP(group)) {
+			GApplication *application = g_application_get_default();
+			const gchar *prefix;
+
+			prefix = gplugin_plugin_info_get_id(info);
+			pidgin_application_add_action_group(PIDGIN_APPLICATION(application),
+			                                    prefix, group);
+			g_object_unref(group);
+
+			g_queue_push_tail(menu->plugins, g_object_ref(plugin));
+		}
 	}
+
+	g_clear_object(&info);
 }
 
 static void
-pidgin_plugins_menu_add_plugin_actions(PidginPluginsMenu *menu,
-                                       PurplePlugin *plugin)
-{
-	GPluginPluginInfo *info = NULL;
-	PurplePluginActionsCb actions_cb = NULL;
-	GList *actions = NULL;
-	GtkWidget *submenu = NULL, *item = NULL;
-	gint i = 0;
-
-	info = gplugin_plugin_get_info(GPLUGIN_PLUGIN(plugin));
-
-	actions_cb = purple_plugin_info_get_actions_cb(PURPLE_PLUGIN_INFO(info));
-	if(actions_cb == NULL) {
-		g_object_unref(G_OBJECT(info));
-
-		return;
-	}
-
-	actions = actions_cb(plugin);
-	if(actions == NULL) {
-		g_object_unref(G_OBJECT(info));
-
-		return;
-	}
-
-	submenu = gtk_menu_new();
-
-	for(i = 0; actions != NULL; i++) {
-		PurplePluginAction *action = NULL;
-		GSimpleAction *gaction = NULL;
-		GtkWidget *action_item = NULL;
-		gchar *action_base_name = NULL;
-		gchar *action_full_name = NULL;
+pidgin_plugins_menu_refresh(PidginPluginsMenu *menu) {
+	GPluginManager *manager = NULL;
+	GSList *loaded = NULL;
+	gint removed = 0, added = 0;
 
-		action = (PurplePluginAction *)actions->data;
-		if(action == NULL) {
-			action_item = gtk_separator_menu_item_new();
-			gtk_widget_show(action_item);
-			gtk_menu_shell_append(GTK_MENU_SHELL(submenu), action_item);
-
-			actions = g_list_delete_link(actions, actions);
-
-			continue;
-		}
-
-		if(action->label == NULL) {
-			actions = g_list_delete_link(actions, actions);
-
-			g_warn_if_reached();
-
-			continue;
-		}
-
-		action_base_name = g_strdup_printf("%s-%d",
-		                                   gplugin_plugin_info_get_id(info),
-		                                   i);
-		action_full_name = g_strdup_printf("%s.%s",
-		                                   PIDGIN_PLUGINS_MENU_ACTION_PREFIX,
-		                                   action_base_name);
-
-		/* create the menu item with the full action name */
-		action_item = gtk_menu_item_new_with_label(action->label);
-		gtk_actionable_set_action_name(GTK_ACTIONABLE(action_item),
-		                               action_full_name);
-		gtk_widget_show(action_item);
-		g_free(action_full_name);
-
-		/* add our action item to the menu */
-		gtk_menu_shell_append(GTK_MENU_SHELL(submenu), action_item);
-
-		/* now create the gaction with the base name */
-		gaction = g_simple_action_new(action_base_name, NULL);
-		g_free(action_base_name);
+	removed = g_queue_get_length(menu->plugins);
+	g_queue_clear_full(menu->plugins, g_object_unref);
 
-		/* now connect to the activate signal of the action using
-		 * g_signal_connect_data with a destroy notify to free the plugin action
-		 * when the signal handler is removed.
-		 */
-		g_signal_connect_data(G_OBJECT(gaction), "activate",
-		                      G_CALLBACK(pidgin_plugins_menu_action_activated),
-		                      action,
-		                      (GClosureNotify)purple_plugin_action_free,
-		                      0);
-
-		/* finally add the action to the action group and remove our ref */
-		g_action_map_add_action(G_ACTION_MAP(menu->action_group),
-		                        G_ACTION(gaction));
-		g_object_unref(G_OBJECT(gaction));
-
-		actions = g_list_delete_link(actions, actions);
-	}
-
-	item = gtk_menu_item_new_with_label(gplugin_plugin_info_get_name(info));
-	gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
-	gtk_widget_show(item);
-
-	gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
-
-	g_hash_table_insert(menu->plugin_items,
-	                    g_object_ref(G_OBJECT(plugin)),
-	                    item);
-
-	g_object_unref(G_OBJECT(info));
-
-	/* make sure that our separator is visible */
-	gtk_widget_show(menu->separator);
-}
+	manager = gplugin_manager_get_default();
+	loaded = gplugin_manager_find_plugins_with_state(manager,
+	                                                 GPLUGIN_PLUGIN_STATE_LOADED);
 
-static void
-pidgin_plugins_menu_remove_plugin_actions(PidginPluginsMenu *menu,
-                                          PurplePlugin *plugin)
-{
-	GPluginPluginInfo *info = NULL;
-	PurplePluginActionsCb actions_cb = NULL;
-	GList *actions = NULL;
-	gint i = 0;
-
-	/* try remove the menu item from plugin from the hash table.  If we didn't
-	 * remove anything, we have nothing to do so bail.
-	 */
-	if(!g_hash_table_remove(menu->plugin_items, plugin)) {
-		return;
-	}
-
-	info = gplugin_plugin_get_info(GPLUGIN_PLUGIN(plugin));
-
-	actions_cb = purple_plugin_info_get_actions_cb(PURPLE_PLUGIN_INFO(info));
-	if(actions_cb == NULL) {
-		g_object_unref(G_OBJECT(info));
-
-		return;
-	}
+	g_slist_foreach(loaded, pidgin_plugins_menu_add_item, menu);
 
-	actions = actions_cb(plugin);
-	if(actions == NULL) {
-		g_object_unref(G_OBJECT(info));
-
-		return;
-	}
-
-	/* now walk through the actions and remove them from the action group. */
-	for(i = 0; actions != NULL; i++) {
-		gchar *name = NULL;
-
-		name = g_strdup_printf("%s-%d", gplugin_plugin_info_get_id(info), i);
+	added = g_queue_get_length(menu->plugins);
 
-		g_action_map_remove_action(G_ACTION_MAP(menu->action_group), name);
-		g_free(name);
-
-		actions = g_list_delete_link(actions, actions);
-	}
-
-	g_object_unref(G_OBJECT(info));
-
-	/* finally, if this was the last item in the list, hide the separator. */
-	if(g_hash_table_size(menu->plugin_items) == 0) {
-		gtk_widget_hide(menu->separator);
-	}
+	/* Tell our listeners that our menu has changed. */
+	g_menu_model_items_changed(G_MENU_MODEL(menu), 0, removed, added);
 }
 
 /******************************************************************************
  * Callbacks
  *****************************************************************************/
 static void
-pidgin_plugins_menu_plugin_loaded_cb(GObject *manager, GPluginPlugin *plugin,
+pidgin_plugins_menu_plugin_loaded_cb(G_GNUC_UNUSED GObject *manager,
+                                     G_GNUC_UNUSED GPluginPlugin *plugin,
                                      gpointer data)
 {
-	pidgin_plugins_menu_add_plugin_actions(PIDGIN_PLUGINS_MENU(data), plugin);
+	pidgin_plugins_menu_refresh(PIDGIN_PLUGINS_MENU(data));
+}
+
+static void
+pidgin_plugins_menu_plugin_unloaded_cb(G_GNUC_UNUSED GObject *manager,
+                                       GPluginPlugin *plugin,
+                                       gpointer data)
+{
+	GApplication *application = NULL;
+	GPluginPluginInfo *info = NULL;
+	const gchar *prefix;
+
+	/* Remove the action group that the plugin added. */
+	info = gplugin_plugin_get_info(plugin);
+	if(GPLUGIN_IS_PLUGIN_INFO(info)) {
+		prefix = gplugin_plugin_info_get_id(info);
+
+		if(prefix != NULL) {
+			application = g_application_get_default();
+			pidgin_application_add_action_group(PIDGIN_APPLICATION(application),
+			                                    prefix, NULL);
+		}
+	}
+
+	/* Refresh the list */
+	pidgin_plugins_menu_refresh(PIDGIN_PLUGINS_MENU(data));
+}
+
+/******************************************************************************
+ * GMenuModel Implementation
+ *****************************************************************************/
+static gboolean
+pidgin_plugins_menu_is_mutable(GMenuModel *model) {
+	return TRUE;
+}
+
+static gint
+pidgin_plugins_menu_get_n_items(GMenuModel *model) {
+	PidginPluginsMenu *menu = PIDGIN_PLUGINS_MENU(model);
+
+	return g_queue_get_length(menu->plugins);
 }
 
 static void
-pidgin_plugins_menu_plugin_unloaded_cb(GObject *manager, GPluginPlugin *plugin,
-                                       gpointer data)
+pidgin_plugins_menu_get_item_attributes(GMenuModel *model, gint index,
+                                        GHashTable **attributes)
 {
-	pidgin_plugins_menu_remove_plugin_actions(PIDGIN_PLUGINS_MENU(data),
-	                                          plugin);
+	PidginPluginsMenu *menu = PIDGIN_PLUGINS_MENU(model);
+	GPluginPlugin *plugin = NULL;
+	GPluginPluginInfo *info = NULL;
+	GVariant *value = NULL;
+
+	/* Create our hash table of attributes to return. */
+	*attributes = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
+	                                    (GDestroyNotify)g_variant_unref);
+
+	/* Get the plugin the caller is interested in. */
+	plugin = g_queue_peek_nth(menu->plugins, index);
+	if(plugin == NULL) {
+		return;
+	}
+
+	/* Grab the plugin info and set the label attribute to the name of the
+	 * plugin and set the action name space to the plugin's id.
+	 */
+	info = gplugin_plugin_get_info(plugin);
+	value = g_variant_new_string(gplugin_plugin_info_get_name(info));
+	g_hash_table_insert(*attributes, G_MENU_ATTRIBUTE_LABEL,
+	                    g_variant_ref_sink(value));
+
+	value = g_variant_new_string(gplugin_plugin_info_get_id(info));
+	g_hash_table_insert(*attributes, G_MENU_ATTRIBUTE_ACTION_NAMESPACE,
+	                    g_variant_ref_sink(value));
+
+	g_object_unref(info);
+}
+
+static void
+pidgin_plugins_menu_get_item_links(GMenuModel *model, gint index,
+                                   GHashTable **links)
+{
+	PidginPluginsMenu *menu = PIDGIN_PLUGINS_MENU(model);
+	PurplePluginInfo *purple_info = NULL;
+	GPluginPlugin *plugin = NULL;
+	GPluginPluginInfo *info = NULL;
+	GMenuModel *actions_model = NULL;
+
+	*links = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
+	                               g_object_unref);
+
+	plugin = g_queue_peek_nth(menu->plugins, index);
+	if(!GPLUGIN_IS_PLUGIN(plugin)) {
+		return;
+	}
+
+	info = gplugin_plugin_get_info(plugin);
+	purple_info = PURPLE_PLUGIN_INFO(info);
+
+	actions_model = purple_plugin_info_get_action_menu(purple_info);
+	if(G_IS_MENU_MODEL(actions_model)) {
+		g_hash_table_insert(*links, G_MENU_LINK_SUBMENU, actions_model);
+	}
+
+	g_object_unref(info);
 }
 
 /******************************************************************************
  * GObject Implementation
  *****************************************************************************/
-G_DEFINE_TYPE(PidginPluginsMenu, pidgin_plugins_menu, GTK_TYPE_MENU)
+static void
+pidgin_plugins_menu_finalize(GObject *obj) {
+	PidginPluginsMenu *menu = PIDGIN_PLUGINS_MENU(obj);
+
+	g_queue_free_full(menu->plugins, g_object_unref);
+
+	G_OBJECT_CLASS(pidgin_plugins_menu_parent_class)->finalize(obj);
+}
+
+static void
+pidgin_plugins_menu_constructed(GObject *obj) {
+	G_OBJECT_CLASS(pidgin_plugins_menu_parent_class)->constructed(obj);
+
+	pidgin_plugins_menu_refresh(PIDGIN_PLUGINS_MENU(obj));
+}
 
 static void
 pidgin_plugins_menu_init(PidginPluginsMenu *menu) {
 	GPluginManager *manager = NULL;
 
-	/* initialize our template */
-	gtk_widget_init_template(GTK_WIDGET(menu));
-
-	/* create our internal action group and assign it to ourself */
-	menu->action_group = g_simple_action_group_new();
-	gtk_widget_insert_action_group(GTK_WIDGET(menu),
-	                               PIDGIN_PLUGINS_MENU_ACTION_PREFIX,
-	                               G_ACTION_GROUP(menu->action_group));
-
-	/* create our storage for the items */
-	menu->plugin_items = g_hash_table_new_full(g_direct_hash, g_direct_equal,
-	                                           g_object_unref,
-	                                           (GDestroyNotify)gtk_widget_destroy);
+	menu->plugins = g_queue_new();
 
 	/* Connect to the plugin manager's signals so we can stay up to date. */
 	manager = gplugin_manager_get_default();
@@ -267,22 +239,22 @@
 
 static void
 pidgin_plugins_menu_class_init(PidginPluginsMenuClass *klass) {
-	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GMenuModelClass *model_class = G_MENU_MODEL_CLASS(klass);
 
-	gtk_widget_class_set_template_from_resource(
-	    widget_class,
-	    "/im/pidgin/Pidgin3/Plugins/menu.ui"
-	);
+	obj_class->finalize = pidgin_plugins_menu_finalize;
+	obj_class->constructed = pidgin_plugins_menu_constructed;
 
-	gtk_widget_class_bind_template_child(widget_class, PidginPluginsMenu,
-	                                     separator);
+	model_class->is_mutable = pidgin_plugins_menu_is_mutable;
+	model_class->get_n_items = pidgin_plugins_menu_get_n_items;
+	model_class->get_item_attributes = pidgin_plugins_menu_get_item_attributes;
+	model_class->get_item_links = pidgin_plugins_menu_get_item_links;
 }
 
 /******************************************************************************
  * Public API
  *****************************************************************************/
-GtkWidget *
+GMenuModel *
 pidgin_plugins_menu_new(void) {
-	return GTK_WIDGET(g_object_new(PIDGIN_TYPE_PLUGINS_MENU, NULL));
+	return g_object_new(PIDGIN_TYPE_PLUGINS_MENU, NULL);
 }
-
--- a/pidgin/pidginpluginsmenu.h	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/pidginpluginsmenu.h	Fri May 27 14:52:34 2022 -0500
@@ -1,5 +1,6 @@
 /*
- * pidgin
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
  *
  * Pidgin is the legal property of its developers, whose names are too numerous
  * to list here.  Please refer to the COPYRIGHT file distributed with this
@@ -16,8 +17,7 @@
  * 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, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
  */
 
 #if !defined(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION)
@@ -34,18 +34,15 @@
 /**
  * PidginPluginsMenu:
  *
- * #PidginPluginsMenu is a #GtkMenu that provides an interface to users to open
- * the plugin manager as well as activate plugin actions.
- *
- * It manages itself as plugins are loaded and unloaded and can be added as a
- * submenu to any #GtkMenuItem.
+ * #PidginPluginsMenu is a [class@Gio.MenuModel] that automatically updates
+ * itself when plugins are loaded and unloaded.
  *
  * Since: 3.0.0
  */
 
 #define PIDGIN_TYPE_PLUGINS_MENU (pidgin_plugins_menu_get_type())
 G_DECLARE_FINAL_TYPE(PidginPluginsMenu, pidgin_plugins_menu, PIDGIN,
-                     PLUGINS_MENU, GtkMenu)
+                     PLUGINS_MENU, GMenuModel)
 
 /**
  * pidgin_plugins_menu_new:
@@ -54,7 +51,7 @@
  *
  * Returns: (transfer full): The new #PidginPluginsMenu instance.
  */
-GtkWidget *pidgin_plugins_menu_new(void);
+GMenuModel *pidgin_plugins_menu_new(void);
 
 G_END_DECLS
 
--- a/pidgin/resources/BuddyList/window.ui	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/resources/BuddyList/window.ui	Fri May 27 14:52:34 2022 -0500
@@ -69,14 +69,6 @@
                 <property name="use-underline">True</property>
               </object>
             </child>
-            <child>
-              <object class="GtkMenuItem" id="plugins">
-                <property name="visible">True</property>
-                <property name="can-focus">False</property>
-                <property name="label" translatable="yes">_Plugins</property>
-                <property name="use-underline">True</property>
-              </object>
-            </child>
           </object>
           <packing>
             <property name="expand">False</property>
@@ -91,8 +83,4 @@
     <property name="visible">True</property>
     <property name="can-focus">False</property>
   </object>
-  <object class="PidginPluginsMenu" id="plugins_menu">
-    <property name="visible">True</property>
-    <property name="can-focus">False</property>
-  </object>
 </interface>
--- a/pidgin/resources/gtk/menus.ui	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/resources/gtk/menus.ui	Fri May 27 14:52:34 2022 -0500
@@ -235,9 +235,7 @@
           <attribute name="accel">&lt;Primary&gt;U</attribute>
         </item>
       </section>
-      <section>
-        <attribute name="id">plugins-menu</attribute>
-      </section>
+      <section id="plugins-menu"/>
     </submenu>
     <submenu>
       <attribute name="label" translatable="yes">_Help</attribute>
--- a/pidgin/resources/pidgin.gresource.xml	Fri May 27 04:11:45 2022 -0500
+++ b/pidgin/resources/pidgin.gresource.xml	Fri May 27 14:52:34 2022 -0500
@@ -22,7 +22,6 @@
     <file compressed="true">Debug/plugininfo.ui</file>
     <file compressed="true">Log/log-viewer.ui</file>
     <file compressed="true">Plugins/dialog.ui</file>
-    <file compressed="true">Plugins/menu.ui</file>
     <file compressed="true">Prefs/away.ui</file>
     <file compressed="true">Prefs/conversation.ui</file>
     <file compressed="true">Prefs/credentials.ui</file>

mercurial