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