pidgin/pidgin

Parents 4f7076b8234a
Children 3631bc23f0b5
Add purple_menu_populate_dynamic_targets to dynamically update GMenu's

This is going to be used in the GMenu's for plugins, protocols, accounts, etc where there are dynamic items and we need to be able to set a target for them dynamically.

Testing Done:
Ran the new unit tests.

Reviewed at https://reviews.imfreedom.org/r/1356/
--- 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',
'purplekeyvaluepair.c',
'purplemarkup.c',
+ 'purplemenu.c',
'purplemessage.c',
'purplenoopcredentialprovider.c',
'purpleoptions.c',
@@ -154,6 +155,7 @@
'purpleattachment.h',
'purplekeyvaluepair.h',
'purplemarkup.h',
+ 'purplemenu.h',
'purplemessage.h',
'purplenoopcredentialprovider.h',
'purpleoptions.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
+ * 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;
+
+ g_return_if_fail(G_IS_MENU(model));
+
+ 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);
+}
--- /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
+ * 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 */
--- 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 @@
'image',
'keyvaluepair',
'markup',
+ 'menu',
'protocol_action',
'protocol_xfer',
'purplepath',
--- /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/>.
+ */
+
+#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;
+ 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",
+ &actual);
+
+ g_assert_cmpstr(item->dynamic_target, ==, actual);
+ g_free(actual);
+
+ /* Check that the target value matches our expectations. */
+ found = g_menu_model_get_item_attribute(model, index,
+ G_MENU_ATTRIBUTE_TARGET, "s",
+ &actual);
+
+ if(item->target != NULL) {
+ g_assert_true(found);
+
+ g_assert_cmpstr(item->target, ==, actual);
+ g_free(actual);
+ } else {
+ g_assert_false(found);
+ }
+
+ /* Free our data and move to the next item. */
+ g_free(item);
+ 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_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",
+ "foo");
+ g_menu_append_item(menu, item);
+ g_object_unref(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);
+
+ 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/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);
+
+ return g_test_run();
+}
\ 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 <glib/gi18n.h>
+#include <gtk/gtk.h>
+
#include "pidginaccountsenabledmenu.h"
#include "pidgincore.h"
@@ -29,47 +31,33 @@
/******************************************************************************
* 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)) {
+ 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
@@ -113,6 +101,20 @@
}
static void
+pidgin_accounts_enabled_menu_connected_cb(G_GNUC_UNUSED PurpleAccount *account,
+ gpointer data)
+{
+ pidgin_accounts_enabled_menu_refresh(data);
+}
+
+static void
+pidgin_accounts_enabled_menu_disconnected_cb(G_GNUC_UNUSED PurpleAccount *account,
+ gpointer data)
+{
+ pidgin_accounts_enabled_menu_refresh(data);
+}
+
+static void
pidgin_accounts_enabled_menu_weak_notify_cb(G_GNUC_UNUSED gpointer data,
GObject *obj)
{
@@ -146,5 +148,15 @@
G_CALLBACK(pidgin_accounts_enabled_menu_disabled_cb),
menu);
+ /* For the account actions, we also need to know when an account is online
+ * or offline.
+ */
+ purple_signal_connect(handle, "account-signed-on", menu,
+ G_CALLBACK(pidgin_accounts_enabled_menu_connected_cb),
+ menu);
+ purple_signal_connect(handle, "account-signed-off", menu,
+ G_CALLBACK(pidgin_accounts_enabled_menu_disconnected_cb),
+ menu);
+
return menu;
}
--- 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 @@
</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>