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