Merged in default (pull request #521)

Thu, 18 Jul 2019 03:45:06 +0000

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 18 Jul 2019 03:45:06 +0000
changeset 39701
338202356e44
parent 39691
a413ed09ec85 (current diff)
parent 39700
bfbdf95860ed (diff)
child 39702
8102041c0288

Merged in default (pull request #521)

Convert buddy list UI ops into a derivable GObject instead

Approved-by: Gary Kramlich

ChangeLog.API file | annotate | diff | comparison | revisions
libpurple/buddylist.h file | annotate | diff | comparison | revisions
--- a/ChangeLog.API	Wed Jul 17 09:52:28 2019 +0000
+++ b/ChangeLog.API	Thu Jul 18 03:45:06 2019 +0000
@@ -403,8 +403,10 @@
 		* purple_account_set_current_error
 		* purple_account_set_ui_data
 		* purple_base64_*. Use g_base64_* instead
+		* purple_blist_get_ui_data
 		* purple_blist_load
 		* purple_blist_new
+		* purple_blist_set_ui_data
 		* purple_set_blist
 		* purple_blist_update_buddy_icon
 		* purple_buddy_get_local_alias
--- a/finch/finch.h	Wed Jul 17 09:52:28 2019 +0000
+++ b/finch/finch.h	Thu Jul 18 03:45:06 2019 +0000
@@ -34,9 +34,6 @@
 
 #define FINCH_PREFS_ROOT "/finch"
 
-#define FINCH_GET_DATA(obj)        (obj)->ui_data
-#define FINCH_SET_DATA(obj, data)  (obj)->ui_data = data
-
 /**
  * finch_start:
  *
--- a/finch/gntblist.c	Wed Jul 17 09:52:28 2019 +0000
+++ b/finch/gntblist.c	Thu Jul 18 03:45:06 2019 +0000
@@ -65,8 +65,9 @@
 
 #define SHOW_EMPTY_GROUP_TIMEOUT  60
 
-typedef struct
-{
+struct _FinchBuddyList {
+	PurpleBuddyList parent;
+
 	GntWidget *window;
 	GntWidget *tree;
 
@@ -96,7 +97,7 @@
 	guint new_group_timeout;
 
 	FinchBlistManager *manager;
-} FinchBlist;
+};
 
 typedef struct
 {
@@ -122,23 +123,24 @@
 	} u;
 } StatusBoxItem;
 
-static FinchBlist *ggblist;
-
-static void add_buddy(PurpleBuddy *buddy, FinchBlist *ggblist);
-static void add_contact(PurpleContact *contact, FinchBlist *ggblist);
-static void add_group(PurpleGroup *group, FinchBlist *ggblist);
-static void add_chat(PurpleChat *chat, FinchBlist *ggblist);
-static void add_node(PurpleBlistNode *node, FinchBlist *ggblist);
+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(FinchBlist *ggblist);
+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(FinchBlist *ggblist);
+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, FinchBlist *ggblist);
-static void update_buddy_display(PurpleBuddy *buddy, FinchBlist *ggblist);
+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,
@@ -372,7 +374,7 @@
 }
 
 static GntTextFormatFlags
-get_blist_node_flag(PurpleBlistNode *node)
+get_blist_node_flag(FinchBuddyList *ggblist, PurpleBlistNode *node)
 {
 	GntTextFormatFlags flag = 0;
 	FinchBlistNode *fnode = purple_blist_node_get_ui_data(node);
@@ -393,9 +395,10 @@
 }
 
 static void
-blist_update_row_flags(PurpleBlistNode *node)
+blist_update_row_flags(FinchBuddyList *ggblist, PurpleBlistNode *node)
 {
-	gnt_tree_set_row_flags(GNT_TREE(ggblist->tree), node, get_blist_node_flag(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));
 }
 
@@ -405,7 +408,7 @@
 }
 
 static void
-add_node(PurpleBlistNode *node, FinchBlist *ggblist)
+add_node(PurpleBlistNode *node, FinchBuddyList *ggblist)
 {
 	if (purple_blist_node_get_ui_data(node))
 		return;
@@ -431,7 +434,7 @@
 }
 
 static void
-remove_tooltip(FinchBlist *ggblist)
+remove_tooltip(FinchBuddyList *ggblist)
 {
 	gnt_widget_destroy(ggblist->tooltip);
 	ggblist->tooltip = NULL;
@@ -441,7 +444,7 @@
 static void
 node_remove(PurpleBuddyList *list, PurpleBlistNode *node)
 {
-	FinchBlist *ggblist = FINCH_GET_DATA(list);
+	FinchBuddyList *ggblist = FINCH_BUDDY_LIST(list);
 	PurpleBlistNode *parent;
 
 	if (ggblist == NULL || purple_blist_node_get_ui_data(node) == NULL)
@@ -474,14 +477,15 @@
 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);
 
-	if (FINCH_GET_DATA(list)== NULL)
-		return;   /* XXX: this is probably the place to auto-join chats */
-
+	ggblist = FINCH_BUDDY_LIST(list);
 	if (ggblist->window == NULL)
 		return;
 
@@ -489,7 +493,7 @@
 		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(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);
@@ -497,60 +501,36 @@
 
 	if (PURPLE_IS_BUDDY(node)) {
 		PurpleBuddy *buddy = (PurpleBuddy*)node;
-		add_node((PurpleBlistNode*)buddy, FINCH_GET_DATA(list));
+		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_GET_DATA(list));
+		add_node(node, FINCH_BUDDY_LIST(list));
 	} else if (PURPLE_IS_CONTACT(node)) {
 		if (purple_blist_node_get_ui_data(node)== 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_GET_DATA(list));
+				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_GET_DATA(list));
+			add_node(node, FINCH_BUDDY_LIST(list));
 	}
 	if (ggblist->tnode == node) {
 		draw_tooltip(ggblist);
 	}
 }
 
-static void
-new_list(PurpleBuddyList *list)
-{
-	if (ggblist)
-		return;
-
-	ggblist = g_new0(FinchBlist, 1);
-	FINCH_SET_DATA(list, ggblist);
-	ggblist->manager = finch_blist_manager_find(purple_prefs_get_string(PREF_ROOT "/grouping"));
-	if (!ggblist->manager)
-		ggblist->manager = &default_manager;
-}
-
-static void destroy_list(PurpleBuddyList *list)
-{
-	if (ggblist == NULL)
-		return;
-
-	gnt_widget_destroy(ggblist->window);
-	g_free(ggblist);
-	ggblist = NULL;
-}
-
 static gboolean
 remove_new_empty_group(gpointer data)
 {
 	PurpleBuddyList *list;
-
-	if (!ggblist)
-		return FALSE;
+	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) {
@@ -746,13 +726,14 @@
 }
 
 static void
-add_group_cb(gpointer null, const char *group)
+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;
 	}
 
@@ -762,9 +743,6 @@
 		purple_blist_add_group(grp, NULL);
 	}
 
-	if (!ggblist)
-		return;
-
 	/* 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 */
@@ -781,35 +759,20 @@
 			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"), NULL,
-			NULL, NULL);
+	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 PurpleBlistUiOps blist_ui_ops =
-{
-	new_list,
-	new_node,
-	blist_show,
-	node_update,
-	node_remove,
-	destroy_list,
-	NULL,
-	finch_request_add_buddy,
-	finch_request_add_chat,
-	finch_request_add_group,
-	NULL,
-	NULL,
-	NULL,
-	NULL, NULL, NULL, NULL
-};
-
 static gpointer
 finch_blist_get_handle(void)
 {
@@ -819,7 +782,7 @@
 }
 
 static void
-add_group(PurpleGroup *group, FinchBlist *ggblist)
+add_group(PurpleGroup *group, FinchBuddyList *ggblist)
 {
 	gpointer parent;
 	PurpleBlistNode *node = (PurpleBlistNode *)group;
@@ -892,7 +855,7 @@
 }
 
 static void
-add_chat(PurpleChat *chat, FinchBlist *ggblist)
+add_chat(PurpleChat *chat, FinchBuddyList *ggblist)
 {
 	gpointer parent;
 	PurpleBlistNode *node = (PurpleBlistNode *)chat;
@@ -909,7 +872,7 @@
 }
 
 static void
-add_contact(PurpleContact *contact, FinchBlist *ggblist)
+add_contact(PurpleContact *contact, FinchBuddyList *ggblist)
 {
 	gpointer parent;
 	PurpleBlistNode *node = (PurpleBlistNode*)contact;
@@ -932,7 +895,7 @@
 }
 
 static void
-add_buddy(PurpleBuddy *buddy, FinchBlist *ggblist)
+add_buddy(PurpleBuddy *buddy, FinchBuddyList *ggblist)
 {
 	gpointer parent;
 	PurpleBlistNode *node = (PurpleBlistNode *)buddy;
@@ -948,18 +911,14 @@
 				gnt_tree_create_row(GNT_TREE(ggblist->tree), get_display_name(node)),
 				parent, NULL));
 
-	blist_update_row_flags((PurpleBlistNode*)buddy);
-	if (buddy == purple_contact_get_priority_buddy(contact))
-		blist_update_row_flags((PurpleBlistNode*)contact);
-}
-
-PurpleBlistUiOps *finch_blist_get_ui_ops()
-{
-	return &blist_ui_ops;
+	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, FinchBlist *ggblist)
+selection_activate(GntWidget *widget, FinchBuddyList *ggblist)
 {
 	GntTree *tree = GNT_TREE(ggblist->tree);
 	PurpleBlistNode *node = gnt_tree_get_selection_data(tree);
@@ -1559,13 +1518,13 @@
 }
 
 static void
-context_menu_destroyed(GntWidget *widget, FinchBlist *ggblist)
+context_menu_destroyed(GntWidget *widget, FinchBuddyList *ggblist)
 {
 	ggblist->context = NULL;
 }
 
 static void
-draw_context_menu(FinchBlist *ggblist)
+draw_context_menu(FinchBuddyList *ggblist)
 {
 	PurpleBlistNode *node = NULL;
 	GntWidget *context = NULL;
@@ -1720,7 +1679,7 @@
 }
 
 static gboolean
