finch/gntblist.c

Tue, 27 Jul 2021 01:31:54 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Tue, 27 Jul 2021 01:31:54 -0500
changeset 40999
9e5a5eb54a57
parent 40978
2cb285cacbfd
child 41008
dc06703548a1
permissions
-rw-r--r--

bump the minimum meson to 0.56.0 and fix the deprecations

Testing Done:
Verfied meson didn't complain about any deprecations.

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

/*
 * finch
 *
 * Finch 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 */

#include NCURSES_HEADER

#include <string.h>

#include <glib/gi18n-lib.h>

#include <gplugin.h>

#include <purple.h>

#include <gnt.h>

#include "gntblist.h"
#include "gntconv.h"
#include "gntlog.h"
#include "gntmenuutil.h"
#include "gntstatus.h"

#define PREF_ROOT "/finch/blist"
#define TYPING_TIMEOUT_S 4

#define UI_DATA "ui-finch"

#define SHOW_EMPTY_GROUP_TIMEOUT  60

struct _FinchBuddyList {
	PurpleBuddyList parent;

	GntWidget *window;
	GntWidget *tree;

	GntWidget *tooltip;
	PurpleBlistNode *tnode;  /* Who is the tooltip being displayed for? */
	GList *tagged;           /* A list of tagged blistnodes             */

	GntWidget *context;
	PurpleBlistNode *cnode;

	/* XXX: I am KISSing */
	GntWidget *status;      /* Dropdown with the statuses  */
	GntWidget *statustext;  /* Status message              */
	int typing;

	GntWidget *menu;
	/* These are the menuitems that get regenerated */
	GntMenuItem *accounts;
	GntMenuItem *plugins;
	GntMenuItem *grouping;

	/* When a new group is manually added, it is empty, but we still want to show it
	 * for a while (SHOW_EMPTY_GROUP_TIMEOUT seconds) even if 'show empty groups' is
	 * not selected.
	 */
	GList *new_group;
	guint new_group_timeout;

	FinchBlistManager *manager;
};

typedef struct
{
	gpointer row;        /* the row in the GntTree             */
	guint signed_timer;  /* used when 'recently' signed on/off */
} FinchBlistNode;

typedef enum
{
	STATUS_PRIMITIVE = 0,
	STATUS_SAVED_POPULAR,
	STATUS_SAVED_ALL,
	STATUS_SAVED_NEW
} StatusType;

typedef struct
{
	StatusType type;
	union
	{
		PurpleStatusPrimitive prim;
		PurpleSavedStatus *saved;
	} u;
} StatusBoxItem;

static FinchBuddyList *ggblist;

static void add_buddy(PurpleBuddy *buddy, FinchBuddyList *ggblist);
static void add_contact(PurpleContact *contact, FinchBuddyList *ggblist);
static void add_group(PurpleGroup *group, FinchBuddyList *ggblist);
static void add_chat(PurpleChat *chat, FinchBuddyList *ggblist);
static void add_node(PurpleBlistNode *node, FinchBuddyList *ggblist);
static void node_update(PurpleBuddyList *list, PurpleBlistNode *node);
static void draw_tooltip(FinchBuddyList *ggblist);
static void tooltip_for_buddy(PurpleBuddy *buddy, GString *str, gboolean full);
static gboolean remove_typing_cb(gpointer null);
static void remove_peripherals(FinchBuddyList *ggblist);
static const char * get_display_name(PurpleBlistNode *node);
static void savedstatus_changed(PurpleSavedStatus *now, PurpleSavedStatus *old);
static void blist_show(PurpleBuddyList *list);
static void update_node_display(PurpleBlistNode *buddy,
                                FinchBuddyList *ggblist);
static void update_buddy_display(PurpleBuddy *buddy, FinchBuddyList *ggblist);
static gboolean account_autojoin_cb(PurpleConnection *pc, gpointer null);
static void finch_request_add_buddy(PurpleBuddyList *list,
                                    PurpleAccount *account,
                                    const char *username, const char *grp,
                                    const char *alias);
static void menu_group_set_cb(GntMenuItem *item, gpointer null);

/* Sort functions */
static int blist_node_compare_position(PurpleBlistNode *n1, PurpleBlistNode *n2);
static int blist_node_compare_text(PurpleBlistNode *n1, PurpleBlistNode *n2);
static int blist_node_compare_status(PurpleBlistNode *n1, PurpleBlistNode *n2);
static int blist_node_compare_log(PurpleBlistNode *n1, PurpleBlistNode *n2);

static int color_available;
static int color_away;
static int color_offline;
static int color_idle;

/*
 * Buddy List Manager functions.
 */

static gboolean default_can_add_node(PurpleBlistNode *node)
{
	gboolean offline = purple_prefs_get_bool(PREF_ROOT "/showoffline");

	if (PURPLE_IS_BUDDY(node)) {
		PurpleBuddy *buddy = (PurpleBuddy*)node;
		FinchBlistNode *fnode = g_object_get_data(G_OBJECT(node), UI_DATA);

		if (!purple_buddy_get_contact(buddy))
			return FALSE; /* When a new buddy is added and show-offline is set */
		if (PURPLE_BUDDY_IS_ONLINE(buddy))
			return TRUE;  /* The buddy is online */
		if (!purple_account_is_connected(purple_buddy_get_account(buddy)))
			return FALSE; /* The account is disconnected. Do not show */
		if (offline)
			return TRUE;  /* We want to see offline buddies too */
		if (fnode && fnode->signed_timer)
			return TRUE;  /* Show if the buddy just signed off */
		if (purple_blist_node_get_bool(node, "show_offline"))
			return TRUE;
	} else if (PURPLE_IS_CONTACT(node)) {
		PurpleBlistNode *nd;
		for (nd = purple_blist_node_get_first_child(node);
				nd; nd = purple_blist_node_get_sibling_next(nd)) {
			if (default_can_add_node(nd))
				return TRUE;
		}
	} else if (PURPLE_IS_CHAT(node)) {
		PurpleChat *chat = (PurpleChat*)node;
		if (purple_account_is_connected(purple_chat_get_account(chat)))
			return TRUE;  /* Show whenever the account is online */
	} else if (PURPLE_IS_GROUP(node)) {
		PurpleBlistNode *nd;
		gboolean empty = purple_prefs_get_bool(PREF_ROOT "/emptygroups");
		if (empty)
			return TRUE;  /* If we want to see empty groups, we can show any group */

		for (nd = purple_blist_node_get_first_child(node);
				nd; nd = purple_blist_node_get_sibling_next(nd)) {
			if (default_can_add_node(nd))
				return TRUE;
		}

		if (ggblist && ggblist->new_group && g_list_find(ggblist->new_group, node))
			return TRUE;
	}

	return FALSE;
}

static gpointer default_find_parent(PurpleBlistNode *node)
{
	gpointer ret = NULL;

	if (PURPLE_IS_BUDDY(node) || PURPLE_IS_CONTACT(node) || PURPLE_IS_CHAT(node))
		ret = purple_blist_node_get_parent(node);

	if (ret)
		add_node(ret, ggblist);

	return ret;
}

static gboolean default_create_tooltip(gpointer selected_row, GString **body, char **tool_title)
{
	GString *str;
	PurpleBlistNode *node = selected_row;
	int lastseen = 0;
	char *title;

	str = g_string_new("");

	if (PURPLE_IS_CONTACT(node)) {
		PurpleBuddy *pr = purple_contact_get_priority_buddy((PurpleContact*)node);
		gboolean offline = !PURPLE_BUDDY_IS_ONLINE(pr);
		gboolean showoffline = purple_prefs_get_bool(PREF_ROOT "/showoffline");
		const char *name = purple_buddy_get_name(pr);

		title = g_strdup(name);
		tooltip_for_buddy(pr, str, TRUE);
		for (node = purple_blist_node_get_first_child(node); node; node = purple_blist_node_get_sibling_next(node)) {
			PurpleBuddy *buddy = (PurpleBuddy*)node;
			if (offline) {
				int value = purple_blist_node_get_int(node, "last_seen");
				if (value > lastseen)
					lastseen = value;
			}
			if (node == (PurpleBlistNode*)pr)
				continue;
			if (!purple_account_is_connected(purple_buddy_get_account(buddy)))
				continue;
			if (!showoffline && !PURPLE_BUDDY_IS_ONLINE(buddy))
				continue;
			str = g_string_append(str, "\n----------\n");
			tooltip_for_buddy(buddy, str, FALSE);
		}
	} else if (PURPLE_IS_BUDDY(node)) {
		PurpleBuddy *buddy = (PurpleBuddy *)node;
		tooltip_for_buddy(buddy, str, TRUE);
		title = g_strdup(purple_buddy_get_name(buddy));
		if (!PURPLE_BUDDY_IS_ONLINE((PurpleBuddy*)node))
			lastseen = purple_blist_node_get_int(node, "last_seen");
	} else if (PURPLE_IS_GROUP(node)) {
		PurpleGroup *group = (PurpleGroup *)node;

		g_string_append_printf(str, _("Online: %d\nTotal: %d"),
				purple_counting_node_get_online_count(PURPLE_COUNTING_NODE(group)),
				purple_counting_node_get_current_size(PURPLE_COUNTING_NODE(group)));

		title = g_strdup(purple_group_get_name(group));
	} else if (PURPLE_IS_CHAT(node)) {
		PurpleChat *chat = (PurpleChat *)node;
		PurpleAccount *account = purple_chat_get_account(chat);

		g_string_append_printf(str, _("Account: %s (%s)"),
				purple_account_get_username(account),
				purple_account_get_protocol_name(account));

		title = g_strdup(purple_chat_get_name(chat));
	} else {
		g_string_free(str, TRUE);
		return FALSE;
	}

	if (lastseen > 0) {
		char *tmp = purple_str_seconds_to_string(time(NULL) - lastseen);
		g_string_append_printf(str, _("\nLast Seen: %s ago"), tmp);
		g_free(tmp);
	}

	if (tool_title)
		*tool_title = title;
	else
		g_free(title);

	if (body)
		*body = str;
	else
		g_string_free(str, TRUE);

	return TRUE;
}

static FinchBlistManager default_manager =
{
	"default",
	N_("Default"),
	NULL,
	NULL,
	default_can_add_node,
	default_find_parent,
	default_create_tooltip,
	{NULL, NULL, NULL, NULL}
};
static GList *managers;

static void
finch_blist_node_free(FinchBlistNode *node) {
	if(node->signed_timer) {
		g_source_remove(node->signed_timer);
	}

	g_free(node);
}

static FinchBlistNode *
create_finch_blist_node(PurpleBlistNode *node, gpointer row)
{
	FinchBlistNode *fnode = g_object_get_data(G_OBJECT(node), UI_DATA);
	if (!fnode) {
		fnode = g_new0(FinchBlistNode, 1);
		fnode->signed_timer = 0;

		g_object_set_data_full(G_OBJECT(node), UI_DATA, fnode,
		                       (GDestroyNotify)finch_blist_node_free);
	}
	fnode->row = row;
	return fnode;
}

static int
get_display_color(PurpleBlistNode  *node)
{
	PurpleBuddy *buddy;
	int color = 0;

	if (PURPLE_IS_CONTACT(node))
		node = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(node)));
	if (!PURPLE_IS_BUDDY(node))
		return 0;

	buddy = (PurpleBuddy*)node;
	if (purple_presence_is_idle(purple_buddy_get_presence(buddy))) {
		color = color_idle;
	} else if (purple_presence_is_available(purple_buddy_get_presence(buddy))) {
		color = color_available;
	} else if (purple_presence_is_online(purple_buddy_get_presence(buddy)) &&
			!purple_presence_is_available(purple_buddy_get_presence(buddy))) {
		color = color_away;
	} else if (!purple_presence_is_online(purple_buddy_get_presence(buddy))) {
		color = color_offline;
	}

	return color;
}

static GntTextFormatFlags
get_blist_node_flag(FinchBuddyList *ggblist, PurpleBlistNode *node)
{
	GntTextFormatFlags flag = 0;
	FinchBlistNode *fnode = g_object_get_data(G_OBJECT(node), UI_DATA);

	if (ggblist->tagged && g_list_find(ggblist->tagged, node))
		flag |= GNT_TEXT_FLAG_BOLD;

	if (fnode && fnode->signed_timer)
		flag |= GNT_TEXT_FLAG_BLINK;
	else if (PURPLE_IS_CONTACT(node)) {
		node = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(node)));
		fnode = g_object_get_data(G_OBJECT(node), UI_DATA);
		if (fnode && fnode->signed_timer)
			flag |= GNT_TEXT_FLAG_BLINK;
	}

	return flag;
}

static void
blist_update_row_flags(FinchBuddyList *ggblist, PurpleBlistNode *node)
{
	gnt_tree_set_row_flags(GNT_TREE(ggblist->tree), node,
	                       get_blist_node_flag(ggblist, node));
	gnt_tree_set_row_color(GNT_TREE(ggblist->tree), node, get_display_color(node));
}

static void
new_node(PurpleBuddyList *list, PurpleBlistNode *node)
{
}

static void
add_node(PurpleBlistNode *node, FinchBuddyList *ggblist)
{
	if(g_object_get_data(G_OBJECT(node), UI_DATA)) {
		return;
	}

	if(!ggblist->manager->can_add_node(node)) {
		return;
	}

	if(PURPLE_IS_BUDDY(node)) {
		add_buddy((PurpleBuddy*)node, ggblist);
	} else if (PURPLE_IS_CONTACT(node)) {
		add_contact((PurpleContact*)node, ggblist);
	} else if (PURPLE_IS_GROUP(node)) {
		add_group((PurpleGroup*)node, ggblist);
	} else if (PURPLE_IS_CHAT(node)) {
		add_chat((PurpleChat *)node, ggblist);
	}

	draw_tooltip(ggblist);
}

void finch_blist_manager_add_node(PurpleBlistNode *node)
{
	add_node(node, ggblist);
}

static void
remove_tooltip(FinchBuddyList *ggblist)
{
	gnt_widget_destroy(ggblist->tooltip);
	ggblist->tooltip = NULL;
	ggblist->tnode = NULL;
}

