pidgin/pidginaccounteditor.c

Wed, 01 Jan 2025 15:32:28 -0600

author
Gary Kramlich <grim@reaperworld.com>
date
Wed, 01 Jan 2025 15:32:28 -0600
changeset 43120
a7fd7e06c486
parent 43072
a59a119b74f5
child 43300
0604c6839974
permissions
-rw-r--r--

Prepare for the 2.90.1 release

Testing Done:
Ran `meson dist`

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

/*
 * 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 "pidginaccounteditor.h"

#include "pidginprotocolchooser.h"

struct _PidginAccountEditor {
	AdwPreferencesPage parent;

	PurpleAccount *account;

	gboolean valid;

	/* Login Options */
	GtkWidget *login_options;
	GtkWidget *protocol;
	GtkWidget *username;
	GtkWidget *user_splits;
	GtkWidget *require_password_row;
	GtkWidget *require_password;

	GList *user_split_rows;

	/* User Options */
	GtkWidget *alias;

	GtkFileDialog *avatar_dialog;
	GdkTexture *avatar_texture;
	GtkWidget *avatar_row;
	GtkWidget *use_custom_avatar;
	GtkWidget *avatar;

	/* Advanced Options */
	GtkWidget *advanced_group;
	GtkWidget *advanced_toggle;

	GList *advanced_entries;
	GList *advanced_rows;

	/* Proxy Options */
	GtkWidget *proxy_type;
	GtkWidget *proxy_options;
	GtkWidget *proxy_host;
	GtkWidget *proxy_port;
	GtkWidget *proxy_username;
	GtkWidget *proxy_password;
};

enum {
	PROP_0,
	PROP_ACCOUNT,
	PROP_VALID,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

/******************************************************************************
 * Prototypes
 *****************************************************************************/
static void pidgin_account_editor_connection_changed_cb(GObject *obj,
                                                        GParamSpec *pspec,
                                                        gpointer data);

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
pidgin_account_editor_set_valid(PidginAccountEditor *editor, gboolean valid) {
	g_return_if_fail(PIDGIN_IS_ACCOUNT_EDITOR(editor));

	if(editor->valid != valid) {
		editor->valid = valid;

		g_object_notify_by_pspec(G_OBJECT(editor), properties[PROP_VALID]);
	}
}

static void
pidgin_account_editor_add_user_split(gpointer data, gpointer user_data) {
	PurpleAccountUserSplit *split = data;
	PidginAccountEditor *editor = user_data;

	if(!purple_account_user_split_is_constant(split)) {
		GtkWidget *row = NULL;
		gboolean sensitive = TRUE;

		if(PURPLE_IS_ACCOUNT(editor->account)) {
			if(purple_account_is_connected(editor->account)) {
				sensitive = FALSE;
			}
		}

		row = adw_entry_row_new();
		editor->user_split_rows = g_list_append(editor->user_split_rows, row);
		gtk_list_box_append(GTK_LIST_BOX(editor->user_splits), row);

		gtk_widget_set_focusable(row, FALSE);
		if(!sensitive) {
			gtk_widget_set_sensitive(row, sensitive);
		}

		adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row),
		                              purple_account_user_split_get_text(split));
	}
}

static gboolean
pidgin_account_editor_update_login_options(PidginAccountEditor *editor,
                                           PurpleProtocol *protocol)
{
	PurpleProtocolOptions options;
	GList *user_splits = NULL;
	GList *split_item = NULL;
	GList *row_item = NULL;
	gchar *username = NULL;
	gboolean require_password = FALSE;
	gboolean ret = FALSE;

	/* Make the username field sensitive as it may have previously be made
	 * insensitive.
	 */
	gtk_widget_set_sensitive(editor->username, TRUE);

	/* If we have an account, populate its values. */
	if(PURPLE_IS_ACCOUNT(editor->account)) {
		/* The username will be split apart below and eventually set as the
		 * text in the username entry.
		 */
		username = g_strdup(purple_account_get_username(editor->account));
		require_password = purple_account_get_require_password(editor->account);

		if(purple_account_is_connected(editor->account)) {
			gtk_widget_set_sensitive(editor->username, FALSE);
		}
	}

	/* Now remove the rows we added to the preference group for each non
	 * constant user split.
	 */
	while(editor->user_split_rows != NULL) {
		gtk_list_box_remove(GTK_LIST_BOX(editor->user_splits),
		                    editor->user_split_rows->data);

		editor->user_split_rows = g_list_delete_link(editor->user_split_rows,
		                                             editor->user_split_rows);
	}

	/* Add the user splits for the protocol. */
	user_splits = purple_protocol_get_user_splits(protocol);
	g_list_foreach(user_splits, pidgin_account_editor_add_user_split, editor);

	/* Filling out the user splits is a pain. If we have an account, we created
	 * a copy of the username above. We then iterate the user splits backwards
	 * so we can insert a null terminator at the start of each split we find in
	 * the username.
	 */
	split_item = g_list_last(user_splits);
	row_item = g_list_last(editor->user_split_rows);
	while(split_item != NULL && row_item != NULL) {
		GtkWidget *row = row_item->data;
		PurpleAccountUserSplit *split = split_item->data;
		gchar *ptr = NULL;
		const gchar *value = NULL;

		if(username != NULL) {
			gchar sep = purple_account_user_split_get_separator(split);

			if(purple_account_user_split_get_reverse(split)) {
				ptr = strrchr(username, sep);
			} else {
				ptr = strchr(username, sep);
			}

			if(ptr != NULL) {
				/* Insert a null terminator in place of the separator. */
				*ptr = '\0';

				/* Set the value to the first byte after the separator. */
				value = ptr + 1;
			}
		}

		if(value == NULL) {
			value = purple_account_user_split_get_default_value(split);
		}

		if(value != NULL && GTK_IS_EDITABLE(row)) {
			gtk_editable_set_text(GTK_EDITABLE(row), value);
		}

		split_item = split_item->prev;
		row_item = row_item->prev;
	}

	/* Free the user splits. */
	g_list_free_full(user_splits,
	                 (GDestroyNotify)purple_account_user_split_destroy);

	/* Set the username entry to the remaining text in username and free our
	 * copy of said username.
	 */
	if(username != NULL) {
		gtk_editable_set_text(GTK_EDITABLE(editor->username), username);
		g_free(username);
		ret = TRUE;
	}

	options = purple_protocol_get_options(protocol);
	gtk_widget_set_visible(editor->require_password_row,
	                       options & OPT_PROTO_PASSWORD_OPTIONAL);

	gtk_switch_set_active(GTK_SWITCH(editor->require_password),
	                      require_password);

	return ret;
}

