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