Convert PidginAccountsEnabledMenu from a menu controller to a GMenuModel

Thu, 28 Jul 2022 21:17:04 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 28 Jul 2022 21:17:04 -0500
changeset 41457
f0e7534a555d
parent 41456
26dc4a668ef1
child 41458
90276efac79d

Convert PidginAccountsEnabledMenu from a menu controller to a GMenuModel

Testing Done:
Enabled and disabled a bunch of accounts from the menus while running under valgrind.

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

libpurple/purplemenu.c file | annotate | diff | comparison | revisions
libpurple/purplemenu.h file | annotate | diff | comparison | revisions
pidgin/pidginaccountsenabledmenu.c file | annotate | diff | comparison | revisions
pidgin/pidginaccountsenabledmenu.h file | annotate | diff | comparison | revisions
pidgin/pidginapplication.c file | annotate | diff | comparison | revisions
--- a/libpurple/purplemenu.c	Thu Jul 21 01:18:36 2022 -0500
+++ b/libpurple/purplemenu.c	Thu Jul 28 21:17:04 2022 -0500
@@ -57,6 +57,49 @@
 	}
 }
 
+static void
+purple_menu_copy_helper(GMenuModel *source, GMenu *destination) {
+	gint index = 0;
+
+	for(index = 0; index < g_menu_model_get_n_items(source); index++) {
+		GMenuItem *item = NULL;
+		GMenuAttributeIter *attr_iter = NULL;
+		GMenuLinkIter *link_iter = NULL;
+
+		item = g_menu_item_new(NULL, NULL);
+
+		attr_iter = g_menu_model_iterate_item_attributes(source, index);
+		while(g_menu_attribute_iter_next(attr_iter)) {
+			const gchar *name = g_menu_attribute_iter_get_name(attr_iter);
+			GVariant *value = g_menu_attribute_iter_get_value(attr_iter);
+
+			if(value != NULL) {
+				g_menu_item_set_attribute_value(item, name, value);
+				g_variant_unref(value);
+			}
+		}
+		g_clear_object(&attr_iter);
+
+		link_iter = g_menu_model_iterate_item_links(source, index);
+		while(g_menu_link_iter_next(link_iter)) {
+			GMenuModel *link_source = g_menu_link_iter_get_value(link_iter);
+			GMenu *link_destination = g_menu_new();
+
+			purple_menu_copy_helper(link_source, link_destination);
+
+			g_menu_item_set_link(item, g_menu_link_iter_get_name(link_iter),
+			                     G_MENU_MODEL(link_destination));
+
+			g_clear_object(&link_source);
+			g_clear_object(&link_destination);
+		}
+		g_clear_object(&link_iter);
+
+		g_menu_append_item(destination, item);
+		g_clear_object(&item);
+	}
+}
+
 /******************************************************************************
  * Public API
  *****************************************************************************/
@@ -118,3 +161,16 @@
 
 	g_hash_table_unref(table);
 }
+
+GMenu *
+purple_menu_copy(GMenuModel *model) {
+	GMenu *menu = NULL;
+
+	g_return_val_if_fail(G_IS_MENU_MODEL(model), NULL);
+
+	menu = g_menu_new();
+
+	purple_menu_copy_helper(model, menu);
+
+	return menu;
+}
--- a/libpurple/purplemenu.h	Thu Jul 21 01:18:36 2022 -0500
+++ b/libpurple/purplemenu.h	Thu Jul 28 21:17:04 2022 -0500
@@ -84,6 +84,19 @@
  */
 void purple_menu_populate_dynamic_targets(GMenu *menu, const gchar *first_property, ...) G_GNUC_NULL_TERMINATED;
 
+/**
+ * purple_menu_copy:
+ * @model: The [class@Gio.MenuModel] instance to copy.
+ *
+ * Creates a full copy of @model as a new [class@Gio.Menu]. If @model was not
+ * a [class@Gio.Menu] instance, any additional functionality will be lost.
+ *
+ * Returns: (transfer full): The new menu.
+ *
+ * Since: 3.0.0
+ */
+GMenu *purple_menu_copy(GMenuModel *model);
+
 G_END_DECLS
 
 #endif /* PURPLE_MENU_H */
--- a/pidgin/pidginaccountsenabledmenu.c	Thu Jul 21 01:18:36 2022 -0500
+++ b/pidgin/pidginaccountsenabledmenu.c	Thu Jul 28 21:17:04 2022 -0500
@@ -26,118 +26,222 @@
 
 #include "pidginaccountsenabledmenu.h"
 