static void
pidgin_account_editor_update_user_options(PidginAccountEditor *editor,
                                          G_GNUC_UNUSED PurpleProtocol *protocol)
{
	PurpleImage *image = NULL;
	gboolean show_avatar_opts = TRUE;
	const gchar *svalue = "";
	gboolean use_global = TRUE;

	/* Check if the protocol supports avatars. */
	show_avatar_opts = FALSE;

	gtk_widget_set_visible(editor->avatar_row, show_avatar_opts);

	/* Determine our values. */
	if(editor->account != NULL) {
		PurpleContactInfo *info = NULL;

		info = purple_account_get_contact_info(editor->account);
		svalue = purple_contact_info_get_alias(info);
		use_global = purple_account_get_bool(editor->account,
		                                     "use-global-buddyicon", TRUE);
	}

	if(svalue == NULL) {
		svalue = "";
	}

	gtk_editable_set_text(GTK_EDITABLE(editor->alias), svalue);
	gtk_switch_set_active(GTK_SWITCH(editor->use_custom_avatar), !use_global);

	g_clear_object(&editor->avatar_texture);
	if(PURPLE_IS_IMAGE(image)) {
		GBytes *contents = NULL;
		GError *error = NULL;

		contents = purple_image_get_contents(image);
		editor->avatar_texture = gdk_texture_new_from_bytes(contents, &error);
		if(error != NULL) {
			g_warning("Failed to create texture from file %s: %s",
			          purple_image_get_path(image),
			          error->message);
			g_clear_error(&error);
		} else {
			gtk_image_set_from_paintable(GTK_IMAGE(editor->avatar),
			                             GDK_PAINTABLE(editor->avatar_texture));
		}
		g_bytes_unref(contents);
	} else {
		gtk_image_set_from_icon_name(GTK_IMAGE(editor->avatar),
		                             "select-avatar");
	}
}

static gboolean
pidgin_account_editor_advanced_option_use_default(PidginAccountEditor *editor) {
	PurpleProtocol *protocol = NULL;

	/* If this is the new dialog, use the default value. */
	if(!PURPLE_IS_ACCOUNT(editor->account)) {
		return TRUE;
	}

	/* If we have an existing account, check if the protocol has changed. */
	protocol = pidgin_protocol_chooser_get_protocol(PIDGIN_PROTOCOL_CHOOSER(editor->protocol));
	if(protocol != purple_account_get_protocol(editor->account)) {
		return TRUE;
	}

	return FALSE;
}

static GtkWidget *
pidgin_account_editor_add_advanced_boolean(PidginAccountEditor *editor,
                                           PurpleAccountOption *option)
{
	GtkWidget *row = NULL;
	GtkWidget *toggle = NULL;
	gboolean value = FALSE;
	gchar *title = NULL;

	if(pidgin_account_editor_advanced_option_use_default(editor)) {
		value = purple_account_option_get_default_bool(option);
	} else {
		const gchar *setting = purple_account_option_get_setting(option);
		gboolean def_value = purple_account_option_get_default_bool(option);

		value = purple_account_get_bool(editor->account, setting, def_value);
	}

	/* Create the row and set its title with a mnemonic. */
	row = adw_action_row_new();
	g_object_bind_property(editor->advanced_toggle, "active", row, "visible",
	                       G_BINDING_SYNC_CREATE);
	gtk_widget_set_focusable(row, FALSE);
	adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(row), TRUE);
	title = g_strdup_printf("_%s", purple_account_option_get_text(option));
	adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), title);
	g_free(title);

	adw_preferences_group_add(ADW_PREFERENCES_GROUP(editor->advanced_group),
	                          row);

	/* Add the row to the editor's list of advanced rows. */
	editor->advanced_rows = g_list_append(editor->advanced_rows, row);

	/* Create the input widget. */
	toggle = gtk_switch_new();
	gtk_switch_set_active(GTK_SWITCH(toggle), value);
	gtk_widget_set_valign(toggle, GTK_ALIGN_CENTER);
	adw_action_row_add_suffix(ADW_ACTION_ROW(row), toggle);
	adw_action_row_set_activatable_widget(ADW_ACTION_ROW(row), toggle);

	return toggle;
}