static void
node_remove(PurpleBuddyList *list, PurpleBlistNode *node)
{
	FinchBuddyList *ggblist = FINCH_BUDDY_LIST(list);
	PurpleBlistNode *parent;

	if (ggblist == NULL || g_object_get_data(G_OBJECT(node), UI_DATA) == NULL)
		return;

	if (PURPLE_IS_GROUP(node) && ggblist->new_group) {
		ggblist->new_group = g_list_remove(ggblist->new_group, node);
	}

	gnt_tree_remove(GNT_TREE(ggblist->tree), node);
	if (ggblist->tagged)
		ggblist->tagged = g_list_remove(ggblist->tagged, node);

	parent = purple_blist_node_get_parent(node);
	for (node = purple_blist_node_get_first_child(node); node;
			node = purple_blist_node_get_sibling_next(node))
		node_remove(list, node);

	if (parent) {
		if (!ggblist->manager->can_add_node(parent))
			node_remove(list, parent);
		else
			node_update(list, parent);
	}

	draw_tooltip(ggblist);
}

static void
node_update(PurpleBuddyList *list, PurpleBlistNode *node)
{
	FinchBuddyList *ggblist;

	g_return_if_fail(FINCH_IS_BUDDY_LIST(list));
	/* It really looks like this should never happen ... but it does.
           This will at least emit a warning to the log when it
           happens, so maybe someone will figure it out. */
	g_return_if_fail(node != NULL);

	ggblist = FINCH_BUDDY_LIST(list);
	if (ggblist->window == NULL) {
		return;
	}

	if(g_object_get_data(G_OBJECT(node), UI_DATA) != NULL) {
		gnt_tree_change_text(GNT_TREE(ggblist->tree), node, 0,
		                     get_display_name(node));
		gnt_tree_sort_row(GNT_TREE(ggblist->tree), node);
		blist_update_row_flags(ggblist, node);
		if (gnt_tree_get_parent_key(GNT_TREE(ggblist->tree), node) !=
				ggblist->manager->find_parent(node))
		{
			node_remove(list, node);
		}
	}

	if (PURPLE_IS_BUDDY(node)) {
		PurpleBuddy *buddy = (PurpleBuddy*)node;
		add_node((PurpleBlistNode *)buddy, FINCH_BUDDY_LIST(list));
		node_update(list, purple_blist_node_get_parent(node));
	} else if (PURPLE_IS_CHAT(node)) {
		add_node(node, FINCH_BUDDY_LIST(list));
	} else if (PURPLE_IS_CONTACT(node)) {
		if (g_object_get_data(G_OBJECT(node), UI_DATA) == NULL) {
			/* The core seems to expect the UI to add the buddies. */
			for (node = purple_blist_node_get_first_child(node); node; node = purple_blist_node_get_sibling_next(node))
				add_node(node, FINCH_BUDDY_LIST(list));
		}
	} else if (PURPLE_IS_GROUP(node)) {
		if (!ggblist->manager->can_add_node(node))
			node_remove(list, node);
		else
			add_node(node, FINCH_BUDDY_LIST(list));
	}
	if (ggblist->tnode == node) {
		draw_tooltip(ggblist);
	}
}

static gboolean
remove_new_empty_group(gpointer data)
{
	PurpleBuddyList *list;
	FinchBuddyList *ggblist;

	list = purple_blist_get_default();
	g_return_val_if_fail(list, FALSE);
	ggblist = FINCH_BUDDY_LIST(list);

	ggblist->new_group_timeout = 0;
	while (ggblist->new_group) {
		PurpleBlistNode *group = ggblist->new_group->data;
		ggblist->new_group = g_list_delete_link(ggblist->new_group, ggblist->new_group);
		node_update(list, group);
	}

	return FALSE;
}

static void
add_buddy_cb(void *data, PurpleRequestFields *allfields)
{
	const char *username = purple_request_fields_get_string(allfields, "screenname");
	const char *alias = purple_request_fields_get_string(allfields, "alias");
	const char *group = purple_request_fields_get_string(allfields, "group");
	const char *invite = purple_request_fields_get_string(allfields, "invite");
	PurpleAccount *account = purple_request_fields_get_account(allfields, "account");
	const char *error = NULL;
	PurpleGroup *grp;
	PurpleBuddy *buddy;

	if (!username)
		error = _("You must provide a username for the buddy.");
	else if (!group)
		error = _("You must provide a group.");
	else if (!account)
		error = _("You must select an account.");
	else if (!purple_account_is_connected(account))
		error = _("The selected account is not online.");

	if (error)
	{
		finch_request_add_buddy(purple_blist_get_default(), account,
		                        username, group, alias);
		purple_notify_error(NULL, _("Error"), _("Error adding buddy"),
			error, purple_request_cpar_from_account(account));
		return;
	}

	grp = purple_blist_find_group(group);
	if (!grp)
	{
		grp = purple_group_new(group);
		purple_blist_add_group(grp, NULL);
	}

	/* XXX: Ask to merge if there's already a buddy with the same alias in the same group (#4553) */

	if ((buddy = purple_blist_find_buddy_in_group(account, username, grp)) == NULL)
	{
		buddy = purple_buddy_new(account, username, alias);
		purple_blist_add_buddy(buddy, NULL, grp, NULL);
	}

	purple_account_add_buddy(account, buddy, invite);
}

static void
finch_request_add_buddy(PurpleBuddyList *list, PurpleAccount *account,
                        const char *username, const char *grp,
                        const char *alias)
{
	PurpleRequestFields *fields = purple_request_fields_new();
	PurpleRequestFieldGroup *group = purple_request_field_group_new(NULL);
	PurpleRequestField *field;

	purple_request_fields_add_group(fields, group);

	field = purple_request_field_string_new("screenname", _("Username"), username, FALSE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_string_new("alias", _("Alias (optional)"), alias, FALSE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_string_new("invite", _("Invite message (optional)"), NULL, FALSE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_string_new("group", _("Add in group"), grp, FALSE);
	purple_request_field_group_add_field(group, field);
	purple_request_field_set_type_hint(field, "group");

	field = purple_request_field_account_new("account", _("Account"), NULL);
	purple_request_field_account_set_show_all(field, FALSE);
	if (account)
		purple_request_field_account_set_value(field, account);
	purple_request_field_group_add_field(group, field);

	purple_request_fields(NULL, _("Add Buddy"), NULL, _("Please enter buddy information."),
			fields,
			_("Add"), G_CALLBACK(add_buddy_cb),
			_("Cancel"), NULL,
			purple_request_cpar_from_account(account),
			NULL);
}

static void
join_chat(PurpleChat *chat)
{
	PurpleAccount *account = purple_chat_get_account(chat);
	PurpleConversationManager *manager;
	const char *name;
	PurpleConversation *conv;

	name = purple_chat_get_name_only(chat);
	manager = purple_conversation_manager_get_default();
	conv = purple_conversation_manager_find_chat(manager, account, name);

	if (!conv || purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))) {
		purple_serv_join_chat(purple_account_get_connection(account),
				purple_chat_get_components(chat));
	} else if (conv) {
		purple_conversation_present(conv);
	}
}

static void
add_chat_cb(void *data, PurpleRequestFields *allfields)
{
	PurpleAccount *account;
	const char *alias, *name, *group;
	PurpleChat *chat;
	PurpleGroup *grp;
	GHashTable *hash = NULL;
	PurpleConnection *gc;
	gboolean autojoin;
	PurpleProtocol *protocol;

	account = purple_request_fields_get_account(allfields, "account");
	name = purple_request_fields_get_string(allfields, "name");
	alias = purple_request_fields_get_string(allfields, "alias");
	group = purple_request_fields_get_string(allfields, "group");
	autojoin = purple_request_fields_get_bool(allfields, "autojoin");

	if (!purple_account_is_connected(account) || !name || !*name)
		return;

	if (!group || !*group)
		group = _("Chats");

	gc = purple_account_get_connection(account);
	protocol = purple_connection_get_protocol(gc);
	hash = purple_protocol_chat_info_defaults(PURPLE_PROTOCOL_CHAT(protocol), gc, name);

	chat = purple_chat_new(account, name, hash);

	if (chat != NULL) {
		if ((grp = purple_blist_find_group(group)) == NULL) {
			grp = purple_group_new(group);
			purple_blist_add_group(grp, NULL);
		}
		purple_blist_add_chat(chat, grp, NULL);
		purple_chat_set_alias(chat, alias);
		purple_blist_node_set_bool((PurpleBlistNode*)chat, "gnt-autojoin", autojoin);
		if (autojoin) {
			join_chat(chat);
		}
	}
}

static void
finch_request_add_chat(PurpleBuddyList *list, PurpleAccount *account,
                       PurpleGroup *grp, const char *alias, const char *name)
{
	PurpleRequestFields *fields = purple_request_fields_new();
	PurpleRequestFieldGroup *group = purple_request_field_group_new(NULL);
	PurpleRequestField *field;

	purple_request_fields_add_group(fields, group);

	field = purple_request_field_account_new("account", _("Account"), NULL);
	purple_request_field_account_set_show_all(field, FALSE);
	if (account)
		purple_request_field_account_set_value(field, account);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_string_new("name", _("Name"), name, FALSE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_string_new("alias", _("Alias"), alias, FALSE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_string_new("group", _("Group"), grp ? purple_group_get_name(grp) : NULL, FALSE);
	purple_request_field_group_add_field(group, field);
	purple_request_field_set_type_hint(field, "group");

	field = purple_request_field_bool_new("autojoin", _("Auto-join"), FALSE);
	purple_request_field_group_add_field(group, field);

	purple_request_fields(NULL, _("Add Chat"), NULL,
			_("You can edit more information from the context menu later."),
			fields, _("Add"), G_CALLBACK(add_chat_cb), _("Cancel"), NULL,
			NULL, NULL);
}

static void
add_group_cb(FinchBuddyList *ggblist, const char *group)
{
	PurpleGroup *grp;

	if (!group || !*group) {
		purple_notify_error(NULL, _("Error"), _("Error adding group"),
				_("You must give a name for the group to add."), NULL);
		g_object_unref(ggblist);
		return;
	}

	grp = purple_blist_find_group(group);
	if (!grp) {
		grp = purple_group_new(group);
		purple_blist_add_group(grp, NULL);
	}

	/* Treat the group as a new group even if it had existed before. This should
	 * make things easier to add buddies to empty groups (new or old) without having
	 * to turn on 'show empty groups' setting */
	ggblist->new_group = g_list_prepend(ggblist->new_group, grp);
	if (ggblist->new_group_timeout)
		g_source_remove(ggblist->new_group_timeout);
	ggblist->new_group_timeout = g_timeout_add_seconds(SHOW_EMPTY_GROUP_TIMEOUT,
			remove_new_empty_group, NULL);

	/* Select the group */
	if (ggblist->tree) {
		FinchBlistNode *fnode = g_object_get_data(G_OBJECT(grp), UI_DATA);
		if (!fnode)
			add_node((PurpleBlistNode*)grp, ggblist);
		gnt_tree_set_selected(GNT_TREE(ggblist->tree), grp);
	}

	g_object_unref(ggblist);
}

static void
finch_request_add_group(PurpleBuddyList *list)
{
	purple_request_input(NULL, _("Add Group"), NULL,
	                     _("Enter the name of the group"), NULL, FALSE,
	                     FALSE, NULL, _("Add"), G_CALLBACK(add_group_cb),
	                     _("Cancel"), G_CALLBACK(g_object_unref), NULL,
	                     g_object_ref(list));
}

static gpointer
finch_blist_get_handle(void)
{
	static int handle;

	return &handle;
}

static void
add_group(PurpleGroup *group, FinchBuddyList *ggblist)
{
	gpointer parent;
	PurpleBlistNode *node = (PurpleBlistNode *)group;
	if(g_object_get_data(G_OBJECT(node), UI_DATA)) {
		return;
	}
	parent = ggblist->manager->find_parent((PurpleBlistNode*)group);
	create_finch_blist_node(node, gnt_tree_add_row_after(GNT_TREE(ggblist->tree), group,
			gnt_tree_create_row(GNT_TREE(ggblist->tree), get_display_name(node)),
			parent, NULL));
	gnt_tree_set_expanded(GNT_TREE(ggblist->tree), node,
		!purple_blist_node_get_bool(node, "collapsed"));
}

static const char *
get_display_name(PurpleBlistNode *node)
{
	static char text[2096];
	char status[8] = " ";
	const char *name = NULL;

	if (PURPLE_IS_CONTACT(node))
		node = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(node)));  /* XXX: this can return NULL?! */

	if (node == NULL)
		return NULL;

	if (PURPLE_IS_BUDDY(node))
	{
		PurpleBuddy *buddy = (PurpleBuddy *)node;
		PurpleStatusPrimitive prim;
		PurplePresence *presence;
		PurpleStatus *now;
		gboolean ascii = gnt_ascii_only();

		presence = purple_buddy_get_presence(buddy);
		if (purple_presence_is_status_primitive_active(presence, PURPLE_STATUS_MOBILE))
			strncpy(status, ascii ? ":" : "☎", sizeof(status) - 1);
		else {
			now = purple_presence_get_active_status(presence);

			prim = purple_status_type_get_primitive(purple_status_get_status_type(now));

			switch(prim) {
				case PURPLE_STATUS_OFFLINE:
					strncpy(status, ascii ? "x" : "⊗", sizeof(status) - 1);
					break;
				case PURPLE_STATUS_AVAILABLE:
					strncpy(status, ascii ? "o" : "â—¯", sizeof(status) - 1);
					break;
				default:
					strncpy(status, ascii ? "." : "⊖", sizeof(status) - 1);
					break;
			}
		}
		name = purple_buddy_get_alias(buddy);
	}
	else if (PURPLE_IS_CHAT(node))
	{
		PurpleChat *chat = (PurpleChat*)node;
		name = purple_chat_get_name(chat);

		strncpy(status, "~", sizeof(status) - 1);
	}
	else if (PURPLE_IS_GROUP(node))
		return purple_group_get_name((PurpleGroup*)node);

	g_snprintf(text, sizeof(text) - 1, "%s %s", status, name);

	return text;
}

static void
add_chat(PurpleChat *chat, FinchBuddyList *ggblist)
{
	gpointer parent;
	PurpleBlistNode *node = (PurpleBlistNode *)chat;
	if(g_object_get_data(G_OBJECT(node), UI_DATA)) {
		return;
	}
	if(!purple_account_is_connected(purple_chat_get_account(chat))) {
		return;
	}

	parent = ggblist->manager->find_parent((PurpleBlistNode*)chat);

	create_finch_blist_node(node, gnt_tree_add_row_after(GNT_TREE(ggblist->tree), chat,
				gnt_tree_create_row(GNT_TREE(ggblist->tree), get_display_name(node)),
				parent, NULL));
}

static void
add_contact(PurpleContact *contact, FinchBuddyList *ggblist)
{
	gpointer parent;
	PurpleBlistNode *node = (PurpleBlistNode*)contact;
	const char *name;

	if(g_object_get_data(G_OBJECT(node), UI_DATA)) {
		return;
	}

	name = get_display_name(node);
	if (name == NULL)
		return;

	parent = ggblist->manager->find_parent((PurpleBlistNode*)contact);

	create_finch_blist_node(node, gnt_tree_add_row_after(GNT_TREE(ggblist->tree), contact,
				gnt_tree_create_row(GNT_TREE(ggblist->tree), name),
				parent, NULL));

	gnt_tree_set_expanded(GNT_TREE(ggblist->tree), contact, FALSE);
}

static void
add_buddy(PurpleBuddy *buddy, FinchBuddyList *ggblist)
{
	gpointer parent;
	PurpleBlistNode *node = (PurpleBlistNode *)buddy;
	PurpleContact *contact;

	if(g_object_get_data(G_OBJECT(node), UI_DATA)) {
		return;
	}

	contact = purple_buddy_get_contact(buddy);
	parent = ggblist->manager->find_parent((PurpleBlistNode*)buddy);

	create_finch_blist_node(node, gnt_tree_add_row_after(GNT_TREE(ggblist->tree), buddy,
				gnt_tree_create_row(GNT_TREE(ggblist->tree), get_display_name(node)),
				parent, NULL));

	blist_update_row_flags(ggblist, (PurpleBlistNode *)buddy);
	if (buddy == purple_contact_get_priority_buddy(contact)) {
		blist_update_row_flags(ggblist, (PurpleBlistNode *)contact);
	}
}

static void
selection_activate(GntWidget *widget, FinchBuddyList *ggblist)
{
	GntTree *tree = GNT_TREE(ggblist->tree);
	PurpleBlistNode *node = gnt_tree_get_selection_data(tree);

	if (!node)
		return;

	if (PURPLE_IS_CONTACT(node))
		node = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(node)));

	if (PURPLE_IS_BUDDY(node))
	{
		PurpleBuddy *buddy = (PurpleBuddy *)node;
		PurpleConversation *im;
		PurpleConversationManager *manager;

		manager = purple_conversation_manager_get_default();
		im = purple_conversation_manager_find_im(manager,
		                                         purple_buddy_get_account(buddy),
		                                         purple_buddy_get_name(buddy));

		if(!PURPLE_IS_IM_CONVERSATION(im)) {
			im = purple_im_conversation_new(purple_buddy_get_account(buddy),
			                                purple_buddy_get_name(buddy));
		} else {
			FinchConv *ggconv = FINCH_CONV(im);
			gnt_window_present(ggconv->window);
		}
		finch_conversation_set_active(im);
	}
	else if (PURPLE_IS_CHAT(node))
	{
		join_chat((PurpleChat*)node);
	}
}

