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