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