static void
append_proto_menu(GntMenu *menu, PurpleConnection *gc, PurpleBlistNode *node)
{
	GList *list;
	PurpleProtocol *protocol = purple_connection_get_protocol(gc);

	if (!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CLIENT, blist_node_menu)) {
		return;
	}

	for(list = purple_protocol_client_blist_node_menu(PURPLE_PROTOCOL_CLIENT(protocol), node);
			list; list = g_list_delete_link(list, list))
	{
		PurpleActionMenu *act = (PurpleActionMenu *) list->data;
		if (!act)
			continue;
		purple_action_menu_set_data(act, node);
		finch_append_menu_action(menu, act, node);
	}
}

static void
add_custom_action(GntMenu *menu, const char *label, PurpleCallback callback,
		gpointer data)
{
	PurpleActionMenu *action = purple_action_menu_new(label, callback, data, NULL);
	finch_append_menu_action(menu, action, NULL);
}

static void
chat_components_edit_ok(PurpleChat *chat, PurpleRequestFields *allfields)
{
	GList *groups, *fields;

	for (groups = purple_request_fields_get_groups(allfields); groups; groups = groups->next) {
		fields = purple_request_field_group_get_fields(groups->data);
		for (; fields; fields = fields->next) {
			PurpleRequestField *field = fields->data;
			const char *id;
			char *val;

			id = purple_request_field_get_id(field);
			if (purple_request_field_get_field_type(field) == PURPLE_REQUEST_FIELD_INTEGER)
				val = g_strdup_printf("%d", purple_request_field_int_get_value(field));
			else
				val = g_strdup(purple_request_field_string_get_value(field));

			if (!val) {
				g_hash_table_remove(purple_chat_get_components(chat), id);
			} else {
				g_hash_table_replace(purple_chat_get_components(chat), g_strdup(id), val);  /* val should not be free'd */
			}
		}
	}
}

static void
chat_components_edit(PurpleBlistNode *selected, PurpleChat *chat)
{
	PurpleRequestFields *fields = purple_request_fields_new();
	PurpleRequestFieldGroup *group = purple_request_field_group_new(NULL);
	PurpleRequestField *field;
	GList *parts, *iter;
	PurpleProtocol *protocol;
	PurpleProtocolChatEntry *pce;
	PurpleConnection *gc;

	purple_request_fields_add_group(fields, group);

	gc = purple_account_get_connection(purple_chat_get_account(chat));
	protocol = purple_connection_get_protocol(gc);
	parts = purple_protocol_chat_info(PURPLE_PROTOCOL_CHAT(protocol), gc);

	for (iter = parts; iter; iter = iter->next) {
		pce = iter->data;
		if (pce->is_int) {
			int val;
			const char *str = g_hash_table_lookup(purple_chat_get_components(chat), pce->identifier);
			if (!str || sscanf(str, "%d", &val) != 1)
				val = pce->min;
			field = purple_request_field_int_new(pce->identifier, pce->label, val, INT_MIN, INT_MAX);
		} else {
			field = purple_request_field_string_new(pce->identifier, pce->label,
					g_hash_table_lookup(purple_chat_get_components(chat), pce->identifier), FALSE);
			if (pce->secret)
				purple_request_field_string_set_masked(field, TRUE);
		}

		if (pce->required)
			purple_request_field_set_required(field, TRUE);

		purple_request_field_group_add_field(group, field);
		g_free(pce);
	}

	g_list_free(parts);

	purple_request_fields(NULL, _("Edit Chat"), NULL, _("Please Update the necessary fields."),
			fields, _("Edit"), G_CALLBACK(chat_components_edit_ok), _("Cancel"), NULL,
			NULL, chat);
}

static void
autojoin_toggled(GntMenuItem *item, gpointer data)
{
	PurpleActionMenu *action = data;
	purple_blist_node_set_bool(purple_action_menu_get_data(action), "gnt-autojoin",
				gnt_menuitem_check_get_checked(GNT_MENU_ITEM_CHECK(item)));
}

static void
create_chat_menu(GntMenu *menu, PurpleChat *chat)
{
	PurpleActionMenu *action = purple_action_menu_new(_("Auto-join"), NULL, chat, NULL);
	GntMenuItem *check = gnt_menuitem_check_new(
			purple_action_menu_get_label(action));
	gnt_menuitem_check_set_checked(GNT_MENU_ITEM_CHECK(check),
				purple_blist_node_get_bool((PurpleBlistNode*)chat, "gnt-autojoin"));
	gnt_menu_add_item(menu, check);
	gnt_menuitem_set_callback(check, autojoin_toggled, action);
	g_signal_connect_swapped(G_OBJECT(menu), "destroy",
			G_CALLBACK(purple_action_menu_free), action);

	/* Protocol actions */
	append_proto_menu(menu,
			purple_account_get_connection(purple_chat_get_account(chat)),
			(PurpleBlistNode*)chat);

	add_custom_action(menu, _("Edit Settings"), (PurpleCallback)chat_components_edit, chat);
}

static void
finch_add_buddy(PurpleBlistNode *selected, PurpleGroup *grp)
{
	purple_blist_request_add_buddy(NULL, NULL, grp ? purple_group_get_name(grp) : NULL, NULL);
}

static void
finch_add_group(PurpleBlistNode *selected, PurpleGroup *grp)
{
	purple_blist_request_add_group();
}

static void
finch_add_chat(PurpleBlistNode *selected, PurpleGroup *grp)
{
	purple_blist_request_add_chat(NULL, grp, NULL, NULL);
}

static void
create_group_menu(GntMenu *menu, PurpleGroup *group)
{
	add_custom_action(menu, _("Add Buddy"),
			PURPLE_CALLBACK(finch_add_buddy), group);
	add_custom_action(menu, _("Add Chat"),
			PURPLE_CALLBACK(finch_add_chat), group);
	add_custom_action(menu, _("Add Group"),
			PURPLE_CALLBACK(finch_add_group), group);
}

gpointer finch_retrieve_user_info(PurpleConnection *conn, const char *name)
{
	PurpleNotifyUserInfo *info = purple_notify_user_info_new();
	gpointer uihandle;
	purple_notify_user_info_add_pair_plaintext(info, _("Information"), _("Retrieving..."));
	uihandle = purple_notify_userinfo(conn, name, info, NULL, NULL);
	purple_notify_user_info_destroy(info);

	purple_serv_get_info(conn, name);
	return uihandle;
}

static void
finch_blist_get_buddy_info_cb(PurpleBlistNode *selected, PurpleBuddy *buddy)
{
	finch_retrieve_user_info(purple_account_get_connection(purple_buddy_get_account(buddy)), purple_buddy_get_name(buddy));
}

static void
finch_blist_menu_send_file_cb(PurpleBlistNode *selected, PurpleBuddy *buddy)
{
	purple_serv_send_file(purple_account_get_connection(purple_buddy_get_account(buddy)), purple_buddy_get_name(buddy), NULL);
}

static void
toggle_block_buddy(GntMenuItem *item, gpointer buddy)
{
	gboolean block = gnt_menuitem_check_get_checked(GNT_MENU_ITEM_CHECK(item));
	PurpleAccount *account = purple_buddy_get_account(buddy);
	const char *name = purple_buddy_get_name(buddy);

	block ? purple_account_privacy_deny(account, name) :
		purple_account_privacy_allow(account, name);
}

static void
toggle_show_offline(GntMenuItem *item, gpointer buddy)
{
	purple_blist_node_set_bool(buddy, "show_offline",
			!purple_blist_node_get_bool(buddy, "show_offline"));
	if (!ggblist->manager->can_add_node(buddy))
		node_remove(purple_blist_get_default(), buddy);
	else
		node_update(purple_blist_get_default(), buddy);
}

static void
create_buddy_menu(GntMenu *menu, PurpleBuddy *buddy)
{
	PurpleAccount *account;
	gboolean permitted;
	GntMenuItem *item;
	PurpleProtocol *protocol;
	PurpleConnection *gc = purple_account_get_connection(purple_buddy_get_account(buddy));

	protocol = purple_connection_get_protocol(gc);
	if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info))
	{
		add_custom_action(menu, _("Get Info"),
				PURPLE_CALLBACK(finch_blist_get_buddy_info_cb), buddy);
	}

	if (PURPLE_IS_PROTOCOL_XFER(protocol))
	{
		if (purple_protocol_xfer_can_receive(
			PURPLE_PROTOCOL_XFER(protocol),
			gc,
			purple_buddy_get_name(buddy))
		) {
			add_custom_action(menu, _("Send File"),
					PURPLE_CALLBACK(finch_blist_menu_send_file_cb), buddy);
		}
	}

	account = purple_buddy_get_account(buddy);
	permitted = purple_account_privacy_check(account, purple_buddy_get_name(buddy));

	item = gnt_menuitem_check_new(_("Blocked"));
	gnt_menuitem_check_set_checked(GNT_MENU_ITEM_CHECK(item), !permitted);
	gnt_menuitem_set_callback(item, toggle_block_buddy, buddy);
	gnt_menu_add_item(menu, item);

	item = gnt_menuitem_check_new(_("Show when offline"));
	gnt_menuitem_check_set_checked(GNT_MENU_ITEM_CHECK(item), purple_blist_node_get_bool((PurpleBlistNode*)buddy, "show_offline"));
	gnt_menuitem_set_callback(item, toggle_show_offline, buddy);
	gnt_menu_add_item(menu, item);

	/* Protocol actions */
	append_proto_menu(menu,
			purple_account_get_connection(purple_buddy_get_account(buddy)),
			(PurpleBlistNode*)buddy);
}

static void
append_extended_menu(GntMenu *menu, PurpleBlistNode *node)
{
	GList *iter;

	for (iter = purple_blist_node_get_extended_menu(node);
			iter; iter = g_list_delete_link(iter, iter))
	{
		finch_append_menu_action(menu, iter->data, node);
	}
}

/* Xerox'd from gtkdialogs.c:purple_gtkdialogs_remove_contact_cb */
static void
remove_contact(PurpleContact *contact)
{
	PurpleBlistNode *bnode, *cnode;
	PurpleGroup *group;

	cnode = (PurpleBlistNode *)contact;
	group = (PurpleGroup*)purple_blist_node_get_parent(cnode);
	for (bnode = purple_blist_node_get_first_child(cnode); bnode; bnode = purple_blist_node_get_sibling_next(bnode)) {
		PurpleBuddy *buddy = (PurpleBuddy*)bnode;
		PurpleAccount *account = purple_buddy_get_account(buddy);
		if (purple_account_is_connected(account))
			purple_account_remove_buddy(account, buddy, group);
	}
	purple_blist_remove_contact(contact);
}

