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