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