]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
Adium: Fix wrong html fallbacks
[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.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         GtkWidget            *inspector_window;
65         GSettings            *gsettings_chat;
66         gboolean              has_focus;
67         gboolean              has_unread_message;
68         gboolean              allow_scrolling;
69 } EmpathyThemeAdiumPriv;
70
71 struct _EmpathyAdiumData {
72         gint  ref_count;
73         gchar *path;
74         gchar *basedir;
75         gchar *default_avatar_filename;
76         gchar *default_incoming_avatar_filename;
77         gchar *default_outgoing_avatar_filename;
78         GHashTable *info;
79         guint version;
80         gboolean custom_template;
81
82         /* HTML bits */
83         const gchar *template_html;
84         const gchar *content_html;
85         const gchar *in_content_html;
86         const gchar *in_context_html;
87         const gchar *in_nextcontent_html;
88         const gchar *in_nextcontext_html;
89         const gchar *out_content_html;
90         const gchar *out_context_html;
91         const gchar *out_nextcontent_html;
92         const gchar *out_nextcontext_html;
93         const gchar *status_html;
94
95         /* Above html strings are pointers to strings stored in this array.
96          * We do this because of fallbacks, some htmls could be pointing the
97          * same string. */
98         GPtrArray *strings_to_free;
99 };
100
101 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
102
103 enum {
104         PROP_0,
105         PROP_ADIUM_DATA,
106 };
107
108 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
109                          WEBKIT_TYPE_WEB_VIEW,
110                          G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
111                                                 theme_adium_iface_init));
112
113 static void
114 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
115 {
116         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
117         WebKitWebView  *web_view = WEBKIT_WEB_VIEW (theme);
118         gboolean        enable_webkit_developer_tools;
119
120         enable_webkit_developer_tools = g_settings_get_boolean (
121                         priv->gsettings_chat,
122                         EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
123
124         g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
125                       "enable-developer-extras",
126                       enable_webkit_developer_tools,
127                       NULL);
128 }
129
130 static void
131 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings   *gsettings,
132                                                      const gchar *key,
133                                                      gpointer     user_data)
134 {
135         EmpathyThemeAdium  *theme = user_data;
136
137         theme_adium_update_enable_webkit_developer_tools (theme);
138 }
139
140 static gboolean
141 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView             *view,
142                                                      WebKitWebFrame            *web_frame,
143                                                      WebKitNetworkRequest      *request,
144                                                      WebKitWebNavigationAction *action,
145                                                      WebKitWebPolicyDecision   *decision,
146                                                      gpointer                   data)
147 {
148         const gchar *uri;
149
150         /* Only call url_show on clicks */
151         if (webkit_web_navigation_action_get_reason (action) !=
152             WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
153                 webkit_web_policy_decision_use (decision);
154                 return TRUE;
155         }
156
157         uri = webkit_network_request_get_uri (request);
158         empathy_url_show (GTK_WIDGET (view), uri);
159
160         webkit_web_policy_decision_ignore (decision);
161         return TRUE;
162 }
163
164 static void
165 theme_adium_copy_address_cb (GtkMenuItem *menuitem,
166                              gpointer     user_data)
167 {
168         WebKitHitTestResult   *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
169         gchar                 *uri;
170         GtkClipboard          *clipboard;
171
172         g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
173
174         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
175         gtk_clipboard_set_text (clipboard, uri, -1);
176
177         clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
178         gtk_clipboard_set_text (clipboard, uri, -1);
179
180         g_free (uri);
181 }
182
183 static void
184 theme_adium_open_address_cb (GtkMenuItem *menuitem,
185                              gpointer     user_data)
186 {
187         WebKitHitTestResult   *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
188         gchar                 *uri;
189
190         g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
191
192         empathy_url_show (GTK_WIDGET (menuitem), uri);
193
194         g_free (uri);
195 }
196
197 /* Replace each %@ in format with string passed in args */
198 static gchar *
199 string_with_format (const gchar *format,
200                     const gchar *first_string,
201                     ...)
202 {
203         va_list args;
204         const gchar *str;
205         GString *result;
206
207         va_start (args, first_string);
208         result = g_string_sized_new (strlen (format));
209         for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
210                 const gchar *next;
211
212                 next = strstr (format, "%@");
213                 if (next == NULL) {
214                         break;
215                 }
216
217                 g_string_append_len (result, format, next - format);
218                 g_string_append (result, str);
219                 format = next + 2;
220         }
221         g_string_append (result, format);
222         va_end (args);
223
224         return g_string_free (result, FALSE);
225 }
226
227 static void
228 theme_adium_match_newline (const gchar *text,
229                            gssize len,
230                            EmpathyStringReplace replace_func,
231                            EmpathyStringParser *sub_parsers,
232                            gpointer user_data)
233 {
234         GString *string = user_data;
235         gint i;
236         gint prev = 0;
237
238         if (len < 0) {
239                 len = G_MAXSSIZE;
240         }
241
242         /* Replace \n by <br/> */
243         for (i = 0; i < len && text[i] != '\0'; i++) {
244                 if (text[i] == '\n') {
245                         empathy_string_parser_substr (text + prev,
246                                                       i - prev, sub_parsers,
247                                                       user_data);
248                         g_string_append (string, "<br/>");
249                         prev = i + 1;
250                 }
251         }
252         empathy_string_parser_substr (text + prev, i - prev,
253                                       sub_parsers, user_data);
254 }
255
256 static void
257 theme_adium_replace_smiley (const gchar *text,
258                             gssize len,
259                             gpointer match_data,
260                             gpointer user_data)
261 {
262         EmpathySmileyHit *hit = match_data;
263         GString *string = user_data;
264
265         /* Replace smiley by a <img/> tag */
266         g_string_append_printf (string,
267                                 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
268                                 hit->path, (int)len, text, (int)len, text);
269 }
270
271 static EmpathyStringParser string_parsers[] = {
272         {empathy_string_match_link, empathy_string_replace_link},
273         {theme_adium_match_newline, NULL},
274         {empathy_string_match_all, empathy_string_replace_escaped},
275         {NULL, NULL}
276 };
277
278 static EmpathyStringParser string_parsers_with_smiley[] = {
279         {empathy_string_match_link, empathy_string_replace_link},
280         {empathy_string_match_smiley, theme_adium_replace_smiley},
281         {theme_adium_match_newline, NULL},
282         {empathy_string_match_all, empathy_string_replace_escaped},
283         {NULL, NULL}
284 };
285
286 static gchar *
287 theme_adium_parse_body (EmpathyThemeAdium *self,
288         const gchar *text)
289 {
290         EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
291         EmpathyStringParser *parsers;
292         GString *string;
293
294         /* Check if we have to parse smileys */
295         if (g_settings_get_boolean (priv->gsettings_chat,
296           EMPATHY_PREFS_CHAT_SHOW_SMILEYS))
297                 parsers = string_parsers_with_smiley;
298         else
299                 parsers = string_parsers;
300
301         /* Parse text and construct string with links and smileys replaced
302          * by html tags. Also escape text to make sure html code is
303          * displayed verbatim. */
304         string = g_string_sized_new (strlen (text));
305         empathy_string_parser_substr (text, -1, parsers, string);
306
307         /* Wrap body in order to make tabs and multiple spaces displayed
308          * properly. See bug #625745. */
309         g_string_prepend (string, "<div style=\"display: inline; "
310                                                "white-space: pre-wrap\"'>");
311         g_string_append (string, "</div>");
312
313         return g_string_free (string, FALSE);
314 }
315
316 static void
317 escape_and_append_len (GString *string, const gchar *str, gint len)
318 {
319         while (str != NULL && *str != '\0' && len != 0) {
320                 switch (*str) {
321                 case '\\':
322                         /* \ becomes \\ */
323                         g_string_append (string, "\\\\");
324                         break;
325                 case '\"':
326                         /* " becomes \" */
327                         g_string_append (string, "\\\"");
328                         break;
329                 case '\n':
330                         /* Remove end of lines */
331                         break;
332                 default:
333                         g_string_append_c (string, *str);
334                 }
335
336                 str++;
337                 len--;
338         }
339 }
340
341 /* If *str starts with match, returns TRUE and move pointer to the end */
342 static gboolean
343 theme_adium_match (const gchar **str,
344                    const gchar *match)
345 {
346         gint len;
347
348         len = strlen (match);
349         if (strncmp (*str, match, len) == 0) {
350                 *str += len - 1;
351                 return TRUE;
352         }
353
354         return FALSE;
355 }
356
357 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
358 static gboolean
359 theme_adium_match_with_format (const gchar **str,
360                                const gchar *match,
361                                gchar **format)
362 {
363         const gchar *cur = *str;
364         const gchar *end;
365
366         if (!theme_adium_match (&cur, match)) {
367                 return FALSE;
368         }
369         cur++;
370
371         end = strstr (cur, "}%");
372         if (!end) {
373                 return FALSE;
374         }
375
376         *format = g_strndup (cur , end - cur);
377         *str = end + 1;
378         return TRUE;
379 }
380
381 /* List of colors used by %senderColor%. Copied from
382  * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
383  */
384 static gchar *colors[] = {
385         "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
386         "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
387         "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
388         "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
389         "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
390         "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
391         "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
392         "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
393         "lightblue", "lightcoral",
394         "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
395         "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
396         "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
397         "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
398         "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
399         "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
400         "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
401         "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
402         "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
403         "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
404         "yellowgreen",
405 };
406
407 static void
408 theme_adium_append_html (EmpathyThemeAdium *theme,
409                          const gchar       *func,
410                          const gchar       *html,
411                          const gchar       *message,
412                          const gchar       *avatar_filename,
413                          const gchar       *name,
414                          const gchar       *contact_id,
415                          const gchar       *service_name,
416                          const gchar       *message_classes,
417                          gint64             timestamp,
418                          gboolean           is_backlog)
419 {
420         GString     *string;
421         const gchar *cur = NULL;
422         gchar       *script;
423
424         /* Make some search-and-replace in the html code */
425         string = g_string_sized_new (strlen (html) + strlen (message));
426         g_string_append_printf (string, "%s(\"", func);
427         for (cur = html; *cur != '\0'; cur++) {
428                 const gchar *replace = NULL;
429                 gchar       *dup_replace = NULL;
430                 gchar       *format = NULL;
431
432                 /* Those are all well known keywords that needs replacement in
433                  * html files. Please keep them in the same order than the adium
434                  * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
435                 if (theme_adium_match (&cur, "%userIconPath%")) {
436                         replace = avatar_filename;
437                 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
438                         replace = contact_id;
439                 } else if (theme_adium_match (&cur, "%sender%")) {
440                         replace = name;
441                 } else if (theme_adium_match (&cur, "%senderColor%")) {
442                         /* A color derived from the user's name.
443                          * FIXME: If a colon separated list of HTML colors is at
444                          * Incoming/SenderColors.txt it will be used instead of
445                          * the default colors.
446                          */
447                         if (contact_id != NULL) {
448                                 guint hash = g_str_hash (contact_id);
449                                 replace = colors[hash % G_N_ELEMENTS (colors)];
450                         }
451                 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
452                         /* FIXME: The path to the status icon of the sender
453                          * (available, away, etc...)
454                          */
455                 } else if (theme_adium_match (&cur, "%messageDirection%")) {
456                         /* FIXME: The text direction of the message
457                          * (either rtl or ltr)
458                          */
459                 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
460                         /* FIXME: The serverside (remotely set) name of the
461                          * sender, such as an MSN display name.
462                          *
463                          *  We don't have access to that yet so we use
464                          * local alias instead.
465                          */
466                         replace = name;
467                 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
468                         /* FIXME: This keyword is used to represent the
469                          * highlight background color. "X" is the opacity of the
470                          * background, ranges from 0 to 1 and can be any decimal
471                          * between.
472                          */
473                 } else if (theme_adium_match (&cur, "%message%")) {
474                         replace = message;
475                 } else if (theme_adium_match (&cur, "%time%") ||
476                            theme_adium_match_with_format (&cur, "%time{", &format)) {
477                         /* FIXME: format is not exactly strftime.
478                          * See NSDateFormatter spec:
479                          * http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/
480                          */
481                         if (is_backlog) {
482                                 dup_replace = empathy_time_to_string_local (timestamp,
483                                         format ? format : EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
484                         } else {
485                                 dup_replace = empathy_time_to_string_local (timestamp,
486                                         format ? format : EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
487                         }
488                         replace = dup_replace;
489                 } else if (theme_adium_match (&cur, "%shortTime%")) {
490                         dup_replace = empathy_time_to_string_local (timestamp,
491                                 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
492                         replace = dup_replace;
493                 } else if (theme_adium_match (&cur, "%service%")) {
494                         replace = service_name;
495                 } else if (theme_adium_match (&cur, "%variant%")) {
496                         /* FIXME: The name of the active message style variant,
497                          * with all spaces replaced with an underscore.
498                          * A variant named "Alternating Messages - Blue Red"
499                          * will become "Alternating_Messages_-_Blue_Red".
500                          */
501                 } else if (theme_adium_match (&cur, "%userIcons%")) {
502                         /* FIXME: mus t be "hideIcons" if use preference is set
503                          * to hide avatars */
504                         replace = "showIcons";
505                 } else if (theme_adium_match (&cur, "%messageClasses%")) {
506                         replace = message_classes;
507                 } else if (theme_adium_match (&cur, "%status%")) {
508                         /* FIXME: A description of the status event. This is
509                          * neither in the user's local language nor expected to
510                          * be displayed; it may be useful to use a different div
511                          * class to present different types of status messages.
512                          * The following is a list of some of the more important
513                          * status messages; your message style should be able to
514                          * handle being shown a status message not in this list,
515                          * as even at present the list is incomplete and is
516                          * certain to become out of date in the future:
517                          *      online
518                          *      offline
519                          *      away
520                          *      away_message
521                          *      return_away
522                          *      idle
523                          *      return_idle
524                          *      date_separator
525                          *      contact_joined (group chats)
526                          *      contact_left
527                          *      error
528                          *      timed_out
529                          *      encryption (all OTR messages use this status)
530                          *      purple (all IRC topic and join/part messages use this status)
531                          *      fileTransferStarted
532                          *      fileTransferCompleted
533                          */
534                 } else {
535                         escape_and_append_len (string, cur, 1);
536                         continue;
537                 }
538
539                 /* Here we have a replacement to make */
540                 escape_and_append_len (string, replace, -1);
541
542                 g_free (dup_replace);
543                 g_free (format);
544         }
545         g_string_append (string, "\")");
546
547         script = g_string_free (string, FALSE);
548         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
549         g_free (script);
550 }
551
552 static void
553 theme_adium_append_event_escaped (EmpathyChatView *view,
554                                   const gchar     *escaped)
555 {
556         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
557         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
558
559         theme_adium_append_html (theme, "appendMessage",
560                                  priv->data->status_html, escaped, NULL, NULL, NULL,
561                                  NULL, "event",
562                                  empathy_time_get_current (), FALSE);
563
564         /* There is no last contact */
565         if (priv->last_contact) {
566                 g_object_unref (priv->last_contact);
567                 priv->last_contact = NULL;
568         }
569 }
570
571 static void
572 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme)
573 {
574         WebKitDOMDocument *dom;
575         WebKitDOMNodeList *nodes;
576         guint i;
577         GError *error = NULL;
578
579         dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
580         if (dom == NULL) {
581                 return;
582         }
583
584         /* Get all nodes with focus class */
585         nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
586         if (nodes == NULL) {
587                 DEBUG ("Error getting focus nodes: %s",
588                         error ? error->message : "No error");
589                 g_clear_error (&error);
590                 return;
591         }
592
593         /* Remove focus and firstFocus class */
594         for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
595                 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
596                 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
597                 gchar *class_name;
598                 gchar **classes, **iter;
599                 GString *new_class_name;
600                 gboolean first = TRUE;
601
602                 if (element == NULL) {
603                         continue;
604                 }
605
606                 class_name = webkit_dom_html_element_get_class_name (element);
607                 classes = g_strsplit (class_name, " ", -1);
608                 new_class_name = g_string_sized_new (strlen (class_name));
609                 for (iter = classes; *iter != NULL; iter++) {
610                         if (tp_strdiff (*iter, "focus") &&
611                             tp_strdiff (*iter, "firstFocus")) {
612                                 if (!first) {
613                                         g_string_append_c (new_class_name, ' ');
614                                 }
615                                 g_string_append (new_class_name, *iter);
616                                 first = FALSE;
617                         }
618                 }
619
620                 webkit_dom_html_element_set_class_name (element, new_class_name->str);
621
622                 g_free (class_name);
623                 g_strfreev (classes);
624                 g_string_free (new_class_name, TRUE);
625         }
626 }
627
628 static void
629 theme_adium_append_message (EmpathyChatView *view,
630                             EmpathyMessage  *msg)
631 {
632         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
633         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
634         EmpathyContact        *sender;
635         TpAccount             *account;
636         gchar                 *body_escaped;
637         const gchar           *body;
638         const gchar           *name;
639         const gchar           *contact_id;
640         EmpathyAvatar         *avatar;
641         const gchar           *avatar_filename = NULL;
642         gint64                 timestamp;
643         const gchar           *html = NULL;
644         const gchar           *func;
645         const gchar           *service_name;
646         GString               *message_classes = NULL;
647         gboolean               is_backlog;
648         gboolean               consecutive;
649         gboolean               action;
650
651         if (priv->pages_loading != 0) {
652                 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
653                 g_value_set_object (value, msg);
654                 g_queue_push_tail (&priv->message_queue, value);
655                 return;
656         }
657
658         /* Get information */
659         sender = empathy_message_get_sender (msg);
660         account = empathy_contact_get_account (sender);
661         service_name = empathy_protocol_name_to_display_name
662                 (tp_account_get_protocol (account));
663         if (service_name == NULL)
664                 service_name = tp_account_get_protocol (account);
665         timestamp = empathy_message_get_timestamp (msg);
666         body = empathy_message_get_body (msg);
667         body_escaped = theme_adium_parse_body (theme, body);
668         name = empathy_contact_get_alias (sender);
669         contact_id = empathy_contact_get_id (sender);
670         action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
671
672         /* If this is a /me probably */
673         if (action) {
674                 gchar *str;
675
676                 if (priv->data->version >= 4 || !priv->data->custom_template) {
677                         str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
678                                                "<span class='actionMessageBody'>%s</span>",
679                                                name, body_escaped);
680                 } else {
681                         str = g_strdup_printf ("*%s*", body_escaped);
682                 }
683                 g_free (body_escaped);
684                 body_escaped = str;
685         }
686
687         /* Get the avatar filename, or a fallback */
688         avatar = empathy_contact_get_avatar (sender);
689         if (avatar) {
690                 avatar_filename = avatar->filename;
691         }
692         if (!avatar_filename) {
693                 if (empathy_contact_is_user (sender)) {
694                         avatar_filename = priv->data->default_outgoing_avatar_filename;
695                 } else {
696                         avatar_filename = priv->data->default_incoming_avatar_filename;
697                 }
698                 if (!avatar_filename) {
699                         if (!priv->data->default_avatar_filename) {
700                                 priv->data->default_avatar_filename =
701                                         empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
702                                                                          GTK_ICON_SIZE_DIALOG);
703                         }
704                         avatar_filename = priv->data->default_avatar_filename;
705                 }
706         }
707
708         /* We want to join this message with the last one if
709          * - senders are the same contact,
710          * - last message was recieved recently,
711          * - last message and this message both are/aren't backlog, and
712          * - DisableCombineConsecutive is not set in theme's settings */
713         is_backlog = empathy_message_is_backlog (msg);
714         consecutive = empathy_contact_equal (priv->last_contact, sender) &&
715                 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
716                 (is_backlog == priv->last_is_backlog) &&
717                 !tp_asv_get_boolean (priv->data->info,
718                                      "DisableCombineConsecutive", NULL);
719
720         /* Define message classes */
721         message_classes = g_string_new ("message");
722         if (!priv->has_focus && !is_backlog) {
723                 if (!priv->has_unread_message) {
724                         /* This is the first message we receive since we lost
725                          * focus; remove previous unread marks. */
726                         theme_adium_remove_focus_marks (theme);
727
728                         g_string_append (message_classes, " firstFocus");
729                         priv->has_unread_message = TRUE;
730                 }
731                 g_string_append (message_classes, " focus");
732         }
733         if (is_backlog) {
734                 g_string_append (message_classes, " history");
735         }
736         if (consecutive) {
737                 g_string_append (message_classes, " consecutive");
738         }
739         if (empathy_contact_is_user (sender)) {
740                 g_string_append (message_classes, " outgoing");
741         } else {
742                 g_string_append (message_classes, " incoming");
743         }
744         if (empathy_message_should_highlight (msg)) {
745                 g_string_append (message_classes, " mention");
746         }
747         if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
748                 g_string_append (message_classes, " autoreply");
749         }
750         if (action) {
751                 g_string_append (message_classes, " action");
752         }
753         /* FIXME: other classes:
754          * status - the message is a status change
755          * event - the message is a notification of something happening
756          *         (for example, encryption being turned on)
757          * %status% - See %status% in theme_adium_append_html ()
758          */
759
760         /* Define javascript function to use */
761         if (consecutive) {
762                 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
763         } else {
764                 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
765         }
766
767         if (empathy_contact_is_user (sender)) {
768                 /* out */
769                 if (is_backlog) {
770                         /* context */
771                         html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
772                 } else {
773                         /* content */
774                         html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
775                 }
776         } else {
777                 /* in */
778                 if (is_backlog) {
779                         /* context */
780                         html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
781                 } else {
782                         /* content */
783                         html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
784                 }
785         }
786
787         theme_adium_append_html (theme, func, html, body_escaped,
788                                  avatar_filename, name, contact_id,
789                                  service_name, message_classes->str,
790                                  timestamp, is_backlog);
791
792         /* Keep the sender of the last displayed message */
793         if (priv->last_contact) {
794                 g_object_unref (priv->last_contact);
795         }
796         priv->last_contact = g_object_ref (sender);
797         priv->last_timestamp = timestamp;
798         priv->last_is_backlog = is_backlog;
799
800         g_free (body_escaped);
801         g_string_free (message_classes, TRUE);
802 }
803
804 static void
805 theme_adium_append_event (EmpathyChatView *view,
806                           const gchar     *str)
807 {
808         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
809         gchar *str_escaped;
810
811         if (priv->pages_loading != 0) {
812                 g_queue_push_tail (&priv->message_queue,
813                         tp_g_value_slice_new_string (str));
814                 return;
815         }
816
817         str_escaped = g_markup_escape_text (str, -1);
818         theme_adium_append_event_escaped (view, str_escaped);
819         g_free (str_escaped);
820 }
821
822 static void
823 theme_adium_scroll (EmpathyChatView *view,
824                     gboolean         allow_scrolling)
825 {
826         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
827
828         priv->allow_scrolling = allow_scrolling;
829         if (allow_scrolling) {
830                 empathy_chat_view_scroll_down (view);
831         }
832 }
833
834 static void
835 theme_adium_scroll_down (EmpathyChatView *view)
836 {
837         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
838 }
839
840 static gboolean
841 theme_adium_get_has_selection (EmpathyChatView *view)
842 {
843         return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
844 }
845
846 static void
847 theme_adium_clear (EmpathyChatView *view)
848 {
849         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
850         gchar *basedir_uri;
851
852         priv->pages_loading++;
853         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
854         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view),
855                                           priv->data->template_html,
856                                           basedir_uri);
857         g_free (basedir_uri);
858
859         /* Clear last contact to avoid trying to add a 'joined'
860          * message when we don't have an insertion point. */
861         if (priv->last_contact) {
862                 g_object_unref (priv->last_contact);
863                 priv->last_contact = NULL;
864         }
865 }
866
867 static gboolean
868 theme_adium_find_previous (EmpathyChatView *view,
869                            const gchar     *search_criteria,
870                            gboolean         new_search,
871                            gboolean         match_case)
872 {
873         /* FIXME: Doesn't respect new_search */
874         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
875                                             search_criteria, match_case,
876                                             FALSE, TRUE);
877 }
878
879 static gboolean
880 theme_adium_find_next (EmpathyChatView *view,
881                        const gchar     *search_criteria,
882                        gboolean         new_search,
883                        gboolean         match_case)
884 {
885         /* FIXME: Doesn't respect new_search */
886         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
887                                             search_criteria, match_case,
888                                             TRUE, TRUE);
889 }
890
891 static void
892 theme_adium_find_abilities (EmpathyChatView *view,
893                             const gchar    *search_criteria,
894                             gboolean        match_case,
895                             gboolean       *can_do_previous,
896                             gboolean       *can_do_next)
897 {
898         /* FIXME: Does webkit provide an API for that? We have wrap=true in
899          * find_next and find_previous to work around this problem. */
900         if (can_do_previous)
901                 *can_do_previous = TRUE;
902         if (can_do_next)
903                 *can_do_next = TRUE;
904 }
905
906 static void
907 theme_adium_highlight (EmpathyChatView *view,
908                        const gchar     *text,
909                        gboolean         match_case)
910 {
911         webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
912         webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
913                                            text, match_case, 0);
914         webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
915                                                     TRUE);
916 }
917
918 static void
919 theme_adium_copy_clipboard (EmpathyChatView *view)
920 {
921         webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
922 }
923
924 static void
925 theme_adium_focus_toggled (EmpathyChatView *view,
926                            gboolean         has_focus)
927 {
928         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
929
930         priv->has_focus = has_focus;
931         if (priv->has_focus) {
932                 priv->has_unread_message = FALSE;
933         }
934 }
935
936 static void
937 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
938 {
939         WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
940
941         g_object_unref (hit_test_result);
942 }
943
944 static void
945 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
946 {
947         WebKitWebView              *view = WEBKIT_WEB_VIEW (theme);
948         WebKitHitTestResult        *hit_test_result;
949         WebKitHitTestResultContext  context;
950         GtkWidget                  *menu;
951         GtkWidget                  *item;
952
953         hit_test_result = webkit_web_view_get_hit_test_result (view, event);
954         g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
955
956         /* The menu */
957         menu = empathy_context_menu_new (GTK_WIDGET (view));
958
959         /* Select all item */
960         item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
961         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
962
963         g_signal_connect_swapped (item, "activate",
964                                   G_CALLBACK (webkit_web_view_select_all),
965                                   view);
966
967         /* Copy menu item */
968         if (webkit_web_view_can_copy_clipboard (view)) {
969                 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
970                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
971
972                 g_signal_connect_swapped (item, "activate",
973                                           G_CALLBACK (webkit_web_view_copy_clipboard),
974                                           view);
975         }
976
977         /* Clear menu item */
978         item = gtk_separator_menu_item_new ();
979         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
980
981         item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
982         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
983
984         g_signal_connect_swapped (item, "activate",
985                                   G_CALLBACK (empathy_chat_view_clear),
986                                   view);
987
988         /* We will only add the following menu items if we are
989          * right-clicking a link */
990         if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
991                 /* Separator */
992                 item = gtk_separator_menu_item_new ();
993                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
994
995                 /* Copy Link Address menu item */
996                 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
997                 g_signal_connect (item, "activate",
998                                   G_CALLBACK (theme_adium_copy_address_cb),
999                                   hit_test_result);
1000                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1001
1002                 /* Open Link menu item */
1003                 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1004                 g_signal_connect (item, "activate",
1005                                   G_CALLBACK (theme_adium_open_address_cb),
1006                                   hit_test_result);
1007                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1008         }
1009
1010         g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1011                           G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1012                           hit_test_result);
1013
1014         /* Display the menu */
1015         gtk_widget_show_all (menu);
1016         gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1017                         event->button, event->time);
1018 }
1019
1020 static gboolean
1021 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1022 {
1023         if (event->button == 3) {
1024                 gboolean developer_tools_enabled;
1025
1026                 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1027                               "enable-developer-extras", &developer_tools_enabled, NULL);
1028
1029                 /* We currently have no way to add an inspector menu
1030                  * item ourselves, so we disable our customized menu
1031                  * if the developer extras are enabled. */
1032                 if (!developer_tools_enabled) {
1033                         theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1034                         return TRUE;
1035                 }
1036         }
1037
1038         return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1039 }
1040
1041 static void
1042 theme_adium_iface_init (EmpathyChatViewIface *iface)
1043 {
1044         iface->append_message = theme_adium_append_message;
1045         iface->append_event = theme_adium_append_event;
1046         iface->scroll = theme_adium_scroll;
1047         iface->scroll_down = theme_adium_scroll_down;
1048         iface->get_has_selection = theme_adium_get_has_selection;
1049         iface->clear = theme_adium_clear;
1050         iface->find_previous = theme_adium_find_previous;
1051         iface->find_next = theme_adium_find_next;
1052         iface->find_abilities = theme_adium_find_abilities;
1053         iface->highlight = theme_adium_highlight;
1054         iface->copy_clipboard = theme_adium_copy_clipboard;
1055         iface->focus_toggled = theme_adium_focus_toggled;
1056 }
1057
1058 static void
1059 theme_adium_load_finished_cb (WebKitWebView  *view,
1060                               WebKitWebFrame *frame,
1061                               gpointer        user_data)
1062 {
1063         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1064         EmpathyChatView       *chat_view = EMPATHY_CHAT_VIEW (view);
1065         GList                 *l;
1066
1067         DEBUG ("Page loaded");
1068         priv->pages_loading--;
1069
1070         if (priv->pages_loading != 0)
1071                 return;
1072
1073         /* Display queued messages */
1074         for (l = priv->message_queue.head; l != NULL; l = l->next) {
1075                 GValue *value = l->data;
1076
1077                 if (G_VALUE_HOLDS_OBJECT (value)) {
1078                         theme_adium_append_message (chat_view,
1079                                 g_value_get_object (value));
1080                 } else {
1081                         theme_adium_append_event (chat_view,
1082                                 g_value_get_string (value));
1083                 }
1084
1085                 tp_g_value_slice_free (value);
1086         }
1087         g_queue_clear (&priv->message_queue);
1088 }
1089
1090 static void
1091 theme_adium_finalize (GObject *object)
1092 {
1093         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1094
1095         empathy_adium_data_unref (priv->data);
1096         g_object_unref (priv->gsettings_chat);
1097
1098         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1099 }
1100
1101 static void
1102 theme_adium_dispose (GObject *object)
1103 {
1104         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1105
1106         if (priv->smiley_manager) {
1107                 g_object_unref (priv->smiley_manager);
1108                 priv->smiley_manager = NULL;
1109         }
1110
1111         if (priv->last_contact) {
1112                 g_object_unref (priv->last_contact);
1113                 priv->last_contact = NULL;
1114         }
1115
1116         if (priv->inspector_window) {
1117                 gtk_widget_destroy (priv->inspector_window);
1118                 priv->inspector_window = NULL;
1119         }
1120
1121         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1122 }
1123
1124 static gboolean
1125 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1126                                       EmpathyThemeAdium  *theme)
1127 {
1128         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1129
1130         if (priv->inspector_window) {
1131                 gtk_widget_show_all (priv->inspector_window);
1132         }
1133
1134         return TRUE;
1135 }
1136
1137 static gboolean
1138 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1139                                        EmpathyThemeAdium  *theme)
1140 {
1141         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1142
1143         if (priv->inspector_window) {
1144                 gtk_widget_hide (priv->inspector_window);
1145         }
1146
1147         return TRUE;
1148 }
1149
1150 static WebKitWebView *
1151 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1152                                  WebKitWebView      *web_view,
1153                                  EmpathyThemeAdium  *theme)
1154 {
1155         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1156         GtkWidget             *scrolled_window;
1157         GtkWidget             *inspector_web_view;
1158
1159         if (!priv->inspector_window) {
1160                 /* Create main window */
1161                 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1162                 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1163                                              800, 600);
1164                 g_signal_connect (priv->inspector_window, "delete-event",
1165                                   G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1166
1167                 /* Pack a scrolled window */
1168                 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1169                 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1170                                                 GTK_POLICY_AUTOMATIC,
1171                                                 GTK_POLICY_AUTOMATIC);
1172                 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1173                                    scrolled_window);
1174                 gtk_widget_show  (scrolled_window);
1175
1176                 /* Pack a webview in the scrolled window. That webview will be
1177                  * used to render the inspector tool.  */
1178                 inspector_web_view = webkit_web_view_new ();
1179                 gtk_container_add (GTK_CONTAINER (scrolled_window),
1180                                    inspector_web_view);
1181                 gtk_widget_show (scrolled_window);
1182
1183                 return WEBKIT_WEB_VIEW (inspector_web_view);
1184         }
1185
1186         return NULL;
1187 }
1188
1189 static PangoFontDescription *
1190 theme_adium_get_default_font (void)
1191 {
1192         GSettings *gsettings;
1193         PangoFontDescription *pango_fd;
1194         gchar *font_family;
1195
1196         gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1197
1198         font_family = g_settings_get_string (gsettings,
1199                      EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1200
1201         if (font_family == NULL)
1202                 return NULL;
1203
1204         pango_fd = pango_font_description_from_string (font_family);
1205         g_free (font_family);
1206         g_object_unref (gsettings);
1207         return pango_fd;
1208 }
1209
1210 static void
1211 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1212                              const gchar *name,
1213                              gint size)
1214 {
1215         g_object_set (w_settings, "default-font-family", name, NULL);
1216         g_object_set (w_settings, "default-font-size", size, NULL);
1217 }
1218
1219 static void
1220 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1221 {
1222         PangoFontDescription *default_font_desc;
1223         GdkScreen *current_screen;
1224         gdouble dpi = 0;
1225         gint pango_font_size = 0;
1226
1227         default_font_desc = theme_adium_get_default_font ();
1228         if (default_font_desc == NULL)
1229                 return ;
1230         pango_font_size = pango_font_description_get_size (default_font_desc)
1231                 / PANGO_SCALE ;
1232         if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1233                 current_screen = gdk_screen_get_default ();
1234                 if (current_screen != NULL) {
1235                         dpi = gdk_screen_get_resolution (current_screen);
1236                 } else {
1237                         dpi = BORING_DPI_DEFAULT;
1238                 }
1239                 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1240         }
1241         theme_adium_set_webkit_font (w_settings,
1242                 pango_font_description_get_family (default_font_desc),
1243                 pango_font_size);
1244         pango_font_description_free (default_font_desc);
1245 }
1246
1247 static void
1248 theme_adium_constructed (GObject *object)
1249 {
1250         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1251         gchar                 *basedir_uri;
1252         const gchar           *font_family = NULL;
1253         gint                   font_size = 0;
1254         WebKitWebView         *webkit_view = WEBKIT_WEB_VIEW (object);
1255         WebKitWebSettings     *webkit_settings;
1256         WebKitWebInspector    *webkit_inspector;
1257
1258         /* Set default settings */
1259         font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1260         font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1261         webkit_settings = webkit_web_view_get_settings (webkit_view);
1262
1263         if (font_family && font_size) {
1264                 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1265         } else {
1266                 theme_adium_set_default_font (webkit_settings);
1267         }
1268
1269         /* Setup webkit inspector */
1270         webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1271         g_signal_connect (webkit_inspector, "inspect-web-view",
1272                           G_CALLBACK (theme_adium_inspect_web_view_cb),
1273                           object);
1274         g_signal_connect (webkit_inspector, "show-window",
1275                           G_CALLBACK (theme_adium_inspector_show_window_cb),
1276                           object);
1277         g_signal_connect (webkit_inspector, "close-window",
1278                           G_CALLBACK (theme_adium_inspector_close_window_cb),
1279                           object);
1280
1281         /* Load template */
1282         priv->pages_loading = 1;
1283
1284         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
1285         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
1286                                           priv->data->template_html,
1287                                           basedir_uri);
1288         g_free (basedir_uri);
1289 }
1290
1291 static void
1292 theme_adium_get_property (GObject    *object,
1293                           guint       param_id,
1294                           GValue     *value,
1295                           GParamSpec *pspec)
1296 {
1297         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1298
1299         switch (param_id) {
1300         case PROP_ADIUM_DATA:
1301                 g_value_set_boxed (value, priv->data);
1302                 break;
1303         default:
1304                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1305                 break;
1306         };
1307 }
1308
1309 static void
1310 theme_adium_set_property (GObject      *object,
1311                           guint         param_id,
1312                           const GValue *value,
1313                           GParamSpec   *pspec)
1314 {
1315         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1316
1317         switch (param_id) {
1318         case PROP_ADIUM_DATA:
1319                 g_assert (priv->data == NULL);
1320                 priv->data = g_value_dup_boxed (value);
1321                 break;
1322         default:
1323                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1324                 break;
1325         };
1326 }
1327
1328 static void
1329 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1330 {
1331         GObjectClass *object_class = G_OBJECT_CLASS (klass);
1332         GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1333
1334         object_class->finalize = theme_adium_finalize;
1335         object_class->dispose = theme_adium_dispose;
1336         object_class->constructed = theme_adium_constructed;
1337         object_class->get_property = theme_adium_get_property;
1338         object_class->set_property = theme_adium_set_property;
1339
1340         widget_class->button_press_event = theme_adium_button_press_event;
1341
1342         g_object_class_install_property (object_class,
1343                                          PROP_ADIUM_DATA,
1344                                          g_param_spec_boxed ("adium-data",
1345                                                              "The theme data",
1346                                                              "Data for the adium theme",
1347                                                               EMPATHY_TYPE_ADIUM_DATA,
1348                                                               G_PARAM_CONSTRUCT_ONLY |
1349                                                               G_PARAM_READWRITE |
1350                                                               G_PARAM_STATIC_STRINGS));
1351
1352         g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1353 }
1354
1355 static void
1356 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1357 {
1358         EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1359                 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1360
1361         theme->priv = priv;
1362
1363         g_queue_init (&priv->message_queue);
1364         priv->allow_scrolling = TRUE;
1365         priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1366
1367         g_signal_connect (theme, "load-finished",
1368                           G_CALLBACK (theme_adium_load_finished_cb),
1369                           NULL);
1370         g_signal_connect (theme, "navigation-policy-decision-requested",
1371                           G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1372                           NULL);
1373
1374         priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1375         g_signal_connect (priv->gsettings_chat,
1376                 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1377                 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1378                 theme);
1379
1380         theme_adium_update_enable_webkit_developer_tools (theme);
1381 }
1382
1383 EmpathyThemeAdium *
1384 empathy_theme_adium_new (EmpathyAdiumData *data)
1385 {
1386         g_return_val_if_fail (data != NULL, NULL);
1387
1388         return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1389                              "adium-data", data,
1390                              NULL);
1391 }
1392
1393 gboolean
1394 empathy_adium_path_is_valid (const gchar *path)
1395 {
1396         gboolean ret;
1397         gchar   *file;
1398
1399         /* The theme is not valid if there is no Info.plist */
1400         file = g_build_filename (path, "Contents", "Info.plist",
1401                                  NULL);
1402         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1403         g_free (file);
1404
1405         if (!ret)
1406                 return FALSE;
1407
1408         /* We ship a default Template.html as fallback if there is any problem
1409          * with the one inside the theme. The only other required file is
1410          * Content.html OR Incoming/Content.html*/
1411         file = g_build_filename (path, "Contents", "Resources", "Content.html",
1412                                  NULL);
1413         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1414         g_free (file);
1415
1416         if (!ret) {
1417                 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1418                                          "Content.html", NULL);
1419                 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1420                 g_free (file);
1421         }
1422
1423         return ret;
1424 }
1425
1426 GHashTable *
1427 empathy_adium_info_new (const gchar *path)
1428 {
1429         gchar *file;
1430         GValue *value;
1431         GHashTable *info = NULL;
1432
1433         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1434
1435         file = g_build_filename (path, "Contents", "Info.plist", NULL);
1436         value = empathy_plist_parse_from_file (file);
1437         g_free (file);
1438
1439         if (value == NULL)
1440                 return NULL;
1441
1442         info = g_value_dup_boxed (value);
1443         tp_g_value_slice_free (value);
1444
1445         /* Insert the theme's path into the hash table,
1446          * keys have to be dupped */
1447         tp_asv_set_string (info, g_strdup ("path"), path);
1448
1449         return info;
1450 }
1451
1452 static guint
1453 adium_info_get_version (GHashTable *info)
1454 {
1455         return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1456 }
1457
1458 static const gchar *
1459 adium_info_get_no_variant_name (GHashTable *info)
1460 {
1461         const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1462         return name ? name : _("Normal");
1463 }
1464
1465 static const gchar *
1466 adium_info_get_default_or_first_variant (GHashTable *info)
1467 {
1468         const gchar *name;
1469         GPtrArray *variants;
1470
1471         name = empathy_adium_info_get_default_variant (info);
1472         if (name != NULL) {
1473                 return name;
1474         }
1475
1476         variants = empathy_adium_info_get_available_variants (info);
1477         g_assert (variants->len > 0);
1478         return g_ptr_array_index (variants, 0);
1479 }
1480
1481 static gchar *
1482 adium_info_dup_path_for_variant (GHashTable *info,
1483                                  const gchar *variant)
1484 {
1485         guint version = adium_info_get_version (info);
1486         const gchar *no_variant = adium_info_get_no_variant_name (info);
1487
1488         if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1489                 return g_strdup ("main.css");
1490         }
1491
1492         return g_strdup_printf ("Variants/%s.css", variant);
1493
1494 }
1495
1496 const gchar *
1497 empathy_adium_info_get_default_variant (GHashTable *info)
1498 {
1499         if (adium_info_get_version (info) <= 2) {
1500                 return adium_info_get_no_variant_name (info);
1501         }
1502
1503         return tp_asv_get_string (info, "DefaultVariant");
1504 }
1505
1506 GPtrArray *
1507 empathy_adium_info_get_available_variants (GHashTable *info)
1508 {
1509         GPtrArray *variants;
1510         const gchar *path;
1511         gchar *dirpath;
1512         GDir *dir;
1513
1514         variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1515         if (variants != NULL) {
1516                 return variants;
1517         }
1518
1519         variants = g_ptr_array_new_with_free_func (g_free);
1520         tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1521                 G_TYPE_PTR_ARRAY, variants);
1522
1523         path = tp_asv_get_string (info, "path");
1524         dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1525         dir = g_dir_open (dirpath, 0, NULL);
1526         if (dir != NULL) {
1527                 const gchar *name;
1528
1529                 for (name = g_dir_read_name (dir);
1530                      name != NULL;
1531                      name = g_dir_read_name (dir)) {
1532                         gchar *display_name;
1533
1534                         if (!g_str_has_suffix (name, ".css")) {
1535                                 continue;
1536                         }
1537
1538                         display_name = g_strdup (name);
1539                         strstr (display_name, ".css")[0] = '\0';
1540                         g_ptr_array_add (variants, display_name);
1541                 }
1542                 g_dir_close (dir);
1543         }
1544         g_free (dirpath);
1545
1546         if (adium_info_get_version (info) <= 2) {
1547                 g_ptr_array_add (variants,
1548                         g_strdup (adium_info_get_no_variant_name (info)));
1549         }
1550
1551         return variants;
1552 }
1553
1554 GType
1555 empathy_adium_data_get_type (void)
1556 {
1557   static GType type_id = 0;
1558
1559   if (!type_id)
1560     {
1561       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1562           (GBoxedCopyFunc) empathy_adium_data_ref,
1563           (GBoxedFreeFunc) empathy_adium_data_unref);
1564     }
1565
1566   return type_id;
1567 }
1568
1569 EmpathyAdiumData  *
1570 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1571 {
1572         EmpathyAdiumData *data;
1573         gchar            *template_html = NULL;
1574         gchar            *footer_html = NULL;
1575         gchar            *variant_path;
1576         gchar            *tmp;
1577
1578         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1579
1580         data = g_slice_new0 (EmpathyAdiumData);
1581         data->ref_count = 1;
1582         data->path = g_strdup (path);
1583         data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1584                 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1585         data->info = g_hash_table_ref (info);
1586         data->version = adium_info_get_version (info);
1587         data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1588
1589         DEBUG ("Loading theme at %s", path);
1590
1591 #define LOAD(path, var) \
1592                 tmp = g_build_filename (data->basedir, path, NULL); \
1593                 g_file_get_contents (tmp, &var, NULL, NULL); \
1594                 g_free (tmp); \
1595
1596 #define LOAD_CONST(path, var) \
1597         { \
1598                 gchar *content; \
1599                 LOAD (path, content); \
1600                 if (content != NULL) { \
1601                         g_ptr_array_add (data->strings_to_free, content); \
1602                 } \
1603                 var = content; \
1604         }
1605
1606         /* Load html files */
1607         LOAD_CONST ("Content.html", data->content_html);
1608         LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1609         LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1610         LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1611         LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1612         LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1613         LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1614         LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1615         LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1616         LOAD_CONST ("Status.html", data->status_html);
1617         LOAD ("Template.html", template_html);
1618         LOAD ("Footer.html", footer_html);
1619
1620 #undef LOAD_CONST
1621 #undef LOAD
1622
1623         /* HTML fallbacks: If we have at least content OR in_content, then
1624          * everything else gets a fallback */
1625
1626 #define FALLBACK(html, fallback) \
1627         if (html == NULL) { \
1628                 html = fallback; \
1629         }
1630
1631         /* in_nextcontent -> in_content -> content */
1632         FALLBACK (data->in_content_html,      data->content_html);
1633         FALLBACK (data->in_nextcontent_html,  data->in_content_html);
1634
1635         /* context -> content */
1636         FALLBACK (data->in_context_html,      data->in_content_html);
1637         FALLBACK (data->in_nextcontext_html,  data->in_nextcontent_html);
1638         FALLBACK (data->out_context_html,     data->out_content_html);
1639         FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1640
1641         /* out -> in */
1642         FALLBACK (data->out_content_html,     data->in_content_html);
1643         FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1644         FALLBACK (data->out_context_html,     data->in_context_html);
1645         FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1646
1647         /* status -> in_content */
1648         FALLBACK (data->status_html,          data->in_content_html);
1649
1650 #undef FALLBACK
1651
1652         /* template -> empathy's template */
1653         data->custom_template = (template_html != NULL);
1654         if (template_html == NULL) {
1655                 tmp = empathy_file_lookup ("Template.html", "data");
1656                 g_file_get_contents (tmp, &template_html, NULL, NULL);
1657                 g_free (tmp);
1658         }
1659
1660         /* Default avatar */
1661         tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1662         if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1663                 data->default_incoming_avatar_filename = tmp;
1664         } else {
1665                 g_free (tmp);
1666         }
1667         tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1668         if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1669                 data->default_outgoing_avatar_filename = tmp;
1670         } else {
1671                 g_free (tmp);
1672         }
1673
1674         variant_path = adium_info_dup_path_for_variant (info,
1675                 adium_info_get_default_or_first_variant (info));
1676
1677         /* Old custom templates had only 4 parameters.
1678          * New templates have 5 parameters */
1679         if (data->version <= 2 && data->custom_template) {
1680                 tmp = string_with_format (template_html,
1681                         data->basedir,
1682                         variant_path,
1683                         "", /* The header */
1684                         footer_html ? footer_html : "",
1685                         NULL);
1686         } else {
1687                 tmp = string_with_format (template_html,
1688                         data->basedir,
1689                         data->version <= 2 ? "" : "@import url( \"main.css\" );",
1690                         variant_path,
1691                         "", /* The header */
1692                         footer_html ? footer_html : "",
1693                         NULL);
1694         }
1695         g_ptr_array_add (data->strings_to_free, tmp);
1696         data->template_html = tmp;
1697
1698         g_free (template_html);
1699         g_free (footer_html);
1700         g_free (variant_path);
1701
1702         return data;
1703 }
1704
1705 EmpathyAdiumData  *
1706 empathy_adium_data_new (const gchar *path)
1707 {
1708         EmpathyAdiumData *data;
1709         GHashTable *info;
1710
1711         info = empathy_adium_info_new (path);
1712         data = empathy_adium_data_new_with_info (path, info);
1713         g_hash_table_unref (info);
1714
1715         return data;
1716 }
1717
1718 EmpathyAdiumData  *
1719 empathy_adium_data_ref (EmpathyAdiumData *data)
1720 {
1721         g_return_val_if_fail (data != NULL, NULL);
1722
1723         g_atomic_int_inc (&data->ref_count);
1724
1725         return data;
1726 }
1727
1728 void
1729 empathy_adium_data_unref (EmpathyAdiumData *data)
1730 {
1731         g_return_if_fail (data != NULL);
1732
1733         if (g_atomic_int_dec_and_test (&data->ref_count)) {
1734                 g_free (data->path);
1735                 g_free (data->basedir);
1736                 g_free (data->default_avatar_filename);
1737                 g_free (data->default_incoming_avatar_filename);
1738                 g_free (data->default_outgoing_avatar_filename);
1739                 g_hash_table_unref (data->info);
1740                 g_ptr_array_unref (data->strings_to_free);
1741
1742                 g_slice_free (EmpathyAdiumData, data);
1743         }
1744 }
1745
1746 GHashTable *
1747 empathy_adium_data_get_info (EmpathyAdiumData *data)
1748 {
1749         g_return_val_if_fail (data != NULL, NULL);
1750
1751         return data->info;
1752 }
1753
1754 const gchar *
1755 empathy_adium_data_get_path (EmpathyAdiumData *data)
1756 {
1757         g_return_val_if_fail (data != NULL, NULL);
1758
1759         return data->path;
1760 }
1761