static GtkWidget *
pidgin_account_editor_add_advanced_int(PidginAccountEditor *editor,
                                       PurpleAccountOption *option)
{
	GtkWidget *row = NULL;
	gint value = 0;
	gchar *title = NULL;
	gchar *svalue = NULL;

	if(pidgin_account_editor_advanced_option_use_default(editor)) {
		value = purple_account_option_get_default_int(option);
	} else {
		const gchar *setting = purple_account_option_get_setting(option);
		gint def_value = purple_account_option_get_default_int(option);

		value = purple_account_get_int(editor->account, setting, def_value);
	}

	/* Create the row and set its title with a mnemonic. */
	row = adw_entry_row_new();
	g_object_bind_property(editor->advanced_toggle, "active", row, "visible",
	                       G_BINDING_SYNC_CREATE);
	gtk_widget_set_focusable(row, FALSE);
	adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(row), TRUE);
	adw_entry_row_set_input_purpose(ADW_ENTRY_ROW(row),
	                                GTK_INPUT_PURPOSE_NUMBER);
	title = g_strdup_printf("_%s", purple_account_option_get_text(option));
	adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), title);
	g_free(title);

	adw_preferences_group_add(ADW_PREFERENCES_GROUP(editor->advanced_group),
	                          row);

	/* Add the row to the editor's list of advanced rows. */
	editor->advanced_rows = g_list_append(editor->advanced_rows, row);

	/* Set the default value. */
	svalue = g_strdup_printf("%d", value);
	gtk_editable_set_text(GTK_EDITABLE(row), svalue);
	g_free(svalue);

	return row;
}

static GtkWidget *
pidgin_account_editor_add_advanced_string(PidginAccountEditor *editor,
                                          PurpleAccountOption *option)
{
	GtkWidget *row = NULL;
	gchar *title = NULL;
	const gchar *value = NULL;

	if(pidgin_account_editor_advanced_option_use_default(editor)) {
		value = purple_account_option_get_default_string(option);
	} else {
		const gchar *setting = purple_account_option_get_setting(option);
		const gchar *def_value = NULL;

		def_value = purple_account_option_get_default_string(option);

		value = purple_account_get_string(editor->account, setting, def_value);
	}

	/* Create the row depending on the masked hint. */
	if(purple_account_option_string_get_masked(option)) {
		row = adw_password_entry_row_new();
	} else {
		row = adw_entry_row_new();
	}

	g_object_bind_property(editor->advanced_toggle, "active", row, "visible",
	                       G_BINDING_SYNC_CREATE);
	gtk_widget_set_focusable(row, FALSE);
	adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(row), TRUE);
	title = g_strdup_printf("_%s", purple_account_option_get_text(option));
	adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), title);
	g_free(title);

	adw_preferences_group_add(ADW_PREFERENCES_GROUP(editor->advanced_group),
	                          row);

	/* Add the row to the editor's list of advanced rows. */
	editor->advanced_rows = g_list_append(editor->advanced_rows, row);

	if(value != NULL) {
		gtk_editable_set_text(GTK_EDITABLE(row), value);
	}

	return row;
}

static GtkWidget *
pidgin_account_editor_add_advanced_list(PidginAccountEditor *editor,
                                        PurpleAccountOption *option)
{
	GtkWidget *row = NULL;
	GtkStringList *model = NULL;
	GList *data = NULL;
	GList *items = NULL;
	gchar *title = NULL;
	const gchar *value = FALSE;
	guint index = 0;
	guint position = 0;

	if(pidgin_account_editor_advanced_option_use_default(editor)) {
		value = purple_account_option_get_default_list_value(option);
	} else {
		const gchar *setting = purple_account_option_get_setting(option);
		const gchar *def_value = NULL;

		def_value = purple_account_option_get_default_list_value(option);

		value = purple_account_get_string(editor->account, setting, def_value);
	}

	/* Create the row and set its title with a mnemonic. */
	row = adw_combo_row_new();
	g_object_bind_property(editor->advanced_toggle, "active", row, "visible",
	                       G_BINDING_SYNC_CREATE);
	adw_preferences_row_set_use_underline(ADW_PREFERENCES_ROW(row), TRUE);
	adw_combo_row_set_use_subtitle(ADW_COMBO_ROW(row), TRUE);
	title = g_strdup_printf("_%s", purple_account_option_get_text(option));
	adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), title);
	g_free(title);

	adw_preferences_group_add(ADW_PREFERENCES_GROUP(editor->advanced_group),
	                          row);

	/* Add the row to the editor's list of advanced rows. */
	editor->advanced_rows = g_list_append(editor->advanced_rows, row);

	/* Create the model and data for the expression. */
	items = purple_account_option_get_list(option);
	model = gtk_string_list_new(NULL);

	for(GList *l = items; l != NULL; l = l->next) {
		PurpleKeyValuePair *kvp = l->data;

		if(kvp != NULL) {
			if(purple_strequal(kvp->value, value)) {
				position = index;
			}

			data = g_list_append(data, kvp->value);
			gtk_string_list_append(model, kvp->key);
		}

		index++;
	}

	adw_combo_row_set_model(ADW_COMBO_ROW(row), G_LIST_MODEL(model));
	adw_combo_row_set_selected(ADW_COMBO_ROW(row), position);
	g_object_set_data_full(G_OBJECT(row), "keys", data,
	                       (GDestroyNotify)g_list_free);

	return row;
}