static void
rename_blist_node(PurpleBlistNode *node, const char *newname)
{
	const char *name = newname;
	if (name && !*name)
		name = NULL;

	if (PURPLE_IS_CONTACT(node)) {
		PurpleContact *contact = (PurpleContact*)node;
		PurpleBuddy *buddy = purple_contact_get_priority_buddy(contact);
		purple_contact_set_alias(contact, name);
		purple_buddy_set_local_alias(buddy, name);
		purple_serv_alias_buddy(buddy);
	} else if (PURPLE_IS_BUDDY(node)) {
		purple_buddy_set_local_alias((PurpleBuddy*)node, name);
		purple_serv_alias_buddy((PurpleBuddy*)node);
	} else if (PURPLE_IS_CHAT(node))
		purple_chat_set_alias((PurpleChat*)node, name);
	else if (PURPLE_IS_GROUP(node) && (name != NULL))
		purple_group_set_name((PurpleGroup*)node, name);
	else
		g_return_if_reached();
}

static void
finch_blist_rename_node_cb(PurpleBlistNode *selected, PurpleBlistNode *node)
{
	const char *name = NULL;
	char *prompt;
	const char *text;

	if (PURPLE_IS_CONTACT(node))
		name = purple_contact_get_alias((PurpleContact*)node);
	else if (PURPLE_IS_BUDDY(node))
		name = purple_buddy_get_contact_alias((PurpleBuddy*)node);
	else if (PURPLE_IS_CHAT(node))
		name = purple_chat_get_name((PurpleChat*)node);
	else if (PURPLE_IS_GROUP(node))
		name = purple_group_get_name((PurpleGroup*)node);
	else
		g_return_if_reached();

	prompt = g_strdup_printf(_("Please enter the new name for %s"), name);

	text = PURPLE_IS_GROUP(node) ? _("Rename") : _("Set Alias");
	purple_request_input(node, text, prompt, _("Enter empty string to reset the name."),
			name, FALSE, FALSE, NULL, text, G_CALLBACK(rename_blist_node),
			_("Cancel"), NULL,
			NULL, node);

	g_free(prompt);
}


static void showlog_cb(PurpleBlistNode *sel, PurpleBlistNode *node)
{
	PurpleLogType type;
	PurpleAccount *account;
	char *name = NULL;

	if (PURPLE_IS_BUDDY(node)) {
		PurpleBuddy *b = (PurpleBuddy*) node;
		type = PURPLE_LOG_IM;
		name = g_strdup(purple_buddy_get_name(b));
		account = purple_buddy_get_account(b);
	} else if (PURPLE_IS_CHAT(node)) {
		PurpleChat *c = (PurpleChat*) node;
		PurpleProtocol *protocol = NULL;
		type = PURPLE_LOG_CHAT;
		account = purple_chat_get_account(c);
		protocol = purple_account_get_protocol(account);
		if (protocol) {
			name = purple_protocol_chat_get_name(PURPLE_PROTOCOL_CHAT(protocol), purple_chat_get_components(c));
		}
	} else if (PURPLE_IS_CONTACT(node)) {
		finch_log_show_contact((PurpleContact *)node);
		return;
	} else {
		/* This callback should not have been registered for a node
		 * that doesn't match the type of one of the blocks above. */
		g_return_if_reached();
	}

	if (name && account) {
		finch_log_show(type, name, account);
		g_free(name);
	}
}


/* Xeroxed from gtkdialogs.c:purple_gtkdialogs_remove_group_cb*/
static void
remove_group(PurpleGroup *group)
{
	PurpleBlistNode *cnode, *bnode;

	cnode = purple_blist_node_get_first_child(((PurpleBlistNode*)group));

	while (cnode) {
		if (PURPLE_IS_CONTACT(cnode)) {
			bnode = purple_blist_node_get_first_child(cnode);
			cnode = purple_blist_node_get_sibling_next(cnode);
			while (bnode) {
				PurpleBuddy *buddy;
				if (PURPLE_IS_BUDDY(bnode)) {
					PurpleAccount *account;
					buddy = (PurpleBuddy*)bnode;
					bnode = purple_blist_node_get_sibling_next(bnode);
					account = purple_buddy_get_account(buddy);
					if (purple_account_is_connected(account)) {
						purple_account_remove_buddy(account, buddy, group);
						purple_blist_remove_buddy(buddy);
					}
				} else {
					bnode = purple_blist_node_get_sibling_next(bnode);
				}
			}
		} else if (PURPLE_IS_CHAT(cnode)) {
			PurpleChat *chat = (PurpleChat *)cnode;
			cnode = purple_blist_node_get_sibling_next(cnode);
			if (purple_account_is_connected(purple_chat_get_account(chat)))
				purple_blist_remove_chat(chat);
		} else {
			cnode = purple_blist_node_get_sibling_next(cnode);
		}
	}

	purple_blist_remove_group(group);
}

static void
finch_blist_remove_node(PurpleBlistNode *node)
{
	if (PURPLE_IS_CONTACT(node)) {
		remove_contact((PurpleContact*)node);
	} else if (PURPLE_IS_BUDDY(node)) {
		PurpleBuddy *buddy = (PurpleBuddy*)node;
		PurpleGroup *group = purple_buddy_get_group(buddy);
		purple_account_remove_buddy(purple_buddy_get_account(buddy), buddy, group);
		purple_blist_remove_buddy(buddy);
	} else if (PURPLE_IS_CHAT(node)) {
		purple_blist_remove_chat((PurpleChat*)node);
	} else if (PURPLE_IS_GROUP(node)) {
		remove_group((PurpleGroup*)node);
	}
}

static void
finch_blist_remove_node_cb(PurpleBlistNode *selected, PurpleBlistNode *node)
{
	PurpleAccount *account = NULL;
	char *primary;
	const char *name, *sec = NULL;

	if (PURPLE_IS_CONTACT(node)) {
		PurpleContact *c = (PurpleContact*)node;
		name = purple_contact_get_alias(c);
		if (purple_counting_node_get_total_size(PURPLE_COUNTING_NODE(c)) > 1)
			sec = _("Removing this contact will also remove all the buddies in the contact");
	} else if (PURPLE_IS_BUDDY(node)) {
		name = purple_buddy_get_name((PurpleBuddy*)node);
		account = purple_buddy_get_account((PurpleBuddy*)node);
	} else if (PURPLE_IS_CHAT(node)) {
		name = purple_chat_get_name((PurpleChat*)node);
	} else if (PURPLE_IS_GROUP(node)) {
		name = purple_group_get_name((PurpleGroup*)node);
		sec = _("Removing this group will also remove all the buddies in the group");
	}
	else
		return;

	primary = g_strdup_printf(_("Are you sure you want to remove %s?"), name);

	/* XXX: anything to do with the returned ui-handle? */
	purple_request_action(node, _("Confirm Remove"),
			primary, sec,
			1,
			purple_request_cpar_from_account(account),
			node, 2,
			_("Remove"), finch_blist_remove_node,
			_("Cancel"), NULL);
	g_free(primary);
}

static void
finch_blist_toggle_tag_buddy(PurpleBlistNode *node)
{
	GList *iter;
	if (node == NULL)
		return;
	if (ggblist->tagged && (iter = g_list_find(ggblist->tagged, node)) != NULL) {
		ggblist->tagged = g_list_delete_link(ggblist->tagged, iter);
	} else {
		ggblist->tagged = g_list_prepend(ggblist->tagged, node);
	}
	if (PURPLE_IS_CONTACT(node))
		update_buddy_display(purple_contact_get_priority_buddy(PURPLE_CONTACT(node)), ggblist);
	else if (PURPLE_IS_BUDDY(node))
		update_buddy_display((PurpleBuddy*)node, ggblist);
	else
		update_node_display(node, ggblist);
}

static void
finch_blist_place_tagged(PurpleBlistNode *target)
{
	PurpleGroup *tg = NULL;
	PurpleContact *tc = NULL;

	if (PURPLE_IS_GROUP(target))
		tg = (PurpleGroup*)target;
	else if (PURPLE_IS_BUDDY(target)) {
		tc = (PurpleContact*)purple_blist_node_get_parent(target);
		tg = (PurpleGroup*)purple_blist_node_get_parent((PurpleBlistNode*)tc);
	} else if (PURPLE_IS_CONTACT(target)) {
		tc = (PurpleContact *)target;
		tg = (PurpleGroup *)purple_blist_node_get_parent(target);
	} else if (PURPLE_IS_CHAT(target)) {
		tg = (PurpleGroup*)purple_blist_node_get_parent(target);
	} else {
		return;
	}

	if (ggblist->tagged) {
		GList *list = ggblist->tagged;
		ggblist->tagged = NULL;
		while (list) {
			PurpleBlistNode *node = list->data;
			list = g_list_delete_link(list, list);

			if (PURPLE_IS_GROUP(node)) {
				update_node_display(node, ggblist);
				/* Add the group after the current group */
				purple_blist_add_group((PurpleGroup*)node, (PurpleBlistNode*)tg);
			} else if (PURPLE_IS_CONTACT(node)) {
				update_buddy_display(purple_contact_get_priority_buddy((PurpleContact*)node), ggblist);
				if (PURPLE_BLIST_NODE(tg) == target) {
					/* The target is a group, just add the contact to the group. */
					purple_blist_add_contact((PurpleContact*)node, tg, NULL);
				} else if (tc) {
					/* The target is either a buddy, or a contact. Merge with that contact. */
					purple_contact_merge((PurpleContact*)node, (PurpleBlistNode*)tc);
				} else {
					/* The target is a chat. Add the contact to the group after this chat. */
					purple_blist_add_contact((PurpleContact*)node, NULL, target);
				}
			} else if (PURPLE_IS_BUDDY(node)) {
				update_buddy_display((PurpleBuddy*)node, ggblist);
				if (PURPLE_BLIST_NODE(tg) == target) {
					/* The target is a group. Add this buddy in a new contact under this group. */
					purple_blist_add_buddy((PurpleBuddy*)node, NULL, tg, NULL);
				} else if (PURPLE_IS_CONTACT(target)) {
					/* Add to the contact. */
					purple_blist_add_buddy((PurpleBuddy*)node, tc, NULL, NULL);
				} else if (PURPLE_IS_BUDDY(target)) {
					/* Add to the contact after the selected buddy. */
					purple_blist_add_buddy((PurpleBuddy*)node, NULL, NULL, target);
				} else if (PURPLE_IS_CHAT(target)) {
					/* Add to the selected chat's group. */
					purple_blist_add_buddy((PurpleBuddy*)node, NULL, tg, NULL);
				}
			} else if (PURPLE_IS_CHAT(node)) {
				update_node_display(node, ggblist);
				if (PURPLE_BLIST_NODE(tg) == target)
					purple_blist_add_chat((PurpleChat*)node, tg, NULL);
				else
					purple_blist_add_chat((PurpleChat*)node, NULL, target);
			}
		}
	}
}

static void
context_menu_destroyed(GntWidget *widget, FinchBuddyList *ggblist)
{
	ggblist->context = NULL;
}

static void
draw_context_menu(FinchBuddyList *ggblist)
{
	PurpleBlistNode *node = NULL;
	GntWidget *context = NULL;
	GntTree *tree = NULL;
	int x, y, top, width;
	char *title = NULL;

	if (ggblist->context)
		return;

	tree = GNT_TREE(ggblist->tree);

	node = gnt_tree_get_selection_data(tree);
	if (node && !(PURPLE_IS_BUDDY(node) || PURPLE_IS_CONTACT(node) ||
	              PURPLE_IS_GROUP(node) || PURPLE_IS_CHAT(node)))
		return;

	if (ggblist->tooltip)
		remove_tooltip(ggblist);

	ggblist->cnode = node;

	ggblist->context = context = gnt_menu_new(GNT_MENU_POPUP);
	g_signal_connect(G_OBJECT(context), "destroy", G_CALLBACK(context_menu_destroyed), ggblist);
	g_signal_connect(G_OBJECT(context), "hide", G_CALLBACK(gnt_widget_destroy), NULL);

	if (!node) {
		create_group_menu(GNT_MENU(context), NULL);
		title = g_strdup(_("Buddy List"));
	} else if (PURPLE_IS_CONTACT(node)) {
		ggblist->cnode = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(node)));
		create_buddy_menu(GNT_MENU(context), (PurpleBuddy*)ggblist->cnode);
		title = g_strdup(purple_contact_get_alias((PurpleContact*)node));
	} else if (PURPLE_IS_BUDDY(node)) {
		PurpleBuddy *buddy = (PurpleBuddy *)node;
		create_buddy_menu(GNT_MENU(context), buddy);
		title = g_strdup(purple_buddy_get_name(buddy));
	} else if (PURPLE_IS_CHAT(node)) {
		PurpleChat *chat = (PurpleChat*)node;
		create_chat_menu(GNT_MENU(context), chat);
		title = g_strdup(purple_chat_get_name(chat));
	} else if (PURPLE_IS_GROUP(node)) {
		PurpleGroup *group = (PurpleGroup *)node;
		create_group_menu(GNT_MENU(context), group);
		title = g_strdup(purple_group_get_name(group));
	}

	append_extended_menu(GNT_MENU(context), node);

	/* These are common for everything */
	if (node) {
		add_custom_action(GNT_MENU(context),
				PURPLE_IS_GROUP(node) ? _("Rename") : _("Alias"),
				PURPLE_CALLBACK(finch_blist_rename_node_cb), node);
		add_custom_action(GNT_MENU(context), _("Remove"),
				PURPLE_CALLBACK(finch_blist_remove_node_cb), node);

		if (ggblist->tagged && (PURPLE_IS_CONTACT(node)
				|| PURPLE_IS_GROUP(node))) {
			add_custom_action(GNT_MENU(context), _("Place tagged"),
					PURPLE_CALLBACK(finch_blist_place_tagged), node);
		}

		if (PURPLE_IS_BUDDY(node) || PURPLE_IS_CONTACT(node)) {
			add_custom_action(GNT_MENU(context), _("Toggle Tag"),
					PURPLE_CALLBACK(finch_blist_toggle_tag_buddy), node);
		}
		if (!PURPLE_IS_GROUP(node)) {
			add_custom_action(GNT_MENU(context), _("View Log"),
					PURPLE_CALLBACK(showlog_cb), node);
		}
	}

	/* Set the position for the popup */
	gnt_widget_get_position(GNT_WIDGET(tree), &x, &y);
	gnt_widget_get_size(GNT_WIDGET(tree), &width, NULL);
	top = gnt_tree_get_selection_visible_line(tree);

	x += width;
	y += top - 1;

	gnt_widget_set_position(context, x, y);
	gnt_screen_menu_show(GNT_MENU(context));
	g_free(title);
}

