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