-#include "pidgincore.h"
-
-/******************************************************************************
- * Helpers
- *****************************************************************************/
-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)) {
-		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 = gtk_application_get_menu_by_id(GTK_APPLICATION(application),
-		                                         "enabled-account");
-
-		if(PURPLE_IS_CONNECTION(connection)) {
-			connection_id = purple_connection_get_id(connection);
-		}
+struct _PidginAccountsEnabledMenu {
+	GMenuModel parent;
 
-		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
-		 * account.
-		 */
-		label = g_strdup_printf(_("%s (%s)"), account_name, protocol_name);
-
-		g_menu_append_submenu(menu, label, G_MENU_MODEL(submenu));
+	GQueue *accounts;
+};
 
-		g_free(label);
-	}
-}
-
-static void
-pidgin_accounts_enabled_menu_refresh(GMenu *menu) {
-	PurpleAccountManager *manager = NULL;
-
-	g_menu_remove_all(menu);
-
-	manager = purple_account_manager_get_default();
-	purple_account_manager_foreach(manager,
-	                               pidgin_accounts_enabled_menu_refresh_helper,
-	                               menu);
-}
+G_DEFINE_TYPE(PidginAccountsEnabledMenu, pidgin_accounts_enabled_menu,
+              G_TYPE_MENU_MODEL)
 
 /******************************************************************************
  * Callbacks
  *****************************************************************************/
 static void
-pidgin_accounts_enabled_menu_enabled_cb(G_GNUC_UNUSED PurpleAccount *account,
-                                        gpointer data)
+pidgin_accounts_enabled_menu_enabled_cb(PurpleAccount *account, gpointer data) {
+	PidginAccountsEnabledMenu *menu = data;
+
+	/* Add the account to the start of the list. */
+	g_queue_push_head(menu->accounts, g_object_ref(account));
+
+	/* Tell everyone our model added a new item at position 0. */
+	g_menu_model_items_changed(G_MENU_MODEL(menu), 0, 0, 1);
+}
+
+static void
+pidgin_accounts_enabled_menu_disabled_cb(PurpleAccount *account, gpointer data)
 {
-	pidgin_accounts_enabled_menu_refresh(data);
+	PidginAccountsEnabledMenu *menu = data;
+	gint index = -1;
+
+	index = g_queue_index(menu->accounts, account);
+	if(index >= 0) {
+		g_queue_pop_nth(menu->accounts, index);
+
+		/* Tell the model that we removed one item at the given index. */
+		g_menu_model_items_changed(G_MENU_MODEL(menu), index, 1, 0);
+
+		g_object_unref(account);
+	}
 }
 
 static void
-pidgin_accounts_enabled_menu_disabled_cb(G_GNUC_UNUSED PurpleAccount *account,
-                                         gpointer data)
+pidgin_accounts_enabled_menu_connected_cb(PurpleAccount *account, gpointer data)
 {
-	pidgin_accounts_enabled_menu_refresh(data);
+	PidginAccountsEnabledMenu *menu = data;
+	gint index = -1;
+
+	index = g_queue_index(menu->accounts, account);
+	if(index >= 0) {
+		/* Tell the model that the account needs to be updated. */
+		g_menu_model_items_changed(G_MENU_MODEL(menu), index, 0, 0);
+	}
 }
 
 static void
-pidgin_accounts_enabled_menu_connected_cb(G_GNUC_UNUSED PurpleAccount *account,
-                                          gpointer data)
+pidgin_accounts_enabled_menu_disconnected_cb(PurpleAccount *account,
+                                             gpointer data)
 {
-	pidgin_accounts_enabled_menu_refresh(data);
+	PidginAccountsEnabledMenu *menu = data;
+	gint index = -1;
+
+	index = g_queue_index(menu->accounts, account);
+	if(index >= 0) {
+		/* Tell the model that the account needs to be updated. */
+		g_menu_model_items_changed(G_MENU_MODEL(menu), index, 0, 0);
+	}
+}
+
+/******************************************************************************
+ * GMenuModel Implementation
+ *****************************************************************************/
+static gboolean
+pidgin_accounts_enabled_menu_is_mutable(GMenuModel *model) {
+	return TRUE;
+}
+
+static gint
+pidgin_accounts_enabled_menu_get_n_items(GMenuModel *model) {
+	PidginAccountsEnabledMenu *menu = PIDGIN_ACCOUNTS_ENABLED_MENU(model);
+
+	return g_queue_get_length(menu->accounts);
 }
 
 static void
