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