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