-pidgin_accounts_enabled_menu_disconnected_cb(G_GNUC_UNUSED PurpleAccount *account,
-                                             gpointer data)
+pidgin_accounts_enabled_menu_get_item_attributes(GMenuModel *model, gint index,
+                                                 GHashTable **attributes)
 {
-	pidgin_accounts_enabled_menu_refresh(data);
+	PidginAccountsEnabledMenu *menu = PIDGIN_ACCOUNTS_ENABLED_MENU(model);
+	PurpleAccount *account = NULL;
+	PurpleProtocol *protocol = NULL;
+	GVariant *value = NULL;
+	gchar *label = NULL;
+	const gchar *account_name = NULL, *protocol_name = NULL, *icon_name = NULL;
+
+	/* Create our hash table of attributes to return. This must always be
+	 * populated.
+	 */
+	*attributes = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
+	                                    (GDestroyNotify)g_variant_unref);
+
+	/* Get the account the caller is interested in. */
+	account = g_queue_peek_nth(menu->accounts, index);
+	if(!PURPLE_IS_ACCOUNT(account)) {
+		return;
+	}
+
+	account_name = purple_account_get_username(account);
+
+	/* Get the protocol from the account. */
+	protocol = purple_account_get_protocol(account);
+	if(PURPLE_IS_PROTOCOL(protocol)) {
+		protocol_name = purple_protocol_get_name(protocol);
+		icon_name = purple_protocol_get_icon_name(protocol);
+	}
+
+	/* Add the label. */
+
+	/* translators: This format string is intended to contain the account
+	 * name followed by the protocol name to uniquely identify a specific
+	 * account.
+	 */
+	label = g_strdup_printf(_("%s (%s)"), account_name, protocol_name);
+	value = g_variant_new_string(label);
+	g_free(label);
+	g_hash_table_insert(*attributes, G_MENU_ATTRIBUTE_LABEL,
+	                    g_variant_ref_sink(value));
+
+	/* Add the icon if we have one. */
+	if(icon_name != NULL) {
+		value = g_variant_new_string(icon_name);
+		g_hash_table_insert(*attributes, G_MENU_ATTRIBUTE_ICON,
+		                    g_variant_ref_sink(value));
+	}
 }
 
 static void
-pidgin_accounts_enabled_menu_weak_notify_cb(G_GNUC_UNUSED gpointer data,
-                                            GObject *obj)
+pidgin_accounts_enabled_menu_get_item_links(GMenuModel *model, gint index,
+                                            GHashTable **links)
 {
-	purple_signals_disconnect_by_handle(obj);
+	PidginAccountsEnabledMenu *menu = PIDGIN_ACCOUNTS_ENABLED_MENU(model);
+	PurpleAccount *account = NULL;
+	PurpleConnection *connection = NULL;
+	GApplication *application = g_application_get_default();
+	GMenu *submenu = NULL, *template = NULL;
+	const gchar *account_id = NULL, *connection_id = NULL;
+
+	/* Create our hash table for links, this must always be populated. */
+	*links = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
+	                               g_object_unref);
+
+	account = g_queue_peek_nth(menu->accounts, index);
+	if(!PURPLE_IS_ACCOUNT(account)) {
+		return;
+	}
+
+	account_id = purple_account_get_id(account);
+
+	connection = purple_account_get_connection(account);
+	if(PURPLE_IS_CONNECTION(connection)) {
+		connection_id = purple_connection_get_id(connection);
+	}
+
+	/* Create a copy of our template menu. */
+	template = gtk_application_get_menu_by_id(GTK_APPLICATION(application),
+	                                          "enabled-account");
+	submenu = purple_menu_copy(G_MENU_MODEL(template));
+
+	purple_menu_populate_dynamic_targets(submenu,
+	                                     "account", account_id,
+	                                     "connection", connection_id,
+	                                     NULL);
+
+	g_hash_table_insert(*links, G_MENU_LINK_SUBMENU, submenu);
 }
 
 /******************************************************************************
- * Public API
+ * GObject Implementation
  *****************************************************************************/