static void
pidgin_account_editor_add_advanced_option(PidginAccountEditor *editor,
                                          PurpleAccountOption *option)
{
	PurplePrefType type;
	GtkWidget *widget = NULL;

	type = purple_account_option_get_pref_type(option);
	switch(type) {
		case PURPLE_PREF_BOOLEAN:
			widget = pidgin_account_editor_add_advanced_boolean(editor, option);
			break;
		case PURPLE_PREF_INT:
			widget = pidgin_account_editor_add_advanced_int(editor, option);
			break;
		case PURPLE_PREF_STRING:
			widget = pidgin_account_editor_add_advanced_string(editor, option);
			break;
		case PURPLE_PREF_STRING_LIST:
			widget = pidgin_account_editor_add_advanced_list(editor, option);
			break;
		default:
			purple_debug_error("PidginAccountEditor",
			                   "Invalid Account Option pref type (%d)", type);
			break;
	}

	if(GTK_IS_WIDGET(widget)) {
		g_object_set_data_full(G_OBJECT(widget), "option", option,
		                       (GDestroyNotify)purple_account_option_destroy);

		editor->advanced_entries = g_list_append(editor->advanced_entries,
		                                         widget);
	} else {
		purple_account_option_destroy(option);
	}
}

static void
pidgin_account_editor_update_advanced_options(PidginAccountEditor *editor,
                                              PurpleProtocol *protocol)
{
	GList *options = NULL;

	g_clear_pointer(&editor->advanced_entries, g_list_free);
	while(editor->advanced_rows != NULL) {
		adw_preferences_group_remove(ADW_PREFERENCES_GROUP(editor->advanced_group),
		                             GTK_WIDGET(editor->advanced_rows->data));

		editor->advanced_rows = g_list_delete_link(editor->advanced_rows,
		                                           editor->advanced_rows);
	}

	if(!PURPLE_IS_PROTOCOL(protocol)) {
		gtk_widget_set_visible(editor->advanced_group, FALSE);

		return;
	}

	options = purple_protocol_get_account_options(protocol);
	if(options == NULL) {
		gtk_widget_set_visible(editor->advanced_group, FALSE);

		return;
	}

	/* Iterate the options and call our helper which will take ownership of the
	 * option itself, but we'll delete the list item as we go.
	 */
	while(options != NULL) {
		pidgin_account_editor_add_advanced_option(editor, options->data);

		options = g_list_delete_link(options, options);
	}

	gtk_widget_set_visible(editor->advanced_group, TRUE);
}

static void
pidgin_account_editor_update_proxy_options(PidginAccountEditor *editor) {
	PurpleProxyInfo *info = NULL;
	GListModel *model = NULL;
	char *str = NULL;
	const char *type = "global";
	const char *hostname = NULL;
	const char *username = NULL;
	const char *password = NULL;
	int port = 8080;
	guint position = 0;

	if(!PURPLE_IS_ACCOUNT(editor->account)) {
		return;
	}

	info = purple_account_get_proxy_info(editor->account);
	if(PURPLE_IS_PROXY_INFO(info)) {
		switch(purple_proxy_info_get_proxy_type(info)) {
			case PURPLE_PROXY_TYPE_USE_GLOBAL:
				type = "global";
				break;
			case PURPLE_PROXY_TYPE_NONE:
				type = "none";
				break;
			case PURPLE_PROXY_TYPE_SOCKS4:
				type = "socks4";
				break;
			case PURPLE_PROXY_TYPE_SOCKS5:
				type = "socks5";
				break;
			case PURPLE_PROXY_TYPE_TOR:
				type = "tor";
				break;
			case PURPLE_PROXY_TYPE_HTTP:
				type = "http";
				break;
			case PURPLE_PROXY_TYPE_USE_ENVVAR:
				type = "envvar";
				break;
		}

		hostname = purple_proxy_info_get_hostname(info);
		port = purple_proxy_info_get_port(info);
		username = purple_proxy_info_get_username(info);
		password = purple_proxy_info_get_password(info);
	}

	model = adw_combo_row_get_model(ADW_COMBO_ROW(editor->proxy_type));
	for(guint index = 0; index < g_list_model_get_n_items(model); index++) {
		const char *value = gtk_string_list_get_string(GTK_STRING_LIST(model),
		                                               index);

		if(purple_strequal(type, value)) {
			position = index;
			break;
		}
	}
	adw_combo_row_set_selected(ADW_COMBO_ROW(editor->proxy_type), position);

	if(hostname == NULL) {
		hostname = "";
	}
	gtk_editable_set_text(GTK_EDITABLE(editor->proxy_host), hostname);


	str = g_strdup_printf("%d", port);
	gtk_editable_set_text(GTK_EDITABLE(editor->proxy_port), str);
	g_free(str);

	if(username == NULL) {
		username = "";
	}
	gtk_editable_set_text(GTK_EDITABLE(editor->proxy_username), username);

	if(password == NULL) {
		password = "";
	}
	gtk_editable_set_text(GTK_EDITABLE(editor->proxy_password), password);
}

static void
pidgin_account_editor_update(PidginAccountEditor *editor) {
	PurpleProtocol *protocol = NULL;
	gboolean sensitive = FALSE;

	/* Reset the sensitivity of the protocol chooser to sensitive. */
	gtk_widget_set_sensitive(editor->protocol, TRUE);
	if(PURPLE_IS_ACCOUNT(editor->account)) {
		if(purple_account_is_connected(editor->account)) {
			/* If the account is connected, disable the protocol chooser. */
			gtk_widget_set_sensitive(editor->protocol, FALSE);
		}
	}

	protocol = pidgin_protocol_chooser_get_protocol(PIDGIN_PROTOCOL_CHOOSER(editor->protocol));

	sensitive = pidgin_account_editor_update_login_options(editor, protocol);
	pidgin_account_editor_update_user_options(editor, protocol);
	pidgin_account_editor_update_advanced_options(editor, protocol);
	pidgin_account_editor_update_proxy_options(editor);

	pidgin_account_editor_set_valid(editor, sensitive);
}