static void
tooltip_for_buddy(PurpleBuddy *buddy, GString *str, gboolean full)
{
	PurpleProtocol *protocol;
	PurpleAccount *account;
	PurpleNotifyUserInfo *user_info;
	PurplePresence *presence;
	const char *alias = purple_buddy_get_alias(buddy);
	char *tmp, *strip;

	user_info = purple_notify_user_info_new();

	account = purple_buddy_get_account(buddy);
	presence = purple_buddy_get_presence(buddy);

	if (!full || g_utf8_collate(purple_buddy_get_name(buddy), alias)) {
		purple_notify_user_info_add_pair_plaintext(user_info, _("Nickname"), alias);
	}

	tmp = g_strdup_printf("%s (%s)",
			purple_account_get_username(account),
			purple_account_get_protocol_name(account));
	purple_notify_user_info_add_pair_plaintext(user_info, _("Account"), tmp);
	g_free(tmp);

	protocol = purple_account_get_protocol(account);
	if (protocol) {
		purple_protocol_client_tooltip_text(PURPLE_PROTOCOL_CLIENT(protocol), buddy, user_info, full);
	}

	if (purple_prefs_get_bool("/finch/blist/idletime")) {
		PurplePresence *pre = purple_buddy_get_presence(buddy);
		if (purple_presence_is_idle(pre)) {
			time_t idle = purple_presence_get_idle_time(pre);
			if (idle > 0) {
				char *st = purple_str_seconds_to_string(time(NULL) - idle);
				purple_notify_user_info_add_pair_plaintext(user_info, _("Idle"), st);
				g_free(st);
			}
		}
	}

	tmp = purple_notify_user_info_get_text_with_newline(user_info, "<BR>");
	purple_notify_user_info_destroy(user_info);

	strip = purple_markup_strip_html(tmp);
	g_string_append(str, strip);

	if (purple_presence_is_status_primitive_active(presence, PURPLE_STATUS_MOBILE)) {
		g_string_append(str, "\n");
		g_string_append(str, _("On Mobile"));
	}

	g_free(strip);
	g_free(tmp);
}

static GString*
make_sure_text_fits(GString *string)
{
	int maxw = getmaxx(stdscr) - 3;
	char *str = gnt_util_onscreen_fit_string(string->str, maxw);
	string = g_string_assign(string, str);
	g_free(str);
	return string;
}

static gboolean
draw_tooltip_real(FinchBuddyList *ggblist)
{
	PurpleBlistNode *node;
	int x, y, top, width, w, h;
	GString *str = NULL;
	GntTree *tree;
	GntWidget *widget, *box, *tv;
	char *title = NULL;

	widget = ggblist->tree;
	tree = GNT_TREE(widget);

	if (!gnt_widget_has_focus(ggblist->tree) ||
			(ggblist->context && gnt_widget_get_visible(ggblist->context)))
		return FALSE;

	if (ggblist->tooltip)
	{
		/* XXX: Once we can properly redraw on expose events, this can be removed at the end
		 * to avoid the blinking*/
		remove_tooltip(ggblist);
	}

	node = gnt_tree_get_selection_data(tree);
	if (!node)
		return FALSE;

	if (!ggblist->manager->create_tooltip(node, &str, &title))
		return FALSE;

	gnt_widget_get_position(widget, &x, &y);
	gnt_widget_get_size(widget, &width, NULL);
	top = gnt_tree_get_selection_visible_line(tree);

	x += width;
	y += top - 1;

	box = gnt_box_new(FALSE, FALSE);
	gnt_box_set_toplevel(GNT_BOX(box), TRUE);
	gnt_widget_set_has_shadow(box, FALSE);
	gnt_box_set_title(GNT_BOX(box), title);

	str = make_sure_text_fits(str);
	gnt_util_get_text_bound(str->str, &w, &h);
	h = MAX(1, h);
	tv = gnt_text_view_new();
	gnt_widget_set_size(tv, w + 1, h);
	gnt_text_view_set_flag(GNT_TEXT_VIEW(tv), GNT_TEXT_VIEW_NO_SCROLL);
	gnt_box_add_widget(GNT_BOX(box), tv);

	if (x + w >= getmaxx(stdscr))
		x -= w + width + 2;
	gnt_widget_set_position(box, x, y);
	gnt_widget_set_take_focus(box, FALSE);
	gnt_widget_set_transient(box, TRUE);
	gnt_widget_draw(box);

	gnt_text_view_append_text_with_flags(GNT_TEXT_VIEW(tv), str->str, GNT_TEXT_FLAG_NORMAL);
	gnt_text_view_scroll(GNT_TEXT_VIEW(tv), 0);

	g_free(title);
	g_string_free(str, TRUE);
	ggblist->tooltip = box;
	ggblist->tnode = node;

	gnt_widget_set_name(ggblist->tooltip, "tooltip");
	return FALSE;
}

static void
draw_tooltip(FinchBuddyList *ggblist)
{
	/* When an account has signed off, it removes one buddy at a time.
	 * Drawing the tooltip after removing each buddy is expensive. On
	 * top of that, if the selected buddy belongs to the disconnected
	 * account, then retreiving the tooltip for that causes crash. So
	 * let's make sure we wait for all the buddies to be removed first.*/
	int id = g_timeout_add(0, (GSourceFunc)draw_tooltip_real, ggblist);
	g_object_set_data_full(G_OBJECT(ggblist->window), "draw_tooltip_calback",
				GINT_TO_POINTER(id), (GDestroyNotify)g_source_remove);
}

static void
selection_changed(GntWidget *widget, gpointer old, gpointer current,
                  FinchBuddyList *ggblist)
{
	remove_peripherals(ggblist);
	draw_tooltip(ggblist);
}

static gboolean
context_menu(GntWidget *widget, FinchBuddyList *ggblist)
{
	draw_context_menu(ggblist);
	return TRUE;
}

static gboolean
key_pressed(GntWidget *widget, const char *text, FinchBuddyList *ggblist)
{
	if (text[0] == 27 && text[1] == 0) {
		/* Escape was pressed */
		if (gnt_tree_is_searching(GNT_TREE(ggblist->tree)))
			gnt_bindable_perform_action_named(GNT_BINDABLE(ggblist->tree), "end-search", NULL);
		remove_peripherals(ggblist);
	} else if (purple_strequal(text, GNT_KEY_INS)) {
		PurpleBlistNode *node = gnt_tree_get_selection_data(GNT_TREE(ggblist->tree));
		purple_blist_request_add_buddy(NULL, NULL,
				node && PURPLE_IS_GROUP(node) ? purple_group_get_name(PURPLE_GROUP(node)) : NULL,
				NULL);
	} else if (!gnt_tree_is_searching(GNT_TREE(ggblist->tree))) {
		if (purple_strequal(text, "t")) {
			finch_blist_toggle_tag_buddy(gnt_tree_get_selection_data(GNT_TREE(ggblist->tree)));
			gnt_bindable_perform_action_named(GNT_BINDABLE(ggblist->tree), "move-down", NULL);
		} else if (purple_strequal(text, "a")) {
			finch_blist_place_tagged(gnt_tree_get_selection_data(GNT_TREE(ggblist->tree)));
		} else
			return FALSE;
	} else
		return FALSE;

	return TRUE;
}

static void
update_node_display(PurpleBlistNode *node, FinchBuddyList *ggblist)
{
	GntTextFormatFlags flag = get_blist_node_flag(ggblist, node);
	gnt_tree_set_row_flags(GNT_TREE(ggblist->tree), node, flag);
}

static void
update_buddy_display(PurpleBuddy *buddy, FinchBuddyList *ggblist)
{
	PurpleContact *contact;

	contact = purple_buddy_get_contact(buddy);

	gnt_tree_change_text(GNT_TREE(ggblist->tree), buddy, 0, get_display_name((PurpleBlistNode*)buddy));
	gnt_tree_change_text(GNT_TREE(ggblist->tree), contact, 0, get_display_name((PurpleBlistNode*)contact));

	blist_update_row_flags(ggblist, (PurpleBlistNode *)buddy);
	if (buddy == purple_contact_get_priority_buddy(contact))
		blist_update_row_flags(ggblist, (PurpleBlistNode *)contact);

	if (ggblist->tnode == (PurpleBlistNode *)buddy) {
		draw_tooltip(ggblist);
	}
}

static void
buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *now,
                     FinchBuddyList *ggblist)
{
	update_buddy_display(buddy, ggblist);
}

static void
buddy_idle_changed(PurpleBuddy *buddy, int old, int new,
                   FinchBuddyList *ggblist)
{
	update_buddy_display(buddy, ggblist);
}

static void
remove_peripherals(FinchBuddyList *ggblist)
{
	if (ggblist->tooltip)
		remove_tooltip(ggblist);
	else if (ggblist->context)
		gnt_widget_destroy(ggblist->context);
}

static void
size_changed_cb(GntWidget *w, int wi, int h)
{
	int width, height;
	gnt_widget_get_size(w, &width, &height);
	purple_prefs_set_int(PREF_ROOT "/size/width", width);
	purple_prefs_set_int(PREF_ROOT "/size/height", height);
}

static void
save_position_cb(GntWidget *w, int x, int y)
{
	purple_prefs_set_int(PREF_ROOT "/position/x", x);
	purple_prefs_set_int(PREF_ROOT "/position/y", y);
}

static void
reset_blist_window(GntWidget *window, gpointer null)
{
	purple_signals_disconnect_by_handle(finch_blist_get_handle());

	if (ggblist->typing)
		g_source_remove(ggblist->typing);
	remove_peripherals(ggblist);
	if (ggblist->tagged)
		g_list_free(ggblist->tagged);

	if (ggblist->new_group_timeout)
		g_source_remove(ggblist->new_group_timeout);
	if (ggblist->new_group)
		g_list_free(ggblist->new_group);

	ggblist = NULL;
}

static void
populate_buddylist(void)
{
	PurpleBlistNode *node;
	PurpleBuddyList *list;

	if (ggblist->manager->init)
		ggblist->manager->init();

	if (purple_strequal(purple_prefs_get_string(PREF_ROOT "/sort_type"), "text")) {
		gnt_tree_set_compare_func(GNT_TREE(ggblist->tree),
			(GCompareFunc)blist_node_compare_text);
	} else if (purple_strequal(purple_prefs_get_string(PREF_ROOT "/sort_type"), "status")) {
		gnt_tree_set_compare_func(GNT_TREE(ggblist->tree),
			(GCompareFunc)blist_node_compare_status);
	} else if (purple_strequal(purple_prefs_get_string(PREF_ROOT "/sort_type"), "log")) {
		gnt_tree_set_compare_func(GNT_TREE(ggblist->tree),
			(GCompareFunc)blist_node_compare_log);
	}

	list = purple_blist_get_default();
	node = purple_blist_get_root(list);
	while (node)
	{
		node_update(list, node);
		node = purple_blist_node_next(node, FALSE);
	}
}

static void
destroy_status_list(GList *list)
{
	g_list_free_full(list, g_free);
}

static void
populate_status_dropdown(void)
{
	int i;
	GList *iter;
	GList *items = NULL;
	StatusBoxItem *item = NULL;

	/* First the primitives */
	PurpleStatusPrimitive prims[] = {PURPLE_STATUS_AVAILABLE, PURPLE_STATUS_AWAY,
			PURPLE_STATUS_INVISIBLE, PURPLE_STATUS_OFFLINE, PURPLE_STATUS_UNSET};

	gnt_combo_box_remove_all(GNT_COMBO_BOX(ggblist->status));

	for (i = 0; prims[i] != PURPLE_STATUS_UNSET; i++)
	{
		item = g_new0(StatusBoxItem, 1);
		item->type = STATUS_PRIMITIVE;
		item->u.prim = prims[i];
		items = g_list_prepend(items, item);
		gnt_combo_box_add_data(GNT_COMBO_BOX(ggblist->status), item,
				purple_primitive_get_name_from_type(prims[i]));
	}

	/* Now the popular statuses */
	for (iter = purple_savedstatuses_get_popular(6); iter; iter = g_list_delete_link(iter, iter))
	{
		item = g_new0(StatusBoxItem, 1);
		item->type = STATUS_SAVED_POPULAR;
		item->u.saved = iter->data;
		items = g_list_prepend(items, item);
		gnt_combo_box_add_data(GNT_COMBO_BOX(ggblist->status), item,
				purple_savedstatus_get_title(iter->data));
	}

	/* New savedstatus */
	item = g_new0(StatusBoxItem, 1);
	item->type = STATUS_SAVED_NEW;
	items = g_list_prepend(items, item);
	gnt_combo_box_add_data(GNT_COMBO_BOX(ggblist->status), item,
			_("New..."));

	/* More savedstatuses */
	item = g_new0(StatusBoxItem, 1);
	item->type = STATUS_SAVED_ALL;
	items = g_list_prepend(items, item);
	gnt_combo_box_add_data(GNT_COMBO_BOX(ggblist->status), item,
			_("Saved..."));

	/* The keys for the combobox are created here, and never used
	 * anywhere else. So make sure the keys are freed when the widget
	 * is destroyed. */
	g_object_set_data_full(G_OBJECT(ggblist->status), "list of statuses",
			items, (GDestroyNotify)destroy_status_list);
}

static void
redraw_blist(const char *name, PurplePrefType type, gconstpointer val, gpointer data)
{
	PurpleBlistNode *sel;
	FinchBlistManager *manager;

	if (ggblist == NULL)
		return;

	manager = finch_blist_manager_find(purple_prefs_get_string(PREF_ROOT "/grouping"));
	if (manager == NULL)
		manager = &default_manager;
	if (ggblist->manager != manager) {
		if (ggblist->manager->uninit)
			ggblist->manager->uninit();

		ggblist->manager = manager;
		if (manager->can_add_node == NULL)
			manager->can_add_node = default_can_add_node;
		if (manager->find_parent == NULL)
			manager->find_parent = default_find_parent;
		if (manager->create_tooltip == NULL)
			manager->create_tooltip = default_create_tooltip;
	}

	if (ggblist->window == NULL)
		return;

	sel = gnt_tree_get_selection_data(GNT_TREE(ggblist->tree));
	gnt_tree_remove_all(GNT_TREE(ggblist->tree));

	populate_buddylist();
	gnt_tree_set_selected(GNT_TREE(ggblist->tree), sel);
	draw_tooltip(ggblist);
}

