pidgin/pidgin

Re-design account manager as a GtkListBox

15 months ago, Elliott Sales de Andrade
f3090252de57
Parents b1826d4885af
Children 23137e3bd76d
Re-design account manager as a GtkListBox

Moves to a nicely spaced `GtkListBox`. The avatar is implemented using `AdwAvatar` solely for the autogenerated colour for accounts without an icon to give them some uniqueness, but it could be moved to `PidginAvatar`.

As a followup, I'd probably move the Account Editors from a separate dialog to stack within the manager, similar to how the Plugin dialog works. Neither the previous manager nor this one prevents you from opening two editors for an account, but this would remove that issue as well.

Testing Done:
Toggled enabled/disabled to check that account status worked and was displayed. Added a few bogus accounts to confirm that errors are shown. Activated a row to show the editor. Hit the delete button and cancelled/approved and confirmed that account was kept/removed.

Reviewed at https://reviews.imfreedom.org/r/2079/
--- a/pidgin/meson.build Tue Jan 17 02:27:41 2023 -0600
+++ b/pidgin/meson.build Tue Jan 17 02:29:19 2023 -0600
@@ -24,6 +24,7 @@
'pidginaccountfilterconnected.c',
'pidginaccountfilterprotocol.c',
'pidginaccountmanager.c',
+ 'pidginaccountrow.c',
'pidginaccountsdisabledmenu.c',
'pidginaccountsenabledmenu.c',
'pidginactiongroup.c',
@@ -90,6 +91,7 @@
'pidginaccountfilterconnected.h',
'pidginaccountfilterprotocol.h',
'pidginaccountmanager.h',
+ 'pidginaccountrow.h',
'pidginaccountsdisabledmenu.h',
'pidginaccountsenabledmenu.h',
'pidginactiongroup.h',
--- a/pidgin/pidginaccountmanager.c Tue Jan 17 02:27:41 2023 -0600
+++ b/pidgin/pidginaccountmanager.c Tue Jan 17 02:29:19 2023 -0600
@@ -29,167 +29,33 @@
#include "gtkaccount.h"
#include "pidgincore.h"
#include "pidginaccounteditor.h"
+#include "pidginaccountrow.h"
struct _PidginAccountManager {
GtkDialog parent;
- GtkListStore *model;
- GtkTreeSelection *selection;
-
- GtkWidget *modify_button;
- GtkWidget *remove_button;
+ GtkListBox *list_box;
+ GtkWidget *add;
};
enum {
RESPONSE_ADD,
- RESPONSE_MODIFY,
- RESPONSE_REMOVE,
-};
-
-enum {
- COLUMN_ENABLED,
- COLUMN_AVATAR,
- COLUMN_USERNAME,
- COLUMN_PROTOCOL_ICON,
- COLUMN_PROTOCOL_NAME,
- COLUMN_ACCOUNT
};
G_DEFINE_TYPE(PidginAccountManager, pidgin_account_manager, GTK_TYPE_DIALOG)
-static void pidgin_account_manager_account_notify_cb(GObject *obj, GParamSpec *pspec, gpointer data);
-
/******************************************************************************
* Helpers
*****************************************************************************/
-static gboolean
-pidgin_account_manager_find_account(PidginAccountManager *manager,
- PurpleAccount *account, GtkTreeIter *iter)
+static GtkWidget *
+pidgin_account_manager_create_widget(gpointer item,
+ G_GNUC_UNUSED gpointer data)
{
- GtkTreeModel *model = GTK_TREE_MODEL(manager->model);
-
- if(!gtk_tree_model_get_iter_first(model, iter)) {
- return FALSE;
- }
-
- do {
- PurpleAccount *current = NULL;
-
- gtk_tree_model_get(model, iter,
- COLUMN_ACCOUNT, &current,
- -1);
-
- if(current == account) {
- g_clear_object(&current);
-
- return TRUE;
- }
-
- g_clear_object(&current);
- } while(gtk_tree_model_iter_next(model, iter));
-
- return FALSE;
-}
-
-static PurpleAccount *
-pidgin_account_manager_get_selected_account(PidginAccountManager *manager) {
- PurpleAccount *account = NULL;
- GtkTreeIter iter;
-
- if(gtk_tree_selection_count_selected_rows(manager->selection) == 0) {
+ if(!PURPLE_IS_ACCOUNT(item)) {
return NULL;
}
- gtk_tree_selection_get_selected(manager->selection, NULL, &iter);
-
- gtk_tree_model_get(GTK_TREE_MODEL(manager->model), &iter,
- COLUMN_ACCOUNT, &account,
- -1);
-
- return account;
-}
-
-static void
-pidgin_account_manager_refresh_account(PidginAccountManager *manager,
- PurpleAccount *account,
- GtkTreeIter *iter)
-{
- PurpleContactInfo *info = NULL;
- PurpleImage *image = NULL;
- PurpleProtocol *protocol = NULL;
- GdkPixbuf *avatar = NULL;
- const gchar *protocol_icon = NULL, *protocol_name = NULL;
-
- /* Try to find the avatar for the account. */
- image = purple_buddy_icons_find_account_icon(account);
- if(image != NULL) {
- GdkPixbuf *raw = NULL;
-
- raw = purple_gdk_pixbuf_from_image(image);
- g_object_unref(image);
-
- avatar = gdk_pixbuf_scale_simple(raw, 22, 22, GDK_INTERP_HYPER);
- g_clear_object(&raw);
- }
-
- /* Get the protocol fields. */
- protocol = purple_account_get_protocol(account);
- if(PURPLE_IS_PROTOCOL(protocol)) {
- protocol_name = purple_protocol_get_name(protocol);
- protocol_icon = purple_protocol_get_icon_name(protocol);
- } else {
- protocol_name = _("Unknown");
- }
-
- info = PURPLE_CONTACT_INFO(account);
- gtk_list_store_set(manager->model, iter,
- COLUMN_ENABLED, purple_account_get_enabled(account),
- COLUMN_AVATAR, avatar,
- COLUMN_USERNAME, purple_contact_info_get_username(info),
- COLUMN_PROTOCOL_ICON, protocol_icon,
- COLUMN_PROTOCOL_NAME, protocol_name,
- COLUMN_ACCOUNT, account,
- -1);
-
- g_clear_object(&avatar);
-}
-
-static void
-pidgin_account_manager_update_account(PidginAccountManager *manager,
- PurpleAccount *account)
-{
- GtkTreeIter iter;
-
- if(pidgin_account_manager_find_account(manager, account, &iter)) {
- pidgin_account_manager_refresh_account(manager, account, &iter);
- }
-}
-
-static void
-pidgin_account_manager_add_account(PidginAccountManager *manager,
- PurpleAccount *account)
-{
- GtkTreeIter iter;
-
- gtk_list_store_append(manager->model, &iter);
-
- pidgin_account_manager_refresh_account(manager, account, &iter);
-
- g_signal_connect_object(account, "notify",
- G_CALLBACK(pidgin_account_manager_account_notify_cb),
- manager, 0);
-}
-
-static void
-pidgin_account_manager_populate_helper(PurpleAccount *account, gpointer data) {
- pidgin_account_manager_add_account(PIDGIN_ACCOUNT_MANAGER(data), account);
-}
-
-static void
-pidgin_account_manager_populate(PidginAccountManager *manager) {
- purple_account_manager_foreach(purple_account_manager_get_default(),
- pidgin_account_manager_populate_helper,
- manager);
+ return pidgin_account_row_new(PURPLE_ACCOUNT(item));
}
static void
@@ -200,51 +66,22 @@
gtk_window_present_with_time(GTK_WINDOW(editor), GDK_CURRENT_TIME);
}
-static void
-pidgin_account_manager_edit_selected_account(PidginAccountManager *manager) {
- PurpleAccount *account = NULL;
- GtkWidget *editor = NULL;
-
- account = pidgin_account_manager_get_selected_account(manager);
-
- editor = pidgin_account_editor_new(account);
- gtk_window_set_transient_for(GTK_WINDOW(editor),
- GTK_WINDOW(manager));
- gtk_window_present_with_time(GTK_WINDOW(editor), GDK_CURRENT_TIME);
-
- g_clear_object(&account);
-}
-
-static void
-pidgin_account_manager_remove_selected_account(PidginAccountManager *manager) {
- PurpleAccount *account = NULL;
- PurpleNotificationManager *notification_manager = NULL;
-
- account = pidgin_account_manager_get_selected_account(manager);
-
- /* Remove all notifications including connection errors for the account. */
- notification_manager = purple_notification_manager_get_default();
- purple_notification_manager_remove_with_account(notification_manager,
- account, TRUE);
-
- /* Delete the account. */
- purple_accounts_delete(account);
-
- g_clear_object(&account);
-}
-
/******************************************************************************
* Callbacks
*****************************************************************************/
+
static void
-pidgin_account_manager_account_notify_cb(GObject *obj,
- G_GNUC_UNUSED GParamSpec *pspec,
- gpointer data)
+pidgin_account_manager_refresh_add_cb(GListModel *list,
+ G_GNUC_UNUSED guint position,
+ G_GNUC_UNUSED guint removed,
+ G_GNUC_UNUSED guint added,
+ gpointer data)
{
- PidginAccountManager *manager = PIDGIN_ACCOUNT_MANAGER(data);
- PurpleAccount *account = PURPLE_ACCOUNT(obj);
+ PidginAccountManager *manager = data;
- pidgin_account_manager_update_account(manager, account);
+ /* If there are no accounts, the placeholder is shown, which includes an
+ * Add button. So hide the one in the button box if that's the case. */
+ gtk_widget_set_visible(manager->add, g_list_model_get_n_items(list) != 0);
}
static void
@@ -257,12 +94,6 @@
case RESPONSE_ADD:
pidgin_account_manager_create_account(manager);
break;
- case RESPONSE_MODIFY:
- pidgin_account_manager_edit_selected_account(manager);
- break;
- case RESPONSE_REMOVE:
- pidgin_account_manager_remove_selected_account(manager);
- break;
case GTK_RESPONSE_CLOSE:
case GTK_RESPONSE_DELETE_EVENT:
gtk_window_destroy(GTK_WINDOW(dialog));
@@ -273,100 +104,16 @@
}
static void
-pidgin_account_manager_selection_changed_cb(GtkTreeSelection *selection,
- gpointer data)
-{
- PidginAccountManager *manager = data;
- gboolean sensitive = TRUE;
-
- if(gtk_tree_selection_count_selected_rows(selection) == 0) {
- sensitive = FALSE;
- }
-
- gtk_widget_set_sensitive(manager->modify_button, sensitive);
- gtk_widget_set_sensitive(manager->remove_button, sensitive);
-}
-
-static void
-pidgin_account_manager_row_activated_cb(G_GNUC_UNUSED GtkTreeView *tree_view,
- GtkTreePath *path,
- G_GNUC_UNUSED GtkTreeViewColumn *column,
- gpointer data)
+pidgin_account_manager_row_activated_cb(G_GNUC_UNUSED GtkListBox *box,
+ GtkListBoxRow *row,
+ G_GNUC_UNUSED gpointer data)
{
- PidginAccountManager *manager = data;
- GtkTreeIter iter;
-
- if(gtk_tree_model_get_iter(GTK_TREE_MODEL(manager->model), &iter, path)) {
- GtkWidget *editor = NULL;
- PurpleAccount *account = NULL;
-
- gtk_tree_model_get(GTK_TREE_MODEL(manager->model), &iter,
- COLUMN_ACCOUNT, &account,
- -1);
-
- editor = pidgin_account_editor_new(account);
- gtk_widget_show(editor);
-
- g_clear_object(&account);
- }
-}
-
-static void
-pidgin_account_manager_enable_toggled_cb(G_GNUC_UNUSED GtkCellRendererToggle *renderer,
- gchar *path, gpointer data)
-{
- PidginAccountManager *manager = data;
- GtkTreeModel *model = GTK_TREE_MODEL(manager->model);
- GtkTreeIter iter;
+ GtkWidget *editor = NULL;
+ PurpleAccount *account = NULL;
- if(gtk_tree_model_get_iter_from_string(model, &iter, path)) {
- PurpleAccount *account = NULL;
- gboolean enabled = FALSE;
-
- /* The value of enabled in the model is the old value, so if enabled
- * is currently set to TRUE, we are disabling the account and vice
- * versa.
- */
- gtk_tree_model_get(model, &iter,
- COLUMN_ENABLED, &enabled,
- COLUMN_ACCOUNT, &account,
- -1);
-
- /* The account was just enabled, so set its status. */
- if(!enabled) {
- PurpleSavedStatus *status = purple_savedstatus_get_current();
- purple_savedstatus_activate_for_account(status, account);
- }
-
- purple_account_set_enabled(account, !enabled);
-
- /* We don't update the model here, as it's updated via the notify
- * signal.
- */
- }
-}
-
-static void
-pidgin_account_manager_account_added_cb(G_GNUC_UNUSED PurpleAccountManager *purple_manager,
- PurpleAccount *account,
- gpointer data)
-{
- PidginAccountManager *manager = data;
-
- pidgin_account_manager_add_account(manager, account);
-}
-
-static void
-pidgin_account_manager_account_removed_cb(G_GNUC_UNUSED PurpleAccountManager *purple_manager,
- PurpleAccount *account,
- gpointer data)
-{
- PidginAccountManager *manager = data;
- GtkTreeIter iter;
-
- if(pidgin_account_manager_find_account(manager, account, &iter)) {
- gtk_list_store_remove(manager->model, &iter);
- }
+ account = pidgin_account_row_get_account(PIDGIN_ACCOUNT_ROW(row));
+ editor = pidgin_account_editor_new(account);
+ gtk_widget_show(editor);
}
/******************************************************************************
@@ -374,19 +121,17 @@
*****************************************************************************/
static void
pidgin_account_manager_init(PidginAccountManager *manager) {
- PurpleAccountManager *purple_manager = NULL;
+ GListModel *purple_manager = NULL;
gtk_widget_init_template(GTK_WIDGET(manager));
- pidgin_account_manager_populate(manager);
-
- purple_manager = purple_account_manager_get_default();
- g_signal_connect_object(purple_manager, "added",
- G_CALLBACK(pidgin_account_manager_account_added_cb),
+ purple_manager = purple_account_manager_get_default_as_model();
+ gtk_list_box_bind_model(manager->list_box, purple_manager,
+ pidgin_account_manager_create_widget, NULL, NULL);
+ g_signal_connect_object(purple_manager, "items-changed",
+ G_CALLBACK(pidgin_account_manager_refresh_add_cb),
manager, 0);
- g_signal_connect_object(purple_manager, "removed",
- G_CALLBACK(pidgin_account_manager_account_removed_cb),
- manager, 0);
+ pidgin_account_manager_refresh_add_cb(purple_manager, 0, 0, 0, manager);
}
static void
@@ -399,23 +144,16 @@
);
gtk_widget_class_bind_template_child(widget_class, PidginAccountManager,
- model);
- gtk_widget_class_bind_template_child(widget_class, PidginAccountManager,
- selection);
-
+ list_box);
gtk_widget_class_bind_template_child(widget_class, PidginAccountManager,
- modify_button);
- gtk_widget_class_bind_template_child(widget_class, PidginAccountManager,
- remove_button);
+ add);
gtk_widget_class_bind_template_callback(widget_class,
- pidgin_account_manager_enable_toggled_cb);
- gtk_widget_class_bind_template_callback(widget_class,
pidgin_account_manager_response_cb);
gtk_widget_class_bind_template_callback(widget_class,
pidgin_account_manager_row_activated_cb);
gtk_widget_class_bind_template_callback(widget_class,
- pidgin_account_manager_selection_changed_cb);
+ pidgin_account_manager_create_account);
}
/******************************************************************************
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/pidginaccountrow.c Tue Jan 17 02:29:19 2023 -0600
@@ -0,0 +1,447 @@
+/*
+ * 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
+ * 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 <glib/gi18n-lib.h>
+
+#include <adwaita.h>
+
+#include "pidgin/pidginaccountrow.h"
+
+struct _PidginAccountRow {
+ GtkListBoxRow parent;
+
+ PurpleAccount *account;
+
+ GtkSwitch *enabled;
+ AdwAvatar *avatar;
+ GtkLabel *name;
+ GtkLabel *status;
+};
+
+enum {
+ PROP_0,
+ PROP_ACCOUNT,
+ N_PROPERTIES
+};
+static GParamSpec *properties[N_PROPERTIES] = { NULL, };
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+pidgin_account_row_refresh_buddy_icon(PidginAccountRow *row) {
+ PurpleImage *image = NULL;
+
+#warning FIX call this in the right place when buddy icons are better and can autorefresh
+ if(!PURPLE_IS_ACCOUNT(row->account)) {
+ return;
+ }
+
+ image = purple_buddy_icons_find_account_icon(row->account);
+ if(PURPLE_IS_IMAGE(image)) {
+ GdkTexture *texture = NULL;
+ GBytes *bytes = NULL;
+
+ bytes = purple_image_get_contents(image);
+ texture = gdk_texture_new_from_bytes(bytes, NULL);
+ g_bytes_unref(bytes);
+
+ if(GDK_IS_TEXTURE(texture)) {
+ adw_avatar_set_custom_image(row->avatar, GDK_PAINTABLE(texture));
+ g_object_unref(texture);
+ }
+ }
+}
+
+static void
+pidgin_account_row_refresh_status(PidginAccountRow *row) {
+ const char *status = NULL;
+ gboolean connected = FALSE;
+ gboolean error = FALSE;
+
+ if(PURPLE_IS_ACCOUNT(row->account)) {
+ if(!purple_account_get_enabled(row->account)) {
+ status = _("Disabled");
+ } else {
+ const PurpleConnectionErrorInfo *error_info = NULL;
+
+ error_info = purple_account_get_current_error(row->account);
+ if(error_info != NULL) {
+ status = error_info->description;
+ error = TRUE;
+ } else {
+ PurpleConnection *connection = NULL;
+
+ connection = purple_account_get_connection(row->account);
+ if(PURPLE_IS_CONNECTION(connection)) {
+ switch(purple_connection_get_state(connection)) {
+ case PURPLE_CONNECTION_STATE_DISCONNECTED:
+ status = _("Disconnected");
+ break;
+ case PURPLE_CONNECTION_STATE_DISCONNECTING:
+ status = _("Disconnecting...");
+ break;
+ case PURPLE_CONNECTION_STATE_CONNECTED:
+ status = _("Connected");
+ connected = TRUE;
+ break;
+ case PURPLE_CONNECTION_STATE_CONNECTING:
+ status = _("Connecting...");
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ gtk_switch_set_state(row->enabled, connected);
+ gtk_label_set_text(row->status, status);
+ if(error) {
+ gtk_widget_add_css_class(GTK_WIDGET(row->status), "error");
+ } else {
+ gtk_widget_remove_css_class(GTK_WIDGET(row->status), "error");
+ }
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+pidgin_account_row_state_changed_cb(G_GNUC_UNUSED GObject *obj,
+ G_GNUC_UNUSED GParamSpec *pspec,
+ gpointer data)
+{
+ PidginAccountRow *row = data;
+
+ pidgin_account_row_refresh_status(row);
+}
+
+static void
+pidgin_account_row_connection_changed_cb(G_GNUC_UNUSED GObject *obj,
+ G_GNUC_UNUSED GParamSpec *pspec,
+ gpointer data)
+{
+ PidginAccountRow *row = data;
+ PurpleConnection *connection = NULL;
+
+ connection = purple_account_get_connection(row->account);
+ if(PURPLE_IS_CONNECTION(connection)) {
+ g_signal_connect_object(connection, "notify::state",
+ G_CALLBACK(pidgin_account_row_state_changed_cb),
+ row, 0);
+ }
+
+ pidgin_account_row_refresh_status(row);
+}
+
+static void
+pidgin_account_row_enable_state_set_cb(G_GNUC_UNUSED GtkSwitch *sw,
+ gboolean state, gpointer data)
+{
+ PidginAccountRow *row = data;
+ PurpleAccount *account = row->account;
+
+ if(purple_account_get_enabled(account) == state) {
+ return;
+ }
+
+ /* The account was just enabled, so set its status. */
+ if(state) {
+ PurpleSavedStatus *status = purple_savedstatus_get_current();
+ purple_savedstatus_activate_for_account(status, account);
+ }
+
+ purple_account_set_enabled(account, state);
+}
+
+static char *
+pidgin_account_row_buddyicon_cb(G_GNUC_UNUSED GObject *self,
+ PurpleAccount *account,
+ G_GNUC_UNUSED gpointer data)
+{
+ const char *buddy_icon_path = NULL;
+ char *path = NULL;
+
+ if(!PURPLE_IS_ACCOUNT(account)) {
+ return NULL;
+ }
+
+ buddy_icon_path = purple_account_get_buddy_icon_path(account);
+ if(buddy_icon_path != NULL) {
+ path = g_strdup_printf("file://%s", buddy_icon_path);
+ }
+
+ return path;
+}
+
+static char *
+pidgin_account_row_protocol_name_cb(G_GNUC_UNUSED GObject *self,
+ PurpleAccount *account,
+ G_GNUC_UNUSED gpointer data)
+{
+ const char *name = _("Unknown");
+
+ if(PURPLE_IS_ACCOUNT(account)) {
+ PurpleProtocol *protocol = purple_account_get_protocol(account);
+ if(PURPLE_IS_PROTOCOL(protocol)) {
+ name = purple_protocol_get_name(protocol);
+ }
+ }
+
+ return g_strdup(name);
+}
+
+static char *
+pidgin_account_row_protocol_icon_cb(G_GNUC_UNUSED GObject *self,
+ PurpleAccount *account,
+ G_GNUC_UNUSED gpointer data)
+{
+ const char *icon_name = NULL;
+
+ if(PURPLE_IS_ACCOUNT(account)) {
+ PurpleProtocol *protocol = purple_account_get_protocol(account);
+ if(PURPLE_IS_PROTOCOL(protocol)) {
+ icon_name = purple_protocol_get_icon_name(protocol);
+ }
+ }
+
+ return g_strdup(icon_name);
+}
+
+static void
+pidgin_account_manager_remove_account_cb(G_GNUC_UNUSED AdwMessageDialog *self,
+ char *response, gpointer data)
+{
+ PurpleAccount *account = data;
+ PurpleNotificationManager *notification_manager = NULL;
+
+ if(!purple_strequal(response, "remove")) {
+ return;
+ }
+
+ /* Remove all notifications including connection errors for the account. */
+ notification_manager = purple_notification_manager_get_default();
+ purple_notification_manager_remove_with_account(notification_manager,
+ account, TRUE);
+
+ /* Delete the account. */
+ purple_accounts_delete(account);
+}
+
+static void
+pidgin_account_row_remove_cb(G_GNUC_UNUSED GtkButton *self, gpointer data) {
+ PidginAccountRow *row = data;
+ GtkRoot *root = NULL;
+ AdwMessageDialog *dialog = NULL;
+ PurpleContactInfo *info = NULL;
+ const char *name = NULL;
+ char *protocol_name = NULL;
+
+ info = PURPLE_CONTACT_INFO(row->account);
+ name = purple_contact_info_get_name_for_display(info);
+ protocol_name = pidgin_account_row_protocol_name_cb(NULL, row->account,
+ NULL);
+
+ root = gtk_widget_get_root(GTK_WIDGET(row));
+ dialog = ADW_MESSAGE_DIALOG(adw_message_dialog_new(GTK_WINDOW(root),
+ _("Remove account?"),
+ NULL));
+ adw_message_dialog_format_body(dialog,
+ _("Do you want to remove the %s (%s) "
+ "account from Pidgin?"),
+ name, protocol_name);
+ adw_message_dialog_add_responses(dialog, "cancel", _("Cancel"),
+ "remove", _("Remove"), NULL);
+ adw_message_dialog_set_response_appearance(dialog, "remove",
+ ADW_RESPONSE_DESTRUCTIVE);
+ adw_message_dialog_set_default_response(dialog, "cancel");
+ adw_message_dialog_set_close_response(dialog, "cancel");
+
+ g_signal_connect_object(dialog, "response",
+ G_CALLBACK(pidgin_account_manager_remove_account_cb),
+ row->account, 0);
+
+ gtk_window_present(GTK_WINDOW(dialog));
+
+ g_free(protocol_name);
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_TYPE(PidginAccountRow, pidgin_account_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+pidgin_account_row_get_property(GObject *obj, guint param_id, GValue *value,
+ GParamSpec *pspec)
+{
+ PidginAccountRow *row = PIDGIN_ACCOUNT_ROW(obj);
+
+ switch(param_id) {
+ case PROP_ACCOUNT:
+ g_value_set_object(value, pidgin_account_row_get_account(row));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+pidgin_account_row_set_property(GObject *obj, guint param_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ PidginAccountRow *row = PIDGIN_ACCOUNT_ROW(obj);
+
+ switch(param_id) {
+ case PROP_ACCOUNT:
+ pidgin_account_row_set_account(row, g_value_get_object(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+ break;
+ }
+}
+
+static void
+pidgin_account_row_finalize(GObject *obj) {
+ PidginAccountRow *row = PIDGIN_ACCOUNT_ROW(obj);
+
+ g_clear_object(&row->account);
+
+ G_OBJECT_CLASS(pidgin_account_row_parent_class)->finalize(obj);
+}
+
+static void
+pidgin_account_row_init(PidginAccountRow *row) {
+ gtk_widget_init_template(GTK_WIDGET(row));
+
+ pidgin_account_row_refresh_buddy_icon(row);
+ pidgin_account_row_refresh_status(row);
+}
+
+static void
+pidgin_account_row_class_init(PidginAccountRowClass *klass) {
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+ obj_class->finalize = pidgin_account_row_finalize;
+ obj_class->get_property = pidgin_account_row_get_property;
+ obj_class->set_property = pidgin_account_row_set_property;
+
+ /* properties */
+
+ /**
+ * PidginAccountRow:account:
+ *
+ * The account that this row will be representing.
+ *
+ * Since: 3.0.0
+ */
+ properties[PROP_ACCOUNT] = g_param_spec_object(
+ "account", "account",
+ "The account that this row is representing",
+ PURPLE_TYPE_ACCOUNT,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+ gtk_widget_class_set_template_from_resource(
+ widget_class,
+ "/im/pidgin/Pidgin3/Accounts/account-row.ui"
+ );
+
+ gtk_widget_class_bind_template_child(widget_class, PidginAccountRow,
+ enabled);
+ gtk_widget_class_bind_template_child(widget_class, PidginAccountRow,
+ avatar);
+ gtk_widget_class_bind_template_child(widget_class, PidginAccountRow,
+ name);
+ gtk_widget_class_bind_template_child(widget_class, PidginAccountRow,
+ status);
+
+ gtk_widget_class_bind_template_callback(widget_class,
+ pidgin_account_row_enable_state_set_cb);
+ gtk_widget_class_bind_template_callback(widget_class,
+ pidgin_account_row_buddyicon_cb);
+ gtk_widget_class_bind_template_callback(widget_class,
+ pidgin_account_row_protocol_name_cb);
+ gtk_widget_class_bind_template_callback(widget_class,
+ pidgin_account_row_protocol_icon_cb);
+ gtk_widget_class_bind_template_callback(widget_class,
+ pidgin_account_row_remove_cb);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+GtkWidget *
+pidgin_account_row_new(PurpleAccount *account) {
+ return g_object_new(PIDGIN_TYPE_ACCOUNT_ROW, "account", account, NULL);
+}
+
+PurpleAccount *
+pidgin_account_row_get_account(PidginAccountRow *row) {
+ g_return_val_if_fail(PIDGIN_IS_ACCOUNT_ROW(row), NULL);
+
+ return row->account;
+}
+
+void
+pidgin_account_row_set_account(PidginAccountRow *row, PurpleAccount *account) {
+ PurpleAccount *old = NULL;
+
+ g_return_if_fail(PIDGIN_IS_ACCOUNT_ROW(row));
+
+ old = (row->account != NULL) ? g_object_ref(row->account) : NULL;
+
+ if(g_set_object(&row->account, account)) {
+ if(PURPLE_IS_ACCOUNT(old)) {
+ PurpleConnection *connection = purple_account_get_connection(old);
+
+ if(PURPLE_IS_CONNECTION(connection)) {
+ g_signal_handlers_disconnect_by_func(connection,
+ pidgin_account_row_state_changed_cb,
+ row);
+ }
+
+ g_signal_handlers_disconnect_by_func(old,
+ pidgin_account_row_connection_changed_cb,
+ row);
+ }
+
+ if(PURPLE_IS_ACCOUNT(account)) {
+ g_signal_connect_object(account, "notify::connection",
+ G_CALLBACK(pidgin_account_row_connection_changed_cb),
+ row, 0);
+ g_signal_connect_object(account, "notify::error",
+ G_CALLBACK(pidgin_account_row_state_changed_cb),
+ row, 0);
+ pidgin_account_row_refresh_buddy_icon(row);
+ pidgin_account_row_refresh_status(row);
+ }
+
+ g_object_notify_by_pspec(G_OBJECT(row), properties[PROP_ACCOUNT]);
+ }
+
+ g_clear_object(&old);
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/pidginaccountrow.h Tue Jan 17 02:29:19 2023 -0600
@@ -0,0 +1,85 @@
+/*
+ * 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
+ * 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(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PIDGIN_ACCOUNT_ROW_H
+#define PIDGIN_ACCOUNT_ROW_H
+
+#include <gtk/gtk.h>
+
+#include <purple.h>
+
+/**
+ * PidginAccountRow:
+ *
+ * A [class@Gtk.ListBoxRow] subclass to display a [class@Purple.Account].
+ *
+ * Since: 3.0.0
+ */
+
+#define PIDGIN_TYPE_ACCOUNT_ROW (pidgin_account_row_get_type())
+G_DECLARE_FINAL_TYPE(PidginAccountRow, pidgin_account_row, PIDGIN, ACCOUNT_ROW,
+ GtkListBoxRow)
+
+G_BEGIN_DECLS
+
+/**
+ * pidgin_account_row_new:
+ * @account: (nullable): The account to represent.
+ *
+ * Creates a new #PidginAccountRow to display a [class@Purple.Account].
+ *
+ * Returns: (transfer full): The new account row.
+ *
+ * Since: 3.0.0
+ */
+GtkWidget *pidgin_account_row_new(PurpleAccount *account);
+
+/**
+ * pidgin_account_row_get_account:
+ * @row: The instance.
+ *
+ * Gets the [class@Purple.Account] that @row is displaying.
+ *
+ * Returns: (transfer none): The account if set otherwise %NULL.
+ *
+ * Since: 3.0.0
+ */
+PurpleAccount *pidgin_account_row_get_account(PidginAccountRow *row);
+
+/**
+ * pidgin_account_row_set_account:
+ * @row: The instance.
+ * @account: (nullable): The new account to set, or %NULL to unset.
+ *
+ * Sets the [class@Purple.Account] for @row to display.
+ *
+ * Since: 3.0.0
+ */
+void pidgin_account_row_set_account(PidginAccountRow *row, PurpleAccount *account);
+
+G_END_DECLS
+
+#endif /* PIDGIN_ACCOUNT_ROW_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/resources/Accounts/account-row.ui Tue Jan 17 02:29:19 2023 -0600
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Pidgin - Internet Messenger
+Copyright (C) Pidgin Developers <devel@pidgin.im>
+
+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 library; if not, see <https://www.gnu.org/licenses/>.
+-->
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <!-- interface-license-type gplv2 -->
+ <!-- interface-name Pidgin -->
+ <!-- interface-description Internet Messenger -->
+ <!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
+ <template class="PidginAccountRow" parent="GtkListBoxRow">
+ <property name="selectable">0</property>
+ <property name="child">
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkSwitch" id="enabled">
+ <binding name="active">
+ <lookup name="enabled" type="PurpleAccount">
+ <lookup name="account">PidginAccountRow</lookup>
+ </lookup>
+ </binding>
+ <property name="valign">center</property>
+ <signal name="state-set" handler="pidgin_account_row_enable_state_set_cb" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="AdwAvatar" id="avatar">
+ <property name="size">48</property>
+ <binding name="text">
+ <lookup name="name-for-display" type="PurpleContactInfo">
+ <lookup name="account">PidginAccountRow</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="hexpand">1</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkLabel" id="name">
+ <property name="xalign">0</property>
+ <binding name="label">
+ <lookup name="name-for-display" type="PurpleContactInfo">
+ <lookup name="account">PidginAccountRow</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="status">
+ <property name="css-classes">dim-label</property>
+ <property name="wrap">1</property>
+ <property name="xalign">0</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkImage">
+ <binding name="icon-name">
+ <closure type="gchararray" function="pidgin_account_row_protocol_icon_cb">
+ <lookup name="account">PidginAccountRow</lookup>
+ </closure>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <binding name="label">
+ <closure type="gchararray" function="pidgin_account_row_protocol_name_cb">
+ <lookup name="account">PidginAccountRow</lookup>
+ </closure>
+ </binding>
+ <property name="css-classes">dim-label</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <!-- This button is not really an error, but the destructive-action styling is far too bold to use in a list box. -->
+ <property name="css-classes">circular
+error</property>
+ <property name="icon-name">list-remove-symbolic</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="pidgin_account_row_remove_cb" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
--- a/pidgin/resources/Accounts/manager.ui Tue Jan 17 02:27:41 2023 -0600
+++ b/pidgin/resources/Accounts/manager.ui Tue Jan 17 02:29:19 2023 -0600
@@ -24,118 +24,89 @@
<!-- interface-name Pidgin -->
<!-- interface-description Internet Messenger -->
<!-- interface-copyright Pidgin Developers <devel@pidgin.im> -->
- <object class="GtkListStore" id="model">
- <columns>
- <!-- column-name enabled -->
- <column type="gboolean"/>
- <!-- column-name avatar -->
- <column type="GdkPixbuf"/>
- <!-- column-name username -->
- <column type="gchararray"/>
- <!-- column-name protocol-icon -->
- <column type="gchararray"/>
- <!-- column-name protocol-name -->
- <column type="gchararray"/>
- <!-- column-name account -->
- <column type="GObject"/>
- </columns>
- </object>
<template class="PidginAccountManager" parent="GtkDialog">
<property name="title" translatable="1">Accounts</property>
- <property name="default-width">500</property>
- <property name="default-height">300</property>
+ <property name="default-width">640</property>
+ <property name="default-height">480</property>
<signal name="response" handler="pidgin_account_manager_response_cb" swapped="no"/>
<child internal-child="content_area">
- <object class="GtkScrolledWindow">
- <property name="vexpand">1</property>
- <property name="focusable">1</property>
+ <object class="GtkBox">
<child>
- <object class="GtkTreeView">
- <property name="focusable">1</property>
- <property name="model">model</property>
- <signal name="row-activated" handler="pidgin_account_manager_row_activated_cb" object="PidginAccountManager" swapped="no"/>
- <child internal-child="selection">
- <object class="GtkTreeSelection" id="selection">
- <signal name="changed" handler="pidgin_account_manager_selection_changed_cb" object="PidginAccountManager" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkTreeViewColumn">
- <property name="title" translatable="1">Enabled</property>
+ <object class="GtkScrolledWindow">
+ <property name="vexpand">1</property>
+ <property name="child">
+ <object class="AdwClamp">
+ <property name="margin-bottom">24</property>
+ <property name="margin-end">24</property>
+ <property name="margin-start">24</property>
+ <property name="margin-top">24</property>
+ <property name="orientation">horizontal</property>
+ <property name="valign">center</property>
<child>
- <object class="GtkCellRendererToggle">
- <signal name="toggled" handler="pidgin_account_manager_enable_toggled_cb" object="PidginAccountManager" swapped="no"/>
+ <object class="GtkListBox" id="list_box">
+ <property name="css-classes">boxed-list
+rich-list</property>
+ <property name="selection-mode">none</property>
+ <property name="show-separators">1</property>
+ <signal name="row-activated" handler="pidgin_account_manager_row_activated_cb" swapped="no"/>
+ <child type="placeholder">
+ <object class="GtkBox">
+ <property name="margin-bottom">48</property>
+ <property name="margin-top">48</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <property name="valign">center</property>
+ <property name="vexpand">1</property>
+ <child>
+ <object class="GtkImage">
+ <property name="css-classes">dim-label</property>
+ <property name="icon-name">view-list-symbolic</property>
+ <property name="pixel-size">128</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="css-classes">title-1</property>
+ <property name="label" translatable="1">No Accounts</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="css-classes">pill
+suggested-action</property>
+ <property name="halign">center</property>
+ <signal name="clicked" handler="pidgin_account_manager_create_account" swapped="yes"/>
+ <property name="child">
+ <object class="AdwButtonContent">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="label" translatable="1">_Add…</property>
+ <property name="use-underline">1</property>
+ </object>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
</object>
- <attributes>
- <attribute name="active">0</attribute>
- </attributes>
</child>
</object>
- </child>
- <child>
- <object class="GtkTreeViewColumn">
- <property name="resizable">1</property>
- <property name="title" translatable="1">Username</property>
- <child>
- <object class="GtkCellRendererPixbuf"/>
- <attributes>
- <attribute name="pixbuf">1</attribute>
- </attributes>
- </child>
- <child>
- <object class="GtkCellRendererText"/>
- <attributes>
- <attribute name="markup">2</attribute>
- </attributes>
- </child>
- </object>
- </child>
- <child>
- <object class="GtkTreeViewColumn">
- <property name="resizable">1</property>
- <property name="title" translatable="1">Protocol</property>
- <child>
- <object class="GtkCellRendererPixbuf"/>
- <attributes>
- <attribute name="icon-name">3</attribute>
- </attributes>
- </child>
- <child>
- <object class="GtkCellRendererText"/>
- <attributes>
- <attribute name="markup">4</attribute>
- </attributes>
- </child>
- </object>
- </child>
+ </property>
</object>
</child>
</object>
</child>
<child type="action">
- <object class="GtkButton" id="button3">
- <property name="label" translatable="1">_Add...</property>
+ <object class="GtkButton" id="add">
+ <property name="css-classes">suggested-action</property>
<property name="focusable">1</property>
<property name="receives-default">1</property>
- <property name="use-underline">1</property>
- </object>
- </child>
- <child type="action">
- <object class="GtkButton" id="modify_button">
- <property name="label" translatable="1">_Modify...</property>
- <property name="sensitive">0</property>
- <property name="focusable">1</property>
- <property name="receives-default">1</property>
- <property name="use-underline">1</property>
- </object>
- </child>
- <child type="action">
- <object class="GtkButton" id="remove_button">
- <property name="label" translatable="1">_Remove</property>
- <property name="sensitive">0</property>
- <property name="focusable">1</property>
- <property name="receives-default">1</property>
- <property name="use-underline">1</property>
+ <property name="child">
+ <object class="AdwButtonContent">
+ <property name="icon-name">list-add-symbolic</property>
+ <property name="label" translatable="1">_Add…</property>
+ <property name="use-underline">1</property>
+ </object>
+ </property>
</object>
</child>
<child type="action">
@@ -147,9 +118,7 @@
</object>
</child>
<action-widgets>
- <action-widget response="0">button3</action-widget>
- <action-widget response="1">modify_button</action-widget>
- <action-widget response="2">remove_button</action-widget>
+ <action-widget response="0">add</action-widget>
<action-widget response="close">button2</action-widget>
</action-widgets>
</template>
--- a/pidgin/resources/pidgin.gresource.xml Tue Jan 17 02:27:41 2023 -0600
+++ b/pidgin/resources/pidgin.gresource.xml Tue Jan 17 02:29:19 2023 -0600
@@ -4,6 +4,7 @@
<file compressed="true" preprocess="xml-stripblanks">About/about.ui</file>
<file compressed="true">About/about.md</file>
<file compressed="true" preprocess="json-stripblanks">About/credits.json</file>
+ <file compressed="true" preprocess="xml-stripblanks">Accounts/account-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">Accounts/chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks">Accounts/editor.ui</file>
<file compressed="true" preprocess="xml-stripblanks">Accounts/manager.ui</file>
--- a/po/POTFILES.in Tue Jan 17 02:27:41 2023 -0600
+++ b/po/POTFILES.in Tue Jan 17 02:29:19 2023 -0600
@@ -263,6 +263,7 @@
pidgin/pidginaccountfilterconnected.c
pidgin/pidginaccountfilterprotocol.c
pidgin/pidginaccountmanager.c
+pidgin/pidginaccountrow.c
pidgin/pidginaccountsdisabledmenu.c
pidgin/pidginaccountsenabledmenu.c
pidgin/pidginactiongroup.c
@@ -315,6 +316,7 @@
pidgin/prefs/pidginproxyprefs.c
pidgin/prefs/pidginvvprefs.c
pidgin/resources/About/about.ui
+pidgin/resources/Accounts/account-row.ui
pidgin/resources/Accounts/chooser.ui
pidgin/resources/Accounts/editor.ui
pidgin/resources/Accounts/manager.ui