-draw_tooltip_real(FinchBlist *ggblist)
+draw_tooltip_real(FinchBuddyList *ggblist)
 {
 	PurpleBlistNode *node;
 	int x, y, top, width, w, h;
@@ -1790,7 +1749,7 @@
 }
 
 static void
-draw_tooltip(FinchBlist *ggblist)
+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
@@ -1803,21 +1762,22 @@
 }
 
 static void
-selection_changed(GntWidget *widget, gpointer old, gpointer current, FinchBlist *ggblist)
+selection_changed(GntWidget *widget, gpointer old, gpointer current,
+                  FinchBuddyList *ggblist)
 {
 	remove_peripherals(ggblist);
 	draw_tooltip(ggblist);
 }
 
 static gboolean
-context_menu(GntWidget *widget, FinchBlist *ggblist)
+context_menu(GntWidget *widget, FinchBuddyList *ggblist)
 {
 	draw_context_menu(ggblist);
 	return TRUE;
 }
 
 static gboolean
-key_pressed(GntWidget *widget, const char *text, FinchBlist *ggblist)
+key_pressed(GntWidget *widget, const char *text, FinchBuddyList *ggblist)
 {
 	if (text[0] == 27 && text[1] == 0) {
 		/* Escape was pressed */
@@ -1844,14 +1804,14 @@
 }
 
 static void
-update_node_display(PurpleBlistNode *node, FinchBlist *ggblist)
+update_node_display(PurpleBlistNode *node, FinchBuddyList *ggblist)
 {
-	GntTextFormatFlags flag = get_blist_node_flag(node);
+	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, FinchBlist *ggblist)
+update_buddy_display(PurpleBuddy *buddy, FinchBuddyList *ggblist)
 {
 	PurpleContact *contact;
 
@@ -1860,28 +1820,31 @@
 	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((PurpleBlistNode *)buddy);
+	blist_update_row_flags(ggblist, (PurpleBlistNode *)buddy);
 	if (buddy == purple_contact_get_priority_buddy(contact))
-		blist_update_row_flags((PurpleBlistNode *)contact);
-
-	if (ggblist->tnode == (PurpleBlistNode*)buddy)
+		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, FinchBlist *ggblist)
+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, FinchBlist *ggblist)
+buddy_idle_changed(PurpleBuddy *buddy, int old, int new,
+                   FinchBuddyList *ggblist)
 {
 	update_buddy_display(buddy, ggblist);
 }
 
 static void