void finch_blist_init()
{
	color_available = gnt_style_get_color(NULL, "color-available");
	if (!color_available)
		color_available = gnt_color_add_pair(COLOR_GREEN, -1);
	color_away = gnt_style_get_color(NULL, "color-away");
	if (!color_away)
		color_away = gnt_color_add_pair(COLOR_BLUE, -1);
	color_idle = gnt_style_get_color(NULL, "color-idle");
	if (!color_idle)
		color_idle = gnt_color_add_pair(COLOR_CYAN, -1);
	color_offline = gnt_style_get_color(NULL, "color-offline");
	if (!color_offline)
		color_offline = gnt_color_add_pair(COLOR_RED, -1);

	purple_prefs_add_none(PREF_ROOT);
	purple_prefs_add_none(PREF_ROOT "/size");
	purple_prefs_add_int(PREF_ROOT "/size/width", 20);
	purple_prefs_add_int(PREF_ROOT "/size/height", 17);
	purple_prefs_add_none(PREF_ROOT "/position");
	purple_prefs_add_int(PREF_ROOT "/position/x", 0);
	purple_prefs_add_int(PREF_ROOT "/position/y", 0);
	purple_prefs_add_bool(PREF_ROOT "/idletime", TRUE);
	purple_prefs_add_bool(PREF_ROOT "/showoffline", FALSE);
	purple_prefs_add_bool(PREF_ROOT "/emptygroups", FALSE);
	purple_prefs_add_string(PREF_ROOT "/sort_type", "text");
	purple_prefs_add_string(PREF_ROOT "/grouping", "default");

	purple_prefs_connect_callback(finch_blist_get_handle(),
			PREF_ROOT "/emptygroups", redraw_blist, NULL);
	purple_prefs_connect_callback(finch_blist_get_handle(),
			PREF_ROOT "/showoffline", redraw_blist, NULL);
	purple_prefs_connect_callback(finch_blist_get_handle(),
			PREF_ROOT "/sort_type", redraw_blist, NULL);
	purple_prefs_connect_callback(finch_blist_get_handle(),
			PREF_ROOT "/grouping", redraw_blist, NULL);

	purple_signal_connect_priority(purple_connections_get_handle(),
	                               "autojoin", purple_blist_get_handle(),
			               G_CALLBACK(account_autojoin_cb), NULL,
	                               PURPLE_SIGNAL_PRIORITY_HIGHEST);

	finch_blist_install_manager(&default_manager);
}

static gboolean
remove_typing_cb(gpointer null)
{
	PurpleSavedStatus *current;
	const char *message, *newmessage;
	char *escnewmessage;
	PurpleStatusPrimitive prim, newprim;
	StatusBoxItem *item;

	current = purple_savedstatus_get_current();
	message = purple_savedstatus_get_message(current);
	prim = purple_savedstatus_get_primitive_type(current);

	newmessage = gnt_entry_get_text(GNT_ENTRY(ggblist->statustext));
	item = gnt_combo_box_get_selected_data(GNT_COMBO_BOX(ggblist->status));
	escnewmessage = newmessage ? g_markup_escape_text(newmessage, -1) : NULL;

	switch (item->type) {
		case STATUS_PRIMITIVE:
			newprim = item->u.prim;
			break;
		case STATUS_SAVED_POPULAR:
			newprim = purple_savedstatus_get_primitive_type(item->u.saved);
			break;
		default:
			goto end;  /* 'New' or 'Saved' is selected, but this should never happen. */
	}

	if (newprim != prim || ((message && !escnewmessage) ||
				(!message && escnewmessage) ||
				(message && escnewmessage && g_utf8_collate(message, escnewmessage) != 0)))
	{
		PurpleSavedStatus *status = purple_savedstatus_find_transient_by_type_and_message(newprim, escnewmessage);
									/* Holy Crap! That's a LAWNG function name */
		if (status == NULL)
		{
			status = purple_savedstatus_new(NULL, newprim);
			purple_savedstatus_set_message(status, escnewmessage);
		}

		purple_savedstatus_activate(status);
	}

	gnt_box_give_focus_to_child(GNT_BOX(ggblist->window), ggblist->tree);
end:
	g_free(escnewmessage);
	if (ggblist->typing)
		g_source_remove(ggblist->typing);
	ggblist->typing = 0;
	return FALSE;
}

static void
status_selection_changed(GntComboBox *box, StatusBoxItem *old, StatusBoxItem *now, gpointer null)
{
	gnt_entry_set_text(GNT_ENTRY(ggblist->statustext), NULL);
	if (now->type == STATUS_SAVED_POPULAR)
	{
		/* Set the status immediately */
		purple_savedstatus_activate(now->u.saved);
	}
	else if (now->type == STATUS_PRIMITIVE)
	{
		/* Move the focus to the entry box */
		/* XXX: Make sure the selected status can have a message */
		gnt_box_move_focus(GNT_BOX(ggblist->window), 1);
		ggblist->typing = g_timeout_add_seconds(TYPING_TIMEOUT_S, (GSourceFunc)remove_typing_cb, NULL);
	}
	else if (now->type == STATUS_SAVED_ALL)
	{
		/* Restore the selection to reflect current status. */
		savedstatus_changed(purple_savedstatus_get_current(), NULL);
		gnt_box_give_focus_to_child(GNT_BOX(ggblist->window), ggblist->tree);
		finch_savedstatus_show_all();
	}
	else if (now->type == STATUS_SAVED_NEW)
	{
		savedstatus_changed(purple_savedstatus_get_current(), NULL);
		gnt_box_give_focus_to_child(GNT_BOX(ggblist->window), ggblist->tree);
		finch_savedstatus_edit(NULL);
	}
	else
		g_return_if_reached();
}

static gboolean
status_text_changed(GntEntry *entry, const char *text, gpointer null)
{
	if ((text[0] == 27 || (text[0] == '\t' && text[1] == '\0')) && ggblist->typing == 0)
		return FALSE;

	if (ggblist->typing)
		g_source_remove(ggblist->typing);
	ggblist->typing = 0;

	if (text[0] == '\r' && text[1] == 0)
	{
		/* Set the status only after you press 'Enter' */
		remove_typing_cb(NULL);
		return TRUE;
	}

	ggblist->typing = g_timeout_add_seconds(TYPING_TIMEOUT_S, (GSourceFunc)remove_typing_cb, NULL);
	return FALSE;
}

static void
savedstatus_changed(PurpleSavedStatus *now, PurpleSavedStatus *old)
{
	GList *list;
	PurpleStatusPrimitive prim;
	const char *message;
	gboolean found = FALSE, saved = TRUE;

	if (!ggblist)
		return;

	/* Block the signals we don't want to emit */
	g_signal_handlers_block_matched(ggblist->status, G_SIGNAL_MATCH_FUNC,
			0, 0, NULL, status_selection_changed, NULL);
	g_signal_handlers_block_matched(ggblist->statustext, G_SIGNAL_MATCH_FUNC,
			0, 0, NULL, status_text_changed, NULL);

	prim = purple_savedstatus_get_primitive_type(now);
	message = purple_savedstatus_get_message(now);

	/* Rebuild the status dropdown */
	populate_status_dropdown();

	while (!found) {
		list = g_object_get_data(G_OBJECT(ggblist->status), "list of statuses");
		for (; list; list = list->next)
		{
			StatusBoxItem *item = list->data;
			if ((saved && item->type != STATUS_PRIMITIVE && item->u.saved == now) ||
					(!saved && item->type == STATUS_PRIMITIVE && item->u.prim == prim))
			{
				char *mess = purple_unescape_html(message);
				gnt_combo_box_set_selected(GNT_COMBO_BOX(ggblist->status), item);
				gnt_entry_set_text(GNT_ENTRY(ggblist->statustext), mess);
				gnt_widget_draw(ggblist->status);
				g_free(mess);
				found = TRUE;
				break;
			}
		}
		if (!saved)
			break;
		saved = FALSE;
	}

	g_signal_handlers_unblock_matched(ggblist->status, G_SIGNAL_MATCH_FUNC,
			0, 0, NULL, status_selection_changed, NULL);
	g_signal_handlers_unblock_matched(ggblist->statustext, G_SIGNAL_MATCH_FUNC,
			0, 0, NULL, status_text_changed, NULL);
}

static int
blist_node_compare_position(PurpleBlistNode *n1, PurpleBlistNode *n2)
{
	while ((n1 = purple_blist_node_get_sibling_prev(n1)) != NULL)
		if (n1 == n2)
			return 1;
	return -1;
}

static int
blist_node_compare_text(PurpleBlistNode *n1, PurpleBlistNode *n2)
{
	const char *s1, *s2;
	char *us1, *us2;
	int ret;

	if (G_OBJECT_TYPE(n1) != G_OBJECT_TYPE(n2))
		return blist_node_compare_position(n1, n2);

	if (PURPLE_IS_CHAT(n1)) {
		s1 = purple_chat_get_name((PurpleChat*)n1);
		s2 = purple_chat_get_name((PurpleChat*)n2);
	} else if (PURPLE_IS_BUDDY(n1)) {
		return purple_buddy_presence_compare(
			PURPLE_BUDDY_PRESENCE(purple_buddy_get_presence(PURPLE_BUDDY(n1))),
			PURPLE_BUDDY_PRESENCE(purple_buddy_get_presence(PURPLE_BUDDY(n2))));
	} else if (PURPLE_IS_CONTACT(n1)) {
		s1 = purple_contact_get_alias((PurpleContact*)n1);
		s2 = purple_contact_get_alias((PurpleContact*)n2);
	} else {
		return blist_node_compare_position(n1, n2);
	}

	us1 = g_utf8_strup(s1, -1);
	us2 = g_utf8_strup(s2, -1);
	ret = g_utf8_collate(us1, us2);
	g_free(us1);
	g_free(us2);

	return ret;
}

static int
blist_node_compare_status(PurpleBlistNode *n1, PurpleBlistNode *n2)
{
	int ret;

	if (G_OBJECT_TYPE(n1) != G_OBJECT_TYPE(n2))
		return blist_node_compare_position(n1, n2);

	if (PURPLE_IS_CONTACT(n1))
		n1 = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(n1)));
	if (PURPLE_IS_CONTACT(n2))
		n2 = PURPLE_BLIST_NODE(purple_contact_get_priority_buddy(PURPLE_CONTACT(n2)));

	if (PURPLE_IS_BUDDY(n1) && PURPLE_IS_BUDDY(n2)) {
		ret = purple_buddy_presence_compare(
			PURPLE_BUDDY_PRESENCE(purple_buddy_get_presence(PURPLE_BUDDY(n1))),
			PURPLE_BUDDY_PRESENCE(purple_buddy_get_presence(PURPLE_BUDDY(n2))));
		if (ret != 0)
			return ret;
	} else {
		return blist_node_compare_position(n1, n2);
	}

	/* Sort alphabetically if presence is not comparable */
	ret = blist_node_compare_text(n1, n2);

	return ret;
}

static int
get_contact_log_size(PurpleBlistNode *c)
{
	int log = 0;
	PurpleBlistNode *node;

	for (node = purple_blist_node_get_first_child(c); node; node = purple_blist_node_get_sibling_next(node)) {
		PurpleBuddy *b = (PurpleBuddy*)node;
		log += purple_log_get_total_size(PURPLE_LOG_IM, purple_buddy_get_name(b),
				purple_buddy_get_account(b));
	}

	return log;
}

static int
blist_node_compare_log(PurpleBlistNode *n1, PurpleBlistNode *n2)
{
	int ret;
	PurpleBuddy *b1, *b2;

	if (G_OBJECT_TYPE(n1) != G_OBJECT_TYPE(n2))
		return blist_node_compare_position(n1, n2);

	if (PURPLE_IS_BUDDY(n1)) {
		b1 = (PurpleBuddy*)n1;
		b2 = (PurpleBuddy*)n2;
		ret = purple_log_get_total_size(PURPLE_LOG_IM, purple_buddy_get_name(b2), purple_buddy_get_account(b2)) -
				purple_log_get_total_size(PURPLE_LOG_IM, purple_buddy_get_name(b1), purple_buddy_get_account(b1));
		if (ret != 0)
			return ret;
	} else if (PURPLE_IS_CONTACT(n1)) {
		ret = get_contact_log_size(n2) - get_contact_log_size(n1);
		if (ret != 0)
			return ret;
	} else {
		return blist_node_compare_position(n1, n2);
	}

	ret = blist_node_compare_text(n1, n2);
	return ret;
}

static void
plugin_action(GntMenuItem *item, gpointer data)
{
	PurplePluginAction *action = data;
	if (action && action->callback)
		action->callback(action);
}

static void
build_plugin_actions(GntMenuItem *item, PurplePlugin *plugin)
{
	GntWidget *sub = gnt_menu_new(GNT_MENU_POPUP);
	PurplePluginActionsCb actions_cb;
	GList *actions;
	GntMenuItem *menuitem;

	actions_cb =
		purple_plugin_info_get_actions_cb(purple_plugin_get_info(plugin));

	gnt_menuitem_set_submenu(item, GNT_MENU(sub));
	for (actions = actions_cb(plugin); actions;
			actions = g_list_delete_link(actions, actions)) {
		if (actions->data) {
			PurplePluginAction *action = actions->data;
			action->plugin = plugin;
			menuitem = gnt_menuitem_new(action->label);
			gnt_menu_add_item(GNT_MENU(sub), menuitem);

			gnt_menuitem_set_callback(menuitem, plugin_action, action);
			g_object_set_data_full(G_OBJECT(menuitem), "plugin_action",
								   action, (GDestroyNotify)purple_plugin_action_free);
		}
	}
}

static void
protocol_action(GntMenuItem *item, gpointer data)
{
	PurpleProtocolAction *action = data;
	if (action && action->callback)
		action->callback(action);
}