-GMenu *
-pidgin_accounts_enabled_menu_new(void) {
-	GMenu *menu = NULL;
+static void
+pidgin_accounts_enabled_menu_dispose(GObject *obj) {
+	PidginAccountsEnabledMenu *menu = PIDGIN_ACCOUNTS_ENABLED_MENU(obj);
+
+	if(menu->accounts != NULL) {
+		g_queue_free_full(menu->accounts, g_object_unref);
+		menu->accounts = NULL;
+	}
+
+	G_OBJECT_CLASS(pidgin_accounts_enabled_menu_parent_class)->dispose(obj);
+
+}
+
+static void
+pidgin_accounts_enabled_menu_constructed(GObject *obj) {
+	PidginAccountsEnabledMenu *menu = PIDGIN_ACCOUNTS_ENABLED_MENU(obj);
+	PurpleAccountManager *manager = NULL;
+	GList *enabled = NULL, *l = NULL;
+	gint count = 0;
+
+	G_OBJECT_CLASS(pidgin_accounts_enabled_menu_parent_class)->constructed(obj);
+
+	manager = purple_account_manager_get_default();
+	enabled = purple_account_manager_get_enabled(manager);
+
+	for(l = enabled; l != NULL; l = l->next) {
+		g_queue_push_head(menu->accounts, g_object_ref(l->data));
+		count++;
+	}
+	g_list_free(enabled);
+
+	g_menu_model_items_changed(G_MENU_MODEL(obj), 0, 0, count);
+}
+
+static void
+pidgin_accounts_enabled_menu_init(PidginAccountsEnabledMenu *menu) {
 	gpointer handle = NULL;
 
-	/* Create the menu and set our instance as data on it so it'll be freed
-	 * when the menu is destroyed.
-	 */
-	menu = g_menu_new();
-	g_object_weak_ref(G_OBJECT(menu),
-	                  pidgin_accounts_enabled_menu_weak_notify_cb, NULL);
-
-	/* Populate ourselves with any accounts that are already enabled. */
-	pidgin_accounts_enabled_menu_refresh(menu);
+	menu->accounts = g_queue_new();
 
 	/* Wire up the purple signals we care about. */
 	handle = purple_accounts_get_handle();
@@ -157,6 +261,26 @@
 	purple_signal_connect(handle, "account-signed-off", menu,
 	                      G_CALLBACK(pidgin_accounts_enabled_menu_disconnected_cb),
 	                      menu);
+}
 
-	return menu;
+static void
+pidgin_accounts_enabled_menu_class_init(PidginAccountsEnabledMenuClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GMenuModelClass *model_class = G_MENU_MODEL_CLASS(klass);
+
+	obj_class->constructed = pidgin_accounts_enabled_menu_constructed;
+	obj_class->dispose = pidgin_accounts_enabled_menu_dispose;
+
+	model_class->is_mutable = pidgin_accounts_enabled_menu_is_mutable;
+	model_class->get_n_items = pidgin_accounts_enabled_menu_get_n_items;
+	model_class->get_item_attributes = pidgin_accounts_enabled_menu_get_item_attributes;
+	model_class->get_item_links = pidgin_accounts_enabled_menu_get_item_links;
 }
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+GMenuModel *
+pidgin_accounts_enabled_menu_new(void) {
+	return g_object_new(PIDGIN_TYPE_ACCOUNTS_ENABLED_MENU, NULL);
+}
--- a/pidgin/pidginaccountsenabledmenu.h	Thu Jul 21 01:18:36 2022 -0500
+++ b/pidgin/pidginaccountsenabledmenu.h	Thu Jul 28 21:17:04 2022 -0500
@@ -35,16 +35,29 @@
 G_BEGIN_DECLS
 
 /**
+ * PidginAccountsEnabledMenu:
+ *
+ * A [class@Gio.MenuModel] that automatically updates itself based on what
+ * accounts are enabled.
+ *
+ * Since: 3.0.0
+ */
+
+#define PIDGIN_TYPE_ACCOUNTS_ENABLED_MENU (pidgin_accounts_enabled_menu_get_type())
+G_DECLARE_FINAL_TYPE(PidginAccountsEnabledMenu, pidgin_accounts_enabled_menu,
+                     PIDGIN, ACCOUNTS_ENABLED_MENU, GMenuModel)
+
+/**
  * pidgin_accounts_enabled_menu_new:
  *
- * Creates a [class@Gio.Menu] that will automatically update itself to include
- * accounts that are enabled in libpurple.
+ * Creates a [class@Gio.MenuModel] that will automatically update itself to
+ * include accounts that are enabled in libpurple.
  *
  * Returns: (transfer full): The new menu instance.
  *
  * Since: 3.0.0
  */
-GMenu *pidgin_accounts_enabled_menu_new(void);
+GMenuModel *pidgin_accounts_enabled_menu_new(void);
 
 G_END_DECLS
 
--- a/pidgin/pidginapplication.c	Thu Jul 21 01:18:36 2022 -0500
+++ b/pidgin/pidginapplication.c	Thu Jul 28 21:17:04 2022 -0500
@@ -121,7 +121,7 @@
 
 static void
 pidgin_application_populate_dynamic_menus(PidginApplication *application) {
-	GMenu *source = NULL, *target = NULL;
+	GMenu *target = NULL;
 	GMenuModel *model = NULL;
 
 	/* Link the AccountsDisabledMenu into its proper location. */
@@ -131,10 +131,10 @@
 	g_menu_append_section(target, NULL, model);
 
 	/* Link the AccountsEnabledMenu into its proper location. */
-	source = pidgin_accounts_enabled_menu_new();
+	model = pidgin_accounts_enabled_menu_new();
 	target = gtk_application_get_menu_by_id(GTK_APPLICATION(application),
 	                                        "enabled-accounts");
-	g_menu_append_section(target, NULL, G_MENU_MODEL(source));
+	g_menu_append_section(target, NULL, model);
 
 	/* Link the PluginsMenu into its proper location. */
 	model = pidgin_plugins_menu_new();

mercurial