-remove_peripherals(FinchBlist *ggblist)
+remove_peripherals(FinchBuddyList *ggblist)
 {
 	if (ggblist->tooltip)
 		remove_tooltip(ggblist);
@@ -1910,7 +1873,6 @@
 {
 	PurpleBlistNode *node;
 	purple_signals_disconnect_by_handle(finch_blist_get_handle());
-	FINCH_SET_DATA(purple_blist_get_default(), NULL);
 
 	node = purple_blist_get_default_root();
 	while (node) {
@@ -1929,7 +1891,6 @@
 	if (ggblist->new_group)
 		g_list_free(ggblist->new_group);
 
-	g_free(ggblist);
 	ggblist = NULL;
 }
 
@@ -3013,9 +2974,7 @@
 static void
 blist_show(PurpleBuddyList *list)
 {
-	if (ggblist == NULL)
-		new_list(list);
-	else if (ggblist->window) {
+	if (ggblist->window) {
 		gnt_window_present(ggblist->window);
 		return;
 	}
@@ -3164,6 +3123,54 @@
 }
 
 /**************************************************************************
+ * 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 *
--- a/finch/gntblist.h	Wed Jul 17 09:52:28 2019 +0000
+++ b/finch/gntblist.h	Thu Jul 18 03:45:06 2019 +0000
@@ -32,6 +32,7 @@
 #include "gnt.h"
 #include "gnttree.h"
 
+#define FINCH_TYPE_BUDDY_LIST (finch_buddy_list_get_type())
 #define FINCH_TYPE_BLIST_MANAGER (finch_blist_manager_get_type())
 
 /**********************************************************************
@@ -74,14 +75,8 @@
  */
 GType finch_blist_manager_get_type(void);
 
-/**
- * finch_blist_get_ui_ops:
- *
- * Get the ui-functions.
- *
- * Returns: The PurpleBlistUiOps structure populated with the appropriate functions.
- */
-PurpleBlistUiOps * finch_blist_get_ui_ops(void);
+G_DECLARE_FINAL_TYPE(FinchBuddyList, finch_buddy_list, FINCH, BUDDY_LIST,
+                     PurpleBuddyList)
 
 /**
  * finch_blist_init:
--- a/finch/gntui.c	Wed Jul 17 09:52:28 2019 +0000
+++ b/finch/gntui.c	Thu Jul 18 03:45:06 2019 +0000
@@ -65,7 +65,7 @@
 
 	/* Initialize the buddy list */
 	finch_blist_init();
-	purple_blist_set_ui_ops(finch_blist_get_ui_ops());
+	purple_blist_set_ui(FINCH_TYPE_BUDDY_LIST);
 
 	/* Initialize sound */
 	purple_sound_set_ui_ops(finch_sound_get_ui_ops());
@@ -122,7 +122,7 @@
 	purple_connections_set_ui_ops(NULL);
 	finch_connections_uninit();
 
-	purple_blist_set_ui_ops(NULL);
+	purple_blist_set_ui(G_TYPE_INVALID);
 	finch_blist_uninit();
 
 	purple_conversations_set_ui_ops(NULL);
--- a/libpurple/buddylist.c	Wed Jul 17 09:52:28 2019 +0000
+++ b/libpurple/buddylist.c	Thu Jul 18 03:45:06 2019 +0000
@@ -35,11 +35,11 @@
 
 /* Private data for a buddy list. */
 typedef struct  {
+	PurpleBlistNode *root;
 	GHashTable *buddies;  /* Every buddy in this list */
 } PurpleBuddyListPrivate;
 
-static PurpleBlistUiOps *blist_ui_ops = NULL;
-
+static GType buddy_list_type = G_TYPE_INVALID;
 static PurpleBuddyList *purplebuddylist = NULL;
 
 G_DEFINE_TYPE_WITH_PRIVATE(PurpleBuddyList, purple_buddy_list, G_TYPE_OBJECT);
@@ -454,11 +454,15 @@
 
 void purple_blist_schedule_save()
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
+
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
 
 	/* Save everything */
-	if (ops && ops->save_account) {
-		ops->save_account(purplebuddylist, NULL);
+	if (klass && klass->save_account) {
+		klass->save_account(purplebuddylist, NULL);
 	}
 }
 
@@ -609,8 +613,8 @@
 	PurpleXmlNode *cnode;
 
 	group = purple_group_new(name);
-	purple_blist_add_group(group,
-			purple_blist_get_last_sibling(purplebuddylist->root));
+	purple_blist_add_group(group, purple_blist_get_last_sibling(
+	                                      purple_blist_get_default_root()));
 
 	for (cnode = groupnode->child; cnode; cnode = cnode->next) {
 		if (cnode->type != PURPLE_XMLNODE_TYPE_TAG)
@@ -707,13 +711,18 @@
  *****************************************************************************/
 
 void
+purple_blist_set_ui(GType type)
+{
+	g_return_if_fail(g_type_is_a(type, PURPLE_TYPE_BUDDY_LIST) ||
+	                 type == G_TYPE_INVALID);
+	buddy_list_type = type;
+}
+
+void
 purple_blist_boot(void)
 {
-	PurpleBlistUiOps *ui_ops;
 	GList *account;
-	PurpleBuddyList *gbl = g_object_new(PURPLE_TYPE_BUDDY_LIST, NULL);
-
-	ui_ops = purple_blist_get_ui_ops();
+	PurpleBuddyList *gbl = g_object_new(buddy_list_type, NULL);
 
 	buddies_cache = g_hash_table_new_full(g_direct_hash, g_direct_equal,
 					 NULL, (GDestroyNotify)g_hash_table_destroy);
@@ -727,9 +736,6 @@
 		purple_blist_buddies_cache_add_account(account->data);
 	}
 
-	if (ui_ops != NULL && ui_ops->new_list != NULL)
-		ui_ops->new_list(gbl);
-
 	purplebuddylist = gbl;
 
 	load_blist();
@@ -744,15 +750,23 @@
 PurpleBlistNode *
 purple_blist_get_default_root(void)
 {
-	return purplebuddylist ? purplebuddylist->root : NULL;
+	if (purplebuddylist) {
+		PurpleBuddyListPrivate *priv =
+		        purple_buddy_list_get_instance_private(purplebuddylist);
+		return priv->root;
+	}
+	return NULL;
 }
 
 PurpleBlistNode *
 purple_blist_get_root(PurpleBuddyList *list)
 {
-	g_return_val_if_fail(PURPLE_IS_BUDDY_LIST(list), NULL);
+	PurpleBuddyListPrivate *priv = NULL;
 
-	return list->root;
+	g_return_val_if_fail(PURPLE_IS_BUDDY_LIST(list), NULL);
+	priv = purple_buddy_list_get_instance_private(list);
+
+	return priv->root;
 }
 
 static void
@@ -776,32 +790,28 @@
 	return buddies;
 }
 
-void *
-purple_blist_get_ui_data()
-{
-	return purplebuddylist->ui_data;
-}
-
-void
-purple_blist_set_ui_data(void *ui_data)
-{
-	purplebuddylist->ui_data = ui_data;
-}
-
 void purple_blist_show()
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 
-	if (ops && ops->show)
-		ops->show(purplebuddylist);
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+
+	if (klass && klass->show) {
+		klass->show(purplebuddylist);
+	}
 }
 
 void purple_blist_set_visible(gboolean show)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 
-	if (ops && ops->set_visible)
-		ops->set_visible(purplebuddylist, show);
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+
+	if (klass && klass->set_visible) {
+		klass->set_visible(purplebuddylist, show);
+	}
 }
 
 void purple_blist_update_buddies_cache(PurpleBuddy *buddy, const char *new_name)
@@ -853,10 +863,12 @@
 void purple_blist_add_chat(PurpleChat *chat, PurpleGroup *group, PurpleBlistNode *node)
 {
 	PurpleBlistNode *cnode = PURPLE_BLIST_NODE(chat);
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 	PurpleCountingNode *group_counter;
 
 	g_return_if_fail(PURPLE_IS_CHAT(chat));
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
 
 	if (node == NULL) {
 		if (group == NULL)
@@ -894,12 +906,13 @@
 		if (cnode->parent->child == cnode)
 			cnode->parent->child = cnode->next;
 
-		if (ops && ops->remove)
-			ops->remove(purplebuddylist, cnode);
+		if (klass && klass->remove) {
+			klass->remove(purplebuddylist, cnode);
+		}
 		/* ops->remove() cleaned up the cnode's ui_data, so we need to
 		 * reinitialize it */
-		if (ops && ops->new_node) {
-			ops->new_node(purplebuddylist, cnode);
+		if (klass && klass->new_node) {
+			klass->new_node(purplebuddylist, cnode);
 		}
 	}
 
@@ -931,12 +944,14 @@
 		}
 	}
 
-	if (ops) {
-		if (ops->save_node) {
-			ops->save_node(purplebuddylist, cnode);
+	if (klass) {
+		if (klass->save_node) {
+			klass->save_node(purplebuddylist, cnode);
 		}
-		if (ops->update)
-			ops->update(purplebuddylist, PURPLE_BLIST_NODE(cnode));
+		if (klass->update) {
+			klass->update(purplebuddylist,
+			              PURPLE_BLIST_NODE(cnode));
+		}
 	}
 
 	purple_signal_emit(purple_blist_get_handle(), "blist-node-added",
@@ -945,19 +960,21 @@
 
 void purple_blist_add_buddy(PurpleBuddy *buddy, PurpleContact *contact, PurpleGroup *group, PurpleBlistNode *node)
 {
+	PurpleBuddyListClass *klass = NULL;
+	PurpleBuddyListPrivate *priv = NULL;
 	PurpleBlistNode *cnode, *bnode;
 	PurpleCountingNode *contact_counter, *group_counter;
 	PurpleGroup *g;
 	PurpleContact *c;
 	PurpleAccount *account;
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
 	struct _purple_hbuddy *hb, *hb2;
 	GHashTable *account_buddies;
-	PurpleBuddyListPrivate *priv =
-			purple_buddy_list_get_instance_private(purplebuddylist);
 
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 	g_return_if_fail(PURPLE_IS_BUDDY(buddy));
 
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	priv = purple_buddy_list_get_instance_private(purplebuddylist);
 	bnode = PURPLE_BLIST_NODE(buddy);
 	account = purple_buddy_get_account(buddy);
 
@@ -979,8 +996,8 @@
 			g = purple_blist_get_default_group();
 		/* Add group to blist if isn't already on it. Fixes #2752. */
 		if (!purple_blist_find_group(purple_group_get_name(g))) {
-			purple_blist_add_group(g,
-					purple_blist_get_last_sibling(purplebuddylist->root));
+			purple_blist_add_group(
+			        g, purple_blist_get_last_sibling(priv->root));
 		}
 		c = purple_contact_new();
 		purple_blist_add_contact(c, g,
@@ -1018,8 +1035,9 @@
 		if (bnode->parent->child == bnode)
 			bnode->parent->child = bnode->next;
 
-		if (ops && ops->remove)
-			ops->remove(purplebuddylist, bnode);
+		if (klass && klass->remove) {
+			klass->remove(purplebuddylist, bnode);
+		}
 
 		if (bnode->parent->parent != (PurpleBlistNode*)g) {
 			struct _purple_hbuddy hb;
@@ -1038,8 +1056,9 @@
 		} else {
 			purple_contact_invalidate_priority_buddy((PurpleContact*)bnode->parent);
 
-			if (ops && ops->update)
-				ops->update(purplebuddylist, bnode->parent);
+			if (klass && klass->update) {
+				klass->update(purplebuddylist, bnode->parent);
+			}
 		}
 	}
 
@@ -1092,13 +1111,15 @@
 
 	purple_contact_invalidate_priority_buddy(purple_buddy_get_contact(buddy));
 
-	if (ops) {
-		if (ops->save_node) {
-			ops->save_node(purplebuddylist,
-			               (PurpleBlistNode *)buddy);
+	if (klass) {
+		if (klass->save_node) {
+			klass->save_node(purplebuddylist,
+			                 (PurpleBlistNode *)buddy);
 		}
-		if (ops->update)
-			ops->update(purplebuddylist, PURPLE_BLIST_NODE(buddy));
+		if (klass->update) {
+			klass->update(purplebuddylist,
+			              PURPLE_BLIST_NODE(buddy));
+		}
 	}
 
 	/* Signal that the buddy has been added */
@@ -1108,18 +1129,21 @@
 
 void purple_blist_add_contact(PurpleContact *contact, PurpleGroup *group, PurpleBlistNode *node)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
+	PurpleBuddyListPrivate *priv = NULL;
 	PurpleGroup *g;
 	PurpleBlistNode *gnode, *cnode, *bnode;
 	PurpleCountingNode *contact_counter, *group_counter;
-	PurpleBuddyListPrivate *priv =
-			purple_buddy_list_get_instance_private(purplebuddylist);
 
 	g_return_if_fail(PURPLE_IS_CONTACT(contact));
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 
 	if (PURPLE_BLIST_NODE(contact) == node)
 		return;
 
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	priv = purple_buddy_list_get_instance_private(purplebuddylist);
+
 	if (node && (PURPLE_IS_CONTACT(node) ||
 				PURPLE_IS_CHAT(node)))
 		g = PURPLE_GROUP(node->parent);
@@ -1205,11 +1229,12 @@
 			purple_counting_node_change_current_size(group_counter, -1);
 		purple_counting_node_change_total_size(group_counter, -1);
 
-		if (ops && ops->remove)
-			ops->remove(purplebuddylist, cnode);
+		if (klass && klass->remove) {
+			klass->remove(purplebuddylist, cnode);
+		}
 
-		if (ops && ops->remove_node) {
-			ops->remove_node(purplebuddylist, cnode);
+		if (klass && klass->remove_node) {
+			klass->remove_node(purplebuddylist, cnode);
 		}
 	}
 
@@ -1239,52 +1264,59 @@
 		purple_counting_node_change_current_size(group_counter, +1);
 	purple_counting_node_change_total_size(group_counter, +1);
 
-	if (ops && ops->save_node)
-	{
+	if (klass && klass->save_node) {
 		if (cnode->child) {
-			ops->save_node(purplebuddylist, cnode);
+			klass->save_node(purplebuddylist, cnode);
 		}
 		for (bnode = cnode->child; bnode; bnode = bnode->next) {
-			ops->save_node(purplebuddylist, bnode);
+			klass->save_node(purplebuddylist, bnode);
 		}
 	}
 
-	if (ops && ops->update)
-	{
-		if (cnode->child)
-			ops->update(purplebuddylist, cnode);
+	if (klass && klass->update) {
+		if (cnode->child) {
+			klass->update(purplebuddylist, cnode);
+		}
 
-		for (bnode = cnode->child; bnode; bnode = bnode->next)
-			ops->update(purplebuddylist, bnode);
+		for (bnode = cnode->child; bnode; bnode = bnode->next) {
+			klass->update(purplebuddylist, bnode);
+		}
 	}
 }
 
 void purple_blist_add_group(PurpleGroup *group, PurpleBlistNode *node)
 {
-	PurpleBlistUiOps *ops;
+	PurpleBuddyListClass *klass = NULL;
+	PurpleBuddyListPrivate *priv = NULL;
 	PurpleBlistNode *gnode = (PurpleBlistNode*)group;
 	gchar* key;
 
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 	g_return_if_fail(PURPLE_IS_GROUP(group));
 
-	ops = purple_blist_get_ui_ops();
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	priv = purple_buddy_list_get_instance_private(purplebuddylist);
 
 	/* if we're moving to overtop of ourselves, do nothing */
 	if (gnode == node) {
-		if (!purplebuddylist->root)
+		if (!priv->root) {
 			node = NULL;
-		else
+		} else {
 			return;
+		}
 	}
 
 	if (purple_blist_find_group(purple_group_get_name(group))) {
 		/* This is just being moved */
 
-		if (ops && ops->remove)
-			ops->remove(purplebuddylist, (PurpleBlistNode *)group);
+		if (klass && klass->remove) {
+			klass->remove(purplebuddylist,
+			              (PurpleBlistNode *)group);
+		}
 
-		if (gnode == purplebuddylist->root)
-			purplebuddylist->root = gnode->next;
+		if (gnode == priv->root) {
+			priv->root = gnode->next;
+		}
 		if (gnode->prev)
 			gnode->prev->next = gnode->next;
 		if (gnode->next)
@@ -1301,24 +1333,26 @@
 			node->next->prev = gnode;
 		node->next = gnode;
 	} else {
-		if (purplebuddylist->root)
-			purplebuddylist->root->prev = gnode;
-		gnode->next = purplebuddylist->root;
+		if (priv->root) {
+			priv->root->prev = gnode;
+		}
+		gnode->next = priv->root;
 		gnode->prev = NULL;
-		purplebuddylist->root = gnode;
+		priv->root = gnode;
 	}
 
-	if (ops && ops->save_node) {
-		ops->save_node(purplebuddylist, gnode);
+	if (klass && klass->save_node) {
+		klass->save_node(purplebuddylist, gnode);
 		for (node = gnode->child; node; node = node->next) {
-			ops->save_node(purplebuddylist, node);
+			klass->save_node(purplebuddylist, node);
 		}
 	}
 
-	if (ops && ops->update) {
-		ops->update(purplebuddylist, gnode);
-		for (node = gnode->child; node; node = node->next)
-			ops->update(purplebuddylist, node);
+	if (klass && klass->update) {
+		klass->update(purplebuddylist, gnode);
+		for (node = gnode->child; node; node = node->next) {
+			klass->update(purplebuddylist, node);
+		}
 	}
 
 	purple_signal_emit(purple_blist_get_handle(), "blist-node-added",
@@ -1327,12 +1361,14 @@
 
 void purple_blist_remove_contact(PurpleContact *contact)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 	PurpleBlistNode *node, *gnode;
 	PurpleGroup *group;
 
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 	g_return_if_fail(PURPLE_IS_CONTACT(contact));
 
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
 	node = (PurpleBlistNode *)contact;
 	gnode = node->parent;
 	group = PURPLE_GROUP(gnode);
@@ -1363,11 +1399,12 @@
 		purple_counting_node_change_total_size(PURPLE_COUNTING_NODE(group), -1);
 
 		/* Update the UI */
-		if (ops && ops->remove)
-			ops->remove(purplebuddylist, node);
+		if (klass && klass->remove) {
+			klass->remove(purplebuddylist, node);
+		}
 
-		if (ops && ops->remove_node) {
-			ops->remove_node(purplebuddylist, node);
+		if (klass && klass->remove_node) {
+			klass->remove_node(purplebuddylist, node);
 		}
 
 		purple_signal_emit(purple_blist_get_handle(), "blist-node-removed",
@@ -1380,9 +1417,8 @@
 
 void purple_blist_remove_buddy(PurpleBuddy *buddy)
 {
-	PurpleBuddyListPrivate *priv =
-			purple_buddy_list_get_instance_private(purplebuddylist);
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
+	PurpleBuddyListPrivate *priv = NULL;
 	PurpleBlistNode *node, *cnode, *gnode;
 	PurpleCountingNode *contact_counter, *group_counter;
 	PurpleContact *contact;
@@ -1391,8 +1427,11 @@
 	GHashTable *account_buddies;
 	PurpleAccount *account;
 
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 	g_return_if_fail(PURPLE_IS_BUDDY(buddy));
 
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	priv = purple_buddy_list_get_instance_private(purplebuddylist);
 	account = purple_buddy_get_account(buddy);
 	node = PURPLE_BLIST_NODE(buddy);
 	cnode = node->parent;
@@ -1429,8 +1468,9 @@
 		if (cnode->child && purple_contact_get_priority_buddy(contact) == buddy) {
 			purple_contact_invalidate_priority_buddy(contact);
 
-			if (ops && ops->update)
-				ops->update(purplebuddylist, cnode);
+			if (klass && klass->update) {
+				klass->update(purplebuddylist, cnode);
+			}
 		}
 	}
 
@@ -1444,11 +1484,12 @@
 	g_hash_table_remove(account_buddies, &hb);
 
 	/* Update the UI */
-	if (ops && ops->remove)
-		ops->remove(purplebuddylist, node);
+	if (klass && klass->remove) {
+		klass->remove(purplebuddylist, node);
+	}
 
-	if (ops && ops->remove_node) {
-		ops->remove_node(purplebuddylist, node);
+	if (klass && klass->remove_node) {
+		klass->remove_node(purplebuddylist, node);
 	}
 
 	/* Remove this buddy's pounces */
@@ -1467,13 +1508,15 @@
 
 void purple_blist_remove_chat(PurpleChat *chat)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 	PurpleBlistNode *node, *gnode;
 	PurpleGroup *group;
 	PurpleCountingNode *group_counter;
 
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 	g_return_if_fail(PURPLE_IS_CHAT(chat));
 
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
 	node = (PurpleBlistNode *)chat;
 	gnode = node->parent;
 	group = (PurpleGroup *)gnode;
@@ -1498,11 +1541,12 @@
 	}
 
 	/* Update the UI */
-	if (ops && ops->remove)
-		ops->remove(purplebuddylist, node);
+	if (klass && klass->remove) {
+		klass->remove(purplebuddylist, node);
+	}
 
-	if (ops && ops->remove_node) {
-		ops->remove_node(purplebuddylist, node);
+	if (klass && klass->remove_node) {
+		klass->remove_node(purplebuddylist, node);
 	}
 
 	purple_signal_emit(purple_blist_get_handle(), "blist-node-removed",
@@ -1514,16 +1558,20 @@
 
 void purple_blist_remove_group(PurpleGroup *group)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
+	PurpleBuddyListPrivate *priv = NULL;
 	PurpleBlistNode *node;
 	GList *l;
 	gchar* key;
 
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 	g_return_if_fail(PURPLE_IS_GROUP(group));
 
 	if (group == purple_blist_get_default_group())
 		purple_debug_warning("buddylist", "cannot remove default group");
 
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	priv = purple_buddy_list_get_instance_private(purplebuddylist);
 	node = (PurpleBlistNode *)group;
 
 	/* Make sure the group is empty */
@@ -1531,8 +1579,9 @@
 		return;
 
 	/* Remove the node from its parent */
-	if (purplebuddylist->root == node)
-		purplebuddylist->root = node->next;
+	if (priv->root == node) {
+		priv->root = node->next;
+	}
 	if (node->prev)
 		node->prev->next = node->next;
 	if (node->next)
@@ -1543,11 +1592,12 @@
 	g_free(key);
 
 	/* Update the UI */
-	if (ops && ops->remove)
-		ops->remove(purplebuddylist, node);
+	if (klass && klass->remove) {
+		klass->remove(purplebuddylist, node);
+	}
 
-	if (ops && ops->remove_node) {
-		ops->remove_node(purplebuddylist, node);
+	if (klass && klass->remove_node) {
+		klass->remove_node(purplebuddylist, node);
 	}
 
 	purple_signal_emit(purple_blist_get_handle(), "blist-node-removed",
@@ -1581,7 +1631,7 @@
 	hb.account = account;
 	hb.name = (gchar *)purple_normalize(account, name);
 
-	for (group = purplebuddylist->root; group; group = group->next) {
+	for (group = priv->root; group; group = group->next) {
 		if (!group->child)
 			continue;
 
@@ -1637,7 +1687,7 @@
 		hb.name = (gchar *)purple_normalize(account, name);
 		hb.account = account;
 
-		for (node = purplebuddylist->root; node != NULL; node = node->next) {
+		for (node = priv->root; node != NULL; node = node->next) {
 			if (!node->child)
 				continue;
 
@@ -1748,14 +1798,16 @@
 
 void purple_blist_add_account(PurpleAccount *account)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 	PurpleBlistNode *gnode, *cnode, *bnode;
 	PurpleCountingNode *contact_counter, *group_counter;
 
 	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 
-	if (!ops || !ops->update)
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	if (!klass || !klass->update) {
 		return;
+	}
 
 	for (gnode = purple_blist_get_default_root(); gnode;
 	     gnode = gnode->next) {
@@ -1773,29 +1825,34 @@
 							purple_counting_node_change_current_size(contact_counter, +1);
 							if (purple_counting_node_get_current_size(contact_counter) == 1)
 								purple_counting_node_change_current_size(group_counter, +1);
-							ops->update(purplebuddylist, bnode);
-						}
-					}
-					if (recompute ||
-							purple_blist_node_get_bool(cnode, "show_offline")) {
-						purple_contact_invalidate_priority_buddy((PurpleContact*)cnode);
-						ops->update(purplebuddylist, cnode);
-					}
+						        klass->update(
+						                purplebuddylist,
+						                bnode);
+					        }
+				        }
+				        if (recompute ||
+				            purple_blist_node_get_bool(
+				                    cnode, "show_offline")) {
+					        purple_contact_invalidate_priority_buddy(
+					                (PurpleContact *)cnode);
+					        klass->update(purplebuddylist,
+					                      cnode);
+				        }
 			} else if (PURPLE_IS_CHAT(cnode) &&
 					purple_chat_get_account(PURPLE_CHAT(cnode)) == account) {
 				group_counter = PURPLE_COUNTING_NODE(gnode);
 				purple_counting_node_change_online_count(group_counter, +1);
 				purple_counting_node_change_current_size(group_counter, +1);
-				ops->update(purplebuddylist, cnode);
+				klass->update(purplebuddylist, cnode);
 			}
 		}
-		ops->update(purplebuddylist, gnode);
+		klass->update(purplebuddylist, gnode);
 	}
 }
 
 void purple_blist_remove_account(PurpleAccount *account)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
+	PurpleBuddyListClass *klass = NULL;
 	PurpleBlistNode *gnode, *cnode, *bnode;
 	PurpleCountingNode *contact_counter, *group_counter;
 	PurpleBuddy *buddy;
@@ -1805,6 +1862,7 @@
 	GList *list = NULL, *iter = NULL;
 
 	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
 
 	for (gnode = purple_blist_get_default_root(); gnode;
 	     gnode = gnode->next) {
@@ -1851,16 +1909,20 @@
 						else
 							recompute = TRUE;
 
-						if (ops && ops->remove) {
-							ops->remove(purplebuddylist, bnode);
+						if (klass && klass->remove) {
+							klass->remove(
+							        purplebuddylist,
+							        bnode);
 						}
 					}
 				}
 				if (recompute) {
 					purple_contact_invalidate_priority_buddy(contact);
 
-					if (ops && ops->update)
-						ops->update(purplebuddylist, cnode);
+					if (klass && klass->update) {
+						klass->update(purplebuddylist,
+						              cnode);
+					}
 				}
 			} else if (PURPLE_IS_CHAT(cnode)) {
 				chat = PURPLE_CHAT(cnode);
@@ -1870,8 +1932,10 @@
 					purple_counting_node_change_current_size(group_counter, -1);
 					purple_counting_node_change_online_count(group_counter, -1);
 
-					if (ops && ops->remove)
-						ops->remove(purplebuddylist, cnode);
+					if (klass && klass->remove) {
+						klass->remove(purplebuddylist,
+						              cnode);
+					}
 				}
 			}
 		}
@@ -1924,13 +1988,14 @@
 purple_blist_request_add_buddy(PurpleAccount *account, const char *username,
 							 const char *group, const char *alias)
 {
-	PurpleBlistUiOps *ui_ops;
+	PurpleBuddyListClass *klass = NULL;
 
-	ui_ops = purple_blist_get_ui_ops();
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 
-	if (ui_ops != NULL && ui_ops->request_add_buddy != NULL) {
-		ui_ops->request_add_buddy(purplebuddylist, account, username,
-		                          group, alias);
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	if (klass != NULL && klass->request_add_buddy != NULL) {
+		klass->request_add_buddy(purplebuddylist, account, username,
+		                         group, alias);
 	}
 }
 
@@ -1938,98 +2003,80 @@
 purple_blist_request_add_chat(PurpleAccount *account, PurpleGroup *group,
 							const char *alias, const char *name)
 {
-	PurpleBlistUiOps *ui_ops;
+	PurpleBuddyListClass *klass = NULL;
 
-	ui_ops = purple_blist_get_ui_ops();
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 
-	if (ui_ops != NULL && ui_ops->request_add_chat != NULL) {
-		ui_ops->request_add_chat(purplebuddylist, account, group, alias,
-		                         name);
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	if (klass != NULL && klass->request_add_chat != NULL) {
+		klass->request_add_chat(purplebuddylist, account, group, alias,
+		                        name);
 	}
 }
 
 void
 purple_blist_request_add_group(void)
 {
-	PurpleBlistUiOps *ui_ops;
+	PurpleBuddyListClass *klass = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(purplebuddylist));
 
-	ui_ops = purple_blist_get_ui_ops();
-
-	if (ui_ops != NULL && ui_ops->request_add_group != NULL) {
-		ui_ops->request_add_group(purplebuddylist);
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(purplebuddylist);
+	if (klass != NULL && klass->request_add_group != NULL) {
+		klass->request_add_group(purplebuddylist);
 	}
 }
 
 void
 purple_blist_new_node(PurpleBuddyList *list, PurpleBlistNode *node)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
-	if (ops && ops->new_node) {
-		ops->new_node(list, node);
+	PurpleBuddyListClass *klass = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(list));
+
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(list);
+	if (klass && klass->new_node) {
+		klass->new_node(list, node);
 	}
 }
 
 void
 purple_blist_update_node(PurpleBuddyList *list, PurpleBlistNode *node)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
-	if (ops && ops->update) {
-		ops->update(list, node);
+	PurpleBuddyListClass *klass = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(list));
+
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(list);
+	if (klass && klass->update) {
+		klass->update(list, node);
 	}
 }
 
 void
 purple_blist_save_node(PurpleBuddyList *list, PurpleBlistNode *node)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
-	if (ops && ops->save_node) {
-		ops->save_node(list, node);
+	PurpleBuddyListClass *klass = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(list));
+
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(list);
+	if (klass && klass->save_node) {
+		klass->save_node(list, node);
 	}
 }
 
 void
 purple_blist_save_account(PurpleBuddyList *list, PurpleAccount *account)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
-	if (ops && ops->save_account) {
-		ops->save_account(list, account);
-	}
-}
+	PurpleBuddyListClass *klass = NULL;
+
+	g_return_if_fail(PURPLE_IS_BUDDY_LIST(list));
 
-void
-purple_blist_set_ui_ops(PurpleBlistUiOps *ops)
-{
-	gboolean overrode = FALSE;
-	blist_ui_ops = ops;
-
-	if (!ops)
-		return;
-
-	if (!ops->save_node) {
-		ops->save_node = purple_blist_real_save_node;
-		overrode = TRUE;
+	klass = PURPLE_BUDDY_LIST_GET_CLASS(list);
+	if (klass && klass->save_account) {
+		klass->save_account(list, account);
 	}
-	if (!ops->remove_node) {
-		ops->remove_node = purple_blist_real_save_node;
-		overrode = TRUE;
-	}
-	if (!ops->save_account) {
-		ops->save_account = purple_blist_real_save_account;
-		overrode = TRUE;
-	}
-
-	if (overrode && (ops->save_node != purple_blist_real_save_node ||
-	                 ops->remove_node != purple_blist_real_save_node ||
-	                 ops->save_account != purple_blist_real_save_account)) {
-		purple_debug_warning("buddylist", "Only some of the blist saving UI ops "
-				"were overridden. This probably is not what you want!\n");
-	}
-}
-
-PurpleBlistUiOps *
-purple_blist_get_ui_ops(void)
-{
-	return blist_ui_ops;
 }
 
 const gchar *
@@ -2051,6 +2098,9 @@
 {
 	void *handle = purple_blist_get_handle();
 
+	/* Set a default, which can't be done as a static initializer. */
+	buddy_list_type = PURPLE_TYPE_BUDDY_LIST;
+
 	purple_signal_register(handle, "buddy-status-changed",
 	                     purple_marshal_VOID__POINTER_POINTER_POINTER,
 	                     G_TYPE_NONE, 3, PURPLE_TYPE_BUDDY, PURPLE_TYPE_STATUS, 
@@ -2119,16 +2169,15 @@
 }
 
 static void
-blist_node_destroy(PurpleBlistNode *node)
+blist_node_destroy(PurpleBuddyListClass *klass, PurpleBuddyList *list,
+                   PurpleBlistNode *node)
 {
-	PurpleBlistUiOps *ui_ops;
 	PurpleBlistNode *child, *next_child;
 
-	ui_ops = purple_blist_get_ui_ops();
 	child = node->child;
 	while (child) {
 		next_child = child->next;
-		blist_node_destroy(child);
+		blist_node_destroy(klass, list, child);
 		child = next_child;
 	}
 
@@ -2137,8 +2186,9 @@
 	node->child  = NULL;
 	node->next   = NULL;
 	node->prev   = NULL;
-	if (ui_ops && ui_ops->remove)
-		ui_ops->remove(purplebuddylist, node);
+	if (klass && klass->remove) {
+		klass->remove(list, node);
+	}
 
 	g_object_unref(node);
 }
@@ -2146,9 +2196,6 @@
 void
 purple_blist_uninit(void)
 {
-	PurpleBlistUiOps *ops = purple_blist_get_ui_ops();
-	PurpleBlistNode *node, *next_node;
-
 	/* This happens if we quit before purple_set_blist is called. */
 	if (purplebuddylist == NULL)
 		return;
@@ -2161,25 +2208,13 @@
 
 	purple_debug(PURPLE_DEBUG_INFO, "buddylist", "Destroying\n");
 
-	if (ops && ops->destroy)
-		ops->destroy(purplebuddylist);
-
-	node = purple_blist_get_default_root();
-	while (node) {
-		next_node = node->next;
-		blist_node_destroy(node);
-		node = next_node;
-	}
-	purplebuddylist->root = NULL;
-
 	g_hash_table_destroy(buddies_cache);
 	g_hash_table_destroy(groups_cache);
 
 	buddies_cache = NULL;
 	groups_cache = NULL;
 
-	g_object_unref(purplebuddylist);
-	purplebuddylist = NULL;
+	g_clear_object(&purplebuddylist);
 
 	g_free(localized_default_group_name);
 	localized_default_group_name = NULL;
@@ -2189,36 +2224,6 @@
 }
 
 /**************************************************************************
- * GBoxed code
- **************************************************************************/
-static PurpleBlistUiOps *
-purple_blist_ui_ops_copy(PurpleBlistUiOps *ops)
-{
-	PurpleBlistUiOps *ops_new;
-
-	g_return_val_if_fail(ops != NULL, NULL);
-
-	ops_new = g_new(PurpleBlistUiOps, 1);
-	*ops_new = *ops;
-
-	return ops_new;
-}
-
-GType
-purple_blist_ui_ops_get_type(void)
-{
-	static GType type = 0;
-
-	if (type == 0) {
-		type = g_boxed_type_register_static("PurpleBlistUiOps",
-				(GBoxedCopyFunc)purple_blist_ui_ops_copy,
-				(GBoxedFreeFunc)g_free);
-	}
-
-	return type;
-}
-
-/**************************************************************************
  * GObject code
  **************************************************************************/
 
@@ -2239,10 +2244,22 @@
 static void
 purple_buddy_list_finalize(GObject *object)
 {
-	PurpleBuddyListPrivate *priv = purple_buddy_list_get_instance_private(
-			PURPLE_BUDDY_LIST(object));
+	PurpleBuddyList *list = PURPLE_BUDDY_LIST(object);
+	PurpleBuddyListClass *klass = PURPLE_BUDDY_LIST_GET_CLASS(list);
+	PurpleBuddyListPrivate *priv =
+	        purple_buddy_list_get_instance_private(list);
+	PurpleBlistNode *node, *next_node;
+
 	g_hash_table_destroy(priv->buddies);
 
+	node = priv->root;
+	while (node) {
+		next_node = node->next;
+		blist_node_destroy(klass, list, node);
+		node = next_node;
+	}
+	priv->root = NULL;
+
 	G_OBJECT_CLASS(purple_buddy_list_parent_class)->finalize(object);
 }
 
@@ -2252,5 +2269,8 @@
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
 	obj_class->finalize = purple_buddy_list_finalize;
+
+	klass->save_node = purple_blist_real_save_node;
+	klass->remove_node = purple_blist_real_save_node;
+	klass->save_account = purple_blist_real_save_account;
 }
-
--- a/libpurple/buddylist.h	Wed Jul 17 09:52:28 2019 +0000
+++ b/libpurple/buddylist.h	Thu Jul 18 03:45:06 2019 +0000
@@ -33,19 +33,8 @@
 
 #include "buddy.h"
 
-#define PURPLE_TYPE_BUDDY_LIST             (purple_buddy_list_get_type())
-#define PURPLE_BUDDY_LIST(obj)             (G_TYPE_CHECK_INSTANCE_CAST((obj), PURPLE_TYPE_BUDDY_LIST, PurpleBuddyList))
-#define PURPLE_BUDDY_LIST_CLASS(klass)     (G_TYPE_CHECK_CLASS_CAST((klass), PURPLE_TYPE_BUDDY_LIST, PurpleBuddyListClass))
-#define PURPLE_IS_BUDDY_LIST(obj)          (G_TYPE_CHECK_INSTANCE_TYPE((obj), PURPLE_TYPE_BUDDY_LIST))
-#define PURPLE_IS_BUDDY_LIST_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass), PURPLE_TYPE_BUDDY_LIST))
-#define PURPLE_BUDDY_LIST_GET_CLASS(obj)   (G_TYPE_INSTANCE_GET_CLASS((obj), PURPLE_TYPE_BUDDY_LIST, PurpleBuddyListClass))
-
-typedef struct _PurpleBuddyList       PurpleBuddyList;
-typedef struct _PurpleBuddyListClass  PurpleBuddyListClass;
-
-#define PURPLE_TYPE_BLIST_UI_OPS (purple_blist_ui_ops_get_type())
-
-typedef struct _PurpleBlistUiOps PurpleBlistUiOps;
+#define PURPLE_TYPE_BUDDY_LIST (purple_buddy_list_get_type())
+typedef struct _PurpleBuddyList PurpleBuddyList;
 
 #define PURPLE_BLIST_DEFAULT_GROUP_NAME _("Buddies")
 
@@ -67,55 +56,32 @@
 /**************************************************************************/
 /**
  * PurpleBuddyList:
- * @root:    The first node in the buddy list
- * @ui_data: The UI data associated with this buddy list. This is a convenience
- *           field provided to the UIs -- it is not used by the libpurple core.
  *
  * The Buddy List
  */
-struct _PurpleBuddyList {
-	GObject gparent;
-
-	/*< public >*/
-	PurpleBlistNode *root;
-	gpointer ui_data;
-};
-
-struct _PurpleBuddyListClass {
-	GObjectClass gparent_class;
-
-	/*< private >*/
-	void (*_purple_reserved1)(void);
-	void (*_purple_reserved2)(void);
-	void (*_purple_reserved3)(void);
-	void (*_purple_reserved4)(void);
-};
-
 /**
- * PurpleBlistUiOps:
- * @new_list:     Sets UI-specific data on a buddy list.
+ * PurpleBuddyListClass:
  * @new_node:     Sets UI-specific data on a node.
  * @show:         The core will call this when it's finished doing its core
- *                stuff
+ *                stuff.
  * @update:       This will update a node in the buddy list.
  * @remove:       This removes a node from the list
- * @destroy:      When the list is destroyed, this is called to destroy the UI.
- * @set_visible:  Hides or unhides the buddy list
+ * @set_visible:  Hides or unhides the buddy list.
  * @save_node:    This is called when a node has been modified and should be
  *                saved.
- *                <sbr/>Implementation of this UI op is
+ *                <sbr/>Implementation of this method is
  *                <emphasis>OPTIONAL</emphasis>. If not implemented, it will be
  *                set to a fallback function that saves data to
  *                <filename>blist.xml</filename> like in previous libpurple
  *                versions.
  *                <sbr/>@node: The node which has been modified.
  * @remove_node:  Called when a node is about to be removed from the buddy list.
- *                The UI op should update the relevant data structures to remove
- *                this node (for example, removing a buddy from the group this
- *                node is in).
- *                <sbr/>Implementation of this UI op is
- *                <emphasis>OPTIONAL</emphasis>. If not implemented,
- *                it will be set to a fallback function that saves data to
+ *                The method should update the relevant data structures to
+ *                remove this node (for example, removing a buddy from the
+ *                group this node is in).
+ *                <sbr/>Implementation of this method is
+ *                <emphasis>OPTIONAL</emphasis>. If not implemented, it will be
+ *                set to a fallback function that saves data to
  *                <filename>blist.xml</filename> like in previous libpurple
  *                versions.
  *                <sbr/>@node: The node which has been modified.
@@ -123,7 +89,7 @@
  *                this, the callback must save the privacy and buddy list data
  *                for an account. If the account is %NULL, save the data for all
  *                accounts.
- *                <sbr/>Implementation of this UI op is
+ *                <sbr/>Implementation of this method is
  *                <emphasis>OPTIONAL</emphasis>. If not implemented, it will be
  *                set to a fallback function that saves data to
  *                <filename>blist.xml</filename> like in previous
@@ -131,19 +97,19 @@
  *                <sbr/>@account: The account whose data to save. If %NULL,
  *                                save all data for all accounts.
  *
- * Buddy list UI operations.
+ * Buddy list operations.
  *
- * Any UI representing a buddy list must assign a filled-out PurpleBlistUiOps
- * structure to the buddy list core.
+ * Any UI representing a buddy list must derive a filled-out
+ * @PurpleBuddyListClass and set the GType using purple_blist_set_ui() before a
+ * buddy list is created.
  */
-struct _PurpleBlistUiOps
-{
-	void (*new_list)(PurpleBuddyList *list);
+struct _PurpleBuddyListClass {
+	GObjectClass gparent_class;
+
 	void (*new_node)(PurpleBuddyList *list, PurpleBlistNode *node);
 	void (*show)(PurpleBuddyList *list);
 	void (*update)(PurpleBuddyList *list, PurpleBlistNode *node);
 	void (*remove)(PurpleBuddyList *list, PurpleBlistNode *node);
-	void (*destroy)(PurpleBuddyList *list);
 	void (*set_visible)(PurpleBuddyList *list, gboolean show);
 
 	void (*request_add_buddy)(PurpleBuddyList *list, PurpleAccount *account,
@@ -162,10 +128,7 @@
 	void (*save_account)(PurpleBuddyList *list, PurpleAccount *account);
 
 	/*< private >*/
-	void (*_purple_reserved1)(void);
-	void (*_purple_reserved2)(void);
-	void (*_purple_reserved3)(void);
-	void (*_purple_reserved4)(void);
+	gpointer reserved[4];
 };
 
 G_BEGIN_DECLS
@@ -179,7 +142,8 @@
  *
  * Returns: The #GType for the #PurpleBuddyList object.
  */
-GType purple_buddy_list_get_type(void);
+G_DECLARE_DERIVABLE_TYPE(PurpleBuddyList, purple_buddy_list, PURPLE, BUDDY_LIST,
+                         GObject)
 
 /**
  * purple_blist_get_default:
@@ -228,23 +192,6 @@
 GSList *purple_blist_get_buddies(void);
 
 /**
- * purple_blist_get_ui_data:
- *
- * Returns the UI data for the list.
- *
- * Returns: The UI data for the list.
- */
-gpointer purple_blist_get_ui_data(void);
-
-/**
- * purple_blist_set_ui_data:
- * @ui_data: The UI data for the list.
- *
- * Sets the UI data for the list.
- */
-void purple_blist_set_ui_data(gpointer ui_data);
-
-/**
  * purple_blist_show:
  *
  * Shows the buddy list, creating a new one if necessary.
@@ -524,10 +471,6 @@
  */
 void purple_blist_request_add_group(void);
 
-/**************************************************************************/
-/* Buddy list UI Functions                                                */
-/**************************************************************************/
-
 /**
  * purple_blist_new_node:
  * @list: The list that contains the node.
@@ -583,36 +526,21 @@
 void purple_blist_save_account(PurpleBuddyList *list, PurpleAccount *account);
 
 /**************************************************************************/
-/* UI Registration Functions                                              */
+/* Buddy List Subsystem                                                   */
 /**************************************************************************/
 
 /**
- * purple_blist_ui_ops_get_type:
+ * purple_blist_set_ui:
+ * @type: The @GType of a derived UI implementation of @PurpleBuddyList.
+ *
+ * Set the UI implementation of the buddy list.
  *
- * Returns: The #GType for the #PurpleBlistUiOps boxed structure.
- */
-GType purple_blist_ui_ops_get_type(void);
-
-/**
- * purple_blist_set_ui_ops:
- * @ops: The ops struct.
+ * This must be called before the buddy list is created or you will get the
+ * default libpurple implementation.
  *
- * Sets the UI operations structure to be used for the buddy list.
+ * Since: 3.0.0
  */
-void purple_blist_set_ui_ops(PurpleBlistUiOps *ops);
-
-/**
- * purple_blist_get_ui_ops:
- *
- * Returns the UI operations structure to be used for the buddy list.
- *
- * Returns: The UI operations structure.
- */
-PurpleBlistUiOps *purple_blist_get_ui_ops(void);
-
-/**************************************************************************/
-/* Buddy List Subsystem                                                   */
-/**************************************************************************/
+void purple_blist_set_ui(GType type);
 
 /**
  * purple_blist_get_handle:
--- a/pidgin/gtkblist.c	Wed Jul 17 09:52:28 2019 +0000
+++ b/pidgin/gtkblist.c	Thu Jul 18 03:45:06 2019 +0000
@@ -129,8 +129,8 @@
 
 } PidginBuddyListPrivate;
 
-#define PIDGIN_BUDDY_LIST_GET_PRIVATE(list) \
-	((PidginBuddyListPrivate *)((list)->priv))
+G_DEFINE_TYPE_WITH_PRIVATE(PidginBuddyList, pidgin_buddy_list,
+                           PURPLE_TYPE_BUDDY_LIST)
 
 #define PIDGIN_WINDOW_ICONIFIED(x) \
 	(gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(x))) & \
@@ -249,14 +249,14 @@
 }
 
 static void
-gtk_blist_hide_cb(GtkWidget *widget, gpointer data)
+gtk_blist_hide_cb(GtkWidget *widget, PidginBuddyList *gtkblist)
 {
 	purple_signal_emit(pidgin_blist_get_handle(),
 			"gtkblist-hiding", gtkblist);
 }
 
 static void
-gtk_blist_show_cb(GtkWidget *widget, gpointer data)
+gtk_blist_show_cb(GtkWidget *widget, PidginBuddyList *gtkblist)
 {
 	purple_signal_emit(pidgin_blist_get_handle(),
 			"gtkblist-unhiding", gtkblist);
@@ -407,6 +407,7 @@
 		gchar *path_str,
 		gpointer user_data)
 {
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(user_data);
 	GtkTreeIter iter;
 	GtkTreePath *path = NULL;
 	PurpleBlistNode *node;
@@ -548,6 +549,7 @@
 static void gtk_blist_renderer_edited_cb(GtkCellRendererText *text_rend, char *arg1,
 					 char *arg2, PurpleBuddyList *list)
 {
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(list);
 	GtkTreeIter iter;
 	GtkTreePath *path;
 	PurpleBlistNode *node;
@@ -974,7 +976,7 @@
 	img = gtk_image_new_from_icon_name("dialog-question",
 			GTK_ICON_SIZE_DIALOG);
 
-	gtkblist = PIDGIN_BLIST(purple_blist_get_default());
+	gtkblist = PIDGIN_BUDDY_LIST(purple_blist_get_default());
 	blist_window = gtkblist ? GTK_WINDOW(gtkblist->window) : NULL;
 
 	data->window = gtk_dialog_new();
@@ -1152,6 +1154,7 @@
 
 static void gtk_blist_row_expanded_cb(GtkTreeView *tv, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data)
 {
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(user_data);
 	PurpleBlistNode *node;
 
 	gtk_tree_model_get(GTK_TREE_MODEL(gtkblist->treemodel), iter, NODE_COLUMN, &node, -1);
@@ -1174,6 +1177,7 @@
 
 static void gtk_blist_row_collapsed_cb(GtkTreeView *tv, GtkTreeIter *iter, GtkTreePath *path, gpointer user_data)
 {
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(user_data);
 	PurpleBlistNode *node;
 
 	gtk_tree_model_get(GTK_TREE_MODEL(gtkblist->treemodel), iter, NODE_COLUMN, &node, -1);
@@ -1209,6 +1213,7 @@
 }
 
 static void gtk_blist_row_activated_cb(GtkTreeView *tv, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) {
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(data);
 	PurpleBlistNode *node;
 	GtkTreeIter iter;
 
@@ -4564,7 +4569,7 @@
 static void
 sign_on_off_cb(PurpleConnection *gc, PurpleBuddyList *blist)
 {
-	PidginBuddyList *gtkblist = PIDGIN_BLIST(blist);
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(blist);
 
 	update_menu_bar(gtkblist);
 }
@@ -4793,65 +4798,10 @@
 	}
 }
 
-/**************************************************************************
- * GTK Buddy list GBoxed code
- **************************************************************************/
-static PidginBuddyList *
-pidgin_buddy_list_ref(PidginBuddyList *gtkblist)
-{
-	PidginBuddyListPrivate *priv;
-
-	g_return_val_if_fail(gtkblist != NULL, NULL);
-
-	priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
-	priv->box_count++;
-
-	return gtkblist;
-}
-
-static void
-pidgin_buddy_list_unref(PidginBuddyList *gtkblist)
-{
-	PidginBuddyListPrivate *priv;
-
-	g_return_if_fail(gtkblist != NULL);
-
-	priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
-
-	g_return_if_fail(priv->box_count >= 0);
-
-	if (!priv->box_count--)
-		purple_core_quit();
-}
-
-GType
-pidgin_buddy_list_get_type(void)
-{
-	static GType type = 0;
-
-	if (type == 0) {
-		type = g_boxed_type_register_static("PidginBuddyList",
-				(GBoxedCopyFunc)pidgin_buddy_list_ref,
-				(GBoxedFreeFunc)pidgin_buddy_list_unref);
-	}
-
-	return type;
-}
-
 /**********************************************************************************
  * Public API Functions                                                           *
  **********************************************************************************/
 
-static void pidgin_blist_new_list(PurpleBuddyList *blist)
-{
-	PidginBuddyList *gtkblist;
-
-	gtkblist = g_new0(PidginBuddyList, 1);
-	gtkblist->priv = g_new0(PidginBuddyListPrivate, 1);
-
-	blist->ui_data = gtkblist;
-}
-
 static void
 pidgin_blist_new_node(PurpleBuddyList *list, PurpleBlistNode *node)
 {
@@ -4943,7 +4893,7 @@
 	GList *list = NULL;
 	PidginBuddyListPrivate *priv;
 
-	priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	priv = pidgin_buddy_list_get_instance_private(gtkblist);
 
 	priv->select_notebook_page_timeout = 0;
 
@@ -4963,7 +4913,8 @@
 
 static void pidgin_blist_select_notebook_page(PidginBuddyList *gtkblist)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	priv->select_notebook_page_timeout = g_timeout_add(0,
 		pidgin_blist_select_notebook_page_cb, gtkblist);
 }
@@ -5096,7 +5047,8 @@
 add_error_dialog(PidginBuddyList *gtkblist,
                  GtkWidget *dialog)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	gtk_container_add(GTK_CONTAINER(priv->error_scrollbook), dialog);
 }
 
@@ -5213,7 +5165,8 @@
 static void
 remove_generic_error_dialog(PurpleAccount *account)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	remove_child_widget_by_account(
 		GTK_CONTAINER(priv->error_scrollbook), account);
 }
@@ -5223,7 +5176,8 @@
 update_generic_error_message(PurpleAccount *account,
                              const char *description)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	GtkWidget *mini_dialog = find_child_widget_by_account(
 		GTK_CONTAINER(priv->error_scrollbook), account);
 	pidgin_mini_dialog_set_description(PIDGIN_MINI_DIALOG(mini_dialog),
@@ -5281,7 +5235,8 @@
 static void
 ensure_signed_on_elsewhere_minidialog(PidginBuddyList *gtkblist)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	PidginMiniDialog *mini_dialog;
 
 	if(priv->signed_on_elsewhere)
@@ -5309,7 +5264,8 @@
 static void
 update_signed_on_elsewhere_minidialog_title(void)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	PidginMiniDialog *mini_dialog = priv->signed_on_elsewhere;
 	guint accounts;
 	char *title;
@@ -5363,7 +5319,8 @@
 static void
 add_to_signed_on_elsewhere(PurpleAccount *account)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	PidginMiniDialog *mini_dialog;
 	GtkWidget *account_label;
 
@@ -5383,7 +5340,8 @@
 static void
 remove_from_signed_on_elsewhere(PurpleAccount *account)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	PidginMiniDialog *mini_dialog = priv->signed_on_elsewhere;
 	if(mini_dialog == NULL)
 		return;
@@ -5398,7 +5356,8 @@
 update_signed_on_elsewhere_tooltip(PurpleAccount *account,
                                    const char *description)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	GtkContainer *c = GTK_CONTAINER(priv->signed_on_elsewhere->contents);
 	GtkWidget *label = find_child_widget_by_account(c, account);
 	gtk_widget_set_tooltip_text(label, description);
@@ -5625,7 +5584,10 @@
 							    "cell-background-rgba", BGCOLOR_COLUMN,
 							    "markup", NAME_COLUMN,
 							    NULL);
-			g_signal_connect(G_OBJECT(rend), "editing-started", G_CALLBACK(gtk_blist_renderer_editing_started_cb), NULL);
+			g_signal_connect(
+			        G_OBJECT(rend), "editing-started",
+			        G_CALLBACK(gtk_blist_renderer_editing_started_cb),
+			        list);
 			g_signal_connect(G_OBJECT(rend), "editing-canceled", G_CALLBACK(gtk_blist_renderer_editing_cancelled_cb), list);
 			g_signal_connect(G_OBJECT(rend), "edited", G_CALLBACK(gtk_blist_renderer_edited_cb), list);
 			g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
@@ -5750,8 +5712,8 @@
 		return;
 	}
 
-	gtkblist = PIDGIN_BLIST(list);
-	priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	gtkblist = PIDGIN_BUDDY_LIST(list);
+	priv = pidgin_buddy_list_get_instance_private(gtkblist);
 
 	if (priv->current_theme)
 		g_object_unref(priv->current_theme);
@@ -5777,9 +5739,9 @@
 
 	g_signal_connect(G_OBJECT(gtkblist->window), "delete_event", G_CALLBACK(gtk_blist_delete_cb), NULL);
 	g_signal_connect(G_OBJECT(gtkblist->window), "hide",
-			G_CALLBACK(gtk_blist_hide_cb), NULL);
+	                 G_CALLBACK(gtk_blist_hide_cb), gtkblist);
 	g_signal_connect(G_OBJECT(gtkblist->window), "show",
-			G_CALLBACK(gtk_blist_show_cb), NULL);
+	                 G_CALLBACK(gtk_blist_show_cb), gtkblist);
 	g_signal_connect(G_OBJECT(gtkblist->window), "size-allocate",
 			G_CALLBACK(gtk_blist_size_allocate_cb), NULL);
 	g_signal_connect(G_OBJECT(gtkblist->window), "visibility_notify_event", G_CALLBACK(gtk_blist_visibility_cb), NULL);
@@ -5957,9 +5919,12 @@
 	gtk_tree_view_append_column(GTK_TREE_VIEW(gtkblist->treeview), gtkblist->text_column);
 	pidgin_blist_build_layout(list);
 
-	g_signal_connect(G_OBJECT(gtkblist->treeview), "row-activated", G_CALLBACK(gtk_blist_row_activated_cb), NULL);
-	g_signal_connect(G_OBJECT(gtkblist->treeview), "row-expanded", G_CALLBACK(gtk_blist_row_expanded_cb), NULL);
-	g_signal_connect(G_OBJECT(gtkblist->treeview), "row-collapsed", G_CALLBACK(gtk_blist_row_collapsed_cb), NULL);
+	g_signal_connect(G_OBJECT(gtkblist->treeview), "row-activated",
+	                 G_CALLBACK(gtk_blist_row_activated_cb), gtkblist);
+	g_signal_connect(G_OBJECT(gtkblist->treeview), "row-expanded",
+	                 G_CALLBACK(gtk_blist_row_expanded_cb), gtkblist);
+	g_signal_connect(G_OBJECT(gtkblist->treeview), "row-collapsed",
+	                 G_CALLBACK(gtk_blist_row_collapsed_cb), gtkblist);
 	g_signal_connect(G_OBJECT(gtkblist->treeview), "button-press-event", G_CALLBACK(gtk_blist_button_press_cb), NULL);
 	g_signal_connect(G_OBJECT(gtkblist->treeview), "key-press-event", G_CALLBACK(gtk_blist_key_press_cb), NULL);
 	g_signal_connect(G_OBJECT(gtkblist->treeview), "popup-menu", G_CALLBACK(pidgin_blist_popup_menu_cb), NULL);
@@ -6113,7 +6078,7 @@
 {
 	PurpleBlistNode *node;
 
-	gtkblist = PIDGIN_BLIST(list);
+	gtkblist = PIDGIN_BUDDY_LIST(list);
 	if(!gtkblist || !gtkblist->treeview)
 		return;
 
@@ -6150,7 +6115,7 @@
 	PidginBuddyList *gtkblist;
 
 	blist = purple_blist_get_default();
-	gtkblist = PIDGIN_BLIST(blist);
+	gtkblist = PIDGIN_BUDDY_LIST(blist);
 
 	gtkblist->refresh_timer = g_timeout_add_seconds(30,(GSourceFunc)pidgin_blist_refresh_timer, blist);
 }
@@ -6836,7 +6801,7 @@
 static void pidgin_blist_update(PurpleBuddyList *list, PurpleBlistNode *node)
 {
 	if (list)
-		gtkblist = PIDGIN_BLIST(list);
+		gtkblist = PIDGIN_BUDDY_LIST(list);
 	if(!gtkblist || !gtkblist->treeview || !node)
 		return;
 
@@ -6853,51 +6818,6 @@
 		pidgin_blist_update_chat(list, node);
 }
 
-static void pidgin_blist_destroy(PurpleBuddyList *list)
-{
-	PidginBuddyListPrivate *priv;
-
-	if (!list || !list->ui_data)
-		return;
-
-	g_return_if_fail(list->ui_data == gtkblist);
-
-	purple_signals_disconnect_by_handle(gtkblist);
-
-	gtk_widget_destroy(gtkblist->window);
-
-	pidgin_blist_tooltip_destroy();
-
-	if (gtkblist->refresh_timer)
-		g_source_remove(gtkblist->refresh_timer);
-	if (gtkblist->timeout)
-		g_source_remove(gtkblist->timeout);
-	if (gtkblist->drag_timeout)
-		g_source_remove(gtkblist->drag_timeout);
-
-	gtkblist->refresh_timer = 0;
-	gtkblist->timeout = 0;
-	gtkblist->drag_timeout = 0;
-	gtkblist->window = gtkblist->vbox = gtkblist->treeview = NULL;
-	g_object_unref(G_OBJECT(gtkblist->treemodel));
-	gtkblist->treemodel = NULL;
-	g_object_unref(G_OBJECT(gtkblist->ui));
-	g_object_unref(G_OBJECT(gtkblist->empty_avatar));
-
-	priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
-
-	if (priv->current_theme)
-		g_object_unref(priv->current_theme);
-	if (priv->select_notebook_page_timeout)
-		g_source_remove(priv->select_notebook_page_timeout);
-	g_free(priv);
-
-	g_free(gtkblist);
-	accountmenu = NULL;
-	gtkblist = NULL;
-	purple_prefs_disconnect_by_handle(pidgin_blist_get_handle());
-}
-
 static void pidgin_blist_set_visible(PurpleBuddyList *list, gboolean show)
 {
 	if (!(gtkblist && gtkblist->window))
@@ -7382,31 +7302,6 @@
 		pidgin_set_urgent(GTK_WINDOW(gtkblist->window), TRUE);
 }
 
-static PurpleBlistUiOps blist_ui_ops =
-{
-	pidgin_blist_new_list,
-	pidgin_blist_new_node,
-	pidgin_blist_show,
-	pidgin_blist_update,
-	pidgin_blist_remove,
-	pidgin_blist_destroy,
-	pidgin_blist_set_visible,
-	pidgin_blist_request_add_buddy,
-	pidgin_blist_request_add_chat,
-	pidgin_blist_request_add_group,
-	NULL,
-	NULL,
-	NULL,
-	NULL, NULL, NULL, NULL
-};
-
-
-PurpleBlistUiOps *
-pidgin_blist_get_ui_ops(void)
-{
-	return &blist_ui_ops;
-}
-
 PidginBuddyList *pidgin_blist_get_default_gtk_blist()
 {
 	return gtkblist;
@@ -7485,7 +7380,8 @@
 void
 pidgin_blist_set_theme(PidginBlistTheme *theme)
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 	PurpleBuddyList *list = purple_blist_get_default();
 
 	if (theme != NULL)
@@ -7508,7 +7404,8 @@
 PidginBlistTheme *
 pidgin_blist_get_theme()
 {
-	PidginBuddyListPrivate *priv = PIDGIN_BUDDY_LIST_GET_PRIVATE(gtkblist);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
 
 	return priv->current_theme;
 }
@@ -7576,6 +7473,77 @@
 
 	purple_signals_unregister_by_instance(pidgin_blist_get_handle());
 	purple_signals_disconnect_by_handle(pidgin_blist_get_handle());
+
+	accountmenu = NULL;
+	gtkblist = NULL;
+}
+
+/**************************************************************************
+ * GTK Buddy list GObject code
+ **************************************************************************/
+static void
+pidgin_buddy_list_init(PidginBuddyList *self)
+{
+}
+
+static void
+pidgin_buddy_list_finalize(GObject *obj)
+{
+	PidginBuddyList *gtkblist = PIDGIN_BUDDY_LIST(obj);
+	PidginBuddyListPrivate *priv =
+	        pidgin_buddy_list_get_instance_private(gtkblist);
+
+	purple_signals_disconnect_by_handle(gtkblist);
+
+	gtk_widget_destroy(gtkblist->window);
+
+	pidgin_blist_tooltip_destroy();
+
+	if (gtkblist->refresh_timer) {
+		g_source_remove(gtkblist->refresh_timer);
+		gtkblist->refresh_timer = 0;
+	}
+	if (gtkblist->timeout) {
+		g_source_remove(gtkblist->timeout);
+		gtkblist->timeout = 0;
+	}
+	if (gtkblist->drag_timeout) {
+		g_source_remove(gtkblist->drag_timeout);
+		gtkblist->drag_timeout = 0;
+	}
+
+	gtkblist->window = gtkblist->vbox = gtkblist->treeview = NULL;
+	g_clear_object(&gtkblist->treemodel);
+	g_object_unref(G_OBJECT(gtkblist->ui));
+	g_object_unref(G_OBJECT(gtkblist->empty_avatar));
+
+	g_clear_object(&priv->current_theme);
+	if (priv->select_notebook_page_timeout) {
+		g_source_remove(priv->select_notebook_page_timeout);
+	}
+
+	purple_prefs_disconnect_by_handle(pidgin_blist_get_handle());
+
+	G_OBJECT_CLASS(pidgin_buddy_list_parent_class)->finalize(obj);
+}
+
+static void
+pidgin_buddy_list_class_init(PidginBuddyListClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	PurpleBuddyListClass *purple_blist_class;
+
+	obj_class->finalize = pidgin_buddy_list_finalize;
+
+	purple_blist_class = PURPLE_BUDDY_LIST_CLASS(klass);
+	purple_blist_class->new_node = pidgin_blist_new_node;
+	purple_blist_class->show = pidgin_blist_show;
+	purple_blist_class->update = pidgin_blist_update;
+	purple_blist_class->remove = pidgin_blist_remove;
+	purple_blist_class->set_visible = pidgin_blist_set_visible;
+	purple_blist_class->request_add_buddy = pidgin_blist_request_add_buddy;
+	purple_blist_class->request_add_chat = pidgin_blist_request_add_chat;
+	purple_blist_class->request_add_group = pidgin_blist_request_add_group;
 }
 
 /*********************************************************************
--- a/pidgin/gtkblist.h	Wed Jul 17 09:52:28 2019 +0000
+++ b/pidgin/gtkblist.h	Thu Jul 18 03:45:06 2019 +0000
@@ -106,6 +106,8 @@
  * Like, everything you need to know about the gtk buddy list
  */
 struct _PidginBuddyList {
+	PurpleBuddyList parent;
+
 	GtkWidget *window;
 	GtkWidget *notebook;
 	GtkWidget *main_vbox;
@@ -144,14 +146,8 @@
 
 	GtkWidget *statusbox;
 	GdkPixbuf *empty_avatar;
-
-	gpointer priv;
 };
 
-#define PIDGIN_BLIST(list) ((PidginBuddyList *)purple_blist_get_ui_data())
-#define PIDGIN_IS_PIDGIN_BLIST(list) \
-	(purple_blist_get_ui_ops() == pidgin_blist_get_ui_ops())
-
 G_BEGIN_DECLS
 
 /**************************************************************************
@@ -163,7 +159,8 @@
  *
  * Returns: The #GType for the #PidginBuddyList boxed structure.
  */
-GType pidgin_buddy_list_get_type(void);
+G_DECLARE_FINAL_TYPE(PidginBuddyList, pidgin_buddy_list, PIDGIN, BUDDY_LIST,
+                     PurpleBuddyList)
 
 /**
  * pidgin_blist_get_handle:
@@ -189,15 +186,6 @@
 void pidgin_blist_uninit(void);
 
 /**
- * pidgin_blist_get_ui_ops:
- *
- * Returns the UI operations structure for the buddy list.
- *
- * Returns: The GTK+ list operations structure.
- */
-PurpleBlistUiOps *pidgin_blist_get_ui_ops(void);
-
-/**
  * pidgin_blist_get_default_gtk_blist:
  *
  * Returns the default gtk buddy list
--- a/pidgin/libpidgin.c	Wed Jul 17 09:52:28 2019 +0000
+++ b/pidgin/libpidgin.c	Thu Jul 18 03:45:06 2019 +0000
@@ -225,7 +225,7 @@
 	/* Set the UI operation structures. */
 	purple_accounts_set_ui_ops(pidgin_accounts_get_ui_ops());
 	purple_xfers_set_ui_ops(pidgin_xfers_get_ui_ops());
-	purple_blist_set_ui_ops(pidgin_blist_get_ui_ops());
+	purple_blist_set_ui(PIDGIN_TYPE_BUDDY_LIST);
 	purple_notify_set_ui_ops(pidgin_notify_get_ui_ops());
 	purple_request_set_ui_ops(pidgin_request_get_ui_ops());
 	purple_sound_set_ui_ops(pidgin_sound_get_ui_ops());

mercurial