static void
build_protocol_actions(GntMenuItem *item, PurpleProtocol *protocol,
		PurpleConnection *gc)
{
	GntWidget *sub = gnt_menu_new(GNT_MENU_POPUP);
	GList *actions;
	GntMenuItem *menuitem;

	gnt_menuitem_set_submenu(item, GNT_MENU(sub));
	for (actions = purple_protocol_client_get_actions(PURPLE_PROTOCOL_CLIENT(protocol), gc);
			actions; actions = g_list_delete_link(actions, actions)) {
		if (actions->data) {
			PurpleProtocolAction *action = actions->data;
			action->connection = gc;
			menuitem = gnt_menuitem_new(action->label);
			gnt_menu_add_item(GNT_MENU(sub), menuitem);

			gnt_menuitem_set_callback(menuitem, protocol_action, action);
			g_object_set_data_full(G_OBJECT(menuitem), "protocol_action",
								   action, (GDestroyNotify)purple_protocol_action_free);
		}
	}
}

static gboolean
buddy_recent_signed_on_off(gpointer data)
{
	PurpleBlistNode *node = data;
	FinchBlistNode *fnode = g_object_get_data(G_OBJECT(node), UI_DATA);

	g_source_remove(fnode->signed_timer);
	fnode->signed_timer = 0;

	if (!ggblist->manager->can_add_node(node)) {
		node_remove(purple_blist_get_default(), node);
	} else {
		update_node_display(node, ggblist);
		if (purple_blist_node_get_parent(node) && PURPLE_IS_CONTACT(purple_blist_node_get_parent(node)))
			update_node_display(purple_blist_node_get_parent(node), ggblist);
	}

	g_object_unref(node);
	return FALSE;
}

static gboolean
buddy_signed_on_off_cb(gpointer data)
{
	PurpleBlistNode *node = data;
	FinchBlistNode *fnode = g_object_get_data(G_OBJECT(node), UI_DATA);
	if(!ggblist || !fnode) {
		return FALSE;
	}

	if(fnode->signed_timer) {
		g_source_remove(fnode->signed_timer);
	}

	g_object_ref(node);
	fnode->signed_timer = g_timeout_add_seconds(6, (GSourceFunc)buddy_recent_signed_on_off, data);
	update_node_display(node, ggblist);
	if (purple_blist_node_get_parent(node) && PURPLE_IS_CONTACT(purple_blist_node_get_parent(node)))
		update_node_display(purple_blist_node_get_parent(node), ggblist);
	return FALSE;
}

static void
buddy_signed_on_off(PurpleBuddy* buddy, gpointer null)
{
	g_idle_add(buddy_signed_on_off_cb, buddy);
}

static void
reconstruct_plugins_menu(void)
{
	GntWidget *sub;
	GntMenuItem *plg;
	GList *iter;

	if (!ggblist)
		return;

	if (ggblist->plugins == NULL)
		ggblist->plugins = gnt_menuitem_new(_("Plugins"));

	plg = ggblist->plugins;
	sub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(plg, GNT_MENU(sub));

	for (iter = purple_plugins_get_loaded(); iter; iter = iter->next) {
		PurplePlugin *plugin = iter->data;
		PurplePluginInfo *info = purple_plugin_get_info(plugin);
		GntMenuItem *item;

		if (!purple_plugin_info_get_actions_cb(info))
			continue;

		item = gnt_menuitem_new(_(gplugin_plugin_info_get_name(
		        GPLUGIN_PLUGIN_INFO(info))));
		gnt_menu_add_item(GNT_MENU(sub), item);
		build_plugin_actions(item, plugin);
	}
}

static void
reconstruct_plugins_menu_cb(GObject *plugin_manager, GPluginPlugin *plugin,
                            gpointer data)
{
	reconstruct_plugins_menu();
}

static void
reconstruct_accounts_menu(void)
{
	GntWidget *sub;
	GntMenuItem *acc, *item;
	GList *iter;

	if (!ggblist)
		return;

	if (ggblist->accounts == NULL)
		ggblist->accounts = gnt_menuitem_new(_("Accounts"));

	acc = ggblist->accounts;
	sub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(acc, GNT_MENU(sub));

	for (iter = purple_accounts_get_all_active(); iter;
			iter = g_list_delete_link(iter, iter)) {
		PurpleAccount *account = iter->data;
		PurpleConnection *gc = purple_account_get_connection(account);
		PurpleProtocol *protocol;

		if (!gc || !PURPLE_CONNECTION_IS_CONNECTED(gc))
			continue;
		protocol = purple_connection_get_protocol(gc);

		if (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CLIENT, get_actions)) {
			item = gnt_menuitem_new(purple_account_get_username(account));
			gnt_menu_add_item(GNT_MENU(sub), item);
			build_protocol_actions(item, protocol, gc);
		}
	}
}

static void
reconstruct_grouping_menu(void)
{
	GList *iter;
	GntWidget *subsub;

	if (!ggblist || !ggblist->grouping)
		return;

	subsub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(ggblist->grouping, GNT_MENU(subsub));

	for (iter = managers; iter; iter = iter->next) {
		char menuid[128];
		FinchBlistManager *manager = iter->data;
		GntMenuItem *item = gnt_menuitem_new(_(manager->name));
		g_snprintf(menuid, sizeof(menuid), "grouping-%s", manager->id);
		gnt_menuitem_set_id(GNT_MENU_ITEM(item), menuid);
		gnt_menu_add_item(GNT_MENU(subsub), item);
		g_object_set_data_full(G_OBJECT(item), "grouping-id", g_strdup(manager->id), g_free);
		gnt_menuitem_set_callback(item, menu_group_set_cb, NULL);
	}
}

static gboolean
auto_join_chats(gpointer data)
{
	PurpleBlistNode *node;
	PurpleConnection *pc = data;
	PurpleAccount *account = purple_connection_get_account(pc);

	for (node = purple_blist_get_default_root(); node;
	     node = purple_blist_node_next(node, FALSE)) {
		if (PURPLE_IS_CHAT(node)) {
			PurpleChat *chat = (PurpleChat*)node;
			if (purple_chat_get_account(chat) == account &&
					purple_blist_node_get_bool(node, "gnt-autojoin"))
				purple_serv_join_chat(purple_account_get_connection(account), purple_chat_get_components(chat));
		}
	}
	return FALSE;
}

static gboolean
account_autojoin_cb(PurpleConnection *gc, gpointer null)
{
	g_idle_add(auto_join_chats, gc);
	return TRUE;
}

static void toggle_pref_cb(GntMenuItem *item, gpointer n)
{
	purple_prefs_set_bool(n, !purple_prefs_get_bool(n));
}

static void sort_blist_change_cb(GntMenuItem *item, gpointer n)
{
	purple_prefs_set_string(PREF_ROOT "/sort_type", n);
}

static void
block_select_cb(gpointer data, PurpleRequestFields *fields)
{
	PurpleAccount *account = purple_request_fields_get_account(fields, "account");
	const char *name = purple_request_fields_get_string(fields,  "screenname");
	if (account && name && *name != '\0') {
		if (GPOINTER_TO_INT(purple_request_fields_get_choice(fields, "block")) == 1) {
			purple_account_privacy_deny(account, name);
		} else {
			purple_account_privacy_allow(account, name);
		}
	}
}

static void
block_select(GntMenuItem *item, gpointer n)
{
	PurpleRequestFields *fields;
	PurpleRequestFieldGroup *group;
	PurpleRequestField *field;

	fields = purple_request_fields_new();

	group = purple_request_field_group_new(NULL);
	purple_request_fields_add_group(fields, group);

	field = purple_request_field_string_new("screenname", _("Name"), NULL, FALSE);
	purple_request_field_set_type_hint(field, "screenname");
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_account_new("account", _("Account"), NULL);
	purple_request_field_set_type_hint(field, "account");
	purple_request_field_set_visible(field,
		(purple_connections_get_all() != NULL &&
		 purple_connections_get_all()->next != NULL));
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_choice_new("block", _("Block/Unblock"), GINT_TO_POINTER(1));
	purple_request_field_choice_add(field, _("Block"), GINT_TO_POINTER(1));
	purple_request_field_choice_add(field, _("Unblock"), GINT_TO_POINTER(2));
	purple_request_field_group_add_field(group, field);

	purple_request_fields(
	        purple_blist_get_default(), _("Block/Unblock"), NULL,
	        _("Please enter the username or alias of the person "
	          "you would like to Block/Unblock."),
	        fields, _("OK"), G_CALLBACK(block_select_cb), _("Cancel"), NULL,
	        NULL, NULL);
}

/* send_im_select* -- Xerox */
static void
send_im_select_cb(gpointer data, PurpleRequestFields *fields)
{
	PurpleAccount *account;
	const char *username;
	PurpleConversation *im;

	account  = purple_request_fields_get_account(fields, "account");
	username = purple_request_fields_get_string(fields,  "screenname");

	im = purple_im_conversation_new(account, username);
	purple_conversation_present(im);
}

static void
send_im_select(GntMenuItem *item, gpointer n)
{
	PurpleRequestFields *fields;
	PurpleRequestFieldGroup *group;
	PurpleRequestField *field;

	fields = purple_request_fields_new();

	group = purple_request_field_group_new(NULL);
	purple_request_fields_add_group(fields, group);

	field = purple_request_field_string_new("screenname", _("Name"), NULL, FALSE);
	purple_request_field_set_type_hint(field, "screenname");
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_account_new("account", _("Account"), NULL);
	purple_request_field_set_type_hint(field, "account");
	purple_request_field_set_visible(field,
		(purple_connections_get_all() != NULL &&
		 purple_connections_get_all()->next != NULL));
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	purple_request_fields(
	        purple_blist_get_default(), _("New Instant Message"), NULL,
	        _("Please enter the username or alias of the person "
	          "you would like to IM."),
	        fields, _("OK"), G_CALLBACK(send_im_select_cb), _("Cancel"),
	        NULL, NULL, NULL);
}

static void
join_chat_select_cb(gpointer data, PurpleRequestFields *fields)
{
	PurpleAccount *account;
	const char *name;
	PurpleConnection *gc;
	PurpleConversationManager *manager;
	PurpleChat *chat;
	GHashTable *hash = NULL;
	PurpleConversation *conv;

	account = purple_request_fields_get_account(fields, "account");
	name = purple_request_fields_get_string(fields,  "chat");

	if (!purple_account_is_connected(account))
		return;

	gc = purple_account_get_connection(account);
	manager = purple_conversation_manager_get_default();

	/* Create a new conversation now. This will give focus to the new window.
	 * But it's necessary to pretend that we left the chat, because otherwise
	 * a new conversation window will pop up when we finally join the chat. */
	conv = purple_conversation_manager_find_chat(manager, account, name);
	if(!PURPLE_IS_CHAT_CONVERSATION(conv)) {
		conv = purple_chat_conversation_new(account, name);
		purple_chat_conversation_leave(PURPLE_CHAT_CONVERSATION(conv));
	} else {
		purple_conversation_present(conv);
	}

	chat = purple_blist_find_chat(account, name);
	if (chat == NULL) {
		PurpleProtocol *protocol = purple_connection_get_protocol(gc);
		hash = purple_protocol_chat_info_defaults(PURPLE_PROTOCOL_CHAT(protocol), gc, name);
	} else {
		hash = purple_chat_get_components(chat);
	}
	purple_serv_join_chat(gc, hash);
	if (chat == NULL && hash != NULL)
		g_hash_table_destroy(hash);
}

static void
join_chat_select(GntMenuItem *item, gpointer n)
{
	PurpleRequestFields *fields;
	PurpleRequestFieldGroup *group;
	PurpleRequestField *field;

	fields = purple_request_fields_new();

	group = purple_request_field_group_new(NULL);
	purple_request_fields_add_group(fields, group);

	field = purple_request_field_string_new("chat", _("Channel"), NULL, FALSE);
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_account_new("account", _("Account"), NULL);
	purple_request_field_set_type_hint(field, "account");
	purple_request_field_set_visible(field,
		(purple_connections_get_all() != NULL &&
		 purple_connections_get_all()->next != NULL));
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	purple_request_fields(
	        purple_blist_get_default(), _("Join a Chat"), NULL,
	        _("Please enter the name of the chat you want to join."),
	        fields, _("Join"), G_CALLBACK(join_chat_select_cb), _("Cancel"),
	        NULL, NULL, NULL);
}

static void
view_log_select_cb(gpointer data, PurpleRequestFields *fields)
{
	PurpleAccount *account;
	const char *name;
	PurpleBuddy *buddy;
	PurpleContact *contact;

	account = purple_request_fields_get_account(fields, "account");
	name = purple_request_fields_get_string(fields,  "screenname");

	buddy = purple_blist_find_buddy(account, name);
	if (buddy) {
		contact = purple_buddy_get_contact(buddy);
	} else {
		contact = NULL;
	}

	if (contact) {
		finch_log_show_contact(contact);
	} else {
		finch_log_show(PURPLE_LOG_IM, name, account);
	}
}

static void
view_log_cb(GntMenuItem *item, gpointer n)
{
	PurpleRequestFields *fields;
	PurpleRequestFieldGroup *group;
	PurpleRequestField *field;

	fields = purple_request_fields_new();

	group = purple_request_field_group_new(NULL);
	purple_request_fields_add_group(fields, group);

	field = purple_request_field_string_new("screenname", _("Name"), NULL, FALSE);
	purple_request_field_set_type_hint(field, "screenname-all");
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);

	field = purple_request_field_account_new("account", _("Account"), NULL);
	purple_request_field_set_type_hint(field, "account");
	purple_request_field_set_visible(field,
		(purple_accounts_get_all() != NULL &&
		 purple_accounts_get_all()->next != NULL));
	purple_request_field_set_required(field, TRUE);
	purple_request_field_group_add_field(group, field);
	purple_request_field_account_set_show_all(field, TRUE);

	purple_request_fields(
	        purple_blist_get_default(), _("View Log"), NULL,
	        _("Please enter the username or alias of the person "
	          "whose log you would like to view."),
	        fields, _("OK"), G_CALLBACK(view_log_select_cb), _("Cancel"),
	        NULL, NULL, NULL);
}

static void
view_all_logs_cb(GntMenuItem *item, gpointer n)
{
	finch_log_show(PURPLE_LOG_IM, NULL, NULL);
}

