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