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