static void
menu_add_buddy_cb(GntMenuItem *item, gpointer null)
{
	purple_blist_request_add_buddy(NULL, NULL, NULL, NULL);
}

static void
menu_add_chat_cb(GntMenuItem *item, gpointer null)
{
	purple_blist_request_add_chat(NULL, NULL, NULL, NULL);
}

static void
menu_add_group_cb(GntMenuItem *item, gpointer null)
{
	purple_blist_request_add_group();
}

static void
menu_group_set_cb(GntMenuItem *item, gpointer null)
{
	const char *id = g_object_get_data(G_OBJECT(item), "grouping-id");
	purple_prefs_set_string(PREF_ROOT "/grouping", id);
}

static void
create_menu(void)
{
	GntWidget *menu, *sub, *subsub;
	GntMenuItem *item;
	GntWindow *window;

	if (!ggblist)
		return;

	window = GNT_WINDOW(ggblist->window);
	ggblist->menu = menu = gnt_menu_new(GNT_MENU_TOPLEVEL);
	gnt_window_set_menu(window, GNT_MENU(menu));

	item = gnt_menuitem_new(_("Options"));
	gnt_menu_add_item(GNT_MENU(menu), item);

	sub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(item, GNT_MENU(sub));

	item = gnt_menuitem_new(_("Send IM..."));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "send-im");
	gnt_menu_add_item(GNT_MENU(sub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), send_im_select, NULL);

	item = gnt_menuitem_new(_("Block/Unblock..."));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "block-unblock");
	gnt_menu_add_item(GNT_MENU(sub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), block_select, NULL);

	item = gnt_menuitem_new(_("Join Chat..."));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "join-chat");
	gnt_menu_add_item(GNT_MENU(sub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), join_chat_select, NULL);

	item = gnt_menuitem_new(_("View Log..."));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "view-log");
	gnt_menu_add_item(GNT_MENU(sub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), view_log_cb, NULL);

	item = gnt_menuitem_new(_("View All Logs"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "view-all-logs");
	gnt_menu_add_item(GNT_MENU(sub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), view_all_logs_cb, NULL);

	item = gnt_menuitem_new(_("Show"));
	gnt_menu_add_item(GNT_MENU(sub), item);
	subsub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(item, GNT_MENU(subsub));

	item = gnt_menuitem_check_new(_("Empty groups"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "show-empty-groups");
	gnt_menuitem_check_set_checked(GNT_MENU_ITEM_CHECK(item),
				purple_prefs_get_bool(PREF_ROOT "/emptygroups"));
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), toggle_pref_cb, PREF_ROOT "/emptygroups");

	item = gnt_menuitem_check_new(_("Offline buddies"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "show-offline-buddies");
	gnt_menuitem_check_set_checked(GNT_MENU_ITEM_CHECK(item),
				purple_prefs_get_bool(PREF_ROOT "/showoffline"));
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), toggle_pref_cb, PREF_ROOT "/showoffline");

	item = gnt_menuitem_new(_("Sort"));
	gnt_menu_add_item(GNT_MENU(sub), item);
	subsub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(item, GNT_MENU(subsub));

	item = gnt_menuitem_new(_("By Status"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "sort-status");
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), sort_blist_change_cb, "status");

	item = gnt_menuitem_new(_("Alphabetically"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "sort-alpha");
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), sort_blist_change_cb, "text");

	item = gnt_menuitem_new(_("By Log Size"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "sort-log");
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(GNT_MENU_ITEM(item), sort_blist_change_cb, "log");

	item = gnt_menuitem_new(_("Add"));
	gnt_menu_add_item(GNT_MENU(sub), item);

	subsub = gnt_menu_new(GNT_MENU_POPUP);
	gnt_menuitem_set_submenu(item, GNT_MENU(subsub));

	item = gnt_menuitem_new(_("Buddy"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "add-buddy");
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(item, menu_add_buddy_cb, NULL);

	item = gnt_menuitem_new(_("Chat"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "add-chat");
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(item, menu_add_chat_cb, NULL);

	item = gnt_menuitem_new(_("Group"));
	gnt_menuitem_set_id(GNT_MENU_ITEM(item), "add-group");
	gnt_menu_add_item(GNT_MENU(subsub), item);
	gnt_menuitem_set_callback(item, menu_add_group_cb, NULL);

	ggblist->grouping = item = gnt_menuitem_new(_("Grouping"));
	gnt_menu_add_item(GNT_MENU(sub), item);
	reconstruct_grouping_menu();

	reconstruct_accounts_menu();
	gnt_menu_add_item(GNT_MENU(menu), ggblist->accounts);

	reconstruct_plugins_menu();
	gnt_menu_add_item(GNT_MENU(menu), ggblist->plugins);
}

void finch_blist_show()
{
	blist_show(purple_blist_get_default());
}

static void
group_collapsed(GntWidget *widget, PurpleBlistNode *node, gboolean collapsed, gpointer null)
{
	if (PURPLE_IS_GROUP(node))
		purple_blist_node_set_bool(node, "collapsed", collapsed);
}

static void
blist_show(PurpleBuddyList *list)
{
	GPluginManager *plugin_manager = NULL;

	if (ggblist->window) {
		gnt_window_present(ggblist->window);
		return;
	}

	ggblist->window = gnt_vwindow_new(FALSE);
	gnt_widget_set_name(ggblist->window, "buddylist");
	gnt_box_set_toplevel(GNT_BOX(ggblist->window), TRUE);
	gnt_box_set_title(GNT_BOX(ggblist->window), _("Buddy List"));
	gnt_box_set_pad(GNT_BOX(ggblist->window), 0);

	ggblist->tree = gnt_tree_new();

	gnt_widget_set_has_border(ggblist->tree, FALSE);
	gnt_widget_set_size(ggblist->tree, purple_prefs_get_int(PREF_ROOT "/size/width"),
			purple_prefs_get_int(PREF_ROOT "/size/height"));
	gnt_widget_set_position(ggblist->window, purple_prefs_get_int(PREF_ROOT "/position/x"),
			purple_prefs_get_int(PREF_ROOT "/position/y"));

	gnt_box_add_widget(GNT_BOX(ggblist->window), ggblist->tree);

	ggblist->status = gnt_combo_box_new();
	gnt_box_add_widget(GNT_BOX(ggblist->window), ggblist->status);
	ggblist->statustext = gnt_entry_new(NULL);
	gnt_box_add_widget(GNT_BOX(ggblist->window), ggblist->statustext);

	gnt_widget_show(ggblist->window);

	purple_signal_connect(purple_connections_get_handle(), "signed-on", finch_blist_get_handle(),
				PURPLE_CALLBACK(reconstruct_accounts_menu), NULL);
	purple_signal_connect(purple_connections_get_handle(), "signed-off", finch_blist_get_handle(),
				PURPLE_CALLBACK(reconstruct_accounts_menu), NULL);
	purple_signal_connect(purple_accounts_get_handle(), "account-actions-changed", finch_blist_get_handle(),
				PURPLE_CALLBACK(reconstruct_accounts_menu), NULL);
	purple_signal_connect(purple_blist_get_handle(), "buddy-status-changed", finch_blist_get_handle(),
				PURPLE_CALLBACK(buddy_status_changed), ggblist);
	purple_signal_connect(purple_blist_get_handle(), "buddy-idle-changed", finch_blist_get_handle(),
				PURPLE_CALLBACK(buddy_idle_changed), ggblist);

	plugin_manager = gplugin_manager_get_instance();
	g_signal_connect_object(plugin_manager, "loaded-plugin",
	                        G_CALLBACK(reconstruct_plugins_menu_cb), ggblist, 0);
	g_signal_connect_object(plugin_manager, "unloaded-plugin",
	                        G_CALLBACK(reconstruct_plugins_menu_cb), ggblist, 0);

	purple_signal_connect(purple_blist_get_handle(), "buddy-signed-on", finch_blist_get_handle(),
				PURPLE_CALLBACK(buddy_signed_on_off), ggblist);
	purple_signal_connect(purple_blist_get_handle(), "buddy-signed-off", finch_blist_get_handle(),
				PURPLE_CALLBACK(buddy_signed_on_off), ggblist);

	g_signal_connect(G_OBJECT(ggblist->tree), "selection_changed", G_CALLBACK(selection_changed), ggblist);
	g_signal_connect(G_OBJECT(ggblist->tree), "key_pressed", G_CALLBACK(key_pressed), ggblist);
	g_signal_connect(G_OBJECT(ggblist->tree), "context-menu", G_CALLBACK(context_menu), ggblist);
	g_signal_connect(G_OBJECT(ggblist->tree), "collapse-toggled", G_CALLBACK(group_collapsed), NULL);
	g_signal_connect(G_OBJECT(ggblist->tree), "activate", G_CALLBACK(selection_activate), ggblist);
	g_signal_connect_data(G_OBJECT(ggblist->tree), "gained-focus", G_CALLBACK(draw_tooltip),
				ggblist, 0, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
	g_signal_connect_data(G_OBJECT(ggblist->tree), "lost-focus", G_CALLBACK(remove_peripherals),
				ggblist, 0, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
	g_signal_connect_data(G_OBJECT(ggblist->window), "workspace-hidden", G_CALLBACK(remove_peripherals),
				ggblist, 0, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
	g_signal_connect(G_OBJECT(ggblist->tree), "size_changed", G_CALLBACK(size_changed_cb), NULL);
	g_signal_connect(G_OBJECT(ggblist->window), "position_set", G_CALLBACK(save_position_cb), NULL);
	g_signal_connect(G_OBJECT(ggblist->window), "destroy", G_CALLBACK(reset_blist_window), NULL);

	/* Status signals */
	purple_signal_connect(purple_savedstatuses_get_handle(), "savedstatus-changed", finch_blist_get_handle(),
				PURPLE_CALLBACK(savedstatus_changed), NULL);
	g_signal_connect(G_OBJECT(ggblist->status), "selection_changed",
				G_CALLBACK(status_selection_changed), NULL);
	g_signal_connect(G_OBJECT(ggblist->statustext), "key_pressed",
				G_CALLBACK(status_text_changed), NULL);

	create_menu();

	populate_buddylist();

	savedstatus_changed(purple_savedstatus_get_current(), NULL);
}

void finch_blist_uninit()
{
}

gboolean finch_blist_get_position(int *x, int *y)
{
	if (!ggblist || !ggblist->window)
		return FALSE;
	gnt_widget_get_position(ggblist->window, x, y);
	return TRUE;
}

void finch_blist_set_position(int x, int y)
{
	gnt_widget_set_position(ggblist->window, x, y);
}

gboolean finch_blist_get_size(int *width, int *height)
{
	if (!ggblist || !ggblist->window)
		return FALSE;
	gnt_widget_get_size(ggblist->window, width, height);
	return TRUE;
}

void finch_blist_set_size(int width, int height)
{
	gnt_widget_set_size(ggblist->window, width, height);
}

void finch_blist_install_manager(const FinchBlistManager *manager)
{
	if (!g_list_find(managers, manager)) {
		managers = g_list_append(managers, (gpointer)manager);
		reconstruct_grouping_menu();
		if (purple_strequal(manager->id, purple_prefs_get_string(PREF_ROOT "/grouping")))
			purple_prefs_trigger_callback(PREF_ROOT "/grouping");
	}
}

void finch_blist_uninstall_manager(const FinchBlistManager *manager)
{
	if (g_list_find(managers, manager)) {
		managers = g_list_remove(managers, manager);
		reconstruct_grouping_menu();
		if (purple_strequal(manager->id, purple_prefs_get_string(PREF_ROOT "/grouping")))
			purple_prefs_trigger_callback(PREF_ROOT "/grouping");
	}
}

FinchBlistManager * finch_blist_manager_find(const char *id)
{
	GList *iter = managers;
	if (!id)
		return NULL;

	for (; iter; iter = iter->next) {
		FinchBlistManager *m = iter->data;
		if (purple_strequal(id, m->id))
			return m;
	}
	return NULL;
}

GntTree * finch_blist_get_tree(void)
{
	return ggblist ? GNT_TREE(ggblist->tree) : NULL;
}

/**************************************************************************
 * GObject code
 **************************************************************************/
G_DEFINE_TYPE(FinchBuddyList, finch_buddy_list, PURPLE_TYPE_BUDDY_LIST)

static void
finch_buddy_list_init(FinchBuddyList *self)
{
	if (!ggblist) {
		/* The first buddy list object becomes the default. */
		ggblist = self;
	}

	self->manager = finch_blist_manager_find(
	        purple_prefs_get_string(PREF_ROOT "/grouping"));
	if (!self->manager) {
		self->manager = &default_manager;
	}
}

static void
finch_buddy_list_finalize(GObject *obj)
{
	FinchBuddyList *ggblist = FINCH_BUDDY_LIST(obj);

	gnt_widget_destroy(ggblist->window);

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

static void
finch_buddy_list_class_init(FinchBuddyListClass *klass)
{
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	PurpleBuddyListClass *purple_blist_class;

	obj_class->finalize = finch_buddy_list_finalize;

	purple_blist_class = PURPLE_BUDDY_LIST_CLASS(klass);
	purple_blist_class->new_node = new_node;
	purple_blist_class->show = blist_show;
	purple_blist_class->update = node_update;
	purple_blist_class->remove = node_remove;
	purple_blist_class->request_add_buddy = finch_request_add_buddy;
	purple_blist_class->request_add_chat = finch_request_add_chat;
	purple_blist_class->request_add_group = finch_request_add_group;
}

/**************************************************************************
 * GBoxed code
 **************************************************************************/
static FinchBlistManager *
finch_blist_manager_copy(FinchBlistManager *manager)
{
	FinchBlistManager *manager_new;

	g_return_val_if_fail(manager != NULL, NULL);

	manager_new = g_new(FinchBlistManager, 1);
	*manager_new = *manager;

	return manager_new;
}

static void
finch_blist_manager_free(FinchBlistManager *manager)
{
	g_return_if_fail(manager != NULL);

	g_free(manager);
}

GType
finch_blist_manager_get_type(void)
{
	static GType type = 0;

	if (type == 0) {
		type = g_boxed_type_register_static("FinchBlistManager",
				(GBoxedCopyFunc)finch_blist_manager_copy,
				(GBoxedFreeFunc)finch_blist_manager_free);
	}

	return type;
}

mercurial