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