]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
Updated Greek translations
[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 (account));
868   if (service_name == NULL)
869     service_name = tp_account_get_protocol (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_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1443 }
1444
1445 static void
1446 theme_adium_dispose (GObject *object)
1447 {
1448   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1449
1450   if (self->priv->smiley_manager)
1451     {
1452       g_object_unref (self->priv->smiley_manager);
1453       self->priv->smiley_manager = NULL;
1454     }
1455
1456   if (self->priv->last_contact)
1457     {
1458       g_object_unref (self->priv->last_contact);
1459       self->priv->last_contact = NULL;
1460     }
1461
1462   if (self->priv->inspector_window)
1463     {
1464       gtk_widget_destroy (self->priv->inspector_window);
1465       self->priv->inspector_window = NULL;
1466     }
1467
1468   if (self->priv->acked_messages.length > 0)
1469     {
1470       g_queue_clear (&self->priv->acked_messages);
1471     }
1472
1473   G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1474 }
1475
1476 static gboolean
1477 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1478     EmpathyThemeAdium *self)
1479 {
1480   if (self->priv->inspector_window)
1481     {
1482       gtk_widget_show_all (self->priv->inspector_window);
1483     }
1484
1485   return TRUE;
1486 }
1487
1488 static gboolean
1489 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1490     EmpathyThemeAdium *self)
1491 {
1492   if (self->priv->inspector_window)
1493     {
1494       gtk_widget_hide (self->priv->inspector_window);
1495     }
1496
1497   return TRUE;
1498 }
1499
1500 static WebKitWebView *
1501 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1502     WebKitWebView *web_view,
1503     EmpathyThemeAdium *self)
1504 {
1505   GtkWidget *scrolled_window;
1506   GtkWidget *inspector_web_view;
1507
1508   if (!self->priv->inspector_window)
1509     {
1510       /* Create main window */
1511       self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1512
1513       gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1514                  800, 600);
1515
1516       g_signal_connect (self->priv->inspector_window, "delete-event",
1517             G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1518
1519       /* Pack a scrolled window */
1520       scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1521
1522       gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1523               GTK_POLICY_AUTOMATIC,
1524               GTK_POLICY_AUTOMATIC);
1525       gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1526              scrolled_window);
1527       gtk_widget_show (scrolled_window);
1528
1529       /* Pack a webview in the scrolled window. That webview will be
1530        * used to render the inspector tool. */
1531       inspector_web_view = webkit_web_view_new ();
1532       gtk_container_add (GTK_CONTAINER (scrolled_window),
1533              inspector_web_view);
1534       gtk_widget_show (scrolled_window);
1535
1536       return WEBKIT_WEB_VIEW (inspector_web_view);
1537     }
1538
1539   return NULL;
1540 }
1541
1542 static void
1543 theme_adium_constructed (GObject *object)
1544 {
1545   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1546   const gchar *font_family = NULL;
1547   gint font_size = 0;
1548   WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1549   WebKitWebInspector *webkit_inspector;
1550
1551   /* Set default settings */
1552   font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1553   font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1554
1555   if (font_family && font_size)
1556     {
1557       g_object_set (webkit_web_view_get_settings (webkit_view),
1558         "default-font-family", font_family,
1559         "default-font-size", font_size,
1560         NULL);
1561     }
1562   else
1563     {
1564       empathy_webkit_bind_font_setting (webkit_view,
1565         self->priv->gsettings_desktop,
1566         EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1567     }
1568
1569   /* Setup webkit inspector */
1570   webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1571   g_signal_connect (webkit_inspector, "inspect-web-view",
1572       G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1573   g_signal_connect (webkit_inspector, "show-window",
1574       G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1575   g_signal_connect (webkit_inspector, "close-window",
1576       G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1577
1578   /* Load template */
1579   theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1580
1581   self->priv->in_construction = FALSE;
1582 }
1583
1584 static void
1585 theme_adium_get_property (GObject *object,
1586     guint param_id,
1587     GValue *value,
1588     GParamSpec *pspec)
1589 {
1590   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1591
1592   switch (param_id)
1593     {
1594       case PROP_ADIUM_DATA:
1595         g_value_set_boxed (value, self->priv->data);
1596         break;
1597       case PROP_VARIANT:
1598         g_value_set_string (value, self->priv->variant);
1599         break;
1600       default:
1601         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1602         break;
1603     };
1604 }
1605
1606 static void
1607 theme_adium_set_property (GObject *object,
1608     guint param_id,
1609     const GValue *value,
1610     GParamSpec *pspec)
1611 {
1612   EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1613
1614   switch (param_id)
1615     {
1616       case PROP_ADIUM_DATA:
1617         g_assert (self->priv->data == NULL);
1618         self->priv->data = g_value_dup_boxed (value);
1619         break;
1620       case PROP_VARIANT:
1621         empathy_theme_adium_set_variant (self, g_value_get_string (value));
1622         break;
1623       default:
1624         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1625         break;
1626     };
1627 }
1628
1629 static void
1630 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1631 {
1632   GObjectClass *object_class = G_OBJECT_CLASS (klass);
1633   GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1634
1635   object_class->finalize = theme_adium_finalize;
1636   object_class->dispose = theme_adium_dispose;
1637   object_class->constructed = theme_adium_constructed;
1638   object_class->get_property = theme_adium_get_property;
1639   object_class->set_property = theme_adium_set_property;
1640
1641   widget_class->button_press_event = theme_adium_button_press_event;
1642
1643   g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1644       g_param_spec_boxed ("adium-data",
1645         "The theme data",
1646         "Data for the adium theme",
1647         EMPATHY_TYPE_ADIUM_DATA,
1648         G_PARAM_CONSTRUCT_ONLY |
1649         G_PARAM_READWRITE |
1650         G_PARAM_STATIC_STRINGS));
1651
1652   g_object_class_install_property (object_class, PROP_VARIANT,
1653       g_param_spec_string ("variant",
1654         "The theme variant",
1655         "Variant name for the theme",
1656         NULL,
1657         G_PARAM_CONSTRUCT |
1658         G_PARAM_READWRITE |
1659         G_PARAM_STATIC_STRINGS));
1660
1661   g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1662 }
1663
1664 static void
1665 empathy_theme_adium_init (EmpathyThemeAdium *self)
1666 {
1667   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1668     EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1669
1670   self->priv->in_construction = TRUE;
1671   g_queue_init (&self->priv->message_queue);
1672   self->priv->allow_scrolling = TRUE;
1673   self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1674
1675   /* Show avatars by default. */
1676   self->priv->show_avatars = TRUE;
1677
1678   g_signal_connect (self, "load-finished",
1679       G_CALLBACK (theme_adium_load_finished_cb), NULL);
1680   g_signal_connect (self, "navigation-policy-decision-requested",
1681         G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1682
1683   self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1684   self->priv->gsettings_desktop = g_settings_new (
1685     EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1686
1687   g_signal_connect (self->priv->gsettings_chat,
1688     "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1689     G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1690     self);
1691
1692   theme_adium_update_enable_webkit_developer_tools (self);
1693 }
1694
1695 EmpathyThemeAdium *
1696 empathy_theme_adium_new (EmpathyAdiumData *data,
1697     const gchar *variant)
1698 {
1699   g_return_val_if_fail (data != NULL, NULL);
1700
1701   return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1702       "adium-data", data,
1703       "variant", variant,
1704       NULL);
1705 }
1706
1707 void
1708 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1709     const gchar *variant)
1710 {
1711   gchar *variant_path;
1712   gchar *script;
1713
1714   if (!tp_strdiff (self->priv->variant, variant))
1715     return;
1716
1717   g_free (self->priv->variant);
1718   self->priv->variant = g_strdup (variant);
1719
1720   if (self->priv->in_construction)
1721     return;
1722
1723   DEBUG ("Update view with variant: '%s'", variant);
1724   variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1725     self->priv->variant);
1726   script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1727       variant_path);
1728
1729   webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1730
1731   g_free (variant_path);
1732   g_free (script);
1733
1734   g_object_notify (G_OBJECT (self), "variant");
1735 }
1736
1737 void
1738 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1739 {
1740   WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1741   WebKitWebInspector *inspector;
1742
1743   g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1744       "enable-developer-extras", TRUE, NULL);
1745
1746   inspector = webkit_web_view_get_inspector (web_view);
1747   webkit_web_inspector_show (inspector);
1748 }
1749
1750 gboolean
1751 empathy_adium_path_is_valid (const gchar *path)
1752 {
1753   gboolean ret;
1754   gchar *file;
1755
1756   if (path[0] != '/')
1757     return FALSE;
1758
1759   /* The theme is not valid if there is no Info.plist */
1760   file = g_build_filename (path, "Contents", "Info.plist",
1761          NULL);
1762   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1763   g_free (file);
1764
1765   if (!ret)
1766     return FALSE;
1767
1768   /* We ship a default Template.html as fallback if there is any problem
1769    * with the one inside the theme. The only other required file is
1770    * Content.html OR Incoming/Content.html*/
1771   file = g_build_filename (path, "Contents", "Resources", "Content.html",
1772       NULL);
1773   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1774   g_free (file);
1775
1776   if (!ret)
1777     {
1778       file = g_build_filename (path, "Contents", "Resources", "Incoming",
1779              "Content.html", NULL);
1780       ret = g_file_test (file, G_FILE_TEST_EXISTS);
1781       g_free (file);
1782     }
1783
1784   return ret;
1785 }
1786
1787 GHashTable *
1788 empathy_adium_info_new (const gchar *path)
1789 {
1790   gchar *file;
1791   GValue *value;
1792   GHashTable *info = NULL;
1793
1794   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1795
1796   file = g_build_filename (path, "Contents", "Info.plist", NULL);
1797   value = empathy_plist_parse_from_file (file);
1798   g_free (file);
1799
1800   if (value == NULL)
1801     return NULL;
1802
1803   info = g_value_dup_boxed (value);
1804   tp_g_value_slice_free (value);
1805
1806   /* Insert the theme's path into the hash table,
1807    * keys have to be dupped */
1808   tp_asv_set_string (info, g_strdup ("path"), path);
1809
1810   return info;
1811 }
1812
1813 static guint
1814 adium_info_get_version (GHashTable *info)
1815 {
1816   return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1817 }
1818
1819 static const gchar *
1820 adium_info_get_no_variant_name (GHashTable *info)
1821 {
1822   const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1823   return name ? name : _("Normal");
1824 }
1825
1826 static gchar *
1827 adium_info_dup_path_for_variant (GHashTable *info,
1828     const gchar *variant)
1829 {
1830   guint version = adium_info_get_version (info);
1831   const gchar *no_variant = adium_info_get_no_variant_name (info);
1832   GPtrArray *variants;
1833   guint i;
1834
1835   if (version <= 2 && !tp_strdiff (variant, no_variant))
1836     return g_strdup ("main.css");
1837
1838   variants = empathy_adium_info_get_available_variants (info);
1839   if (variants->len == 0)
1840     return g_strdup ("main.css");
1841
1842   /* Verify the variant exists, fallback to the first one */
1843   for (i = 0; i < variants->len; i++)
1844     {
1845       if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1846         break;
1847     }
1848
1849   if (i == variants->len)
1850     {
1851       DEBUG ("Variant %s does not exist", variant);
1852       variant = g_ptr_array_index (variants, 0);
1853     }
1854
1855   return g_strdup_printf ("Variants/%s.css", variant);
1856
1857 }
1858
1859 const gchar *
1860 empathy_adium_info_get_default_variant (GHashTable *info)
1861 {
1862   if (adium_info_get_version (info) <= 2)
1863     return adium_info_get_no_variant_name (info);
1864
1865   return tp_asv_get_string (info, "DefaultVariant");
1866 }
1867
1868 GPtrArray *
1869 empathy_adium_info_get_available_variants (GHashTable *info)
1870 {
1871   GPtrArray *variants;
1872   const gchar *path;
1873   gchar *dirpath;
1874   GDir *dir;
1875
1876   variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1877   if (variants != NULL)
1878     return variants;
1879
1880   variants = g_ptr_array_new_with_free_func (g_free);
1881   tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1882     G_TYPE_PTR_ARRAY, variants);
1883
1884   path = tp_asv_get_string (info, "path");
1885   dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1886   dir = g_dir_open (dirpath, 0, NULL);
1887   if (dir != NULL)
1888     {
1889       const gchar *name;
1890
1891       for (name = g_dir_read_name (dir);
1892            name != NULL;
1893            name = g_dir_read_name (dir))
1894         {
1895           gchar *display_name;
1896
1897           if (!g_str_has_suffix (name, ".css"))
1898             continue;
1899
1900           display_name = g_strdup (name);
1901           strstr (display_name, ".css")[0] = '\0';
1902           g_ptr_array_add (variants, display_name);
1903         }
1904
1905       g_dir_close (dir);
1906     }
1907   g_free (dirpath);
1908
1909   if (adium_info_get_version (info) <= 2)
1910     g_ptr_array_add (variants,
1911       g_strdup (adium_info_get_no_variant_name (info)));
1912
1913   return variants;
1914 }
1915
1916 GType
1917 empathy_adium_data_get_type (void)
1918 {
1919   static GType type_id = 0;
1920
1921   if (!type_id)
1922     {
1923       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1924           (GBoxedCopyFunc) empathy_adium_data_ref,
1925           (GBoxedFreeFunc) empathy_adium_data_unref);
1926     }
1927
1928   return type_id;
1929 }
1930
1931 EmpathyAdiumData *
1932 empathy_adium_data_new_with_info (const gchar *path,
1933     GHashTable *info)
1934 {
1935   EmpathyAdiumData *data;
1936   gchar *template_html = NULL;
1937   gchar *footer_html = NULL;
1938   gchar *tmp;
1939
1940   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1941
1942   data = g_slice_new0 (EmpathyAdiumData);
1943   data->ref_count = 1;
1944   data->path = g_strdup (path);
1945   data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1946     G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1947   data->info = g_hash_table_ref (info);
1948   data->version = adium_info_get_version (info);
1949   data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1950   data->date_format_cache = g_hash_table_new_full (g_str_hash,
1951     g_str_equal, g_free, g_free);
1952
1953   DEBUG ("Loading theme at %s", path);
1954
1955 #define LOAD(path, var) \
1956     tmp = g_build_filename (data->basedir, path, NULL); \
1957     g_file_get_contents (tmp, &var, NULL, NULL); \
1958     g_free (tmp); \
1959
1960 #define LOAD_CONST(path, var) \
1961   { \
1962     gchar *content; \
1963     LOAD (path, content); \
1964     if (content != NULL) { \
1965       g_ptr_array_add (data->strings_to_free, content); \
1966     } \
1967     var = content; \
1968   }
1969
1970   /* Load html files */
1971   LOAD_CONST ("Content.html", data->content_html);
1972   LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1973   LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1974   LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1975   LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1976   LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1977   LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1978   LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1979   LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1980   LOAD_CONST ("Status.html", data->status_html);
1981   LOAD ("Template.html", template_html);
1982   LOAD ("Footer.html", footer_html);
1983
1984 #undef LOAD_CONST
1985 #undef LOAD
1986
1987   /* HTML fallbacks: If we have at least content OR in_content, then
1988    * everything else gets a fallback */
1989
1990 #define FALLBACK(html, fallback) \
1991   if (html == NULL) { \
1992     html = fallback; \
1993   }
1994
1995   /* in_nextcontent -> in_content -> content */
1996   FALLBACK (data->in_content_html,      data->content_html);
1997   FALLBACK (data->in_nextcontent_html,  data->in_content_html);
1998
1999   /* context -> content */
2000   FALLBACK (data->in_context_html,      data->in_content_html);
2001   FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
2002   FALLBACK (data->out_context_html,     data->out_content_html);
2003   FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2004
2005   /* out -> in */
2006   FALLBACK (data->out_content_html,     data->in_content_html);
2007   FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2008   FALLBACK (data->out_context_html,     data->in_context_html);
2009   FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2010
2011   /* status -> in_content */
2012   FALLBACK (data->status_html,          data->in_content_html);
2013
2014 #undef FALLBACK
2015
2016   /* template -> empathy's template */
2017   data->custom_template = (template_html != NULL);
2018   if (template_html == NULL)
2019     {
2020       GError *error = NULL;
2021
2022       tmp = empathy_file_lookup ("Template.html", "data");
2023
2024       if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2025         g_warning ("couldn't load Empathy's default theme "
2026           "template: %s", error->message);
2027         g_return_val_if_reached (data);
2028       }
2029
2030       g_free (tmp);
2031     }
2032
2033   /* Default avatar */
2034   tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2035   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2036     {
2037       data->default_incoming_avatar_filename = tmp;
2038     }
2039   else
2040     {
2041       g_free (tmp);
2042     }
2043
2044   tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2045   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2046     {
2047       data->default_outgoing_avatar_filename = tmp;
2048     }
2049   else
2050     {
2051       g_free (tmp);
2052     }
2053
2054   /* Old custom templates had only 4 parameters.
2055    * New templates have 5 parameters */
2056   if (data->version <= 2 && data->custom_template)
2057     {
2058       tmp = string_with_format (template_html,
2059         data->basedir,
2060         "%@", /* Leave variant unset */
2061         "", /* The header */
2062         footer_html ? footer_html : "",
2063         NULL);
2064     }
2065   else
2066     {
2067       tmp = string_with_format (template_html,
2068         data->basedir,
2069         data->version <= 2 ? "" : "@import url( \"main.css\" );",
2070         "%@", /* Leave variant unset */
2071         "", /* The header */
2072         footer_html ? footer_html : "",
2073         NULL);
2074     }
2075   g_ptr_array_add (data->strings_to_free, tmp);
2076   data->template_html = tmp;
2077
2078   g_free (template_html);
2079   g_free (footer_html);
2080
2081   return data;
2082 }
2083
2084 EmpathyAdiumData *
2085 empathy_adium_data_new (const gchar *path)
2086 {
2087   EmpathyAdiumData *data;
2088   GHashTable *info;
2089
2090   info = empathy_adium_info_new (path);
2091   data = empathy_adium_data_new_with_info (path, info);
2092   g_hash_table_unref (info);
2093
2094   return data;
2095 }
2096
2097 EmpathyAdiumData *
2098 empathy_adium_data_ref (EmpathyAdiumData *data)
2099 {
2100   g_return_val_if_fail (data != NULL, NULL);
2101
2102   g_atomic_int_inc (&data->ref_count);
2103
2104   return data;
2105 }
2106
2107 void
2108 empathy_adium_data_unref (EmpathyAdiumData *data)
2109 {
2110   g_return_if_fail (data != NULL);
2111
2112   if (g_atomic_int_dec_and_test (&data->ref_count)) {
2113     g_free (data->path);
2114     g_free (data->basedir);
2115     g_free (data->default_avatar_filename);
2116     g_free (data->default_incoming_avatar_filename);
2117     g_free (data->default_outgoing_avatar_filename);
2118     g_hash_table_unref (data->info);
2119     g_ptr_array_unref (data->strings_to_free);
2120     tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2121
2122     g_slice_free (EmpathyAdiumData, data);
2123   }
2124 }
2125
2126 GHashTable *
2127 empathy_adium_data_get_info (EmpathyAdiumData *data)
2128 {
2129   g_return_val_if_fail (data != NULL, NULL);
2130
2131   return data->info;
2132 }
2133
2134 const gchar *
2135 empathy_adium_data_get_path (EmpathyAdiumData *data)
2136 {
2137   g_return_val_if_fail (data != NULL, NULL);
2138
2139   return data->path;
2140 }