]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
Factor out WebKit context menu as a utility
[empathy.git] / libempathy-gtk / empathy-theme-adium.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2008-2009 Collabora Ltd.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Xavier Claessens <xclaesse@gmail.com>
20  */
21
22 #include "config.h"
23
24 #include <string.h>
25 #include <glib/gi18n-lib.h>
26
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
30
31 #include <pango/pango.h>
32 #include <gdk/gdk.h>
33
34 #include <libempathy/empathy-gsettings.h>
35 #include <libempathy/empathy-time.h>
36 #include <libempathy/empathy-utils.h>
37
38 #include "empathy-theme-adium.h"
39 #include "empathy-smiley-manager.h"
40 #include "empathy-ui-utils.h"
41 #include "empathy-plist.h"
42 #include "empathy-images.h"
43 #include "empathy-webkit-utils.h"
44
45 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
46 #include <libempathy/empathy-debug.h>
47
48 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
49
50 #define BORING_DPI_DEFAULT 96
51
52 /* "Join" consecutive messages with timestamps within five minutes */
53 #define MESSAGE_JOIN_PERIOD 5*60
54
55 typedef struct {
56         EmpathyAdiumData     *data;
57         EmpathySmileyManager *smiley_manager;
58         EmpathyContact       *last_contact;
59         gint64                last_timestamp;
60         gboolean              last_is_backlog;
61         guint                 pages_loading;
62         /* Queue of QueuedItem*s containing an EmpathyMessage or string */
63         GQueue                message_queue;
64         /* Queue of guint32 of pending message id to remove unread
65          * marker for when we lose focus. */
66         GQueue                acked_messages;
67         GtkWidget            *inspector_window;
68
69         GSettings            *gsettings_chat;
70         GSettings            *gsettings_desktop;
71
72         gboolean              has_focus;
73         gboolean              has_unread_message;
74         gboolean              allow_scrolling;
75         gchar                *variant;
76         gboolean              in_construction;
77 } EmpathyThemeAdiumPriv;
78
79 struct _EmpathyAdiumData {
80         gint  ref_count;
81         gchar *path;
82         gchar *basedir;
83         gchar *default_avatar_filename;
84         gchar *default_incoming_avatar_filename;
85         gchar *default_outgoing_avatar_filename;
86         GHashTable *info;
87         guint version;
88         gboolean custom_template;
89         /* gchar* -> gchar* both owned */
90         GHashTable *date_format_cache;
91
92         /* HTML bits */
93         const gchar *template_html;
94         const gchar *content_html;
95         const gchar *in_content_html;
96         const gchar *in_context_html;
97         const gchar *in_nextcontent_html;
98         const gchar *in_nextcontext_html;
99         const gchar *out_content_html;
100         const gchar *out_context_html;
101         const gchar *out_nextcontent_html;
102         const gchar *out_nextcontext_html;
103         const gchar *status_html;
104
105         /* Above html strings are pointers to strings stored in this array.
106          * We do this because of fallbacks, some htmls could be pointing the
107          * same string. */
108         GPtrArray *strings_to_free;
109 };
110
111 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
112 static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
113
114 enum {
115         PROP_0,
116         PROP_ADIUM_DATA,
117         PROP_VARIANT,
118 };
119
120 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
121                          WEBKIT_TYPE_WEB_VIEW,
122                          G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
123                                                 theme_adium_iface_init));
124
125 enum {
126         QUEUED_EVENT,
127         QUEUED_MESSAGE,
128         QUEUED_EDIT
129 };
130
131 typedef struct {
132         guint type;
133         EmpathyMessage *msg;
134         char *str;
135 } QueuedItem;
136
137 static QueuedItem *
138 queue_item (GQueue *queue,
139             guint type,
140             EmpathyMessage *msg,
141             const char *str)
142 {
143         QueuedItem *item = g_slice_new0 (QueuedItem);
144
145         item->type = type;
146         if (msg != NULL)
147                 item->msg = g_object_ref (msg);
148         item->str = g_strdup (str);
149
150         g_queue_push_tail (queue, item);
151
152         return item;
153 }
154
155 static void
156 free_queued_item (QueuedItem *item)
157 {
158         tp_clear_object (&item->msg);
159         g_free (item->str);
160
161         g_slice_free (QueuedItem, item);
162 }
163
164 static void
165 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
166 {
167         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
168         WebKitWebView  *web_view = WEBKIT_WEB_VIEW (theme);
169         gboolean        enable_webkit_developer_tools;
170
171         enable_webkit_developer_tools = g_settings_get_boolean (
172                         priv->gsettings_chat,
173                         EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
174
175         g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
176                       "enable-developer-extras",
177                       enable_webkit_developer_tools,
178                       NULL);
179 }
180
181 static void
182 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings   *gsettings,
183                                                      const gchar *key,
184                                                      gpointer     user_data)
185 {
186         EmpathyThemeAdium  *theme = user_data;
187
188         theme_adium_update_enable_webkit_developer_tools (theme);
189 }
190
191 static gboolean
192 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView             *view,
193                                                      WebKitWebFrame            *web_frame,
194                                                      WebKitNetworkRequest      *request,
195                                                      WebKitWebNavigationAction *action,
196                                                      WebKitWebPolicyDecision   *decision,
197                                                      gpointer                   data)
198 {
199         const gchar *uri;
200
201         /* Only call url_show on clicks */
202         if (webkit_web_navigation_action_get_reason (action) !=
203             WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
204                 webkit_web_policy_decision_use (decision);
205                 return TRUE;
206         }
207
208         uri = webkit_network_request_get_uri (request);
209         empathy_url_show (GTK_WIDGET (view), uri);
210
211         webkit_web_policy_decision_ignore (decision);
212         return TRUE;
213 }
214
215 /* Replace each %@ in format with string passed in args */
216 static gchar *
217 string_with_format (const gchar *format,
218                     const gchar *first_string,
219                     ...)
220 {
221         va_list args;
222         const gchar *str;
223         GString *result;
224
225         va_start (args, first_string);
226         result = g_string_sized_new (strlen (format));
227         for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
228                 const gchar *next;
229
230                 next = strstr (format, "%@");
231                 if (next == NULL) {
232                         break;
233                 }
234
235                 g_string_append_len (result, format, next - format);
236                 g_string_append (result, str);
237                 format = next + 2;
238         }
239         g_string_append (result, format);
240         va_end (args);
241
242         return g_string_free (result, FALSE);
243 }
244
245 static void
246 theme_adium_load_template (EmpathyThemeAdium *theme)
247 {
248         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
249         gchar                 *basedir_uri;
250         gchar                 *variant_path;
251         gchar                 *template;
252
253         priv->pages_loading++;
254         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
255         variant_path = adium_info_dup_path_for_variant (priv->data->info,
256                 priv->variant);
257         template = string_with_format (priv->data->template_html,
258                 variant_path, NULL);
259         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
260                                           template, basedir_uri);
261         g_free (basedir_uri);
262         g_free (variant_path);
263         g_free (template);
264 }
265
266 static gchar *
267 theme_adium_parse_body (EmpathyThemeAdium *self,
268         const gchar *text,
269         const gchar *token)
270 {
271         EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
272         EmpathyStringParser *parsers;
273         GString *string;
274
275         /* Check if we have to parse smileys */
276         parsers = empathy_webkit_get_string_parser (
277                 g_settings_get_boolean (priv->gsettings_chat,
278                         EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
279
280         /* Parse text and construct string with links and smileys replaced
281          * by html tags. Also escape text to make sure html code is
282          * displayed verbatim. */
283         string = g_string_sized_new (strlen (text));
284
285         /* wrap this in HTML that allows us to find the message for later
286          * editing */
287         if (!tp_str_empty (token))
288                 g_string_append_printf (string,
289                         "<span id=\"message-token-%s\">",
290                         token);
291
292         empathy_string_parser_substr (text, -1, parsers, string);
293
294         if (!tp_str_empty (token))
295                 g_string_append (string, "</span>");
296
297         /* Wrap body in order to make tabs and multiple spaces displayed
298          * properly. See bug #625745. */
299         g_string_prepend (string, "<div style=\"display: inline; "
300                                                "white-space: pre-wrap\"'>");
301         g_string_append (string, "</div>");
302
303         return g_string_free (string, FALSE);
304 }
305
306 static void
307 escape_and_append_len (GString *string, const gchar *str, gint len)
308 {
309         while (str != NULL && *str != '\0' && len != 0) {
310                 switch (*str) {
311                 case '\\':
312                         /* \ becomes \\ */
313                         g_string_append (string, "\\\\");
314                         break;
315                 case '\"':
316                         /* " becomes \" */
317                         g_string_append (string, "\\\"");
318                         break;
319                 case '\n':
320                         /* Remove end of lines */
321                         break;
322                 default:
323                         g_string_append_c (string, *str);
324                 }
325
326                 str++;
327                 len--;
328         }
329 }
330
331 /* If *str starts with match, returns TRUE and move pointer to the end */
332 static gboolean
333 theme_adium_match (const gchar **str,
334                    const gchar *match)
335 {
336         gint len;
337
338         len = strlen (match);
339         if (strncmp (*str, match, len) == 0) {
340                 *str += len - 1;
341                 return TRUE;
342         }
343
344         return FALSE;
345 }
346
347 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
348 static gboolean
349 theme_adium_match_with_format (const gchar **str,
350                                const gchar *match,
351                                gchar **format)
352 {
353         const gchar *cur = *str;
354         const gchar *end;
355
356         if (!theme_adium_match (&cur, match)) {
357                 return FALSE;
358         }
359         cur++;
360
361         end = strstr (cur, "}%");
362         if (!end) {
363                 return FALSE;
364         }
365
366         *format = g_strndup (cur , end - cur);
367         *str = end + 1;
368         return TRUE;
369 }
370
371 /* List of colors used by %senderColor%. Copied from
372  * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
373  */
374 static gchar *colors[] = {
375         "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
376         "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
377         "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
378         "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
379         "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
380         "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
381         "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
382         "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
383         "lightblue", "lightcoral",
384         "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
385         "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
386         "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
387         "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
388         "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
389         "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
390         "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
391         "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
392         "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
393         "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
394         "yellowgreen",
395 };
396
397 static const gchar *
398 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
399 {
400         /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
401          * to strftime supported by g_date_time_format.
402          * FIXME: table is incomplete, doc of g_date_time_format has a table of
403          *        supported tags.
404          * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
405          *        in 2.29.x we have to explictely request padding with %0x */
406         static const gchar *convert_table[] = {
407                 "a", "%p", // AM/PM
408                 "A", NULL, // 0~86399999 (Millisecond of Day)
409
410                 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
411                 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
412                 "cc", "%u", // 1~7 (Day of Week)
413                 "c", "%u", // 1~7 (Day of Week)
414
415                 "dd", "%d", // 1~31 (0 padded Day of Month)
416                 "d", "%d", // 1~31 (0 padded Day of Month)
417                 "D", "%j", // 1~366 (0 padded Day of Year)
418
419                 "e", "%u", // 1~7 (0 padded Day of Week)
420                 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
421                 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
422                 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
423                 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
424
425                 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
426
427                 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
428                 "GGGG", NULL, // Before Christ/Anno Domini
429                 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
430                 "GG", NULL, // BC/AD (Era Designator Abbreviated)
431                 "G", NULL, // BC/AD (Era Designator Abbreviated)
432
433                 "h", "%I", // 1~12 (0 padded Hour (12hr))
434                 "H", "%H", // 0~23 (0 padded Hour (24hr))
435
436                 "k", NULL, // 1~24 (0 padded Hour (24hr)
437                 "K", NULL, // 0~11 (0 padded Hour (12hr))
438
439                 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
440                 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
441                 "LL", "%m", // 1~12 (0 padded Month)
442                 "L", "%m", // 1~12 (0 padded Month)
443
444                 "m", "%M", // 0~59 (0 padded Minute)
445                 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
446                 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
447                 "MM", "%m", // 1~12 (0 padded Month)
448                 "M", "%m", // 1~12 (0 padded Month)
449
450                 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
451                 "qqq", NULL, // Q1/Q2/Q3/Q4
452                 "qq", NULL, // 1~4 (0 padded Quarter)
453                 "q", NULL, // 1~4 (0 padded Quarter)
454                 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
455                 "QQQ", NULL, // Q1/Q2/Q3/Q4
456                 "QQ", NULL, // 1~4 (0 padded Quarter)
457                 "Q", NULL, // 1~4 (0 padded Quarter)
458
459                 "s", "%S", // 0~59 (0 padded Second)
460                 "S", NULL, // (rounded Sub-Second)
461
462                 "u", "%Y", // (0 padded Year)
463
464                 "vvvv", "%Z", // (General GMT Timezone Name)
465                 "vvv", "%Z", // (General GMT Timezone Abbreviation)
466                 "vv", "%Z", // (General GMT Timezone Abbreviation)
467                 "v", "%Z", // (General GMT Timezone Abbreviation)
468
469                 "w", "%W", // 1~53 (0 padded Week of Year, 1st day of week = Sunday, NB, 1st week of year starts from the last Sunday of last year)
470                 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
471
472                 "yyyy", "%Y", // (Full Year)
473                 "yyy", "%y", // (2 Digits Year)
474                 "yy", "%y", // (2 Digits Year)
475                 "y", "%Y", // (Full Year)
476                 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
477                 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
478                 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
479                 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
480
481                 "zzzz", NULL, // (Specific GMT Timezone Name)
482                 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
483                 "zz", NULL, // (Specific GMT Timezone Abbreviation)
484                 "z", NULL, // (Specific GMT Timezone Abbreviation)
485                 "Z", "%z", // +0000 (RFC 822 Timezone)
486         };
487         const gchar *str;
488         GString *string;
489         guint i, j;
490
491         if (nsdate == NULL) {
492                 return NULL;
493         }
494
495         str = g_hash_table_lookup (data->date_format_cache, nsdate);
496         if (str != NULL) {
497                 return str;
498         }
499
500         /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
501          * by corresponding strftime tag. */
502         string = g_string_sized_new (strlen (nsdate));
503         for (i = 0; nsdate[i] != '\0'; i++) {
504                 gboolean found = FALSE;
505
506                 /* even indexes are NSDateFormatter tag, odd indexes are the
507                  * corresponding strftime tag */
508                 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
509                         if (g_str_has_prefix (nsdate + i, convert_table[j])) {
510                                 found = TRUE;
511                                 break;
512                         }
513                 }
514                 if (found) {
515                         /* If we don't have a replacement, just ignore that tag */
516                         if (convert_table[j + 1] != NULL) {
517                                 g_string_append (string, convert_table[j + 1]);
518                         }
519                         i += strlen (convert_table[j]) - 1;
520                 } else {
521                         g_string_append_c (string, nsdate[i]);
522                 }
523         }
524
525         DEBUG ("Date format converted '%s' â†’ '%s'", nsdate, string->str);
526
527         /* The cache takes ownership of string->str */
528         g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
529         return g_string_free (string, FALSE);
530 }
531
532
533 static void
534 theme_adium_append_html (EmpathyThemeAdium *theme,
535                          const gchar       *func,
536                          const gchar       *html,
537                          const gchar       *message,
538                          const gchar       *avatar_filename,
539                          const gchar       *name,
540                          const gchar       *contact_id,
541                          const gchar       *service_name,
542                          const gchar       *message_classes,
543                          gint64             timestamp,
544                          gboolean           is_backlog)
545 {
546         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
547         GString     *string;
548         const gchar *cur = NULL;
549         gchar       *script;
550
551         /* Make some search-and-replace in the html code */
552         string = g_string_sized_new (strlen (html) + strlen (message));
553         g_string_append_printf (string, "%s(\"", func);
554         for (cur = html; *cur != '\0'; cur++) {
555                 const gchar *replace = NULL;
556                 gchar       *dup_replace = NULL;
557                 gchar       *format = NULL;
558
559                 /* Those are all well known keywords that needs replacement in
560                  * html files. Please keep them in the same order than the adium
561                  * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
562                 if (theme_adium_match (&cur, "%userIconPath%")) {
563                         replace = avatar_filename;
564                 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
565                         replace = contact_id;
566                 } else if (theme_adium_match (&cur, "%sender%")) {
567                         replace = name;
568                 } else if (theme_adium_match (&cur, "%senderColor%")) {
569                         /* A color derived from the user's name.
570                          * FIXME: If a colon separated list of HTML colors is at
571                          * Incoming/SenderColors.txt it will be used instead of
572                          * the default colors.
573                          */
574                         if (contact_id != NULL) {
575                                 guint hash = g_str_hash (contact_id);
576                                 replace = colors[hash % G_N_ELEMENTS (colors)];
577                         }
578                 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
579                         /* FIXME: The path to the status icon of the sender
580                          * (available, away, etc...)
581                          */
582                 } else if (theme_adium_match (&cur, "%messageDirection%")) {
583                         /* FIXME: The text direction of the message
584                          * (either rtl or ltr)
585                          */
586                 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
587                         /* FIXME: The serverside (remotely set) name of the
588                          * sender, such as an MSN display name.
589                          *
590                          *  We don't have access to that yet so we use
591                          * local alias instead.
592                          */
593                         replace = name;
594                 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
595                         /* FIXME: This keyword is used to represent the
596                          * highlight background color. "X" is the opacity of the
597                          * background, ranges from 0 to 1 and can be any decimal
598                          * between.
599                          */
600                 } else if (theme_adium_match (&cur, "%message%")) {
601                         replace = message;
602                 } else if (theme_adium_match (&cur, "%time%") ||
603                            theme_adium_match_with_format (&cur, "%time{", &format)) {
604                         const gchar *strftime_format;
605
606                         strftime_format = nsdate_to_strftime (priv->data, format);
607                         if (is_backlog) {
608                                 dup_replace = empathy_time_to_string_local (timestamp,
609                                         strftime_format ? strftime_format :
610                                         EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
611                         } else {
612                                 dup_replace = empathy_time_to_string_local (timestamp,
613                                         strftime_format ? strftime_format :
614                                         EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
615                         }
616                         replace = dup_replace;
617                 } else if (theme_adium_match (&cur, "%shortTime%")) {
618                         dup_replace = empathy_time_to_string_local (timestamp,
619                                 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
620                         replace = dup_replace;
621                 } else if (theme_adium_match (&cur, "%service%")) {
622                         replace = service_name;
623                 } else if (theme_adium_match (&cur, "%variant%")) {
624                         /* FIXME: The name of the active message style variant,
625                          * with all spaces replaced with an underscore.
626                          * A variant named "Alternating Messages - Blue Red"
627                          * will become "Alternating_Messages_-_Blue_Red".
628                          */
629                 } else if (theme_adium_match (&cur, "%userIcons%")) {
630                         /* FIXME: mus t be "hideIcons" if use preference is set
631                          * to hide avatars */
632                         replace = "showIcons";
633                 } else if (theme_adium_match (&cur, "%messageClasses%")) {
634                         replace = message_classes;
635                 } else if (theme_adium_match (&cur, "%status%")) {
636                         /* FIXME: A description of the status event. This is
637                          * neither in the user's local language nor expected to
638                          * be displayed; it may be useful to use a different div
639                          * class to present different types of status messages.
640                          * The following is a list of some of the more important
641                          * status messages; your message style should be able to
642                          * handle being shown a status message not in this list,
643                          * as even at present the list is incomplete and is
644                          * certain to become out of date in the future:
645                          *      online
646                          *      offline
647                          *      away
648                          *      away_message
649                          *      return_away
650                          *      idle
651                          *      return_idle
652                          *      date_separator
653                          *      contact_joined (group chats)
654                          *      contact_left
655                          *      error
656                          *      timed_out
657                          *      encryption (all OTR messages use this status)
658                          *      purple (all IRC topic and join/part messages use this status)
659                          *      fileTransferStarted
660                          *      fileTransferCompleted
661                          */
662                 } else {
663                         escape_and_append_len (string, cur, 1);
664                         continue;
665                 }
666
667                 /* Here we have a replacement to make */
668                 escape_and_append_len (string, replace, -1);
669
670                 g_free (dup_replace);
671                 g_free (format);
672         }
673         g_string_append (string, "\")");
674
675         script = g_string_free (string, FALSE);
676         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
677         g_free (script);
678 }
679
680 static void
681 theme_adium_append_event_escaped (EmpathyChatView *view,
682                                   const gchar     *escaped)
683 {
684         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
685         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
686
687         theme_adium_append_html (theme, "appendMessage",
688                                  priv->data->status_html, escaped, NULL, NULL, NULL,
689                                  NULL, "event",
690                                  empathy_time_get_current (), FALSE);
691
692         /* There is no last contact */
693         if (priv->last_contact) {
694                 g_object_unref (priv->last_contact);
695                 priv->last_contact = NULL;
696         }
697 }
698
699 static void
700 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
701     WebKitDOMNodeList *nodes)
702 {
703         guint i;
704
705         /* Remove focus and firstFocus class */
706         for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
707                 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
708                 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
709                 gchar *class_name;
710                 gchar **classes, **iter;
711                 GString *new_class_name;
712                 gboolean first = TRUE;
713
714                 if (element == NULL) {
715                         continue;
716                 }
717
718                 class_name = webkit_dom_html_element_get_class_name (element);
719                 classes = g_strsplit (class_name, " ", -1);
720                 new_class_name = g_string_sized_new (strlen (class_name));
721                 for (iter = classes; *iter != NULL; iter++) {
722                         if (tp_strdiff (*iter, "focus") &&
723                             tp_strdiff (*iter, "firstFocus")) {
724                                 if (!first) {
725                                         g_string_append_c (new_class_name, ' ');
726                                 }
727                                 g_string_append (new_class_name, *iter);
728                                 first = FALSE;
729                         }
730                 }
731
732                 webkit_dom_html_element_set_class_name (element, new_class_name->str);
733
734                 g_free (class_name);
735                 g_strfreev (classes);
736                 g_string_free (new_class_name, TRUE);
737         }
738 }
739
740 static void
741 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
742 {
743         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
744         WebKitDOMDocument *dom;
745         WebKitDOMNodeList *nodes;
746         GError *error = NULL;
747
748         if (!priv->has_unread_message)
749                 return;
750
751         priv->has_unread_message = FALSE;
752
753         dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
754         if (dom == NULL) {
755                 return;
756         }
757
758         /* Get all nodes with focus class */
759         nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
760         if (nodes == NULL) {
761                 DEBUG ("Error getting focus nodes: %s",
762                         error ? error->message : "No error");
763                 g_clear_error (&error);
764                 return;
765         }
766
767         theme_adium_remove_focus_marks (theme, nodes);
768 }
769
770 static void
771 theme_adium_append_message (EmpathyChatView *view,
772                             EmpathyMessage  *msg)
773 {
774         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
775         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
776         EmpathyContact        *sender;
777         TpMessage             *tp_msg;
778         TpAccount             *account;
779         gchar                 *body_escaped;
780         const gchar           *name;
781         const gchar           *contact_id;
782         EmpathyAvatar         *avatar;
783         const gchar           *avatar_filename = NULL;
784         gint64                 timestamp;
785         const gchar           *html = NULL;
786         const gchar           *func;
787         const gchar           *service_name;
788         GString               *message_classes = NULL;
789         gboolean               is_backlog;
790         gboolean               consecutive;
791         gboolean               action;
792
793         if (priv->pages_loading != 0) {
794                 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL);
795                 return;
796         }
797
798         /* Get information */
799         sender = empathy_message_get_sender (msg);
800         account = empathy_contact_get_account (sender);
801         service_name = empathy_protocol_name_to_display_name
802                 (tp_account_get_protocol (account));
803         if (service_name == NULL)
804                 service_name = tp_account_get_protocol (account);
805         timestamp = empathy_message_get_timestamp (msg);
806         body_escaped = theme_adium_parse_body (theme,
807                 empathy_message_get_body (msg),
808                 empathy_message_get_token (msg));
809         name = empathy_contact_get_logged_alias (sender);
810         contact_id = empathy_contact_get_id (sender);
811         action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
812
813         /* If this is a /me probably */
814         if (action) {
815                 gchar *str;
816
817                 if (priv->data->version >= 4 || !priv->data->custom_template) {
818                         str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
819                                                "<span class='actionMessageBody'>%s</span>",
820                                                name, body_escaped);
821                 } else {
822                         str = g_strdup_printf ("*%s*", body_escaped);
823                 }
824                 g_free (body_escaped);
825                 body_escaped = str;
826         }
827
828         /* Get the avatar filename, or a fallback */
829         avatar = empathy_contact_get_avatar (sender);
830         if (avatar) {
831                 avatar_filename = avatar->filename;
832         }
833         if (!avatar_filename) {
834                 if (empathy_contact_is_user (sender)) {
835                         avatar_filename = priv->data->default_outgoing_avatar_filename;
836                 } else {
837                         avatar_filename = priv->data->default_incoming_avatar_filename;
838                 }
839                 if (!avatar_filename) {
840                         if (!priv->data->default_avatar_filename) {
841                                 priv->data->default_avatar_filename =
842                                         empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
843                                                                          GTK_ICON_SIZE_DIALOG);
844                         }
845                         avatar_filename = priv->data->default_avatar_filename;
846                 }
847         }
848
849         /* We want to join this message with the last one if
850          * - senders are the same contact,
851          * - last message was recieved recently,
852          * - last message and this message both are/aren't backlog, and
853          * - DisableCombineConsecutive is not set in theme's settings */
854         is_backlog = empathy_message_is_backlog (msg);
855         consecutive = empathy_contact_equal (priv->last_contact, sender) &&
856                 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
857                 (is_backlog == priv->last_is_backlog) &&
858                 !tp_asv_get_boolean (priv->data->info,
859                                      "DisableCombineConsecutive", NULL);
860
861         /* Define message classes */
862         message_classes = g_string_new ("message");
863         if (!priv->has_focus && !is_backlog) {
864                 if (!priv->has_unread_message) {
865                         g_string_append (message_classes, " firstFocus");
866                         priv->has_unread_message = TRUE;
867                 }
868                 g_string_append (message_classes, " focus");
869         }
870         if (is_backlog) {
871                 g_string_append (message_classes, " history");
872         }
873         if (consecutive) {
874                 g_string_append (message_classes, " consecutive");
875         }
876         if (empathy_contact_is_user (sender)) {
877                 g_string_append (message_classes, " outgoing");
878         } else {
879                 g_string_append (message_classes, " incoming");
880         }
881         if (empathy_message_should_highlight (msg)) {
882                 g_string_append (message_classes, " mention");
883         }
884         if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
885                 g_string_append (message_classes, " autoreply");
886         }
887         if (action) {
888                 g_string_append (message_classes, " action");
889         }
890         /* FIXME: other classes:
891          * status - the message is a status change
892          * event - the message is a notification of something happening
893          *         (for example, encryption being turned on)
894          * %status% - See %status% in theme_adium_append_html ()
895          */
896
897         /* This is slightly a hack, but it's the only way to add
898          * arbitrary data to messages in the HTML. We add another
899          * class called "x-empathy-message-id-*" to the message. This
900          * way, we can remove the unread marker for this specific
901          * message later. */
902         tp_msg = empathy_message_get_tp_message (msg);
903         if (tp_msg != NULL) {
904                 guint32 id;
905                 gboolean valid;
906
907                 id = tp_message_get_pending_message_id (tp_msg, &valid);
908                 if (valid) {
909                         g_string_append_printf (message_classes,
910                             " x-empathy-message-id-%u", id);
911                 }
912         }
913
914         /* Define javascript function to use */
915         if (consecutive) {
916                 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
917         } else {
918                 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
919         }
920
921         if (empathy_contact_is_user (sender)) {
922                 /* out */
923                 if (is_backlog) {
924                         /* context */
925                         html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
926                 } else {
927                         /* content */
928                         html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
929                 }
930
931                 /* remove all the unread marks when we are sending a message */
932                 theme_adium_remove_all_focus_marks (theme);
933         } else {
934                 /* in */
935                 if (is_backlog) {
936                         /* context */
937                         html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
938                 } else {
939                         /* content */
940                         html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
941                 }
942         }
943
944         theme_adium_append_html (theme, func, html, body_escaped,
945                                  avatar_filename, name, contact_id,
946                                  service_name, message_classes->str,
947                                  timestamp, is_backlog);
948
949         /* Keep the sender of the last displayed message */
950         if (priv->last_contact) {
951                 g_object_unref (priv->last_contact);
952         }
953         priv->last_contact = g_object_ref (sender);
954         priv->last_timestamp = timestamp;
955         priv->last_is_backlog = is_backlog;
956
957         g_free (body_escaped);
958         g_string_free (message_classes, TRUE);
959 }
960
961 static void
962 theme_adium_append_event (EmpathyChatView *view,
963                           const gchar     *str)
964 {
965         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
966         gchar *str_escaped;
967
968         if (priv->pages_loading != 0) {
969                 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str);
970                 return;
971         }
972
973         str_escaped = g_markup_escape_text (str, -1);
974         theme_adium_append_event_escaped (view, str_escaped);
975         g_free (str_escaped);
976 }
977
978 static void
979 theme_adium_edit_message (EmpathyChatView *view,
980                           EmpathyMessage  *message)
981 {
982         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
983         WebKitDOMDocument *doc;
984         WebKitDOMElement *span;
985         gchar *id, *parsed_body;
986         gchar *tooltip, *timestamp;
987         GtkIconInfo *icon_info;
988         GError *error = NULL;
989
990         if (priv->pages_loading != 0) {
991                 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL);
992                 return;
993         }
994
995         id = g_strdup_printf ("message-token-%s",
996                 empathy_message_get_supersedes (message));
997         /* we don't pass a token here, because doing so will return another
998          * <span> element, and we don't want nested <span> elements */
999         parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1000                 empathy_message_get_body (message), NULL);
1001
1002         /* find the element */
1003         doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1004         span = webkit_dom_document_get_element_by_id (doc, id);
1005
1006         if (span == NULL) {
1007                 DEBUG ("Failed to find id '%s'", id);
1008                 goto except;
1009         }
1010
1011         if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1012                 DEBUG ("Not a HTML element");
1013                 goto except;
1014         }
1015
1016         /* update the HTML */
1017         webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1018                 parsed_body, &error);
1019
1020         if (error != NULL) {
1021                 DEBUG ("Error setting new inner-HTML: %s", error->message);
1022                 g_error_free (error);
1023                 goto except;
1024         }
1025
1026         /* set a tooltip */
1027         timestamp = empathy_time_to_string_local (
1028                 empathy_message_get_timestamp (message),
1029                 "%H:%M:%S");
1030         tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1031
1032         webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1033                 tooltip);
1034
1035         g_free (tooltip);
1036         g_free (timestamp);
1037
1038         /* mark this message as edited */
1039         icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1040                 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1041
1042         if (icon_info != NULL) {
1043                 /* set the icon as a background image using CSS
1044                  * FIXME: the icon won't update in response to theme changes */
1045                 gchar *style = g_strdup_printf (
1046                         "background-image:url('%s');"
1047                         "background-repeat:no-repeat;"
1048                         "background-position:left center;"
1049                         "padding-left:19px;", /* 16px icon + 3px padding */
1050                         gtk_icon_info_get_filename (icon_info));
1051
1052                 webkit_dom_element_set_attribute (span, "style", style, &error);
1053
1054                 if (error != NULL) {
1055                         DEBUG ("Error setting element style: %s",
1056                                 error->message);
1057                         g_clear_error (&error);
1058                         /* not fatal */
1059                 }
1060
1061                 g_free (style);
1062                 gtk_icon_info_free (icon_info);
1063         }
1064
1065         goto finally;
1066
1067 except:
1068         DEBUG ("Could not find message to edit with: %s",
1069                 empathy_message_get_body (message));
1070
1071 finally:
1072         g_free (id);
1073         g_free (parsed_body);
1074 }
1075
1076 static void
1077 theme_adium_scroll (EmpathyChatView *view,
1078                     gboolean         allow_scrolling)
1079 {
1080         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1081
1082         priv->allow_scrolling = allow_scrolling;
1083         if (allow_scrolling) {
1084                 empathy_chat_view_scroll_down (view);
1085         }
1086 }
1087
1088 static void
1089 theme_adium_scroll_down (EmpathyChatView *view)
1090 {
1091         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1092 }
1093
1094 static gboolean
1095 theme_adium_get_has_selection (EmpathyChatView *view)
1096 {
1097         return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1098 }
1099
1100 static void
1101 theme_adium_clear (EmpathyChatView *view)
1102 {
1103         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1104
1105         theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1106
1107         /* Clear last contact to avoid trying to add a 'joined'
1108          * message when we don't have an insertion point. */
1109         if (priv->last_contact) {
1110                 g_object_unref (priv->last_contact);
1111                 priv->last_contact = NULL;
1112         }
1113 }
1114
1115 static gboolean
1116 theme_adium_find_previous (EmpathyChatView *view,
1117                            const gchar     *search_criteria,
1118                            gboolean         new_search,
1119                            gboolean         match_case)
1120 {
1121         /* FIXME: Doesn't respect new_search */
1122         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1123                                             search_criteria, match_case,
1124                                             FALSE, TRUE);
1125 }
1126
1127 static gboolean
1128 theme_adium_find_next (EmpathyChatView *view,
1129                        const gchar     *search_criteria,
1130                        gboolean         new_search,
1131                        gboolean         match_case)
1132 {
1133         /* FIXME: Doesn't respect new_search */
1134         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1135                                             search_criteria, match_case,
1136                                             TRUE, TRUE);
1137 }
1138
1139 static void
1140 theme_adium_find_abilities (EmpathyChatView *view,
1141                             const gchar    *search_criteria,
1142                             gboolean        match_case,
1143                             gboolean       *can_do_previous,
1144                             gboolean       *can_do_next)
1145 {
1146         /* FIXME: Does webkit provide an API for that? We have wrap=true in
1147          * find_next and find_previous to work around this problem. */
1148         if (can_do_previous)
1149                 *can_do_previous = TRUE;
1150         if (can_do_next)
1151                 *can_do_next = TRUE;
1152 }
1153
1154 static void
1155 theme_adium_highlight (EmpathyChatView *view,
1156                        const gchar     *text,
1157                        gboolean         match_case)
1158 {
1159         webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1160         webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1161                                            text, match_case, 0);
1162         webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1163                                                     TRUE);
1164 }
1165
1166 static void
1167 theme_adium_copy_clipboard (EmpathyChatView *view)
1168 {
1169         webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1170 }
1171
1172 static void
1173 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1174                                       guint32 id)
1175 {
1176         WebKitDOMDocument *dom;
1177         WebKitDOMNodeList *nodes;
1178         gchar *class;
1179         GError *error = NULL;
1180
1181         dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1182         if (dom == NULL) {
1183                 return;
1184         }
1185
1186         class = g_strdup_printf (".x-empathy-message-id-%u", id);
1187
1188         /* Get all nodes with focus class */
1189         nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1190         g_free (class);
1191
1192         if (nodes == NULL) {
1193                 DEBUG ("Error getting focus nodes: %s",
1194                         error ? error->message : "No error");
1195                 g_clear_error (&error);
1196                 return;
1197         }
1198
1199         theme_adium_remove_focus_marks (self, nodes);
1200 }
1201
1202 static void
1203 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1204                                                       gpointer user_data)
1205 {
1206         EmpathyThemeAdium *self = user_data;
1207         guint32 id = GPOINTER_TO_UINT (data);
1208
1209         theme_adium_remove_mark_from_message (self, id);
1210 }
1211
1212 static void
1213 theme_adium_focus_toggled (EmpathyChatView *view,
1214                            gboolean         has_focus)
1215 {
1216         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1217
1218         priv->has_focus = has_focus;
1219         if (!priv->has_focus) {
1220                 /* We've lost focus, so let's make sure all the acked
1221                  * messages have lost their unread marker. */
1222                 g_queue_foreach (&priv->acked_messages,
1223                                  theme_adium_remove_acked_message_unread_mark_foreach,
1224                                  view);
1225                 g_queue_clear (&priv->acked_messages);
1226
1227                 priv->has_unread_message = FALSE;
1228         }
1229 }
1230
1231 static void
1232 theme_adium_message_acknowledged (EmpathyChatView *view,
1233                                   EmpathyMessage  *message)
1234 {
1235         EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1236         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1237         TpMessage *tp_msg;
1238         guint32 id;
1239         gboolean valid;
1240
1241         tp_msg = empathy_message_get_tp_message (message);
1242
1243         if (tp_msg == NULL) {
1244                 return;
1245         }
1246
1247         id = tp_message_get_pending_message_id (tp_msg, &valid);
1248         if (!valid) {
1249                 g_warning ("Acknoledged message doesn't have a pending ID");
1250                 return;
1251         }
1252
1253         /* We only want to actually remove the unread marker if the
1254          * view doesn't have focus. If we did it all the time we would
1255          * never see the unread markers, ever! So, we'll queue these
1256          * up, and when we lose focus, we'll remove the markers. */
1257         if (priv->has_focus) {
1258                 g_queue_push_tail (&priv->acked_messages,
1259                                    GUINT_TO_POINTER (id));
1260                 return;
1261         }
1262
1263         theme_adium_remove_mark_from_message (self, id);
1264 }
1265
1266 static gboolean
1267 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1268 {
1269         if (event->button == 3) {
1270                 gboolean developer_tools_enabled;
1271
1272                 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1273                               "enable-developer-extras", &developer_tools_enabled, NULL);
1274
1275                 /* We currently have no way to add an inspector menu
1276                  * item ourselves, so we disable our customized menu
1277                  * if the developer extras are enabled. */
1278                 if (!developer_tools_enabled) {
1279                         empathy_webkit_context_menu_for_event (
1280                                 WEBKIT_WEB_VIEW (widget), event,
1281                                 EMPATHY_WEBKIT_MENU_CLEAR);
1282                         return TRUE;
1283                 }
1284         }
1285
1286         return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1287 }
1288
1289 static void
1290 theme_adium_iface_init (EmpathyChatViewIface *iface)
1291 {
1292         iface->append_message = theme_adium_append_message;
1293         iface->append_event = theme_adium_append_event;
1294         iface->edit_message = theme_adium_edit_message;
1295         iface->scroll = theme_adium_scroll;
1296         iface->scroll_down = theme_adium_scroll_down;
1297         iface->get_has_selection = theme_adium_get_has_selection;
1298         iface->clear = theme_adium_clear;
1299         iface->find_previous = theme_adium_find_previous;
1300         iface->find_next = theme_adium_find_next;
1301         iface->find_abilities = theme_adium_find_abilities;
1302         iface->highlight = theme_adium_highlight;
1303         iface->copy_clipboard = theme_adium_copy_clipboard;
1304         iface->focus_toggled = theme_adium_focus_toggled;
1305         iface->message_acknowledged = theme_adium_message_acknowledged;
1306 }
1307
1308 static void
1309 theme_adium_load_finished_cb (WebKitWebView  *view,
1310                               WebKitWebFrame *frame,
1311                               gpointer        user_data)
1312 {
1313         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1314         EmpathyChatView       *chat_view = EMPATHY_CHAT_VIEW (view);
1315         GList                 *l;
1316
1317         DEBUG ("Page loaded");
1318         priv->pages_loading--;
1319
1320         if (priv->pages_loading != 0)
1321                 return;
1322
1323         /* Display queued messages */
1324         for (l = priv->message_queue.head; l != NULL; l = l->next) {
1325                 QueuedItem *item = l->data;
1326
1327                 switch (item->type)
1328                 {
1329                         case QUEUED_MESSAGE:
1330                                 theme_adium_append_message (chat_view, item->msg);
1331                                 break;
1332
1333                         case QUEUED_EDIT:
1334                                 theme_adium_edit_message (chat_view, item->msg);
1335                                 break;
1336
1337                         case QUEUED_EVENT:
1338                                 theme_adium_append_event (chat_view, item->str);
1339                                 break;
1340                 }
1341
1342                 free_queued_item (item);
1343         }
1344
1345         g_queue_clear (&priv->message_queue);
1346 }
1347
1348 static void
1349 theme_adium_finalize (GObject *object)
1350 {
1351         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1352
1353         empathy_adium_data_unref (priv->data);
1354
1355         g_object_unref (priv->gsettings_chat);
1356         g_object_unref (priv->gsettings_desktop);
1357
1358         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1359 }
1360
1361 static void
1362 theme_adium_dispose (GObject *object)
1363 {
1364         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1365
1366         if (priv->smiley_manager) {
1367                 g_object_unref (priv->smiley_manager);
1368                 priv->smiley_manager = NULL;
1369         }
1370
1371         if (priv->last_contact) {
1372                 g_object_unref (priv->last_contact);
1373                 priv->last_contact = NULL;
1374         }
1375
1376         if (priv->inspector_window) {
1377                 gtk_widget_destroy (priv->inspector_window);
1378                 priv->inspector_window = NULL;
1379         }
1380
1381         if (priv->acked_messages.length > 0) {
1382                 g_queue_clear (&priv->acked_messages);
1383         }
1384
1385         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1386 }
1387
1388 static gboolean
1389 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1390                                       EmpathyThemeAdium  *theme)
1391 {
1392         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1393
1394         if (priv->inspector_window) {
1395                 gtk_widget_show_all (priv->inspector_window);
1396         }
1397
1398         return TRUE;
1399 }
1400
1401 static gboolean
1402 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1403                                        EmpathyThemeAdium  *theme)
1404 {
1405         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1406
1407         if (priv->inspector_window) {
1408                 gtk_widget_hide (priv->inspector_window);
1409         }
1410
1411         return TRUE;
1412 }
1413
1414 static WebKitWebView *
1415 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1416                                  WebKitWebView      *web_view,
1417                                  EmpathyThemeAdium  *theme)
1418 {
1419         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1420         GtkWidget             *scrolled_window;
1421         GtkWidget             *inspector_web_view;
1422
1423         if (!priv->inspector_window) {
1424                 /* Create main window */
1425                 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1426                 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1427                                              800, 600);
1428                 g_signal_connect (priv->inspector_window, "delete-event",
1429                                   G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1430
1431                 /* Pack a scrolled window */
1432                 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1433                 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1434                                                 GTK_POLICY_AUTOMATIC,
1435                                                 GTK_POLICY_AUTOMATIC);
1436                 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1437                                    scrolled_window);
1438                 gtk_widget_show  (scrolled_window);
1439
1440                 /* Pack a webview in the scrolled window. That webview will be
1441                  * used to render the inspector tool.  */
1442                 inspector_web_view = webkit_web_view_new ();
1443                 gtk_container_add (GTK_CONTAINER (scrolled_window),
1444                                    inspector_web_view);
1445                 gtk_widget_show (scrolled_window);
1446
1447                 return WEBKIT_WEB_VIEW (inspector_web_view);
1448         }
1449
1450         return NULL;
1451 }
1452
1453 static void
1454 theme_adium_constructed (GObject *object)
1455 {
1456         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1457         const gchar           *font_family = NULL;
1458         gint                   font_size = 0;
1459         WebKitWebView         *webkit_view = WEBKIT_WEB_VIEW (object);
1460         WebKitWebInspector    *webkit_inspector;
1461
1462         /* Set default settings */
1463         font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1464         font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1465
1466         if (font_family && font_size) {
1467                 g_object_set (webkit_web_view_get_settings (webkit_view),
1468                         "default-font-family", font_family,
1469                         "default-font-size", font_size,
1470                         NULL);
1471         } else {
1472                 empathy_webkit_bind_font_setting (webkit_view,
1473                         priv->gsettings_desktop,
1474                         EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1475         }
1476
1477         /* Setup webkit inspector */
1478         webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1479         g_signal_connect (webkit_inspector, "inspect-web-view",
1480                           G_CALLBACK (theme_adium_inspect_web_view_cb),
1481                           object);
1482         g_signal_connect (webkit_inspector, "show-window",
1483                           G_CALLBACK (theme_adium_inspector_show_window_cb),
1484                           object);
1485         g_signal_connect (webkit_inspector, "close-window",
1486                           G_CALLBACK (theme_adium_inspector_close_window_cb),
1487                           object);
1488
1489         /* Load template */
1490         theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1491
1492         priv->in_construction = FALSE;
1493 }
1494
1495 static void
1496 theme_adium_get_property (GObject    *object,
1497                           guint       param_id,
1498                           GValue     *value,
1499                           GParamSpec *pspec)
1500 {
1501         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1502
1503         switch (param_id) {
1504         case PROP_ADIUM_DATA:
1505                 g_value_set_boxed (value, priv->data);
1506                 break;
1507         case PROP_VARIANT:
1508                 g_value_set_string (value, priv->variant);
1509                 break;
1510         default:
1511                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1512                 break;
1513         };
1514 }
1515
1516 static void
1517 theme_adium_set_property (GObject      *object,
1518                           guint         param_id,
1519                           const GValue *value,
1520                           GParamSpec   *pspec)
1521 {
1522         EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1523         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1524
1525         switch (param_id) {
1526         case PROP_ADIUM_DATA:
1527                 g_assert (priv->data == NULL);
1528                 priv->data = g_value_dup_boxed (value);
1529                 break;
1530         case PROP_VARIANT:
1531                 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1532                 break;
1533         default:
1534                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1535                 break;
1536         };
1537 }
1538
1539 static void
1540 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1541 {
1542         GObjectClass *object_class = G_OBJECT_CLASS (klass);
1543         GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1544
1545         object_class->finalize = theme_adium_finalize;
1546         object_class->dispose = theme_adium_dispose;
1547         object_class->constructed = theme_adium_constructed;
1548         object_class->get_property = theme_adium_get_property;
1549         object_class->set_property = theme_adium_set_property;
1550
1551         widget_class->button_press_event = theme_adium_button_press_event;
1552
1553         g_object_class_install_property (object_class,
1554                                          PROP_ADIUM_DATA,
1555                                          g_param_spec_boxed ("adium-data",
1556                                                              "The theme data",
1557                                                              "Data for the adium theme",
1558                                                               EMPATHY_TYPE_ADIUM_DATA,
1559                                                               G_PARAM_CONSTRUCT_ONLY |
1560                                                               G_PARAM_READWRITE |
1561                                                               G_PARAM_STATIC_STRINGS));
1562         g_object_class_install_property (object_class,
1563                                          PROP_VARIANT,
1564                                          g_param_spec_string ("variant",
1565                                                               "The theme variant",
1566                                                               "Variant name for the theme",
1567                                                               NULL,
1568                                                               G_PARAM_CONSTRUCT |
1569                                                               G_PARAM_READWRITE |
1570                                                               G_PARAM_STATIC_STRINGS));
1571
1572         g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1573 }
1574
1575 static void
1576 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1577 {
1578         EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1579                 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1580
1581         theme->priv = priv;
1582
1583         priv->in_construction = TRUE;
1584         g_queue_init (&priv->message_queue);
1585         priv->allow_scrolling = TRUE;
1586         priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1587
1588         g_signal_connect (theme, "load-finished",
1589                           G_CALLBACK (theme_adium_load_finished_cb),
1590                           NULL);
1591         g_signal_connect (theme, "navigation-policy-decision-requested",
1592                           G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1593                           NULL);
1594
1595         priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1596         priv->gsettings_desktop = g_settings_new (
1597                 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1598
1599         g_signal_connect (priv->gsettings_chat,
1600                 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1601                 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1602                 theme);
1603
1604         theme_adium_update_enable_webkit_developer_tools (theme);
1605 }
1606
1607 EmpathyThemeAdium *
1608 empathy_theme_adium_new (EmpathyAdiumData *data,
1609                          const gchar *variant)
1610 {
1611         g_return_val_if_fail (data != NULL, NULL);
1612
1613         return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1614                              "adium-data", data,
1615                              "variant", variant,
1616                              NULL);
1617 }
1618
1619 void
1620 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1621                                  const gchar *variant)
1622 {
1623         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1624         gchar *variant_path;
1625         gchar *script;
1626
1627         if (!tp_strdiff (priv->variant, variant)) {
1628                 return;
1629         }
1630
1631         g_free (priv->variant);
1632         priv->variant = g_strdup (variant);
1633
1634         if (priv->in_construction) {
1635                 return;
1636         }
1637
1638         DEBUG ("Update view with variant: '%s'", variant);
1639         variant_path = adium_info_dup_path_for_variant (priv->data->info,
1640                 priv->variant);
1641         script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1642
1643         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1644
1645         g_free (variant_path);
1646         g_free (script);
1647
1648         g_object_notify (G_OBJECT (theme), "variant");
1649 }
1650
1651 gboolean
1652 empathy_adium_path_is_valid (const gchar *path)
1653 {
1654         gboolean ret;
1655         gchar   *file;
1656
1657         /* The theme is not valid if there is no Info.plist */
1658         file = g_build_filename (path, "Contents", "Info.plist",
1659                                  NULL);
1660         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1661         g_free (file);
1662
1663         if (!ret)
1664                 return FALSE;
1665
1666         /* We ship a default Template.html as fallback if there is any problem
1667          * with the one inside the theme. The only other required file is
1668          * Content.html OR Incoming/Content.html*/
1669         file = g_build_filename (path, "Contents", "Resources", "Content.html",
1670                                  NULL);
1671         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1672         g_free (file);
1673
1674         if (!ret) {
1675                 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1676                                          "Content.html", NULL);
1677                 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1678                 g_free (file);
1679         }
1680
1681         return ret;
1682 }
1683
1684 GHashTable *
1685 empathy_adium_info_new (const gchar *path)
1686 {
1687         gchar *file;
1688         GValue *value;
1689         GHashTable *info = NULL;
1690
1691         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1692
1693         file = g_build_filename (path, "Contents", "Info.plist", NULL);
1694         value = empathy_plist_parse_from_file (file);
1695         g_free (file);
1696
1697         if (value == NULL)
1698                 return NULL;
1699
1700         info = g_value_dup_boxed (value);
1701         tp_g_value_slice_free (value);
1702
1703         /* Insert the theme's path into the hash table,
1704          * keys have to be dupped */
1705         tp_asv_set_string (info, g_strdup ("path"), path);
1706
1707         return info;
1708 }
1709
1710 static guint
1711 adium_info_get_version (GHashTable *info)
1712 {
1713         return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1714 }
1715
1716 static const gchar *
1717 adium_info_get_no_variant_name (GHashTable *info)
1718 {
1719         const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1720         return name ? name : _("Normal");
1721 }
1722
1723 static gchar *
1724 adium_info_dup_path_for_variant (GHashTable *info,
1725                                  const gchar *variant)
1726 {
1727         guint version = adium_info_get_version (info);
1728         const gchar *no_variant = adium_info_get_no_variant_name (info);
1729         GPtrArray *variants;
1730         guint i;
1731
1732         if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1733                 return g_strdup ("main.css");
1734         }
1735
1736         /* Verify the variant exists, fallback to the first one */
1737         variants = empathy_adium_info_get_available_variants (info);
1738         for (i = 0; i < variants->len; i++) {
1739                 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1740                         break;
1741                 }
1742         }
1743         if (i == variants->len) {
1744                 DEBUG ("Variant %s does not exist", variant);
1745                 variant = g_ptr_array_index (variants, 0);
1746         }
1747
1748         return g_strdup_printf ("Variants/%s.css", variant);
1749
1750 }
1751
1752 const gchar *
1753 empathy_adium_info_get_default_variant (GHashTable *info)
1754 {
1755         if (adium_info_get_version (info) <= 2) {
1756                 return adium_info_get_no_variant_name (info);
1757         }
1758
1759         return tp_asv_get_string (info, "DefaultVariant");
1760 }
1761
1762 GPtrArray *
1763 empathy_adium_info_get_available_variants (GHashTable *info)
1764 {
1765         GPtrArray *variants;
1766         const gchar *path;
1767         gchar *dirpath;
1768         GDir *dir;
1769
1770         variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1771         if (variants != NULL) {
1772                 return variants;
1773         }
1774
1775         variants = g_ptr_array_new_with_free_func (g_free);
1776         tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1777                 G_TYPE_PTR_ARRAY, variants);
1778
1779         path = tp_asv_get_string (info, "path");
1780         dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1781         dir = g_dir_open (dirpath, 0, NULL);
1782         if (dir != NULL) {
1783                 const gchar *name;
1784
1785                 for (name = g_dir_read_name (dir);
1786                      name != NULL;
1787                      name = g_dir_read_name (dir)) {
1788                         gchar *display_name;
1789
1790                         if (!g_str_has_suffix (name, ".css")) {
1791                                 continue;
1792                         }
1793
1794                         display_name = g_strdup (name);
1795                         strstr (display_name, ".css")[0] = '\0';
1796                         g_ptr_array_add (variants, display_name);
1797                 }
1798                 g_dir_close (dir);
1799         }
1800         g_free (dirpath);
1801
1802         if (adium_info_get_version (info) <= 2) {
1803                 g_ptr_array_add (variants,
1804                         g_strdup (adium_info_get_no_variant_name (info)));
1805         }
1806
1807         return variants;
1808 }
1809
1810 GType
1811 empathy_adium_data_get_type (void)
1812 {
1813   static GType type_id = 0;
1814
1815   if (!type_id)
1816     {
1817       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1818           (GBoxedCopyFunc) empathy_adium_data_ref,
1819           (GBoxedFreeFunc) empathy_adium_data_unref);
1820     }
1821
1822   return type_id;
1823 }
1824
1825 EmpathyAdiumData  *
1826 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1827 {
1828         EmpathyAdiumData *data;
1829         gchar            *template_html = NULL;
1830         gchar            *footer_html = NULL;
1831         gchar            *tmp;
1832
1833         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1834
1835         data = g_slice_new0 (EmpathyAdiumData);
1836         data->ref_count = 1;
1837         data->path = g_strdup (path);
1838         data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1839                 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1840         data->info = g_hash_table_ref (info);
1841         data->version = adium_info_get_version (info);
1842         data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1843         data->date_format_cache = g_hash_table_new_full (g_str_hash,
1844                 g_str_equal, g_free, g_free);
1845
1846         DEBUG ("Loading theme at %s", path);
1847
1848 #define LOAD(path, var) \
1849                 tmp = g_build_filename (data->basedir, path, NULL); \
1850                 g_file_get_contents (tmp, &var, NULL, NULL); \
1851                 g_free (tmp); \
1852
1853 #define LOAD_CONST(path, var) \
1854         { \
1855                 gchar *content; \
1856                 LOAD (path, content); \
1857                 if (content != NULL) { \
1858                         g_ptr_array_add (data->strings_to_free, content); \
1859                 } \
1860                 var = content; \
1861         }
1862
1863         /* Load html files */
1864         LOAD_CONST ("Content.html", data->content_html);
1865         LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1866         LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1867         LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1868         LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1869         LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1870         LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1871         LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1872         LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1873         LOAD_CONST ("Status.html", data->status_html);
1874         LOAD ("Template.html", template_html);
1875         LOAD ("Footer.html", footer_html);
1876
1877 #undef LOAD_CONST
1878 #undef LOAD
1879
1880         /* HTML fallbacks: If we have at least content OR in_content, then
1881          * everything else gets a fallback */
1882
1883 #define FALLBACK(html, fallback) \
1884         if (html == NULL) { \
1885                 html = fallback; \
1886         }
1887
1888         /* in_nextcontent -> in_content -> content */
1889         FALLBACK (data->in_content_html,      data->content_html);
1890         FALLBACK (data->in_nextcontent_html,  data->in_content_html);
1891
1892         /* context -> content */
1893         FALLBACK (data->in_context_html,      data->in_content_html);
1894         FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
1895         FALLBACK (data->out_context_html,     data->out_content_html);
1896         FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1897
1898         /* out -> in */
1899         FALLBACK (data->out_content_html,     data->in_content_html);
1900         FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1901         FALLBACK (data->out_context_html,     data->in_context_html);
1902         FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1903
1904         /* status -> in_content */
1905         FALLBACK (data->status_html,          data->in_content_html);
1906
1907 #undef FALLBACK
1908
1909         /* template -> empathy's template */
1910         data->custom_template = (template_html != NULL);
1911         if (template_html == NULL) {
1912                 tmp = empathy_file_lookup ("Template.html", "data");
1913                 g_file_get_contents (tmp, &template_html, NULL, NULL);
1914                 g_free (tmp);
1915         }
1916
1917         /* Default avatar */
1918         tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1919         if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1920                 data->default_incoming_avatar_filename = tmp;
1921         } else {
1922                 g_free (tmp);
1923         }
1924         tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1925         if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1926                 data->default_outgoing_avatar_filename = tmp;
1927         } else {
1928                 g_free (tmp);
1929         }
1930
1931         /* Old custom templates had only 4 parameters.
1932          * New templates have 5 parameters */
1933         if (data->version <= 2 && data->custom_template) {
1934                 tmp = string_with_format (template_html,
1935                         data->basedir,
1936                         "%@", /* Leave variant unset */
1937                         "", /* The header */
1938                         footer_html ? footer_html : "",
1939                         NULL);
1940         } else {
1941                 tmp = string_with_format (template_html,
1942                         data->basedir,
1943                         data->version <= 2 ? "" : "@import url( \"main.css\" );",
1944                         "%@", /* Leave variant unset */
1945                         "", /* The header */
1946                         footer_html ? footer_html : "",
1947                         NULL);
1948         }
1949         g_ptr_array_add (data->strings_to_free, tmp);
1950         data->template_html = tmp;
1951
1952         g_free (template_html);
1953         g_free (footer_html);
1954
1955         return data;
1956 }
1957
1958 EmpathyAdiumData  *
1959 empathy_adium_data_new (const gchar *path)
1960 {
1961         EmpathyAdiumData *data;
1962         GHashTable *info;
1963
1964         info = empathy_adium_info_new (path);
1965         data = empathy_adium_data_new_with_info (path, info);
1966         g_hash_table_unref (info);
1967
1968         return data;
1969 }
1970
1971 EmpathyAdiumData  *
1972 empathy_adium_data_ref (EmpathyAdiumData *data)
1973 {
1974         g_return_val_if_fail (data != NULL, NULL);
1975
1976         g_atomic_int_inc (&data->ref_count);
1977
1978         return data;
1979 }
1980
1981 void
1982 empathy_adium_data_unref (EmpathyAdiumData *data)
1983 {
1984         g_return_if_fail (data != NULL);
1985
1986         if (g_atomic_int_dec_and_test (&data->ref_count)) {
1987                 g_free (data->path);
1988                 g_free (data->basedir);
1989                 g_free (data->default_avatar_filename);
1990                 g_free (data->default_incoming_avatar_filename);
1991                 g_free (data->default_outgoing_avatar_filename);
1992                 g_hash_table_unref (data->info);
1993                 g_ptr_array_unref (data->strings_to_free);
1994                 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
1995
1996                 g_slice_free (EmpathyAdiumData, data);
1997         }
1998 }
1999
2000 GHashTable *
2001 empathy_adium_data_get_info (EmpathyAdiumData *data)
2002 {
2003         g_return_val_if_fail (data != NULL, NULL);
2004
2005         return data->info;
2006 }
2007
2008 const gchar *
2009 empathy_adium_data_get_path (EmpathyAdiumData *data)
2010 {
2011         g_return_val_if_fail (data != NULL, NULL);
2012
2013         return data->path;
2014 }
2015