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