changeset 32698:91a46f726cf4

merge of '7b6523560e839d3775c83d46b3c466732954bb03' and 'aa17fd919c5b0e1d9405ceccffb4b2e83487d6aa'
author Elliott Sales de Andrade <qulogic@pidgin.im>
date Fri, 23 Dec 2011 08:22:03 +0000
parents 48d35c0c6224 (diff) 3828a61c44da (current diff)
children eca1f14826e5
files
diffstat 13 files changed, 2797 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/gg/win32-resolver.c	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,322 @@
+/**
+ * @file win32-resolver.c
+ *
+ * purple
+ *
+ * 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 "win32-resolver.h"
+
+#include <errno.h>
+#include <resolver.h>
+#include "debug.h"
+
+#ifndef _WIN32
+#error "win32thread resolver is not supported on current platform"
+#endif
+
+/**
+ * Deal with the fact that you can't select() on a win32 file fd.
+ * This makes it practically impossible to tie into purple's event loop.
+ *
+ * -This is thanks to Tor Lillqvist.
+ */
+static int ggp_resolver_win32thread_socket_pipe(int *fds)
+{
+	SOCKET temp, socket1 = -1, socket2 = -1;
+	struct sockaddr_in saddr;
+	int len;
+	u_long arg;
+	fd_set read_set, write_set;
+	struct timeval tv;
+
+	purple_debug_misc("gg", "ggp_resolver_win32thread_socket_pipe(&%d)\n",
+		*fds);
+
+	temp = socket(AF_INET, SOCK_STREAM, 0);
+
+	if (temp == INVALID_SOCKET) {
+		goto out0;
+	}
+
+	arg = 1;
+	if (ioctlsocket(temp, FIONBIO, &arg) == SOCKET_ERROR) {
+		goto out0;
+	}
+
+	memset(&saddr, 0, sizeof(saddr));
+	saddr.sin_family = AF_INET;
+	saddr.sin_port = 0;
+	saddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+	if (bind(temp, (struct sockaddr *)&saddr, sizeof (saddr))) {
+		goto out0;
+	}
+
+	if (listen(temp, 1) == SOCKET_ERROR) {
+		goto out0;
+	}
+
+	len = sizeof(saddr);
+	if (getsockname(temp, (struct sockaddr *)&saddr, &len)) {
+		goto out0;
+	}
+
+	socket1 = socket(AF_INET, SOCK_STREAM, 0);
+
+	if (socket1 == INVALID_SOCKET) {
+		goto out0;
+	}
+
+	arg = 1;
+	if (ioctlsocket(socket1, FIONBIO, &arg) == SOCKET_ERROR) {
+		goto out1;
+	}
+
+	if (connect(socket1, (struct sockaddr *)&saddr, len) != SOCKET_ERROR ||
+			WSAGetLastError() != WSAEWOULDBLOCK) {
+		goto out1;
+	}
+
+	FD_ZERO(&read_set);
+	FD_SET(temp, &read_set);
+
+	tv.tv_sec = 0;
+	tv.tv_usec = 0;
+
+	if (select(0, &read_set, NULL, NULL, NULL) == SOCKET_ERROR) {
+		goto out1;
+	}
+
+	if (!FD_ISSET(temp, &read_set)) {
+		goto out1;
+	}
+
+	socket2 = accept(temp, (struct sockaddr *) &saddr, &len);
+	if (socket2 == INVALID_SOCKET) {
+		goto out1;
+	}
+
+	FD_ZERO(&write_set);
+	FD_SET(socket1, &write_set);
+
+	tv.tv_sec = 0;
+	tv.tv_usec = 0;
+
+	if (select(0, NULL, &write_set, NULL, NULL) == SOCKET_ERROR) {
+		goto out2;
+	}
+
+	if (!FD_ISSET(socket1, &write_set)) {
+		goto out2;
+	}
+
+	arg = 0;
+	if (ioctlsocket(socket1, FIONBIO, &arg) == SOCKET_ERROR) {
+		goto out2;
+	}
+
+	arg = 0;
+	if (ioctlsocket(socket2, FIONBIO, &arg) == SOCKET_ERROR) {
+		goto out2;
+	}
+
+	fds[0] = socket1;
+	fds[1] = socket2;
+
+	closesocket (temp);
+
+	return 0;
+
+out2:
+	closesocket (socket2);
+out1:
+	closesocket (socket1);
+out0:
+	closesocket (temp);
+	errno = EIO; /* XXX */
+
+	return -1;
+}
+
+struct ggp_resolver_win32thread_data {
+	char *hostname;
+	int fd;
+};
+
+/**
+ * Copy-paste from gg_resolver_run().
+ */
+static DWORD WINAPI ggp_resolver_win32thread_thread(LPVOID arg)
+{
+	struct ggp_resolver_win32thread_data *data = arg;
+	struct in_addr addr_ip[2], *addr_list;
+	int addr_count;
+
+	purple_debug_info("gg", "ggp_resolver_win32thread_thread() host: %s, "
+		"fd: %i called\n", data->hostname, data->fd);
+
+	if ((addr_ip[0].s_addr = inet_addr(data->hostname)) == INADDR_NONE) {
+		if (gg_gethostbyname_real(data->hostname, &addr_list,
+			&addr_count, 0) == -1) {
+			addr_list = addr_ip;
+			/* addr_ip[0] już zawiera INADDR_NONE */
+		}
+	} else {
+		addr_list = addr_ip;
+		addr_ip[1].s_addr = INADDR_NONE;
+		addr_count = 1;
+	}
+
+	purple_debug_misc("gg", "ggp_resolver_win32thread_thread() "
+		"count = %d\n", addr_count);
+
+	write(data->fd, addr_list, (addr_count + 1) * sizeof(struct in_addr));
+	close(data->fd);
+
+	free(data->hostname);
+	data->hostname = NULL;
+
+	free(data);
+
+	if (addr_list != addr_ip)
+		free(addr_list);
+
+	purple_debug_misc("gg", "ggp_resolver_win32thread_thread() done\n");
+
+	return 0;
+}
+
+
+int ggp_resolver_win32thread_start(int *fd, void **private_data,
+	const char *hostname)
+{
+	struct ggp_resolver_win32thread_data *data = NULL;
+	HANDLE h;
+	DWORD dwTId;
+	int pipes[2], new_errno;
+
+	purple_debug_info("gg", "ggp_resolver_win32thread_start(%p, %p, "
+		"\"%s\");\n", fd, private_data, hostname);
+
+	if (!private_data || !fd || !hostname) {
+		purple_debug_error("gg", "ggp_resolver_win32thread_start() "
+			"invalid arguments\n");
+		errno = EFAULT;
+		return -1;
+	}
+
+	purple_debug_misc("gg", "ggp_resolver_win32thread_start() creating "
+		"pipes...\n");
+
+	if (ggp_resolver_win32thread_socket_pipe(pipes) == -1) {
+		purple_debug_error("gg", "ggp_resolver_win32thread_start() "
+			"unable to create pipes (errno=%d, %s)\n",
+			errno, strerror(errno));
+		return -1;
+	}
+
+	if (!(data = malloc(sizeof(*data)))) {
+		purple_debug_error("gg", "ggp_resolver_win32thread_start() out "
+			"of memory\n");
+		new_errno = errno;
+		goto cleanup;
+	}
+
+	data->hostname = NULL;
+
+	if (!(data->hostname = strdup(hostname))) {
+		purple_debug_error("gg", "ggp_resolver_win32thread_start() out "
+			"of memory\n");
+		new_errno = errno;
+		goto cleanup;
+	}
+
+	data->fd = pipes[1];
+
+	purple_debug_misc("gg", "ggp_resolver_win32thread_start() creating "
+		"thread...\n");
+
+	h = CreateThread(NULL, 0, ggp_resolver_win32thread_thread, data, 0,
+		&dwTId);
+
+	if (h == NULL) {
+		purple_debug_error("gg", "ggp_resolver_win32thread_start() "
+			"unable to create thread\n");
+		new_errno = errno;
+		goto cleanup;
+	}
+
+	*private_data = h;
+	*fd = pipes[0];
+
+	purple_debug_misc("gg", "ggp_resolver_win32thread_start() done\n");
+
+	return 0;
+
+cleanup:
+	if (data) {
+		free(data->hostname);
+		free(data);
+	}
+
+	close(pipes[0]);
+	close(pipes[1]);
+
+	errno = new_errno;
+
+	return -1;
+
+}
+
+void ggp_resolver_win32thread_cleanup(void **private_data, int force)
+{
+	struct ggp_resolver_win32thread_data *data;
+
+	purple_debug_info("gg", "ggp_resolver_win32thread_cleanup() force: %i "
+		"called\n", force);
+
+	if (private_data == NULL || *private_data == NULL) {
+		purple_debug_error("gg", "ggp_resolver_win32thread_cleanup() "
+			"private_data: NULL\n");
+		return;
+	}
+	return; /* XXX */
+
+	data = (struct ggp_resolver_win32thread_data*) *private_data;
+	purple_debug_misc("gg", "ggp_resolver_win32thread_cleanup() data: "
+		"%s called\n", data->hostname);
+	*private_data = NULL;
+
+	if (force) {
+		purple_debug_misc("gg", "ggp_resolver_win32thread_cleanup() "
+			"force called\n");
+		//pthread_cancel(data->thread);
+		//pthread_join(data->thread, NULL);
+	}
+
+	free(data->hostname);
+	data->hostname = NULL;
+
+	if (data->fd != -1) {
+		close(data->fd);
+		data->fd = -1;
+	}
+	purple_debug_info("gg", "ggp_resolver_win32thread_cleanup() done\n");
+	free(data);
+}
+
+/* vim: set ts=8 sts=0 sw=8 noet: */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/gg/win32-resolver.h	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,46 @@
+/**
+ * @file win32-resolver.h
+ *
+ * purple
+ *
+ * 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
+ */
+
+#ifndef _PURPLE_GG_WIN32_RESOLVER
+#define _PURPLE_GG_WIN32_RESOLVER
+
+/**
+ * Starts hostname resolving in new win32 thread.
+ *
+ * @param fd           Pointer to variable, where pipe descriptor will be saved.
+ * @param private_data Pointer to variable, where pointer to private data will
+ *                     be saved.
+ * @param hostname     Hostname to resolve.
+ */
+int ggp_resolver_win32thread_start(int *fd, void **private_data,
+	const char *hostname);
+
+/**
+ * Cleans up resources after hostname resolving.
+ *
+ * @param private_data Pointer to variable storing pointer to private data.
+ * @param force        TRUE, if resources should be cleaned up even, if
+ *                     resolving process didn't finished.
+ */
+void ggp_resolver_win32thread_cleanup(void **private_data, int force);
+
+#endif /* _PURPLE_GG_WIN32_RESOLVER */
+
+/* vim: set ts=8 sts=0 sw=8 noet: */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkconv-theme-loader.c	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,280 @@
+/*
+ * PidginConvThemeLoader for Pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#include "pidgin.h"
+#include "gtkconv-theme-loader.h"
+#include "gtkconv-theme.h"
+
+#include "xmlnode.h"
+#include "debug.h"
+
+/*****************************************************************************
+ * Conversation Theme Builder
+ *****************************************************************************/
+
+static GHashTable *
+read_info_plist(xmlnode *plist)
+{
+	GHashTable *info;
+	xmlnode *key, *value;
+	gboolean fail = FALSE;
+
+	info = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+	for (key = xmlnode_get_child(plist, "dict/key");
+	     key;
+	     key = xmlnode_get_next_twin(key)) {
+		char *keyname;
+		GValue *val;
+
+		;
+		for (value = key->next; value && value->type != XMLNODE_TYPE_TAG; value = value->next)
+			;
+		if (!value) {
+			fail = TRUE;
+			break;
+		}
+
+		val = g_new0(GValue, 1);
+		if (g_str_equal(value->name, "string")) {
+			g_value_init(val, G_TYPE_STRING);
+			g_value_take_string(val, xmlnode_get_data_unescaped(value));
+
+		} else if (g_str_equal(value->name, "true")) {
+			g_value_init(val, G_TYPE_BOOLEAN);
+			g_value_set_boolean(val, TRUE);
+
+		} else if (g_str_equal(value->name, "false")) {
+			g_value_init(val, G_TYPE_BOOLEAN);
+			g_value_set_boolean(val, FALSE);
+
+		} else if (g_str_equal(value->name, "real")) {
+			char *temp = xmlnode_get_data_unescaped(value);
+			g_value_init(val, G_TYPE_FLOAT);
+			g_value_set_float(val, atof(temp));
+			g_free(temp);
+
+		} else if (g_str_equal(value->name, "integer")) {
+			char *temp = xmlnode_get_data_unescaped(value);
+			g_value_init(val, G_TYPE_INT);
+			g_value_set_int(val, atoi(temp));
+			g_free(temp);
+
+		} else {
+			/* NOTE: We don't support array, data, date, or dict as values,
+			   since they don't seem to be needed for styles. */
+			g_free(val);
+			fail = TRUE;
+			break;
+		}
+
+		keyname = xmlnode_get_data_unescaped(key);
+		g_hash_table_insert(info, keyname, val);
+	}
+
+	if (fail) {
+		g_hash_table_destroy(info);
+		info = NULL;
+	}
+
+	return info;
+}
+
+static PurpleTheme *
+pidgin_conv_loader_build(const gchar *dir)
+{
+	PidginConvTheme *theme = NULL;
+	char *contents;
+	xmlnode *plist;
+	GHashTable *info;
+	GValue *val;
+	int MessageViewVersion;
+	const char *CFBundleName;
+	const char *CFBundleIdentifier;
+	GDir *variants;
+	char *variant_dir;
+
+	g_return_val_if_fail(dir != NULL, NULL);
+
+	/* Load Info.plist for theme information */
+	contents = g_build_filename(dir, "Contents", NULL);
+	plist = xmlnode_from_file(contents, "Info.plist", "Info.plist", "gtkconv-theme-loader");
+	g_free(contents);
+	if (plist == NULL) {
+		purple_debug_error("gtkconv-theme-loader",
+		                   "Failed to load Contents/Info.plist in %s\n", dir);
+		return NULL;
+	}
+
+	info = read_info_plist(plist);
+	xmlnode_free(plist);
+	if (info == NULL) {
+		purple_debug_error("gtkconv-theme-loader",
+		                   "Failed to load Contents/Info.plist in %s\n", dir);
+		return NULL;
+	}
+
+	/* Check for required keys: CFBundleName */
+	val = g_hash_table_lookup(info, "CFBundleName");
+	if (!val || !G_VALUE_HOLDS_STRING(val)) {
+		purple_debug_error("gtkconv-theme-loader",
+		                   "%s/Contents/Info.plist missing required string key CFBundleName.\n",
+		                   dir);
+		g_hash_table_destroy(info);
+		return NULL;
+	}
+	CFBundleName = g_value_get_string(val);
+
+	/* Check for required keys: CFBundleIdentifier */
+	val = g_hash_table_lookup(info, "CFBundleIdentifier");
+	if (!val || !G_VALUE_HOLDS_STRING(val)) {
+		purple_debug_error("gtkconv-theme-loader",
+		                   "%s/Contents/Info.plist missing required string key CFBundleIdentifier.\n",
+		                   dir);
+		g_hash_table_destroy(info);
+		return NULL;
+	}
+	CFBundleIdentifier = g_value_get_string(val);
+
+	/* Check for required keys: MessageViewVersion */
+	val = g_hash_table_lookup(info, "MessageViewVersion");
+	if (!val || !G_VALUE_HOLDS_INT(val)) {
+		purple_debug_error("gtkconv-theme-loader",
+		                   "%s/Contents/Info.plist missing required integer key MessageViewVersion.\n",
+		                   dir);
+		g_hash_table_destroy(info);
+		return NULL;
+	}
+
+	MessageViewVersion = g_value_get_int(val);
+	if (MessageViewVersion < 3) {
+		purple_debug_error("gtkconv-theme-loader",
+		                   "%s is a legacy style (version %d) and will not be loaded.\n",
+		                   CFBundleName, MessageViewVersion);
+		g_hash_table_destroy(info);
+		return NULL;
+	}
+
+	theme = g_object_new(PIDGIN_TYPE_CONV_THEME,
+	                     "type", "conversation",
+	                     "name", CFBundleName,
+	                     "directory", dir,
+	                     "info", info, NULL);
+
+	/* Read list of variants */
+	variant_dir = g_build_filename(dir, "Contents", "Resources", "Variants", NULL);
+	variants = g_dir_open(variant_dir, 0, NULL);
+	g_free(variant_dir);
+
+	if (variants) {
+		char *prefname;
+		const char *default_variant = NULL;
+		const char *file;
+
+		/* Make sure prefs exist */
+		prefname = g_strdup_printf(PIDGIN_PREFS_ROOT "/conversations/themes/%s",
+		                           CFBundleIdentifier);
+		purple_prefs_add_none(prefname);
+		g_free(prefname);
+
+		/* Try user-set variant */
+		prefname = g_strdup_printf(PIDGIN_PREFS_ROOT "/conversations/themes/%s/variant",
+		                           CFBundleIdentifier);
+		if (purple_prefs_exists(prefname))
+			default_variant = purple_prefs_get_string(prefname);
+		g_free(prefname);
+
+		if (default_variant && *default_variant) {
+			pidgin_conversation_theme_set_variant(theme, default_variant);
+
+		} else {
+			/* Try theme default */
+			val = g_hash_table_lookup(info, "DefaultVariant");
+			if (val && G_VALUE_HOLDS_STRING(val)) {
+				default_variant = g_value_get_string(val);
+				if (default_variant && *default_variant)
+					pidgin_conversation_theme_set_variant(theme, default_variant);
+				else
+					default_variant = NULL;
+			} else
+				default_variant = NULL;
+		}
+
+		while ((file = g_dir_read_name(variants)) != NULL) {
+			const char *end = g_strrstr(file, ".css");
+			char *name;
+
+			if ((end == NULL) || (*(end + 4) != '\0'))
+				continue;
+
+			name = g_strndup(file, end - file);
+			pidgin_conversation_theme_add_variant(theme, name);
+
+			/* Set variant with first found */
+			if (!default_variant) {
+				pidgin_conversation_theme_set_variant(theme, name);
+				default_variant = name;
+			}
+		}
+
+		g_dir_close(variants);
+	}
+
+	return PURPLE_THEME(theme);
+}
+
+/******************************************************************************
+ * GObject Stuff
+ *****************************************************************************/
+
+static void
+pidgin_conv_theme_loader_class_init(PidginConvThemeLoaderClass *klass)
+{
+	PurpleThemeLoaderClass *loader_klass = PURPLE_THEME_LOADER_CLASS(klass);
+
+	loader_klass->purple_theme_loader_build = pidgin_conv_loader_build;
+}
+
+
+GType
+pidgin_conversation_theme_loader_get_type(void)
+{
+	static GType type = 0;
+	if (type == 0) {
+		static const GTypeInfo info = {
+			sizeof(PidginConvThemeLoaderClass),
+			NULL, /* base_init */
+			NULL, /* base_finalize */
+			(GClassInitFunc)pidgin_conv_theme_loader_class_init, /* class_init */
+			NULL, /* class_finalize */
+			NULL, /* class_data */
+			sizeof (PidginConvThemeLoader),
+			0, /* n_preallocs */
+			NULL, /* instance_init */
+			NULL, /* value table */
+		};
+		type = g_type_register_static(PURPLE_TYPE_THEME_LOADER,
+				"PidginConvThemeLoader", &info, 0);
+	}
+	return type;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkconv-theme-loader.h	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,72 @@
+/**
+ * @file gtkconv-theme-loader.h  Pidgin Conversation Theme Loader Class API
+ */
+
+/* purple
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#ifndef PIDGIN_CONV_THEME_LOADER_H
+#define PIDGIN_CONV_THEME_LOADER_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include "theme-loader.h"
+
+/**
+ * A pidgin conversation theme loader. Extends PurpleThemeLoader (theme-loader.h)
+ * This is a class designed to build conversation themes
+ *
+ * PidginConvThemeLoader is a GObject.
+ */
+typedef struct _PidginConvThemeLoader       PidginConvThemeLoader;
+typedef struct _PidginConvThemeLoaderClass  PidginConvThemeLoaderClass;
+
+#define PIDGIN_TYPE_CONV_THEME_LOADER            (pidgin_conversation_theme_loader_get_type ())
+#define PIDGIN_CONV_THEME_LOADER(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), PIDGIN_TYPE_CONV_THEME_LOADER, PidginConvThemeLoader))
+#define PIDGIN_CONV_THEME_LOADER_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), PIDGIN_TYPE_CONV_THEME_LOADER, PidginConvThemeLoaderClass))
+#define PIDGIN_IS_CONV_THEME_LOADER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PIDGIN_TYPE_CONV_THEME_LOADER))
+#define PIDGIN_IS_CONV_THEME_LOADER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), PIDGIN_TYPE_CONV_THEME_LOADER))
+#define PIDGIN_CONV_THEME_LOADER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), PIDGIN_TYPE_CONV_THEME_LOADER, PidginConvThemeLoaderClass))
+
+struct _PidginConvThemeLoader
+{
+	PurpleThemeLoader parent;
+};
+
+struct _PidginConvThemeLoaderClass
+{
+	PurpleThemeLoaderClass parent_class;
+};
+
+/**************************************************************************/
+/** @name Pidgin Conversation Theme-Loader API                            */
+/**************************************************************************/
+G_BEGIN_DECLS
+
+/**
+ * GObject foo.
+ * @internal.
+ */
+GType pidgin_conversation_theme_loader_get_type(void);
+
+G_END_DECLS
+#endif /* PIDGIN_CONV_THEME_LOADER_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkconv-theme.c	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,750 @@
+/*
+ * Conversation Themes for Pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#include "gtkconv-theme.h"
+
+#include "conversation.h"
+#include "debug.h"
+#include "prefs.h"
+#include "xmlnode.h"
+
+#include "pidgin.h"
+#include "gtkconv.h"
+#include "gtkwebview.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#define PIDGIN_CONV_THEME_GET_PRIVATE(Gobject) \
+	(G_TYPE_INSTANCE_GET_PRIVATE((Gobject), PIDGIN_TYPE_CONV_THEME, PidginConvThemePrivate))
+
+/******************************************************************************
+ * Structs
+ *****************************************************************************/
+
+typedef struct {
+	/* current config options */
+	char     *variant; /* allowed to be NULL if there are no variants */
+	GList    *variants;
+
+	/* Info.plist keys/values */
+	GHashTable *info;
+
+	/* caches */
+	char    *template_html;
+	char    *header_html;
+	char    *footer_html;
+	char    *topic_html;
+	char    *status_html;
+	char    *content_html;
+	char    *incoming_content_html;
+	char    *outgoing_content_html;
+	char    *incoming_next_content_html;
+	char    *outgoing_next_content_html;
+	char    *incoming_context_html;
+	char    *outgoing_context_html;
+	char    *incoming_next_context_html;
+	char    *outgoing_next_context_html;
+	char    *basestyle_css;
+} PidginConvThemePrivate;
+
+/******************************************************************************
+ * Enums
+ *****************************************************************************/
+
+enum {
+	PROP_ZERO = 0,
+	PROP_INFO,
+	PROP_VARIANT,
+	PROP_LAST
+};
+
+/******************************************************************************
+ * Globals
+ *****************************************************************************/
+
+static GObjectClass *parent_class = NULL;
+#if GLIB_CHECK_VERSION(2,26,0)
+static GParamSpec *properties[PROP_LAST];
+#endif
+
+/******************************************************************************
+ * Helper Functions
+ *****************************************************************************/
+
+static const GValue *
+get_key(PidginConvThemePrivate *priv, const char *key, gboolean specific)
+{
+	GValue *val = NULL;
+
+	/* Try variant-specific key */
+	if (specific && priv->variant) {
+		char *name = g_strdup_printf("%s:%s", key, priv->variant);
+		val = g_hash_table_lookup(priv->info, name);
+		g_free(name);
+	}
+
+	/* Try generic key */
+	if (!val) {
+		val = g_hash_table_lookup(priv->info, key);
+	}
+
+	return val;
+}
+
+/* The template path can either come from the theme, or can
+ * be stock Template.html that comes with Pidgin */
+static char *
+get_template_path(const char *dir)
+{
+	char *file;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Template.html", NULL);
+
+	if (!g_file_test(file, G_FILE_TEST_EXISTS)) {
+		g_free(file);
+		file = g_build_filename(DATADIR, "pidgin", "theme", "conversation", "Template.html", NULL);
+	}
+
+	return file;
+}
+
+static const char *
+get_template_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->template_html)
+		return priv->template_html;
+
+	file = get_template_path(dir);
+
+	if (!g_file_get_contents(file, &priv->template_html, NULL, NULL)) {
+		purple_debug_error("webkit", "Could not locate a Template.html (%s)\n", file);
+		priv->template_html = g_strdup("");
+	}
+	g_free(file);
+
+	return priv->template_html;
+}
+
+static const char *
+get_basestyle_css(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->basestyle_css)
+		return priv->basestyle_css;
+
+	file = g_build_filename(dir, "Contents", "Resources", "main.css", NULL);
+	if (!g_file_get_contents(file, &priv->basestyle_css, NULL, NULL))
+		priv->basestyle_css = g_strdup("");
+	g_free(file);
+
+	return priv->basestyle_css;
+}
+
+static const char *
+get_header_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->header_html)
+		return priv->header_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Header.html", NULL);
+	if (!g_file_get_contents(file, &priv->header_html, NULL, NULL))
+		priv->header_html = g_strdup("");
+	g_free(file);
+
+	return priv->header_html;
+}
+
+static const char *
+get_footer_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->footer_html)
+		return priv->footer_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Footer.html", NULL);
+	if (!g_file_get_contents(file, &priv->footer_html, NULL, NULL))
+		priv->footer_html = g_strdup("");
+	g_free(file);
+
+	return priv->footer_html;
+}
+
+static const char *
+get_topic_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->topic_html)
+		return priv->topic_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Topic.html", NULL);
+	if (!g_file_get_contents(file, &priv->topic_html, NULL, NULL)) {
+		purple_debug_info("webkit", "%s could not find Resources/Topic.html\n", dir);
+		priv->topic_html = g_strdup("");
+	}
+	g_free(file);
+
+	return priv->topic_html;
+}
+
+static const char *
+get_content_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->content_html)
+		return priv->content_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Content.html", NULL);
+	if (!g_file_get_contents(file, &priv->content_html, NULL, NULL)) {
+		purple_debug_info("webkit", "%s did not have a Content.html\n", dir);
+		priv->content_html = g_strdup("");
+	}
+	g_free(file);
+
+	return priv->content_html;
+}
+
+static const char *
+get_status_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->status_html)
+		return priv->status_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Status.html", NULL);
+	if (!g_file_get_contents(file, &priv->status_html, NULL, NULL)) {
+		purple_debug_info("webkit", "%s could not find Resources/Status.html\n", dir);
+		priv->status_html = g_strdup(get_content_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->status_html;
+}
+
+static const char *
+get_incoming_content_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->incoming_content_html)
+		return priv->incoming_content_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Incoming", "Content.html", NULL);
+	if (!g_file_get_contents(file, &priv->incoming_content_html, NULL, NULL)) {
+		purple_debug_info("webkit", "%s did not have a Incoming/Content.html\n", dir);
+		priv->incoming_content_html = g_strdup(get_content_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->incoming_content_html;
+}
+
+static const char *
+get_incoming_next_content_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->incoming_next_content_html)
+		return priv->incoming_next_content_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Incoming", "NextContent.html", NULL);
+	if (!g_file_get_contents(file, &priv->incoming_next_content_html, NULL, NULL)) {
+		priv->incoming_next_content_html = g_strdup(get_incoming_content_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->incoming_next_content_html;
+}
+
+static const char *
+get_incoming_context_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->incoming_context_html)
+		return priv->incoming_context_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Incoming", "Context.html", NULL);
+	if (!g_file_get_contents(file, &priv->incoming_context_html, NULL, NULL)) {
+		purple_debug_info("webkit", "%s did not have a Incoming/Context.html\n", dir);
+		priv->incoming_context_html = g_strdup(get_incoming_content_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->incoming_context_html;
+}
+
+static const char *
+get_incoming_next_context_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->incoming_next_context_html)
+		return priv->incoming_next_context_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Incoming", "NextContext.html", NULL);
+	if (!g_file_get_contents(file, &priv->incoming_next_context_html, NULL, NULL)) {
+		priv->incoming_next_context_html = g_strdup(get_incoming_context_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->incoming_next_context_html;
+}
+
+static const char *
+get_outgoing_content_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->outgoing_content_html)
+		return priv->outgoing_content_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Outgoing", "Content.html", NULL);
+	if (!g_file_get_contents(file, &priv->outgoing_content_html, NULL, NULL)) {
+		priv->outgoing_content_html = g_strdup(get_incoming_content_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->outgoing_content_html;
+}
+
+static const char *
+get_outgoing_next_content_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->outgoing_next_content_html)
+		return priv->outgoing_next_content_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Outgoing", "NextContent.html", NULL);
+	if (!g_file_get_contents(file, &priv->outgoing_next_content_html, NULL, NULL)) {
+		priv->outgoing_next_content_html = g_strdup(get_outgoing_content_html(priv, dir));
+	}
+
+	return priv->outgoing_next_content_html;
+}
+
+static const char *
+get_outgoing_context_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->outgoing_context_html)
+		return priv->outgoing_context_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Outgoing", "Context.html", NULL);
+	if (!g_file_get_contents(file, &priv->outgoing_context_html, NULL, NULL)) {
+		priv->outgoing_context_html = g_strdup(get_incoming_context_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->outgoing_context_html;
+}
+
+static const char *
+get_outgoing_next_context_html(PidginConvThemePrivate *priv, const char *dir)
+{
+	char *file;
+
+	if (priv->outgoing_next_context_html)
+		return priv->outgoing_next_context_html;
+
+	file = g_build_filename(dir, "Contents", "Resources", "Outgoing", "NextContext.html", NULL);
+	if (!g_file_get_contents(file, &priv->outgoing_next_context_html, NULL, NULL)) {
+		priv->outgoing_next_context_html = g_strdup(get_outgoing_context_html(priv, dir));
+	}
+	g_free(file);
+
+	return priv->outgoing_next_context_html;
+}
+
+static void
+_set_variant(PidginConvTheme *theme, const char *variant)
+{
+	PidginConvThemePrivate *priv;
+	const GValue *val;
+	char *prefname;
+
+	g_return_if_fail(theme != NULL);
+	g_return_if_fail(variant != NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	g_free(priv->variant);
+	priv->variant = g_strdup(variant);
+
+	val = get_key(priv, "CFBundleIdentifier", FALSE);
+	prefname = g_strdup_printf(PIDGIN_PREFS_ROOT "/conversations/themes/%s/variant",
+	                           g_value_get_string(val));
+	purple_prefs_set_string(prefname, variant);
+	g_free(prefname);
+}
+
+/******************************************************************************
+ * GObject Stuff
+ *****************************************************************************/
+
+static void
+pidgin_conv_theme_get_property(GObject *obj, guint param_id, GValue *value,
+		GParamSpec *psec)
+{
+	PidginConvTheme *theme = PIDGIN_CONV_THEME(obj);
+
+	switch (param_id) {
+		case PROP_INFO:
+			g_value_set_boxed(value, (gpointer)pidgin_conversation_theme_get_info(theme));
+			break;
+
+		case PROP_VARIANT:
+			g_value_set_string(value, pidgin_conversation_theme_get_variant(theme));
+			break;
+
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, psec);
+			break;
+	}
+}
+
+static void
+pidgin_conv_theme_set_property(GObject *obj, guint param_id, const GValue *value,
+		GParamSpec *psec)
+{
+	PidginConvTheme *theme = PIDGIN_CONV_THEME(obj);
+
+	switch (param_id) {
+		case PROP_INFO:
+			pidgin_conversation_theme_set_info(theme, g_value_get_boxed(value));
+			break;
+
+		case PROP_VARIANT:
+			_set_variant(theme, g_value_get_string(value));
+			break;
+
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, psec);
+			break;
+	}
+}
+
+static void
+pidgin_conv_theme_init(GTypeInstance *instance,
+		gpointer klass)
+{
+	PidginConvThemePrivate *priv;
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(instance);
+}
+
+static void
+pidgin_conv_theme_finalize(GObject *obj)
+{
+	PidginConvThemePrivate *priv;
+	GList *list;
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(obj);
+
+	g_free(priv->template_html);
+	g_free(priv->header_html);
+	g_free(priv->footer_html);
+	g_free(priv->topic_html);
+	g_free(priv->status_html);
+	g_free(priv->content_html);
+	g_free(priv->incoming_content_html);
+	g_free(priv->outgoing_content_html);
+	g_free(priv->incoming_next_content_html);
+	g_free(priv->outgoing_next_content_html);
+	g_free(priv->incoming_context_html);
+	g_free(priv->outgoing_context_html);
+	g_free(priv->incoming_next_context_html);
+	g_free(priv->outgoing_next_context_html);
+	g_free(priv->basestyle_css);
+
+	if (priv->info)
+		g_hash_table_destroy(priv->info);
+
+	list = priv->variants;
+	while (list) {
+		g_free(list->data);
+		list = g_list_delete_link(list, list);
+	}
+	g_free(priv->variant);
+
+	parent_class->finalize(obj);
+}
+
+static void
+pidgin_conv_theme_class_init(PidginConvThemeClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+	GParamSpec *pspec;
+
+	parent_class = g_type_class_peek_parent(klass);
+
+	g_type_class_add_private(klass, sizeof(PidginConvThemePrivate));
+
+	obj_class->get_property = pidgin_conv_theme_get_property;
+	obj_class->set_property = pidgin_conv_theme_set_property;
+	obj_class->finalize = pidgin_conv_theme_finalize;
+
+	/* INFO */
+	pspec = g_param_spec_boxed("info", "Info",
+			"The information about this theme",
+			G_TYPE_HASH_TABLE,
+			G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+	g_object_class_install_property(obj_class, PROP_INFO, pspec);
+#if GLIB_CHECK_VERSION(2,26,0)
+	properties[PROP_INFO] = pspec;
+#endif
+
+	/* VARIANT */
+	pspec = g_param_spec_string("variant", "Variant",
+			"The current variant for this theme",
+			NULL, G_PARAM_READWRITE);
+	g_object_class_install_property(obj_class, PROP_VARIANT, pspec);
+#if GLIB_CHECK_VERSION(2,26,0)
+	properties[PROP_VARIANT] = pspec;
+#endif
+}
+
+GType
+pidgin_conversation_theme_get_type(void)
+{
+	static GType type = 0;
+	if (type == 0) {
+		static const GTypeInfo info = {
+			sizeof(PidginConvThemeClass),
+			NULL, /* base_init */
+			NULL, /* base_finalize */
+			(GClassInitFunc)pidgin_conv_theme_class_init, /* class_init */
+			NULL, /* class_finalize */
+			NULL, /* class_data */
+			sizeof(PidginConvTheme),
+			0, /* n_preallocs */
+			pidgin_conv_theme_init, /* instance_init */
+			NULL, /* value table */
+		};
+		type = g_type_register_static(PURPLE_TYPE_THEME,
+				"PidginConvTheme", &info, 0);
+	}
+	return type;
+}
+
+/*****************************************************************************
+ * Public API functions
+ *****************************************************************************/
+
+const GHashTable *
+pidgin_conversation_theme_get_info(const PidginConvTheme *theme)
+{
+	PidginConvThemePrivate *priv;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+	return priv->info;
+}
+
+void
+pidgin_conversation_theme_set_info(PidginConvTheme *theme, GHashTable *info)
+{
+	PidginConvThemePrivate *priv;
+
+	g_return_if_fail(theme != NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	if (priv->info)
+		g_hash_table_destroy(priv->info);
+
+	priv->info = info;
+}
+
+const GValue *
+pidgin_conversation_theme_lookup(PidginConvTheme *theme, const char *key, gboolean specific)
+{
+	PidginConvThemePrivate *priv;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	return get_key(priv, key, specific);
+}
+
+const char *
+pidgin_conversation_theme_get_template(PidginConvTheme *theme, PidginConvThemeTemplateType type)
+{
+	PidginConvThemePrivate *priv;
+	const char *dir;
+	const char *html;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+	dir = purple_theme_get_dir(PURPLE_THEME(theme));
+
+	switch (type) {
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_MAIN:
+			html = get_template_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_HEADER:
+			html = get_header_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_FOOTER:
+			html = get_footer_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_TOPIC:
+			html = get_topic_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_STATUS:
+			html = get_status_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_CONTENT:
+			html = get_content_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_CONTENT:
+			html = get_incoming_content_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_NEXT_CONTENT:
+			html = get_incoming_next_content_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_CONTEXT:
+			html = get_incoming_context_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_NEXT_CONTEXT:
+			html = get_incoming_next_context_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_CONTENT:
+			html = get_outgoing_content_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_NEXT_CONTENT:
+			html = get_outgoing_next_content_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_CONTEXT:
+			html = get_outgoing_context_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_NEXT_CONTEXT:
+			html = get_outgoing_next_context_html(priv, dir);
+			break;
+		case PIDGIN_CONVERSATION_THEME_TEMPLATE_BASESTYLE_CSS:
+			html = get_basestyle_css(priv, dir);
+			break;
+		default:
+			purple_debug_error("gtkconv-theme",
+			                   "Requested invalid template type (%d) for theme %s.\n",
+			                   type, purple_theme_get_name(PURPLE_THEME(theme)));
+			html = NULL;
+	}
+
+	return html;
+}
+
+void
+pidgin_conversation_theme_add_variant(PidginConvTheme *theme, char *variant)
+{
+	PidginConvThemePrivate *priv;
+
+	g_return_if_fail(theme != NULL);
+	g_return_if_fail(variant != NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	priv->variants = g_list_prepend(priv->variants, variant);
+}
+
+const char *
+pidgin_conversation_theme_get_variant(PidginConvTheme *theme)
+{
+	PidginConvThemePrivate *priv;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	return priv->variant;
+}
+
+void
+pidgin_conversation_theme_set_variant(PidginConvTheme *theme, const char *variant)
+{
+	_set_variant(theme, variant);
+#if GLIB_CHECK_VERSION(2,26,0)
+	g_object_notify_by_pspec(G_OBJECT(theme), properties[PROP_VARIANT]);
+#else
+	g_object_notify(G_OBJECT(theme), "variant");
+#endif
+}
+
+const GList *
+pidgin_conversation_theme_get_variants(PidginConvTheme *theme)
+{
+	PidginConvThemePrivate *priv;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	return priv->variants;
+}
+
+char *
+pidgin_conversation_theme_get_template_path(PidginConvTheme *theme)
+{
+	const char *dir;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	dir = purple_theme_get_dir(PURPLE_THEME(theme));
+
+	return get_template_path(dir);
+}
+
+char *
+pidgin_conversation_theme_get_css_path(PidginConvTheme *theme)
+{
+	PidginConvThemePrivate *priv;
+	const char *dir;
+
+	g_return_val_if_fail(theme != NULL, NULL);
+
+	priv = PIDGIN_CONV_THEME_GET_PRIVATE(theme);
+
+	dir = purple_theme_get_dir(PURPLE_THEME(theme));
+	if (!priv->variant) {
+		return g_build_filename(dir, "Contents", "Resources", "main.css", NULL);
+	} else {
+		char *file = g_strdup_printf("%s.css", priv->variant);
+		char *ret = g_build_filename(dir, "Contents", "Resources", "Variants",  file, NULL);
+		g_free(file);
+		return ret;
+	}
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkconv-theme.h	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,196 @@
+/**
+ * @file gtkconv-theme.h  Pidgin Conversation Theme  Class API
+ */
+
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#ifndef PIDGIN_CONV_THEME_H
+#define PIDGIN_CONV_THEME_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include "conversation.h"
+#include "theme.h"
+
+/**
+ * extends PurpleTheme (theme.h)
+ * A pidgin icon theme.
+ * This object represents a Pidgin icon theme.
+ *
+ * PidginConvTheme is a PurpleTheme Object.
+ */
+typedef struct _PidginConvTheme        PidginConvTheme;
+typedef struct _PidginConvThemeClass   PidginConvThemeClass;
+
+#define PIDGIN_TYPE_CONV_THEME            (pidgin_conversation_theme_get_type ())
+#define PIDGIN_CONV_THEME(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), PIDGIN_TYPE_CONV_THEME, PidginConvTheme))
+#define PIDGIN_CONV_THEME_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), PIDGIN_TYPE_CONV_THEME, PidginConvThemeClass))
+#define PIDGIN_IS_CONV_THEME(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), PIDGIN_TYPE_CONV_THEME))
+#define PIDGIN_IS_CONV_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), PIDGIN_TYPE_CONV_THEME))
+#define PIDGIN_CONV_THEME_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), PIDGIN_TYPE_CONV_THEME, PidginConvThemeClass))
+
+struct _PidginConvTheme
+{
+	PurpleTheme parent;
+};
+
+struct _PidginConvThemeClass
+{
+	PurpleThemeClass parent_class;
+};
+
+typedef enum {
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_MAIN,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_HEADER,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_FOOTER,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_TOPIC,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_STATUS,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_CONTENT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_CONTENT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_NEXT_CONTENT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_CONTEXT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_INCOMING_NEXT_CONTEXT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_CONTENT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_NEXT_CONTENT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_CONTEXT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_OUTGOING_NEXT_CONTEXT,
+	PIDGIN_CONVERSATION_THEME_TEMPLATE_BASESTYLE_CSS
+
+} PidginConvThemeTemplateType;
+
+/**************************************************************************/
+/** @name Pidgin Conversation Theme API                                   */
+/**************************************************************************/
+G_BEGIN_DECLS
+
+/**
+ * GObject foo.
+ * @internal.
+ */
+GType pidgin_conversation_theme_get_type(void);
+
+/**
+ * Get the Info.plist hash table from a conversation theme.
+ *
+ * @param theme The conversation theme
+ *
+ * @return The hash table. Keys are strings as outlined for message styles,
+ *         values are GValue*s. This is an internal structure. Take a ref if
+ *         necessary, but don't destroy it yourself.
+ */
+const GHashTable *pidgin_conversation_theme_get_info(const PidginConvTheme *theme);
+
+/**
+ * Set the Info.plist hash table for a conversation theme.
+ *
+ * @param theme The conversation theme
+ * @param info  The new hash table. The theme will take ownership of this hash
+ *              table. Do not use it yourself afterwards with holding a ref.
+ *              For key and value specifications, @see pidgin_conversation_theme_get_info.
+ *
+ */
+void pidgin_conversation_theme_set_info(PidginConvTheme *theme, GHashTable *info);
+
+/**
+ * Lookup a key in a theme
+ *
+ * @param theme    The conversation theme
+ * @param key      The key to find
+ * @param specific Whether to search variant-specific keys
+ *
+ * @return The key information. If @a specific is @c TRUE, then keys are first
+ *         searched by variant, then by general ones. Otherwise, only general
+ *         key values are returned.
+ */
+const GValue *pidgin_conversation_theme_lookup(PidginConvTheme *theme, const char *key, gboolean specific);
+
+/**
+ * Get the template data from a conversation theme.
+ *
+ * @param theme The conversation theme
+ * @param type  The type of template data
+ *
+ * @return The template data requested. Fallback is made as required by styles.
+ *         Subsequent calls to this function will return cached values.
+ */
+const char *pidgin_conversation_theme_get_template(PidginConvTheme *theme, PidginConvThemeTemplateType type);
+
+/**
+ * Add an available variant name to a conversation theme.
+ *
+ * @param theme   The conversation theme
+ * @param variant The name of the variant
+ *
+ * @Note The conversation theme will take ownership of the variant name string.
+ *       This function should normally only be called by the theme loader.
+ */
+void pidgin_conversation_theme_add_variant(PidginConvTheme *theme, char *variant);
+
+/**
+ * Get the currently set variant name for a conversation theme.
+ *
+ * @param theme The conversation theme
+ *
+ * @return The current variant name.
+ */
+const char *pidgin_conversation_theme_get_variant(PidginConvTheme *theme);
+
+/**
+ * Set the variant name for a conversation theme.
+ *
+ * @param theme   The conversation theme
+ * @param variant The name of the variant
+ *
+ */
+void pidgin_conversation_theme_set_variant(PidginConvTheme *theme, const char *variant);
+
+/**
+ * Get a list of available variants for a conversation theme.
+ *
+ * @param theme The conversation theme
+ *
+ * @return The list of variants. This GList and the string data are owned by
+ *         the theme and should not be freed by the caller.
+ */
+const GList *pidgin_conversation_theme_get_variants(PidginConvTheme *theme);
+
+/**
+ * Get the path to the template HTML file.
+ *
+ * @param theme The conversation theme
+ *
+ * @return The path to the HTML file.
+ */
+char *pidgin_conversation_theme_get_template_path(PidginConvTheme *theme);
+
+/**
+ * Get the path to the current variant CSS file.
+ *
+ * @param theme The conversation theme
+ *
+ * @return The path to the CSS file.
+ */
+char *pidgin_conversation_theme_get_css_path(PidginConvTheme *theme);
+
+G_END_DECLS
+#endif /* PIDGIN_CONV_THEME_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkwebview.c	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,419 @@
+/*
+ * @file gtkwebview.c GTK+ WebKitWebView wrapper class.
+ * @ingroup pidgin
+ */
+
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <ctype.h>
+#include <string.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <JavaScriptCore/JavaScript.h>
+
+#include "util.h"
+#include "gtkwebview.h"
+#include "imgstore.h"
+
+static WebKitWebViewClass *parent_class = NULL;
+
+struct GtkWebViewPriv {
+	GHashTable *images; /**< a map from id to temporary file for the image */
+	gboolean empty;  /**< whether anything has been appended **/
+
+	/* JS execute queue */
+	GQueue *js_queue;
+	gboolean is_loading;
+	GtkAdjustment *vadj;
+	guint scroll_src;
+	GTimer *scroll_time;
+};
+
+GtkWidget *
+gtk_webview_new(void)
+{
+	GtkWebView* ret = GTK_WEBVIEW(g_object_new(gtk_webview_get_type(), NULL));
+	return GTK_WIDGET(ret);
+}
+
+static char *
+get_image_filename_from_id(GtkWebView* view, int id)
+{
+	char *filename = NULL;
+	FILE *file;
+	PurpleStoredImage* img;
+
+	if (!view->priv->images)
+		view->priv->images = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
+
+	filename = (char *)g_hash_table_lookup(view->priv->images, GINT_TO_POINTER(id));
+	if (filename)
+		return filename;
+
+	/* else get from img store */
+	file = purple_mkstemp(&filename, TRUE);
+
+	img = purple_imgstore_find_by_id(id);
+
+	fwrite(purple_imgstore_get_data(img), purple_imgstore_get_size(img), 1, file);
+	g_hash_table_insert(view->priv->images, GINT_TO_POINTER(id), filename);
+	fclose(file);
+	return filename;
+}
+
+static void
+clear_single_image(gpointer key, gpointer value, gpointer userdata)
+{
+	g_unlink((char *)value);
+}
+
+static void
+clear_images(GtkWebView *view)
+{
+	if (!view->priv->images)
+		return;
+	g_hash_table_foreach(view->priv->images, clear_single_image, NULL);
+	g_hash_table_unref(view->priv->images);
+}
+
+/*
+ * Replace all <img id=""> tags with <img src="">. I hoped to never
+ * write any HTML parsing code, but I'm forced to do this, until
+ * purple changes the way it works.
+ */
+static char *
+replace_img_id_with_src(GtkWebView *view, const char *html)
+{
+	GString *buffer = g_string_sized_new(strlen(html));
+	const char* cur = html;
+	char *id;
+	int nid;
+
+	while (*cur) {
+		const char *img = strstr(cur, "<img");
+		if (!img) {
+			g_string_append(buffer, cur);
+			break;
+		} else
+			g_string_append_len(buffer, cur, img - cur);
+
+		cur = strstr(img, "/>");
+		if (!cur)
+			cur = strstr(img, ">");
+
+		if (!cur) { /* invalid html? */
+			g_string_printf(buffer, "%s", html);
+			break;
+		}
+
+		if (strstr(img, "src=") || !strstr(img, "id=")) {
+			g_string_printf(buffer, "%s", html);
+			break;
+		}
+
+		/*
+		 * if this is valid HTML, then I can be sure that it
+		 * has an id= and does not have an src=, since
+		 * '=' cannot appear in parameters.
+		 */
+
+		id = strstr(img, "id=") + 3;
+
+		/* *id can't be \0, since a ">" appears after this */
+		if (isdigit(*id))
+			nid = atoi(id);
+		else
+			nid = atoi(id + 1);
+
+		/* let's dump this, tag and then dump the src information */
+		g_string_append_len(buffer, img, cur - img);
+
+		g_string_append_printf(buffer, " src='file://%s' ", get_image_filename_from_id(view, nid));
+	}
+
+	return g_string_free(buffer, FALSE);
+}
+
+static void
+gtk_webview_finalize(GObject *view)
+{
+	gpointer temp;
+
+	while ((temp = g_queue_pop_head(GTK_WEBVIEW(view)->priv->js_queue)))
+		g_free(temp);
+	g_queue_free(GTK_WEBVIEW(view)->priv->js_queue);
+
+	clear_images(GTK_WEBVIEW(view));
+	g_free(GTK_WEBVIEW(view)->priv);
+	G_OBJECT_CLASS(parent_class)->finalize(G_OBJECT(view));
+}
+
+static void
+gtk_webview_class_init(GtkWebViewClass *klass, gpointer userdata)
+{
+	parent_class = g_type_class_ref(webkit_web_view_get_type());
+	G_OBJECT_CLASS(klass)->finalize = gtk_webview_finalize;
+}
+
+static gboolean
+webview_link_clicked(WebKitWebView *view,
+		      WebKitWebFrame *frame,
+		      WebKitNetworkRequest *request,
+		      WebKitWebNavigationAction *navigation_action,
+		      WebKitWebPolicyDecision *policy_decision)
+{
+	const gchar *uri;
+	WebKitWebNavigationReason reason;
+
+	uri = webkit_network_request_get_uri(request);
+	reason = webkit_web_navigation_action_get_reason(navigation_action);
+
+	if (reason == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
+		/* the gtk imhtml way was to create an idle cb, not sure
+		 * why, so right now just using purple_notify_uri directly */
+		purple_notify_uri(NULL, uri);
+	} else
+		webkit_web_policy_decision_use(policy_decision);
+
+	return TRUE;
+}
+
+static gboolean
+process_js_script_queue(GtkWebView *view)
+{
+	char *script;
+	if (view->priv->is_loading)
+		return FALSE; /* we will be called when loaded */
+	if (!view->priv->js_queue || g_queue_is_empty(view->priv->js_queue))
+		return FALSE; /* nothing to do! */
+
+	script = g_queue_pop_head(view->priv->js_queue);
+	webkit_web_view_execute_script(WEBKIT_WEB_VIEW(view), script);
+	g_free(script);
+
+	return TRUE; /* there may be more for now */
+}
+
+static void
+webview_load_started(WebKitWebView *view,
+		      WebKitWebFrame *frame,
+		      gpointer userdata)
+{
+	/* is there a better way to test for is_loading? */
+	GTK_WEBVIEW(view)->priv->is_loading = TRUE;
+}
+
+static void
+webview_load_finished(WebKitWebView *view,
+		       WebKitWebFrame *frame,
+		       gpointer userdata)
+{
+	GTK_WEBVIEW(view)->priv->is_loading = FALSE;
+	g_idle_add((GSourceFunc)process_js_script_queue, view);
+}
+
+void
+gtk_webview_safe_execute_script(GtkWebView *view, const char *script)
+{
+	g_queue_push_tail(view->priv->js_queue, g_strdup(script));
+	g_idle_add((GSourceFunc)process_js_script_queue, view);
+}
+
+static void
+gtk_webview_init(GtkWebView *view, gpointer userdata)
+{
+	view->priv = g_new0(struct GtkWebViewPriv, 1);
+	g_signal_connect(view, "navigation-policy-decision-requested",
+			  G_CALLBACK(webview_link_clicked),
+			  view);
+
+	g_signal_connect(view, "load-started",
+			  G_CALLBACK(webview_load_started),
+			  view);
+
+	g_signal_connect(view, "load-finished",
+			  G_CALLBACK(webview_load_finished),
+			  view);
+
+	view->priv->empty = TRUE;
+	view->priv->js_queue = g_queue_new();
+}
+
+
+void
+gtk_webview_load_html_string_with_imgstore(GtkWebView *view, const char *html)
+{
+	char *html_imged;
+
+	clear_images(view);
+	html_imged = replace_img_id_with_src(view, html);
+	webkit_web_view_load_html_string(WEBKIT_WEB_VIEW(view), html_imged, "file:///");
+	g_free(html_imged);
+}
+
+char *
+gtk_webview_quote_js_string(const char *text)
+{
+	GString *str = g_string_new("\"");
+	const char *cur = text;
+
+	while (cur && *cur) {
+		switch (*cur) {
+			case '\\':
+				g_string_append(str, "\\\\");
+				break;
+			case '\"':
+				g_string_append(str, "\\\"");
+				break;
+			case '\r':
+				g_string_append(str, "<br/>");
+				break;
+			case '\n':
+				break;
+			default:
+				g_string_append_c(str, *cur);
+		}
+		cur++;
+	}
+	g_string_append_c(str, '"');
+	return g_string_free(str, FALSE);
+}
+
+void
+gtk_webview_set_vadjustment(GtkWebView *webview, GtkAdjustment *vadj)
+{
+	webview->priv->vadj = vadj;
+}
+
+/* this is a "hack", my plan is to eventually handle this
+ * correctly using a signals and a plugin: the plugin will have
+ * the information as to what javascript function to call. It seems
+ * wrong to hardcode that here.
+ */
+void
+gtk_webview_append_html(GtkWebView *view, const char *html)
+{
+	char *escaped = gtk_webview_quote_js_string(html);
+	char *script = g_strdup_printf("document.write(%s)", escaped);
+	webkit_web_view_execute_script(WEBKIT_WEB_VIEW(view), script);
+	view->priv->empty = FALSE;
+	gtk_webview_scroll_to_end(view, TRUE);
+	g_free(script);
+	g_free(escaped);
+}
+
+gboolean
+gtk_webview_is_empty(GtkWebView *view)
+{
+	return view->priv->empty;
+}
+
+#define MAX_SCROLL_TIME 0.4 /* seconds */
+#define SCROLL_DELAY 33 /* milliseconds */
+
+/*
+ * Smoothly scroll a WebView.
+ *
+ * @return TRUE if the window needs to be scrolled further, FALSE if we're at the bottom.
+ */
+static gboolean
+smooth_scroll_cb(gpointer data)
+{
+	struct GtkWebViewPriv *priv = data;
+	GtkAdjustment *adj = priv->vadj;
+	gdouble max_val = adj->upper - adj->page_size;
+	gdouble scroll_val = gtk_adjustment_get_value(adj) + ((max_val - gtk_adjustment_get_value(adj)) / 3);
+
+	g_return_val_if_fail(priv->scroll_time != NULL, FALSE);
+
+	if (g_timer_elapsed(priv->scroll_time, NULL) > MAX_SCROLL_TIME || scroll_val >= max_val) {
+		/* time's up. jump to the end and kill the timer */
+		gtk_adjustment_set_value(adj, max_val);
+		g_timer_destroy(priv->scroll_time);
+		priv->scroll_time = NULL;
+		g_source_remove(priv->scroll_src);
+		priv->scroll_src = 0;
+		return FALSE;
+	}
+
+	/* scroll by 1/3rd the remaining distance */
+	gtk_adjustment_set_value(adj, scroll_val);
+	return TRUE;
+}
+
+static gboolean
+scroll_idle_cb(gpointer data)
+{
+	struct GtkWebViewPriv *priv = data;
+	GtkAdjustment *adj = priv->vadj;
+	if (adj) {
+		gtk_adjustment_set_value(adj, adj->upper - adj->page_size);
+	}
+	priv->scroll_src = 0;
+	return FALSE;
+}
+
+void
+gtk_webview_scroll_to_end(GtkWebView *webview, gboolean smooth)
+{
+	struct GtkWebViewPriv *priv = webview->priv;
+	if (priv->scroll_time)
+		g_timer_destroy(priv->scroll_time);
+	if (priv->scroll_src)
+		g_source_remove(priv->scroll_src);
+	if(smooth) {
+		priv->scroll_time = g_timer_new();
+		priv->scroll_src = g_timeout_add_full(G_PRIORITY_LOW, SCROLL_DELAY, smooth_scroll_cb, priv, NULL);
+	} else {
+		priv->scroll_time = NULL;
+		priv->scroll_src = g_idle_add_full(G_PRIORITY_LOW, scroll_idle_cb, priv, NULL);
+	}
+}
+
+GType
+gtk_webview_get_type(void)
+{
+	static GType mview_type = 0;
+	if (G_UNLIKELY(mview_type == 0)) {
+		static const GTypeInfo mview_info = {
+			sizeof(GtkWebViewClass),
+			NULL,
+			NULL,
+			(GClassInitFunc) gtk_webview_class_init,
+			NULL,
+			NULL,
+			sizeof(GtkWebView),
+			0,
+			(GInstanceInitFunc) gtk_webview_init,
+			NULL
+		};
+		mview_type = g_type_register_static(webkit_web_view_get_type(),
+				"GtkWebView", &mview_info, 0);
+	}
+	return mview_type;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkwebview.h	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,147 @@
+/**
+ * @file gtkwebview.h Wrapper over the Gtk WebKitWebView component
+ * @ingroup pidgin
+ */
+
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ *
+ */
+
+#ifndef _PIDGIN_WEBVIEW_H_
+#define _PIDGIN_WEBVIEW_H_
+
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <webkit/webkit.h>
+
+#include "notify.h"
+
+#define GTK_TYPE_WEBVIEW            (gtk_webview_get_type())
+#define GTK_WEBVIEW(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_WEBVIEW, GtkWebView))
+#define GTK_WEBVIEW_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_WEBVIEW, GtkWebViewClass))
+#define GTK_IS_WEBVIEW(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_WEBVIEW))
+#define GTK_IS_WEBVIEW_CLASS(klass)  (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_TYPE_WEBVIEW))
+
+
+struct GtkWebViewPriv;
+
+struct _GtkWebView
+{
+	WebKitWebView webkit_web_view;
+
+	/*< private >*/
+	struct GtkWebViewPriv *priv;
+};
+
+typedef struct _GtkWebView GtkWebView;
+
+struct _GtkWebViewClass
+{
+	WebKitWebViewClass parent;
+};
+
+typedef struct _GtkWebViewClass GtkWebViewClass;
+
+
+/**
+ * Returns the GType for a GtkWebView widget
+ *
+ * @return The GType for GtkWebView widget
+ */
+GType gtk_webview_get_type(void);
+
+/**
+ * Create a new GtkWebView object
+ *
+ * @return A GtkWidget corresponding to the GtkWebView object
+ */
+GtkWidget *gtk_webview_new(void);
+
+/**
+ * Set the vertical adjustment for the GtkWebView.
+ *
+ * @param webview  The GtkWebView object
+ * @param vadj     The GtkAdjustment that control the webview
+ */
+void gtk_webview_set_vadjustment(GtkWebView *webview, GtkAdjustment *vadj);
+
+/**
+ * A very basic routine to append html, which can be considered
+ * equivalent to a "document.write" using JavaScript.
+ *
+ * @param webview The GtkWebView object
+ * @param markup  The html markup to append
+ */
+void gtk_webview_append_html(GtkWebView *webview, const char *markup);
+
+/**
+ * Rather than use webkit_webview_load_string, this routine
+ * parses and displays the \<img id=?\> tags that make use of the
+ * Pidgin imgstore.
+ *
+ * @param webview The GtkWebView object
+ * @param html    The HTML content to load
+ */
+void gtk_webview_load_html_string_with_imgstore(GtkWebView *webview, const char *html);
+
+/**
+ * TODO WEBKIT: Right now this just tests whether an append has been called
+ * since the last clear or since the Widget was created.  So it does not
+ * test for load_string's called in between.
+ *
+ * @param webview The GtkWebView object
+ *
+ * @return gboolean indicating whether the webview is empty
+ */
+gboolean gtk_webview_is_empty(GtkWebView *webview);
+
+/**
+ * Execute the JavaScript only after the webkit_webview_load_string
+ * loads completely. We also guarantee that the scripts are executed
+ * in the order they are called here. This is useful to avoid race
+ * conditions when calling JS functions immediately after opening the
+ * page.
+ *
+ * @param webview The GtkWebView object
+ * @param script  The script to execute
+ */
+void gtk_webview_safe_execute_script(GtkWebView *webview, const char *script);
+
+/**
+ * A convenience routine to quote a string for use as a JavaScript
+ * string. For instance, "hello 'world'" becomes "'hello \\'world\\''"
+ *
+ * @param str The string to escape and quote
+ *
+ * @return The quoted string
+ */
+char *gtk_webview_quote_js_string(const char *str);
+
+/**
+ * Scrolls the Webview to the end of its contents.
+ *
+ * @param webview The GtkWebView object
+ * @param smooth  A boolean indicating if smooth scrolling should be used
+ */
+void gtk_webview_scroll_to_end(GtkWebView *webview, gboolean smooth);
+
+#endif /* _PIDGIN_WEBVIEW_H_ */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/plugins/webkit.c	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,201 @@
+/*
+ * WebKit - Open the inspector on any WebKit views.
+ * Copyright (C) 2011 Elliott Sales de Andrade <qulogic@pidgin.im>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "internal.h"
+
+#include "version.h"
+
+#include "pidgin.h"
+
+#include "gtkconv.h"
+#include "gtkplugin.h"
+#include "gtkwebview.h"
+
+static WebKitWebView *
+create_gtk_window_around_it(WebKitWebInspector *inspector,
+                            WebKitWebView      *webview,
+                            PidginConversation *gtkconv)
+{
+	GtkWidget *win;
+	GtkWidget *view;
+	char *title;
+
+	win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+	title = g_strdup_printf(_("%s - Inspector"),
+	                        gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
+	gtk_window_set_title(GTK_WINDOW(win), title);
+	g_free(title);
+	gtk_window_set_default_size(GTK_WINDOW(win), 600, 400);
+	g_signal_connect_swapped(G_OBJECT(gtkconv->tab_cont), "destroy", G_CALLBACK(gtk_widget_destroy), win);
+
+	view = webkit_web_view_new();
+	gtk_container_add(GTK_CONTAINER(win), view);
+	g_object_set_data(G_OBJECT(webview), "inspector-window", win);
+
+	return WEBKIT_WEB_VIEW(view);
+}
+
+static gboolean
+show_inspector_window(WebKitWebInspector *inspector,
+                      GtkWidget          *webview)
+{
+	GtkWidget *win;
+
+	win = g_object_get_data(G_OBJECT(webview), "inspector-window");
+
+	gtk_widget_show_all(win);
+
+	return TRUE;
+}
+
+static void
+setup_inspector(PidginConversation *gtkconv)
+{
+	GtkWidget *webview = gtkconv->webview;
+	WebKitWebSettings *settings;
+	WebKitWebInspector *inspector;
+
+	settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
+	inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(webview));
+
+	g_object_set(G_OBJECT(settings), "enable-developer-extras", TRUE, NULL);
+
+	g_signal_connect(G_OBJECT(inspector), "inspect-web-view",
+	                 G_CALLBACK(create_gtk_window_around_it), gtkconv);
+	g_signal_connect(G_OBJECT(inspector), "show-window",
+	                 G_CALLBACK(show_inspector_window), webview);
+}
+
+static void
+remove_inspector(PidginConversation *gtkconv)
+{
+	GtkWidget *webview = gtkconv->webview;
+	GtkWidget *win;
+	WebKitWebSettings *settings;
+
+	win = g_object_get_data(G_OBJECT(webview), "inspector-window");
+	gtk_widget_destroy(win);
+	g_object_set_data(G_OBJECT(webview), "inspector-window", NULL);
+
+	settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(webview));
+
+	g_object_set(G_OBJECT(settings), "enable-developer-extras", FALSE, NULL);
+}
+
+static void
+conversation_displayed_cb(PidginConversation *gtkconv)
+{
+	GtkWidget *inspect = NULL;
+
+	inspect = g_object_get_data(G_OBJECT(gtkconv->webview),
+	                            "inspector-window");
+	if (inspect == NULL) {
+		setup_inspector(gtkconv);
+	}
+}
+
+static gboolean
+plugin_load(PurplePlugin *plugin)
+{
+	GList *convs = purple_get_conversations();
+	void *gtk_conv_handle = pidgin_conversations_get_handle();
+
+	purple_signal_connect(gtk_conv_handle, "conversation-displayed", plugin,
+	                      PURPLE_CALLBACK(conversation_displayed_cb), NULL);
+
+	while (convs) {
+		PurpleConversation *conv = (PurpleConversation *)convs->data;
+
+		/* Setup WebKit Inspector */
+		if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
+			setup_inspector(PIDGIN_CONVERSATION(conv));
+		}
+
+		convs = convs->next;
+	}
+
+	return TRUE;
+}
+
+static gboolean
+plugin_unload(PurplePlugin *plugin)
+{
+	GList *convs = purple_get_conversations();
+
+	while (convs) {
+		PurpleConversation *conv = (PurpleConversation *)convs->data;
+
+		/* Remove WebKit Inspector */
+		if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
+			remove_inspector(PIDGIN_CONVERSATION(conv));
+		}
+
+		convs = convs->next;
+	}
+
+	return TRUE;
+}
+
+static PurplePluginInfo info =
+{
+	PURPLE_PLUGIN_MAGIC,
+	PURPLE_MAJOR_VERSION,                           /**< major version */
+	PURPLE_MINOR_VERSION,                           /**< minor version */
+	PURPLE_PLUGIN_STANDARD,                         /**< type */
+	PIDGIN_PLUGIN_TYPE,                             /**< ui_requirement */
+	0,                                              /**< flags */
+	NULL,                                           /**< dependencies */
+	PURPLE_PRIORITY_DEFAULT,                        /**< priority */
+
+	"gtkwebkit-inspect",                            /**< id */
+	N_("WebKit Development"),                       /**< name */
+	DISPLAY_VERSION,                                /**< version */
+	N_("Enables WebKit Inspector."),                /**< summary */
+	N_("Enables WebKit's built-in inspector in a "
+	   "conversation window. This may be viewed "
+	   "by right-clicking a WebKit widget and "
+	   "selecting 'Inspect Element'."),             /**< description */
+	"Elliott Sales de Andrade <qulogic@pidgin.im>", /**< author */
+	PURPLE_WEBSITE,                                 /**< homepage */
+	plugin_load,                                    /**< load */
+	plugin_unload,                                  /**< unload */
+	NULL,                                           /**< destroy */
+	NULL,                                           /**< ui_info */
+	NULL,                                           /**< extra_info */
+	NULL,                                           /**< prefs_info */
+	NULL,                                           /**< actions */
+
+	/* padding */
+	NULL,
+	NULL,
+	NULL,
+	NULL
+};
+
+static void
+init_plugin(PurplePlugin *plugin)
+{
+}
+
+PURPLE_INIT_PLUGIN(webkit-devel, init_plugin, info)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/smileyparser.c	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,169 @@
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ *
+ */
+
+#include <gtk/gtk.h>
+#include <debug.h>
+#include "smileyparser.h"
+#include <smiley.h>
+#include <string.h>
+#include "gtkthemes.h"
+
+static char *
+get_fullpath(const char *filename)
+{
+	if (g_path_is_absolute(filename))
+		return g_strdup(filename);
+	else
+		return g_build_path(g_get_current_dir(), filename, NULL);
+}
+
+static void
+parse_for_shortcut_plaintext(const char *text, const char *shortcut, const char *file, GString *ret)
+{
+	const char *tmp = text;
+
+	for (;*tmp;) {
+		const char *end = strstr(tmp, shortcut);
+		char *path;
+		char *escaped_path;
+
+		if (end == NULL) {
+			g_string_append(ret, tmp);
+			break;
+		}
+		path = get_fullpath(file);
+		escaped_path = g_markup_escape_text(path, -1);
+
+		g_string_append_len(ret, tmp, end-tmp);
+		g_string_append_printf(ret,"<img alt='%s' src='%s' />",
+					shortcut, escaped_path);
+		g_free(path);
+		g_free(escaped_path);
+		g_assert(strlen(tmp) >= strlen(shortcut));
+		tmp = end + strlen(shortcut);
+	}
+}
+
+static char *
+parse_for_shortcut(const char *markup, const char *shortcut, const char *file)
+{
+	GString* ret = g_string_new("");
+	char *local_markup = g_strdup(markup);
+	char *escaped_shortcut = g_markup_escape_text(shortcut, -1);
+
+	char *temp = local_markup;
+
+	for (;*temp;) {
+		char *end = strchr(temp, '<');
+		char *end_of_tag;
+
+		if (!end) {
+			parse_for_shortcut_plaintext(temp, escaped_shortcut, file, ret);
+			break;
+		}
+
+		*end = 0;
+		parse_for_shortcut_plaintext(temp, escaped_shortcut, file, ret);
+		*end = '<';
+
+		/* if this is well-formed, then there should be no '>' within
+		 * the tag. TODO: handle a comment tag better :( */
+		end_of_tag = strchr(end, '>');
+		if (!end_of_tag) {
+			g_string_append(ret, end);
+			break;
+		}
+
+		g_string_append_len(ret, end, end_of_tag - end + 1);
+
+		temp = end_of_tag + 1;
+	}
+	g_free(local_markup);
+	g_free(escaped_shortcut);
+	return g_string_free(ret, FALSE);
+}
+
+static char *
+parse_for_purple_smiley(const char *markup, PurpleSmiley *smiley)
+{
+	char *file = purple_smiley_get_full_path(smiley);
+	char *ret = parse_for_shortcut(markup, purple_smiley_get_shortcut(smiley), file);
+	g_free(file);
+	return ret;
+}
+
+static char *
+parse_for_smiley_list(const char *markup, GHashTable *smileys)
+{
+	GHashTableIter iter;
+	char *key, *value;
+	char *ret = g_strdup(markup);
+
+	g_hash_table_iter_init(&iter, smileys);
+	while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value))
+	{
+		char *temp = parse_for_shortcut(ret, key, value);
+		g_free(ret);
+		ret = temp;
+	}
+
+	return ret;
+}
+
+char *
+smiley_parse_markup(const char *markup, const char *proto_id)
+{
+	GList *smileys = purple_smileys_get_all();
+	char *temp = g_strdup(markup), *temp2;
+	struct smiley_list *list;
+	const char *proto_name = "default";
+
+	if (proto_id != NULL) {
+		PurplePlugin *proto;
+		proto = purple_find_prpl(proto_id);
+		proto_name = proto->info->name;
+	}
+
+	/* unnecessarily slow, but lets manage for now. */
+	for (; smileys; smileys = g_list_next(smileys)) {
+		temp2 = parse_for_purple_smiley(temp, PURPLE_SMILEY(smileys->data));
+		g_free(temp);
+		temp = temp2;
+	}
+
+	/* now for each theme smiley, observe that this does look nasty */
+	if (!current_smiley_theme || !(current_smiley_theme->list)) {
+		purple_debug_warning("smiley", "theme does not exist\n");
+		return temp;
+	}
+
+	for (list = current_smiley_theme->list; list; list = list->next) {
+		if (g_str_equal(list->sml, proto_name)) {
+			temp2 = parse_for_smiley_list(temp, list->files);
+			g_free(temp);
+			temp = temp2;
+		}
+	}
+
+	return temp;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/smileyparser.h	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,25 @@
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ *
+ */
+
+char *
+smiley_parse_markup(const char *markup, const char *sml);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/themes/Makefile.am	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,6 @@
+
+themetemplatedir = $(datadir)/pidgin/theme/conversation
+themetemplate_DATA = Template.html
+
+EXTRA_DIST = $(themetemplate_DATA)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/themes/Template.html	Fri Dec 23 08:22:03 2011 +0000
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+	<base href="%@">
+	<script type="text/ecmascript" defer="defer">
+	
+		//Appending new content to the message view
+		function appendMessage(html) {
+			shouldScroll = nearBottom();
+		
+			//Remove any existing insertion point
+			insert = document.getElementById("insert");
+			if(insert) insert.parentNode.removeChild(insert);
+
+			//Append the new message to the bottom of our chat block
+			chat = document.getElementById("Chat");
+			range = document.createRange();
+			range.selectNode(chat);
+			documentFragment = range.createContextualFragment(html);
+			chat.appendChild(documentFragment);
+			
+			alignChat(shouldScroll);
+		}
+		function appendMessageNoScroll(html) {
+			//Remove any existing insertion point
+			insert = document.getElementById("insert");
+			if(insert) insert.parentNode.removeChild(insert);
+
+			//Append the new message to the bottom of our chat block
+			chat = document.getElementById("Chat");
+			range = document.createRange();
+			range.selectNode(chat);
+			documentFragment = range.createContextualFragment(html);
+			chat.appendChild(documentFragment);
+		}
+		function appendNextMessage(html){
+			shouldScroll = nearBottom();
+
+			//Locate the insertion point
+			insert = document.getElementById("insert");
+		
+			//make new node
+			range = document.createRange();
+			range.selectNode(insert.parentNode);
+			newNode = range.createContextualFragment(html);
+
+			//swap
+			insert.parentNode.replaceChild(newNode,insert);
+			
+			alignChat(shouldScroll);
+		}
+		function appendNextMessageNoScroll(html){
+			//Locate the insertion point
+			insert = document.getElementById("insert");
+		
+			//make new node
+			range = document.createRange();
+			range.selectNode(insert.parentNode);
+			newNode = range.createContextualFragment(html);
+
+			//swap
+			insert.parentNode.replaceChild(newNode,insert);
+		}
+		
+		//Auto-scroll to bottom.  Use nearBottom to determine if a scrollToBottom is desired.
+		function nearBottom() {
+			return ( document.body.scrollTop >= ( document.body.offsetHeight - ( window.innerHeight * 1.2 ) ) );
+		}
+		function scrollToBottom() {
+			document.body.scrollTop = document.body.offsetHeight;
+		}
+
+		//Dynamically exchange the active stylesheet
+		function setStylesheet( id, url ) {
+			code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">";
+			if( url.length ) code += "@import url( \"" + url + "\" );";
+			code += "</style>";
+			range = document.createRange();
+			head = document.getElementsByTagName( "head" ).item(0);
+			range.selectNode( head );
+			documentFragment = range.createContextualFragment( code );
+			head.removeChild( document.getElementById( id ) );
+			head.appendChild( documentFragment );
+		}
+		
+		//Swap an image with its alt-tag text on click, or expand/unexpand an attached image
+		document.onclick = imageCheck;
+		function imageCheck() {		
+			node = event.target;
+			if(node.tagName == 'IMG' && !client.zoomImage(node) && node.alt) {
+				a = document.createElement('a');
+				a.setAttribute('onclick', 'imageSwap(this)');
+				a.setAttribute('src', node.getAttribute('src'));
+				a.className = node.className;
+				text = document.createTextNode(node.alt);
+				a.appendChild(text);
+				node.parentNode.replaceChild(a, node);
+			}
+		}
+
+		function imageSwap(node) {
+			shouldScroll = nearBottom();
+
+			//Swap the image/text
+			img = document.createElement('img');
+			img.setAttribute('src', node.getAttribute('src'));
+			img.setAttribute('alt', node.firstChild.nodeValue);
+			img.className = node.className;
+			node.parentNode.replaceChild(img, node);
+			
+			alignChat(shouldScroll);
+		}
+		
+		//Align our chat to the bottom of the window.  If true is passed, view will also be scrolled down
+		function alignChat(shouldScroll) {
+			var windowHeight = window.innerHeight;
+			
+			if (windowHeight > 0) {
+				var contentElement = document.getElementById('Chat');
+				var contentHeight = contentElement.offsetHeight;
+				if (windowHeight - contentHeight > 0) {
+					contentElement.style.position = 'relative';
+					contentElement.style.top = (windowHeight - contentHeight) + 'px';
+				} else {
+					contentElement.style.position = 'static';
+				}
+			}
+			
+			if (shouldScroll) scrollToBottom();
+		}
+		
+		function windowDidResize(){
+			alignChat(true/*nearBottom()*/); //nearBottom buggy with inactive tabs
+		}
+		
+		window.onresize = windowDidResize;
+	</script>
+	
+	<style type="text/css">
+		.actionMessageUserName:before { content:"*"; }
+		.actionMessageBody:after { content:"*"; }
+		*{ word-wrap:break-word; }
+		img.scaledToFitImage { height:auto; width:100%; }
+	</style>
+	
+	<!-- This style is shared by all variants. !-->
+	<style id="baseStyle" type="text/css" media="screen,print">	
+		%@
+	</style>
+	
+	<!-- Although we call this mainStyle for legacy reasons, it's actually the variant style !-->
+	<style id="mainStyle" type="text/css" media="screen,print">	
+		@import url( "%@" );
+	</style>
+
+</head>
+<body onload="alignChat(true);" style="==bodyBackground==">
+%@
+<div id="Chat">
+</div>
+%@
+</body>
+</html>