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