static void
pidgin_account_editor_login_options_update_editable(PidginAccountEditor *editor)
{
	PidginProtocolChooser *chooser = NULL;
	PurpleProtocol *selected_protocol = NULL;
	PurpleProtocolOptions options;
	gboolean editable = TRUE;

	chooser = PIDGIN_PROTOCOL_CHOOSER(editor->protocol);
	selected_protocol = pidgin_protocol_chooser_get_protocol(chooser);

	if(PURPLE_IS_ACCOUNT(editor->account)) {
		PurpleConnection *connection = NULL;

		connection = purple_account_get_connection(editor->account);

		/* If we have an active connection, we need to disable everything
		 * related to the protocol and username.
		 */
		if(PURPLE_IS_CONNECTION(connection)) {
			PurpleProtocol *connected_protocol = NULL;
			editable = FALSE;

			/* Check if the user changed the protocol. If they did, switch it
			 * back and update the editor to reflect what settings are active.
			 */
			connected_protocol = purple_connection_get_protocol(connection);

			if(connected_protocol != selected_protocol) {
				pidgin_protocol_chooser_set_protocol(chooser, connected_protocol);
				pidgin_account_editor_update(editor);
				selected_protocol = connected_protocol;
			}
		}
	}

	options = purple_protocol_get_options(selected_protocol);
	gtk_widget_set_visible(editor->require_password_row,
	                       options & OPT_PROTO_PASSWORD_OPTIONAL);

	gtk_widget_set_sensitive(editor->protocol, editable);
	gtk_editable_set_editable(GTK_EDITABLE(editor->username), editable);
	for(GList *l = editor->user_split_rows; l != NULL; l = l->next) {
		GtkWidget *row = l->data;

		gtk_editable_set_editable(GTK_EDITABLE(row), editable);
	}
	gtk_widget_set_sensitive(editor->require_password, editable);
}

static gboolean
pidgin_account_editor_save_login_options(PidginAccountEditor *editor) {
	PurpleProtocol *protocol = NULL;
	GList *user_splits = NULL;
	GList *split_item = NULL;
	GList *row_item = NULL;
	GString *username = NULL;
	const gchar *protocol_id = NULL;
	gboolean new_account = FALSE;
	gboolean require_password = FALSE;

	protocol = pidgin_protocol_chooser_get_protocol(PIDGIN_PROTOCOL_CHOOSER(editor->protocol));
	protocol_id = purple_protocol_get_id(protocol);

	username = g_string_new(gtk_editable_get_text(GTK_EDITABLE(editor->username)));

	user_splits = purple_protocol_get_user_splits(protocol);
	split_item = g_list_first(user_splits);
	row_item = editor->user_split_rows;
	while(split_item != NULL && row_item != NULL) {
		PurpleAccountUserSplit *split = split_item->data;
		GtkWidget *row = row_item->data;
		const gchar *value = "";
		gchar sep = '\0';

		sep = purple_account_user_split_get_separator(split);
		g_string_append_c(username, sep);

		if(GTK_IS_EDITABLE(row)) {
			value = gtk_editable_get_text(GTK_EDITABLE(row));
		}

		if(value == NULL || *value == '\0') {
			value = purple_account_user_split_get_default_value(split);
		}

		g_string_append(username, value);

		split_item = split_item->next;
		row_item = row_item->next;
	}
	g_list_free_full(user_splits,
	                 (GDestroyNotify)purple_account_user_split_destroy);

	if(!PURPLE_IS_ACCOUNT(editor->account)) {
		editor->account = purple_account_new(username->str, protocol_id);
		new_account = TRUE;
	} else {
		purple_account_set_username(editor->account, username->str);
		purple_account_set_protocol_id(editor->account, protocol_id);
	}

	g_string_free(username, TRUE);

	require_password = gtk_switch_get_active(GTK_SWITCH(editor->require_password));
	purple_account_set_require_password(editor->account, require_password);

	return new_account;
}

static void
pidgin_account_editor_save_user_options(PidginAccountEditor *editor) {
	PurpleContactInfo *info = NULL;
	const gchar *svalue = NULL;
	gboolean bvalue = FALSE;

	purple_account_freeze_notify_settings(editor->account);

	info = purple_account_get_contact_info(editor->account);

	/* Set the alias. */
	svalue = gtk_editable_get_text(GTK_EDITABLE(editor->alias));
	if(*svalue == '\0') {
		svalue = NULL;
	}
	purple_contact_info_set_alias(info, svalue);

	/* Set whether or not to use the global avatar. */
	bvalue = gtk_switch_get_active(GTK_SWITCH(editor->use_custom_avatar));
	purple_account_set_bool(editor->account, "use-global-buddyicon", !bvalue);

	#pragma message("implement this when buddy icons do not suck so bad.")

	purple_account_thaw_notify_settings(editor->account);
}

