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