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