static void
pidgin_account_editor_save_advanced_options(PidginAccountEditor *editor) {
	purple_account_freeze_notify_settings(editor->account);

	for(GList *l = editor->advanced_entries; l != NULL; l = l->next) {
		GtkWidget *widget = l->data;
		PurpleAccountOption *option = NULL;
		GList *keys = NULL;
		const gchar *setting = NULL;
		const gchar *svalue = NULL;
		gboolean bvalue = FALSE;
		gint ivalue = 0;
		guint selected = 0;

		option = g_object_get_data(G_OBJECT(widget), "option");
		setting = purple_account_option_get_setting(option);

		switch(purple_account_option_get_pref_type(option)) {
			case PURPLE_PREF_STRING:
				svalue = gtk_editable_get_text(GTK_EDITABLE(widget));
				purple_account_set_string(editor->account, setting, svalue);
				break;
			case PURPLE_PREF_INT:
				svalue = gtk_editable_get_text(GTK_EDITABLE(widget));
				ivalue = atoi(svalue);
				purple_account_set_int(editor->account, setting, ivalue);
				break;
			case PURPLE_PREF_BOOLEAN:
				bvalue = gtk_switch_get_active(GTK_SWITCH(widget));
				purple_account_set_bool(editor->account, setting, bvalue);
				break;
			case PURPLE_PREF_STRING_LIST:
				keys = g_object_get_data(G_OBJECT(widget), "keys");
				selected = adw_combo_row_get_selected(ADW_COMBO_ROW(widget));
				svalue = g_list_nth_data(keys, selected);
				purple_account_set_string(editor->account, setting, svalue);
				break;
			default:
				break;
		}
	}

	purple_account_thaw_notify_settings(editor->account);
}

static void
pidgin_account_editor_save_proxy(PidginAccountEditor *editor,
                                 gboolean new_account)
{
	PurpleProxyInfo *info = NULL;
	PurpleProxyType type = PURPLE_PROXY_TYPE_NONE;
	GObject *item = NULL;
	const gchar *svalue = NULL;

	/* Build the ProxyInfo object */
	if(!new_account) {
		info = purple_account_get_proxy_info(editor->account);
	}

	/* If this is a new account, or the account's proxy info is null, create a
	 * new instance, and set it on the account.
	 */
	if(new_account || !PURPLE_IS_PROXY_INFO(info)) {
		info = purple_proxy_info_new();
		purple_account_set_proxy_info(editor->account, info);
	}

	item = adw_combo_row_get_selected_item(ADW_COMBO_ROW(editor->proxy_type));
	svalue = gtk_string_object_get_string(GTK_STRING_OBJECT(item));
	if(purple_strequal(svalue, "global")) {
		type = PURPLE_PROXY_TYPE_USE_GLOBAL;
	} else if(purple_strequal(svalue, "none")) {
		type = PURPLE_PROXY_TYPE_NONE;
	} else if(purple_strequal(svalue, "socks4")) {
		type = PURPLE_PROXY_TYPE_SOCKS4;
	} else if(purple_strequal(svalue, "socks5")) {
		type = PURPLE_PROXY_TYPE_SOCKS5;
	} else if(purple_strequal(svalue, "tor")) {
		type = PURPLE_PROXY_TYPE_TOR;
	} else if(purple_strequal(svalue, "http")) {
		type = PURPLE_PROXY_TYPE_HTTP;
	} else if(purple_strequal(svalue, "envvar")) {
		type = PURPLE_PROXY_TYPE_USE_ENVVAR;
	}
	purple_proxy_info_set_proxy_type(info, type);

	svalue = gtk_editable_get_text(GTK_EDITABLE(editor->proxy_host));
	purple_proxy_info_set_hostname(info, svalue);

	svalue = gtk_editable_get_text(GTK_EDITABLE(editor->proxy_port));
	purple_proxy_info_set_port(info, atoi(svalue));

	svalue = gtk_editable_get_text(GTK_EDITABLE(editor->proxy_username));
	purple_proxy_info_set_username(info, svalue);

	svalue = gtk_editable_get_text(GTK_EDITABLE(editor->proxy_password));
	purple_proxy_info_set_password(info, svalue);
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static void
pidgin_account_editor_connection_changed_cb(G_GNUC_UNUSED GObject *obj,
                                            G_GNUC_UNUSED GParamSpec *pspec,
                                            gpointer data)
{
	PidginAccountEditor *editor = data;

	pidgin_account_editor_login_options_update_editable(editor);
}

static void
pidgin_account_editor_protocol_changed_cb(G_GNUC_UNUSED GObject *obj,
                                          G_GNUC_UNUSED GParamSpec *pspec,
                                          gpointer data)
{
	pidgin_account_editor_update(data);
}

static void
pidgin_account_editor_username_changed_cb(GtkEditable *self, gpointer data) {
	PidginAccountEditor *editor = data;
	const gchar *text = gtk_editable_get_text(self);
	gboolean sensitive = FALSE;

	if(text != NULL && *text != '\0') {
		sensitive = TRUE;
	}

	pidgin_account_editor_set_valid(editor, sensitive);
}

static void
pidgin_account_editor_avatar_response_cb(GObject *obj, GAsyncResult *result,
                                         gpointer data)
{
	PidginAccountEditor *editor = data;
	GFile *file = NULL;

	file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(obj), result, NULL);
	if(file != NULL) {
		GError *error = NULL;

		g_clear_object(&editor->avatar_texture);
		editor->avatar_texture = gdk_texture_new_from_file(file, &error);
		if(error != NULL) {
			char *filename = g_file_get_path(file);
			g_warning("Failed to create texture from file %s: %s", filename,
			          error->message);
			g_free(filename);
			g_clear_error(&error);
		} else {
			gtk_image_set_from_paintable(GTK_IMAGE(editor->avatar),
			                             GDK_PAINTABLE(editor->avatar_texture));
		}

		g_object_unref(file);
	}

	g_clear_object(&editor->avatar_dialog);
}

