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