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