static void
pidgin_account_editor_avatar_set_clicked_cb(G_GNUC_UNUSED GtkButton *self,
                                            gpointer data)
{
	PidginAccountEditor *editor = data;
	GtkRoot *root = NULL;

	root = gtk_widget_get_root(GTK_WIDGET(editor));
	editor->avatar_dialog = gtk_file_dialog_new();
	gtk_file_dialog_set_title(editor->avatar_dialog, _("Buddy Icon"));
	gtk_file_dialog_set_modal(editor->avatar_dialog, TRUE);
	gtk_file_dialog_open(editor->avatar_dialog, GTK_WINDOW(root), NULL,
	                     pidgin_account_editor_avatar_response_cb, editor);
}

static void
pidgin_account_editor_avatar_remove_clicked_cb(G_GNUC_UNUSED GtkButton *self,
                                               gpointer data)
{
	PidginAccountEditor *editor = data;

	gtk_image_set_from_icon_name(GTK_IMAGE(editor->avatar), "select-avatar");

	g_clear_object(&editor->avatar_texture);
}

static gchar *
pidgin_account_editor_proxy_type_expression_cb(GObject *self,
                                               G_GNUC_UNUSED gpointer data)
{
	const gchar *text = "";
	const gchar *value = NULL;

	value = gtk_string_object_get_string(GTK_STRING_OBJECT(self));
	if(purple_strequal(value, "global")) {
		text = _("Use Global Proxy Settings");
	} else if(purple_strequal(value, "none")) {
		text = _("No proxy");
	} else if(purple_strequal(value, "socks4")) {
		text = _("SOCKS 4");
	} else if(purple_strequal(value, "socks5")) {
		text = _("SOCKS 5");
	} else if(purple_strequal(value, "tor")) {
		text = _("Tor/Privacy (SOCKS 5)");
	} else if(purple_strequal(value, "http")) {
		text = _("HTTP");
	} else if(purple_strequal(value, "envvar")) {
		text = _("Use Environmental Settings");
	}

	return g_strdup(text);
}

static void
pidgin_account_editor_proxy_type_changed_cb(G_GNUC_UNUSED GObject *obj,
                                            G_GNUC_UNUSED GParamSpec *pspec,
                                            gpointer data)
{
	PidginAccountEditor *editor = data;
	GObject *selected = NULL;
	gboolean show_options = TRUE;
	const gchar *value = NULL;

	selected = adw_combo_row_get_selected_item(ADW_COMBO_ROW(editor->proxy_type));
	value = gtk_string_object_get_string(GTK_STRING_OBJECT(selected));

	if(purple_strequal(value, "global") || purple_strequal(value, "none") ||
	   purple_strequal(value, "envvar"))
	{
		show_options = FALSE;
	}

	gtk_widget_set_visible(editor->proxy_options, show_options);
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
G_DEFINE_FINAL_TYPE(PidginAccountEditor, pidgin_account_editor,
                    ADW_TYPE_PREFERENCES_PAGE)

static void
pidgin_account_editor_get_property(GObject *obj, guint param_id, GValue *value,
                                   GParamSpec *pspec)
{
	PidginAccountEditor *editor = PIDGIN_ACCOUNT_EDITOR(obj);

	switch(param_id) {
		case PROP_ACCOUNT:
			g_value_set_object(value,
			                   pidgin_account_editor_get_account(editor));
			break;
		case PROP_VALID:
			g_value_set_boolean(value,
			                    pidgin_account_editor_get_is_valid(editor));
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
			break;
	}
}

static void
pidgin_account_editor_set_property(GObject *obj, guint param_id,
                                   const GValue *value, GParamSpec *pspec)
{
	PidginAccountEditor *editor = PIDGIN_ACCOUNT_EDITOR(obj);

	switch(param_id) {
		case PROP_ACCOUNT:
			pidgin_account_editor_set_account(editor,
			                                  g_value_get_object(value));
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
			break;
	}
}

static void
pidgin_account_editor_dispose(GObject *obj) {
	PidginAccountEditor *editor = PIDGIN_ACCOUNT_EDITOR(obj);

	g_clear_object(&editor->account);
	g_clear_object(&editor->avatar_dialog);
	g_clear_object(&editor->avatar_texture);

	G_OBJECT_CLASS(pidgin_account_editor_parent_class)->dispose(obj);
}

static void
pidgin_account_editor_init(PidginAccountEditor *editor) {
	GtkWidget *widget = GTK_WIDGET(editor);

	gtk_widget_init_template(widget);

	pidgin_account_editor_proxy_type_changed_cb(NULL, NULL, editor);
}

static void
pidgin_account_editor_constructed(GObject *obj) {
	PidginAccountEditor *editor = PIDGIN_ACCOUNT_EDITOR(obj);

	G_OBJECT_CLASS(pidgin_account_editor_parent_class)->constructed(obj);

	if(PURPLE_IS_ACCOUNT(editor->account)) {
		pidgin_protocol_chooser_set_protocol(PIDGIN_PROTOCOL_CHOOSER(editor->protocol),
		                                     purple_account_get_protocol(editor->account));
	}

	pidgin_account_editor_update(editor);
	pidgin_account_editor_login_options_update_editable(editor);
}

static void
pidgin_account_editor_finalize(GObject *obj) {
	PidginAccountEditor *editor = PIDGIN_ACCOUNT_EDITOR(obj);

	g_clear_pointer(&editor->user_split_rows, g_list_free);

	g_clear_pointer(&editor->advanced_entries, g_list_free);
	g_clear_pointer(&editor->advanced_rows, g_list_free);

	G_OBJECT_CLASS(pidgin_account_editor_parent_class)->finalize(obj);
}

static void
pidgin_account_editor_class_init(PidginAccountEditorClass *klass) {
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->get_property = pidgin_account_editor_get_property;
	obj_class->set_property = pidgin_account_editor_set_property;
	obj_class->constructed = pidgin_account_editor_constructed;
	obj_class->dispose = pidgin_account_editor_dispose;
	obj_class->finalize = pidgin_account_editor_finalize;

	/**
	 * PidginAccountEditor:account:
	 *
	 * The account that this editor is modifying.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ACCOUNT] = g_param_spec_object(
		"account", NULL, NULL,
		PURPLE_TYPE_ACCOUNT,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PidginAccountEditor:valid:
	 *
	 * Whether or not the account settings are valid and it is okay to save the
	 * account.
	 *
	 * Since: 3.0
	 */
	properties[PROP_VALID] = g_param_spec_boolean(
		"valid", NULL, NULL,
		FALSE,
		G_PARAM_READABLE | 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/editor.ui");

	/* Login Options */
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     login_options);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     protocol);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     username);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     user_splits);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     require_password_row);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     require_password);

	gtk_widget_class_bind_template_callback(widget_class,
	                                        pidgin_account_editor_protocol_changed_cb);
	gtk_widget_class_bind_template_callback(widget_class,
	                                        pidgin_account_editor_username_changed_cb);

	/* User Options */
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     alias);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     avatar_row);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     use_custom_avatar);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     avatar);

	gtk_widget_class_bind_template_callback(widget_class,
	                                        pidgin_account_editor_avatar_set_clicked_cb);
	gtk_widget_class_bind_template_callback(widget_class,
	                                        pidgin_account_editor_avatar_remove_clicked_cb);

	/* Advanced Options */
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     advanced_group);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     advanced_toggle);

	/* Proxy Options */
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     proxy_type);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     proxy_options);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     proxy_host);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     proxy_port);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     proxy_username);
	gtk_widget_class_bind_template_child(widget_class, PidginAccountEditor,
	                                     proxy_password);

	gtk_widget_class_bind_template_callback(widget_class,
	                                        pidgin_account_editor_proxy_type_expression_cb);
	gtk_widget_class_bind_template_callback(widget_class,
	                                        pidgin_account_editor_proxy_type_changed_cb);
}

