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