]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
Updated kn translation
[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_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   gchar **tmp;
1756   const gchar *dir;
1757
1758   if (path[0] != '/')
1759     return FALSE;
1760
1761   /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1762   tmp = g_strsplit (path, "/", 0);
1763   if (tmp == NULL)
1764     {
1765       g_free (tmp);
1766       return FALSE;
1767     }
1768
1769   dir = tmp[g_strv_length (tmp) - 1];
1770
1771   if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1772     {
1773       g_free (tmp);
1774       return FALSE;
1775     }
1776
1777   g_free (tmp);
1778
1779   /* The theme is not valid if there is no Info.plist */
1780   file = g_build_filename (path, "Contents", "Info.plist",
1781          NULL);
1782   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1783   g_free (file);
1784
1785   if (!ret)
1786     return FALSE;
1787
1788   /* We ship a default Template.html as fallback if there is any problem
1789    * with the one inside the theme. The only other required file is
1790    * Content.html OR Incoming/Content.html*/
1791   file = g_build_filename (path, "Contents", "Resources", "Content.html",
1792       NULL);
1793   ret = g_file_test (file, G_FILE_TEST_EXISTS);
1794   g_free (file);
1795
1796   if (!ret)
1797     {
1798       file = g_build_filename (path, "Contents", "Resources", "Incoming",
1799              "Content.html", NULL);
1800       ret = g_file_test (file, G_FILE_TEST_EXISTS);
1801       g_free (file);
1802     }
1803
1804   return ret;
1805 }
1806
1807 GHashTable *
1808 empathy_adium_info_new (const gchar *path)
1809 {
1810   gchar *file;
1811   GValue *value;
1812   GHashTable *info = NULL;
1813
1814   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1815
1816   file = g_build_filename (path, "Contents", "Info.plist", NULL);
1817   value = empathy_plist_parse_from_file (file);
1818   g_free (file);
1819
1820   if (value == NULL)
1821     return NULL;
1822
1823   info = g_value_dup_boxed (value);
1824   tp_g_value_slice_free (value);
1825
1826   /* Insert the theme's path into the hash table,
1827    * keys have to be dupped */
1828   tp_asv_set_string (info, g_strdup ("path"), path);
1829
1830   return info;
1831 }
1832
1833 static guint
1834 adium_info_get_version (GHashTable *info)
1835 {
1836   return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1837 }
1838
1839 static const gchar *
1840 adium_info_get_no_variant_name (GHashTable *info)
1841 {
1842   const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1843   return name ? name : _("Normal");
1844 }
1845
1846 static gchar *
1847 adium_info_dup_path_for_variant (GHashTable *info,
1848     const gchar *variant)
1849 {
1850   guint version = adium_info_get_version (info);
1851   const gchar *no_variant = adium_info_get_no_variant_name (info);
1852   GPtrArray *variants;
1853   guint i;
1854
1855   if (version <= 2 && !tp_strdiff (variant, no_variant))
1856     return g_strdup ("main.css");
1857
1858   variants = empathy_adium_info_get_available_variants (info);
1859   if (variants->len == 0)
1860     return g_strdup ("main.css");
1861
1862   /* Verify the variant exists, fallback to the first one */
1863   for (i = 0; i < variants->len; i++)
1864     {
1865       if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1866         break;
1867     }
1868
1869   if (i == variants->len)
1870     {
1871       DEBUG ("Variant %s does not exist", variant);
1872       variant = g_ptr_array_index (variants, 0);
1873     }
1874
1875   return g_strdup_printf ("Variants/%s.css", variant);
1876
1877 }
1878
1879 const gchar *
1880 empathy_adium_info_get_default_variant (GHashTable *info)
1881 {
1882   if (adium_info_get_version (info) <= 2)
1883     return adium_info_get_no_variant_name (info);
1884
1885   return tp_asv_get_string (info, "DefaultVariant");
1886 }
1887
1888 GPtrArray *
1889 empathy_adium_info_get_available_variants (GHashTable *info)
1890 {
1891   GPtrArray *variants;
1892   const gchar *path;
1893   gchar *dirpath;
1894   GDir *dir;
1895
1896   variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1897   if (variants != NULL)
1898     return variants;
1899
1900   variants = g_ptr_array_new_with_free_func (g_free);
1901   tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1902     G_TYPE_PTR_ARRAY, variants);
1903
1904   path = tp_asv_get_string (info, "path");
1905   dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1906   dir = g_dir_open (dirpath, 0, NULL);
1907   if (dir != NULL)
1908     {
1909       const gchar *name;
1910
1911       for (name = g_dir_read_name (dir);
1912            name != NULL;
1913            name = g_dir_read_name (dir))
1914         {
1915           gchar *display_name;
1916
1917           if (!g_str_has_suffix (name, ".css"))
1918             continue;
1919
1920           display_name = g_strdup (name);
1921           strstr (display_name, ".css")[0] = '\0';
1922           g_ptr_array_add (variants, display_name);
1923         }
1924
1925       g_dir_close (dir);
1926     }
1927   g_free (dirpath);
1928
1929   if (adium_info_get_version (info) <= 2)
1930     g_ptr_array_add (variants,
1931       g_strdup (adium_info_get_no_variant_name (info)));
1932
1933   return variants;
1934 }
1935
1936 GType
1937 empathy_adium_data_get_type (void)
1938 {
1939   static GType type_id = 0;
1940
1941   if (!type_id)
1942     {
1943       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1944           (GBoxedCopyFunc) empathy_adium_data_ref,
1945           (GBoxedFreeFunc) empathy_adium_data_unref);
1946     }
1947
1948   return type_id;
1949 }
1950
1951 EmpathyAdiumData *
1952 empathy_adium_data_new_with_info (const gchar *path,
1953     GHashTable *info)
1954 {
1955   EmpathyAdiumData *data;
1956   gchar *template_html = NULL;
1957   gchar *footer_html = NULL;
1958   gchar *tmp;
1959
1960   g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1961
1962   data = g_slice_new0 (EmpathyAdiumData);
1963   data->ref_count = 1;
1964   data->path = g_strdup (path);
1965   data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1966     G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1967   data->info = g_hash_table_ref (info);
1968   data->version = adium_info_get_version (info);
1969   data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1970   data->date_format_cache = g_hash_table_new_full (g_str_hash,
1971     g_str_equal, g_free, g_free);
1972
1973   DEBUG ("Loading theme at %s", path);
1974
1975 #define LOAD(path, var) \
1976     tmp = g_build_filename (data->basedir, path, NULL); \
1977     g_file_get_contents (tmp, &var, NULL, NULL); \
1978     g_free (tmp); \
1979
1980 #define LOAD_CONST(path, var) \
1981   { \
1982     gchar *content; \
1983     LOAD (path, content); \
1984     if (content != NULL) { \
1985       g_ptr_array_add (data->strings_to_free, content); \
1986     } \
1987     var = content; \
1988   }
1989
1990   /* Load html files */
1991   LOAD_CONST ("Content.html", data->content_html);
1992   LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1993   LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1994   LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1995   LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1996   LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1997   LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1998   LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1999   LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2000   LOAD_CONST ("Status.html", data->status_html);
2001   LOAD ("Template.html", template_html);
2002   LOAD ("Footer.html", footer_html);
2003
2004 #undef LOAD_CONST
2005 #undef LOAD
2006
2007   /* HTML fallbacks: If we have at least content OR in_content, then
2008    * everything else gets a fallback */
2009
2010 #define FALLBACK(html, fallback) \
2011   if (html == NULL) { \
2012     html = fallback; \
2013   }
2014
2015   /* in_nextcontent -> in_content -> content */
2016   FALLBACK (data->in_content_html,      data->content_html);
2017   FALLBACK (data->in_nextcontent_html,  data->in_content_html);
2018
2019   /* context -> content */
2020   FALLBACK (data->in_context_html,      data->in_content_html);
2021   FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
2022   FALLBACK (data->out_context_html,     data->out_content_html);
2023   FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2024
2025   /* out -> in */
2026   FALLBACK (data->out_content_html,     data->in_content_html);
2027   FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2028   FALLBACK (data->out_context_html,     data->in_context_html);
2029   FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2030
2031   /* status -> in_content */
2032   FALLBACK (data->status_html,          data->in_content_html);
2033
2034 #undef FALLBACK
2035
2036   /* template -> empathy's template */
2037   data->custom_template = (template_html != NULL);
2038   if (template_html == NULL)
2039     {
2040       GError *error = NULL;
2041
2042       tmp = empathy_file_lookup ("Template.html", "data");
2043
2044       if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2045         g_warning ("couldn't load Empathy's default theme "
2046           "template: %s", error->message);
2047         g_return_val_if_reached (data);
2048       }
2049
2050       g_free (tmp);
2051     }
2052
2053   /* Default avatar */
2054   tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2055   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2056     {
2057       data->default_incoming_avatar_filename = tmp;
2058     }
2059   else
2060     {
2061       g_free (tmp);
2062     }
2063
2064   tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2065   if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2066     {
2067       data->default_outgoing_avatar_filename = tmp;
2068     }
2069   else
2070     {
2071       g_free (tmp);
2072     }
2073
2074   /* Old custom templates had only 4 parameters.
2075    * New templates have 5 parameters */
2076   if (data->version <= 2 && data->custom_template)
2077     {
2078       tmp = string_with_format (template_html,
2079         data->basedir,
2080         "%@", /* Leave variant unset */
2081         "", /* The header */
2082         footer_html ? footer_html : "",
2083         NULL);
2084     }
2085   else
2086     {
2087       tmp = string_with_format (template_html,
2088         data->basedir,
2089         data->version <= 2 ? "" : "@import url( \"main.css\" );",
2090         "%@", /* Leave variant unset */
2091         "", /* The header */
2092         footer_html ? footer_html : "",
2093         NULL);
2094     }
2095   g_ptr_array_add (data->strings_to_free, tmp);
2096   data->template_html = tmp;
2097
2098   g_free (template_html);
2099   g_free (footer_html);
2100
2101   return data;
2102 }
2103
2104 EmpathyAdiumData *
2105 empathy_adium_data_new (const gchar *path)
2106 {
2107   EmpathyAdiumData *data;
2108   GHashTable *info;
2109
2110   info = empathy_adium_info_new (path);
2111   data = empathy_adium_data_new_with_info (path, info);
2112   g_hash_table_unref (info);
2113
2114   return data;
2115 }
2116
2117 EmpathyAdiumData *
2118 empathy_adium_data_ref (EmpathyAdiumData *data)
2119 {
2120   g_return_val_if_fail (data != NULL, NULL);
2121
2122   g_atomic_int_inc (&data->ref_count);
2123
2124   return data;
2125 }
2126
2127 void
2128 empathy_adium_data_unref (EmpathyAdiumData *data)
2129 {
2130   g_return_if_fail (data != NULL);
2131
2132   if (g_atomic_int_dec_and_test (&data->ref_count)) {
2133     g_free (data->path);
2134     g_free (data->basedir);
2135     g_free (data->default_avatar_filename);
2136     g_free (data->default_incoming_avatar_filename);
2137     g_free (data->default_outgoing_avatar_filename);
2138     g_hash_table_unref (data->info);
2139     g_ptr_array_unref (data->strings_to_free);
2140     tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2141
2142     g_slice_free (EmpathyAdiumData, data);
2143   }
2144 }
2145
2146 GHashTable *
2147 empathy_adium_data_get_info (EmpathyAdiumData *data)
2148 {
2149   g_return_val_if_fail (data != NULL, NULL);
2150
2151   return data->info;
2152 }
2153
2154 const gchar *
2155 empathy_adium_data_get_path (EmpathyAdiumData *data)
2156 {
2157   g_return_val_if_fail (data != NULL, NULL);
2158
2159   return data->path;
2160 }