/******************************************************************************
 * API
 *****************************************************************************/
GtkWidget *
pidgin_account_editor_new(PurpleAccount *account) {
	return g_object_new(PIDGIN_TYPE_ACCOUNT_EDITOR, "account", account, NULL);
}

PurpleAccount *
pidgin_account_editor_get_account(PidginAccountEditor *editor) {
	g_return_val_if_fail(PIDGIN_IS_ACCOUNT_EDITOR(editor), NULL);

	return editor->account;
}

void
pidgin_account_editor_set_account(PidginAccountEditor *editor,
                                  PurpleAccount *account)
{
	g_return_if_fail(PIDGIN_IS_ACCOUNT_EDITOR(editor));

	/* Disconnect the notify handler from the previous account. */
	if(PURPLE_IS_ACCOUNT(editor->account)) {
		g_signal_handlers_disconnect_by_func(editor->account,
		                                     pidgin_account_editor_connection_changed_cb,
		                                     editor);
	}

	if(g_set_object(&editor->account, account)) {
		if(PURPLE_IS_ACCOUNT(account)) {
			pidgin_protocol_chooser_set_protocol(PIDGIN_PROTOCOL_CHOOSER(editor->protocol),
			                                     purple_account_get_protocol(editor->account));

			g_signal_connect_object(account, "notify::connection",
			                        G_CALLBACK(pidgin_account_editor_connection_changed_cb),
			                        editor, 0);
		} else {
			/* If we don't have an account reset the fields that are static. */
			pidgin_protocol_chooser_set_protocol(PIDGIN_PROTOCOL_CHOOSER(editor->protocol),
			                                     NULL);
			gtk_editable_set_text(GTK_EDITABLE(editor->username), "");
		}

		g_object_notify_by_pspec(G_OBJECT(editor), properties[PROP_ACCOUNT]);
	}

	pidgin_account_editor_update(editor);
}

gboolean
pidgin_account_editor_get_is_valid(PidginAccountEditor *editor) {
	g_return_val_if_fail(PIDGIN_IS_ACCOUNT_EDITOR(editor), FALSE);

	return editor->valid;
}

void
pidgin_account_editor_save(PidginAccountEditor *editor) {
	gboolean new_account = FALSE;

	g_return_if_fail(PIDGIN_IS_ACCOUNT_EDITOR(editor));
	g_return_if_fail(editor->valid == TRUE);

	new_account = pidgin_account_editor_save_login_options(editor);
	pidgin_account_editor_save_user_options(editor);
	pidgin_account_editor_save_advanced_options(editor);
	pidgin_account_editor_save_proxy(editor, new_account);

	/* If this is a new account, enable it, and add it to the account manager.
	 */
	if(new_account) {
		PurpleAccountManager *manager = NULL;

		manager = purple_account_manager_get_default();
		purple_account_manager_add(manager, editor->account);

		purple_account_set_enabled(editor->account, TRUE);
	}
}

mercurial