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