]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
c334ce3d762013743ce5a2ccdf7a4fc81ee0a873
[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         /* FIXME: other classes:
730          * autoreply - the message is an automatic response, generally due to an
731          *             away status
732          * status - the message is a status change
733          * event - the message is a notification of something happening
734          *         (for example, encryption being turned on)
735          * %status% - See %status% in theme_adium_append_html ()
736          */
737
738         /* Define javascript function to use */
739         if (consecutive) {
740                 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
741         } else {
742                 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
743         }
744
745         html = priv->data->content_html;
746         len = priv->data->content_len;
747
748         /* Fallback to legacy Outgoing */
749         if (html == NULL && empathy_contact_is_user (sender)) {
750                 if (consecutive) {
751                         if (is_backlog) {
752                                 html = priv->data->out_nextcontext_html;
753                                 len = priv->data->out_nextcontext_len;
754                         }
755
756                         /* Not backlog, or fallback if NextContext.html
757                          * is missing */
758                         if (html == NULL) {
759                                 html = priv->data->out_nextcontent_html;
760                                 len = priv->data->out_nextcontent_len;
761                         }
762                 }
763
764                 /* Not consecutive, or fallback if NextContext.html and/or
765                  * NextContent.html are missing */
766                 if (html == NULL) {
767                         if (is_backlog) {
768                                 html = priv->data->out_context_html;
769                                 len = priv->data->out_context_len;
770                         }
771
772                         if (html == NULL) {
773                                 html = priv->data->out_content_html;
774                                 len = priv->data->out_content_len;
775                         }
776                 }
777         }
778
779         /* Incoming, or fallback if outgoing files are missing */
780         if (html == NULL) {
781                 if (consecutive) {
782                         if (is_backlog) {
783                                 html = priv->data->in_nextcontext_html;
784                                 len = priv->data->in_nextcontext_len;
785                         }
786
787                         /* Note backlog, or fallback if NextContext.html
788                          * is missing */
789                         if (html == NULL) {
790                                 html = priv->data->in_nextcontent_html;
791                                 len = priv->data->in_nextcontent_len;
792                         }
793                 }
794
795                 /* Not consecutive, or fallback if NextContext.html and/or
796                  * NextContent.html are missing */
797                 if (html == NULL) {
798                         if (is_backlog) {
799                                 html = priv->data->in_context_html;
800                                 len = priv->data->in_context_len;
801                         }
802
803                         if (html == NULL) {
804                                 html = priv->data->in_content_html;
805                                 len = priv->data->in_content_len;
806                         }
807                 }
808         }
809
810         if (html != NULL) {
811                 theme_adium_append_html (theme, func, html, len, body_escaped,
812                                          avatar_filename, name, contact_id,
813                                          service_name, message_classes->str,
814                                          timestamp, is_backlog);
815         } else {
816                 DEBUG ("Couldn't find HTML file for this message");
817         }
818
819         /* Keep the sender of the last displayed message */
820         if (priv->last_contact) {
821                 g_object_unref (priv->last_contact);
822         }
823         priv->last_contact = g_object_ref (sender);
824         priv->last_timestamp = timestamp;
825         priv->last_is_backlog = is_backlog;
826
827         g_free (body_escaped);
828         g_string_free (message_classes, TRUE);
829 }
830
831 static void
832 theme_adium_append_event (EmpathyChatView *view,
833                           const gchar     *str)
834 {
835         gchar *str_escaped;
836
837         str_escaped = g_markup_escape_text (str, -1);
838         theme_adium_append_event_escaped (view, str_escaped);
839         g_free (str_escaped);
840 }
841
842 static void
843 theme_adium_scroll (EmpathyChatView *view,
844                     gboolean         allow_scrolling)
845 {
846         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
847
848         priv->allow_scrolling = allow_scrolling;
849         if (allow_scrolling) {
850                 empathy_chat_view_scroll_down (view);
851         }
852 }
853
854 static void
855 theme_adium_scroll_down (EmpathyChatView *view)
856 {
857         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
858 }
859
860 static gboolean
861 theme_adium_get_has_selection (EmpathyChatView *view)
862 {
863         return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
864 }
865
866 static void
867 theme_adium_clear (EmpathyChatView *view)
868 {
869         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
870         gchar *basedir_uri;
871
872         priv->pages_loading++;
873         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
874         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view),
875                                           priv->data->template_html,
876                                           basedir_uri);
877         g_free (basedir_uri);
878
879         /* Clear last contact to avoid trying to add a 'joined'
880          * message when we don't have an insertion point. */
881         if (priv->last_contact) {
882                 g_object_unref (priv->last_contact);
883                 priv->last_contact = NULL;
884         }
885 }
886
887 static gboolean
888 theme_adium_find_previous (EmpathyChatView *view,
889                            const gchar     *search_criteria,
890                            gboolean         new_search,
891                            gboolean         match_case)
892 {
893         /* FIXME: Doesn't respect new_search */
894         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
895                                             search_criteria, match_case,
896                                             FALSE, TRUE);
897 }
898
899 static gboolean
900 theme_adium_find_next (EmpathyChatView *view,
901                        const gchar     *search_criteria,
902                        gboolean         new_search,
903                        gboolean         match_case)
904 {
905         /* FIXME: Doesn't respect new_search */
906         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
907                                             search_criteria, match_case,
908                                             TRUE, TRUE);
909 }
910
911 static void
912 theme_adium_find_abilities (EmpathyChatView *view,
913                             const gchar    *search_criteria,
914                             gboolean        match_case,
915                             gboolean       *can_do_previous,
916                             gboolean       *can_do_next)
917 {
918         /* FIXME: Does webkit provide an API for that? We have wrap=true in
919          * find_next and find_previous to work around this problem. */
920         if (can_do_previous)
921                 *can_do_previous = TRUE;
922         if (can_do_next)
923                 *can_do_next = TRUE;
924 }
925
926 static void
927 theme_adium_highlight (EmpathyChatView *view,
928                        const gchar     *text,
929                        gboolean         match_case)
930 {
931         webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
932         webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
933                                            text, match_case, 0);
934         webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
935                                                     TRUE);
936 }
937
938 static void
939 theme_adium_copy_clipboard (EmpathyChatView *view)
940 {
941         webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
942 }
943
944 static void
945 theme_adium_focus_toggled (EmpathyChatView *view,
946                            gboolean         has_focus)
947 {
948         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
949
950         priv->has_focus = has_focus;
951         if (priv->has_focus) {
952                 priv->has_unread_message = FALSE;
953         }
954 }
955
956 static void
957 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
958 {
959         WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
960
961         g_object_unref (hit_test_result);
962 }
963
964 static void
965 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
966 {
967         WebKitWebView              *view = WEBKIT_WEB_VIEW (theme);
968         WebKitHitTestResult        *hit_test_result;
969         WebKitHitTestResultContext  context;
970         GtkWidget                  *menu;
971         GtkWidget                  *item;
972
973         hit_test_result = webkit_web_view_get_hit_test_result (view, event);
974         g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
975
976         /* The menu */
977         menu = empathy_context_menu_new (GTK_WIDGET (view));
978
979         /* Select all item */
980         item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
981         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
982
983         g_signal_connect_swapped (item, "activate",
984                                   G_CALLBACK (webkit_web_view_select_all),
985                                   view);
986
987         /* Copy menu item */
988         if (webkit_web_view_can_copy_clipboard (view)) {
989                 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
990                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
991
992                 g_signal_connect_swapped (item, "activate",
993                                           G_CALLBACK (webkit_web_view_copy_clipboard),
994                                           view);
995         }
996
997         /* Clear menu item */
998         item = gtk_separator_menu_item_new ();
999         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1000
1001         item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1002         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1003
1004         g_signal_connect_swapped (item, "activate",
1005                                   G_CALLBACK (empathy_chat_view_clear),
1006                                   view);
1007
1008         /* We will only add the following menu items if we are
1009          * right-clicking a link */
1010         if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1011                 /* Separator */
1012                 item = gtk_separator_menu_item_new ();
1013                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1014
1015                 /* Copy Link Address menu item */
1016                 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1017                 g_signal_connect (item, "activate",
1018                                   G_CALLBACK (theme_adium_copy_address_cb),
1019                                   hit_test_result);
1020                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1021
1022                 /* Open Link menu item */
1023                 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1024                 g_signal_connect (item, "activate",
1025                                   G_CALLBACK (theme_adium_open_address_cb),
1026                                   hit_test_result);
1027                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1028         }
1029
1030         g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1031                           G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1032                           hit_test_result);
1033
1034         /* Display the menu */
1035         gtk_widget_show_all (menu);
1036         gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1037                         event->button, event->time);
1038 }
1039
1040 static gboolean
1041 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1042 {
1043         if (event->button == 3) {
1044                 gboolean developer_tools_enabled;
1045
1046                 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1047                               "enable-developer-extras", &developer_tools_enabled, NULL);
1048
1049                 /* We currently have no way to add an inspector menu
1050                  * item ourselves, so we disable our customized menu
1051                  * if the developer extras are enabled. */
1052                 if (!developer_tools_enabled) {
1053                         theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1054                         return TRUE;
1055                 }
1056         }
1057
1058         return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1059 }
1060
1061 static void
1062 theme_adium_iface_init (EmpathyChatViewIface *iface)
1063 {
1064         iface->append_message = theme_adium_append_message;
1065         iface->append_event = theme_adium_append_event;
1066         iface->scroll = theme_adium_scroll;
1067         iface->scroll_down = theme_adium_scroll_down;
1068         iface->get_has_selection = theme_adium_get_has_selection;
1069         iface->clear = theme_adium_clear;
1070         iface->find_previous = theme_adium_find_previous;
1071         iface->find_next = theme_adium_find_next;
1072         iface->find_abilities = theme_adium_find_abilities;
1073         iface->highlight = theme_adium_highlight;
1074         iface->copy_clipboard = theme_adium_copy_clipboard;
1075         iface->focus_toggled = theme_adium_focus_toggled;
1076 }
1077
1078 static void
1079 theme_adium_load_finished_cb (WebKitWebView  *view,
1080                               WebKitWebFrame *frame,
1081                               gpointer        user_data)
1082 {
1083         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1084         EmpathyChatView       *chat_view = EMPATHY_CHAT_VIEW (view);
1085
1086         DEBUG ("Page loaded");
1087         priv->pages_loading--;
1088
1089         if (priv->pages_loading != 0)
1090                 return;
1091
1092         /* Display queued messages */
1093         priv->message_queue = g_list_reverse (priv->message_queue);
1094         while (priv->message_queue) {
1095                 EmpathyMessage *message = priv->message_queue->data;
1096
1097                 theme_adium_append_message (chat_view, message);
1098                 priv->message_queue = g_list_remove (priv->message_queue, message);
1099                 g_object_unref (message);
1100         }
1101 }
1102
1103 static void
1104 theme_adium_finalize (GObject *object)
1105 {
1106         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1107
1108         empathy_adium_data_unref (priv->data);
1109         g_object_unref (priv->gsettings_chat);
1110
1111         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1112 }
1113
1114 static void
1115 theme_adium_dispose (GObject *object)
1116 {
1117         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1118
1119         if (priv->smiley_manager) {
1120                 g_object_unref (priv->smiley_manager);
1121                 priv->smiley_manager = NULL;
1122         }
1123
1124         if (priv->last_contact) {
1125                 g_object_unref (priv->last_contact);
1126                 priv->last_contact = NULL;
1127         }
1128
1129         if (priv->inspector_window) {
1130                 gtk_widget_destroy (priv->inspector_window);
1131                 priv->inspector_window = NULL;
1132         }
1133
1134         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1135 }
1136
1137 static gboolean
1138 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1139                                       EmpathyThemeAdium  *theme)
1140 {
1141         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1142
1143         if (priv->inspector_window) {
1144                 gtk_widget_show_all (priv->inspector_window);
1145         }
1146
1147         return TRUE;
1148 }
1149
1150 static gboolean
1151 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1152                                        EmpathyThemeAdium  *theme)
1153 {
1154         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1155
1156         if (priv->inspector_window) {
1157                 gtk_widget_hide (priv->inspector_window);
1158         }
1159
1160         return TRUE;
1161 }
1162
1163 static WebKitWebView *
1164 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1165                                  WebKitWebView      *web_view,
1166                                  EmpathyThemeAdium  *theme)
1167 {
1168         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1169         GtkWidget             *scrolled_window;
1170         GtkWidget             *inspector_web_view;
1171
1172         if (!priv->inspector_window) {
1173                 /* Create main window */
1174                 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1175                 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1176                                              800, 600);
1177                 g_signal_connect (priv->inspector_window, "delete-event",
1178                                   G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1179
1180                 /* Pack a scrolled window */
1181                 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1182                 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1183                                                 GTK_POLICY_AUTOMATIC,
1184                                                 GTK_POLICY_AUTOMATIC);
1185                 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1186                                    scrolled_window);
1187                 gtk_widget_show  (scrolled_window);
1188
1189                 /* Pack a webview in the scrolled window. That webview will be
1190                  * used to render the inspector tool.  */
1191                 inspector_web_view = webkit_web_view_new ();
1192                 gtk_container_add (GTK_CONTAINER (scrolled_window),
1193                                    inspector_web_view);
1194                 gtk_widget_show (scrolled_window);
1195
1196                 return WEBKIT_WEB_VIEW (inspector_web_view);
1197         }
1198
1199         return NULL;
1200 }
1201
1202 static PangoFontDescription *
1203 theme_adium_get_default_font (void)
1204 {
1205         GSettings *gsettings;
1206         PangoFontDescription *pango_fd;
1207         gchar *font_family;
1208
1209         gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1210
1211         font_family = g_settings_get_string (gsettings,
1212                      EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1213
1214         if (font_family == NULL)
1215                 return NULL;
1216
1217         pango_fd = pango_font_description_from_string (font_family);
1218         g_free (font_family);
1219         g_object_unref (gsettings);
1220         return pango_fd;
1221 }
1222
1223 static void
1224 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1225                              const gchar *name,
1226                              gint size)
1227 {
1228         g_object_set (w_settings, "default-font-family", name, NULL);
1229         g_object_set (w_settings, "default-font-size", size, NULL);
1230 }
1231
1232 static void
1233 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1234 {
1235         PangoFontDescription *default_font_desc;
1236         GdkScreen *current_screen;
1237         gdouble dpi = 0;
1238         gint pango_font_size = 0;
1239
1240         default_font_desc = theme_adium_get_default_font ();
1241         if (default_font_desc == NULL)
1242                 return ;
1243         pango_font_size = pango_font_description_get_size (default_font_desc)
1244                 / PANGO_SCALE ;
1245         if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1246                 current_screen = gdk_screen_get_default ();
1247                 if (current_screen != NULL) {
1248                         dpi = gdk_screen_get_resolution (current_screen);
1249                 } else {
1250                         dpi = BORING_DPI_DEFAULT;
1251                 }
1252                 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1253         }
1254         theme_adium_set_webkit_font (w_settings,
1255                 pango_font_description_get_family (default_font_desc),
1256                 pango_font_size);
1257         pango_font_description_free (default_font_desc);
1258 }
1259
1260 static void
1261 theme_adium_constructed (GObject *object)
1262 {
1263         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1264         gchar                 *basedir_uri;
1265         const gchar           *font_family = NULL;
1266         gint                   font_size = 0;
1267         WebKitWebView         *webkit_view = WEBKIT_WEB_VIEW (object);
1268         WebKitWebSettings     *webkit_settings;
1269         WebKitWebInspector    *webkit_inspector;
1270
1271         /* Set default settings */
1272         font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1273         font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1274         webkit_settings = webkit_web_view_get_settings (webkit_view);
1275
1276         if (font_family && font_size) {
1277                 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1278         } else {
1279                 theme_adium_set_default_font (webkit_settings);
1280         }
1281
1282         /* Setup webkit inspector */
1283         webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1284         g_signal_connect (webkit_inspector, "inspect-web-view",
1285                           G_CALLBACK (theme_adium_inspect_web_view_cb),
1286                           object);
1287         g_signal_connect (webkit_inspector, "show-window",
1288                           G_CALLBACK (theme_adium_inspector_show_window_cb),
1289                           object);
1290         g_signal_connect (webkit_inspector, "close-window",
1291                           G_CALLBACK (theme_adium_inspector_close_window_cb),
1292                           object);
1293
1294         /* Load template */
1295         priv->pages_loading = 1;
1296
1297         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
1298         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
1299                                           priv->data->template_html,
1300                                           basedir_uri);
1301         g_free (basedir_uri);
1302 }
1303
1304 static void
1305 theme_adium_get_property (GObject    *object,
1306                           guint       param_id,
1307                           GValue     *value,
1308                           GParamSpec *pspec)
1309 {
1310         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1311
1312         switch (param_id) {
1313         case PROP_ADIUM_DATA:
1314                 g_value_set_boxed (value, priv->data);
1315                 break;
1316         default:
1317                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1318                 break;
1319         };
1320 }
1321
1322 static void
1323 theme_adium_set_property (GObject      *object,
1324                           guint         param_id,
1325                           const GValue *value,
1326                           GParamSpec   *pspec)
1327 {
1328         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1329
1330         switch (param_id) {
1331         case PROP_ADIUM_DATA:
1332                 g_assert (priv->data == NULL);
1333                 priv->data = g_value_dup_boxed (value);
1334                 break;
1335         default:
1336                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1337                 break;
1338         };
1339 }
1340
1341 static void
1342 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1343 {
1344         GObjectClass *object_class = G_OBJECT_CLASS (klass);
1345         GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1346
1347         object_class->finalize = theme_adium_finalize;
1348         object_class->dispose = theme_adium_dispose;
1349         object_class->constructed = theme_adium_constructed;
1350         object_class->get_property = theme_adium_get_property;
1351         object_class->set_property = theme_adium_set_property;
1352
1353         widget_class->button_press_event = theme_adium_button_press_event;
1354
1355         g_object_class_install_property (object_class,
1356                                          PROP_ADIUM_DATA,
1357                                          g_param_spec_boxed ("adium-data",
1358                                                              "The theme data",
1359                                                              "Data for the adium theme",
1360                                                               EMPATHY_TYPE_ADIUM_DATA,
1361                                                               G_PARAM_CONSTRUCT_ONLY |
1362                                                               G_PARAM_READWRITE |
1363                                                               G_PARAM_STATIC_STRINGS));
1364
1365         g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1366 }
1367
1368 static void
1369 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1370 {
1371         EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1372                 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1373
1374         theme->priv = priv;
1375
1376         priv->allow_scrolling = TRUE;
1377         priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1378
1379         g_signal_connect (theme, "load-finished",
1380                           G_CALLBACK (theme_adium_load_finished_cb),
1381                           NULL);
1382         g_signal_connect (theme, "navigation-policy-decision-requested",
1383                           G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1384                           NULL);
1385
1386         priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1387         g_signal_connect (priv->gsettings_chat,
1388                 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1389                 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1390                 theme);
1391
1392         theme_adium_update_enable_webkit_developer_tools (theme);
1393 }
1394
1395 EmpathyThemeAdium *
1396 empathy_theme_adium_new (EmpathyAdiumData *data)
1397 {
1398         g_return_val_if_fail (data != NULL, NULL);
1399
1400         return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1401                              "adium-data", data,
1402                              NULL);
1403 }
1404
1405 gboolean
1406 empathy_adium_path_is_valid (const gchar *path)
1407 {
1408         gboolean ret;
1409         gchar   *file;
1410
1411         /* The theme is not valid if there is no Info.plist */
1412         file = g_build_filename (path, "Contents", "Info.plist",
1413                                  NULL);
1414         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1415         g_free (file);
1416
1417         if (ret == FALSE)
1418                 return ret;
1419
1420         /* We ship a default Template.html as fallback if there is any problem
1421          * with the one inside the theme. The only other required file is
1422          * Content.html */
1423         file = g_build_filename (path, "Contents", "Resources", "Content.html",
1424                                  NULL);
1425         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1426         g_free (file);
1427
1428         if (ret)
1429                 return ret;
1430
1431         /* Legacy themes have Incoming/Content.html (outgoing fallback to use
1432          * incoming). */
1433         file = g_build_filename (path, "Contents", "Resources", "Incoming",
1434                                  "Content.html", NULL);
1435         ret = g_file_test (file, G_FILE_TEST_EXISTS);
1436         g_free (file);
1437
1438         return ret;
1439 }
1440
1441 GHashTable *
1442 empathy_adium_info_new (const gchar *path)
1443 {
1444         gchar *file;
1445         GValue *value;
1446         GHashTable *info = NULL;
1447
1448         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1449
1450         file = g_build_filename (path, "Contents", "Info.plist", NULL);
1451         value = empathy_plist_parse_from_file (file);
1452         g_free (file);
1453
1454         if (value == NULL)
1455                 return NULL;
1456
1457         info = g_value_dup_boxed (value);
1458         tp_g_value_slice_free (value);
1459
1460         /* Insert the theme's path into the hash table,
1461          * keys have to be dupped */
1462         tp_asv_set_string (info, g_strdup ("path"), path);
1463
1464         return info;
1465 }
1466
1467 GType
1468 empathy_adium_data_get_type (void)
1469 {
1470   static GType type_id = 0;
1471
1472   if (!type_id)
1473     {
1474       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1475           (GBoxedCopyFunc) empathy_adium_data_ref,
1476           (GBoxedFreeFunc) empathy_adium_data_unref);
1477     }
1478
1479   return type_id;
1480 }
1481
1482 EmpathyAdiumData  *
1483 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1484 {
1485         EmpathyAdiumData *data;
1486         gchar            *file;
1487         gchar            *template_html = NULL;
1488         gsize             template_len;
1489         gchar            *footer_html = NULL;
1490         gsize             footer_len;
1491         GString          *string;
1492         gchar           **strv = NULL;
1493         gchar            *css_path;
1494         guint             len = 0;
1495         guint             i = 0;
1496
1497         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1498
1499         data = g_slice_new0 (EmpathyAdiumData);
1500         data->ref_count = 1;
1501         data->path = g_strdup (path);
1502         data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1503                 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1504         data->info = g_hash_table_ref (info);
1505
1506         DEBUG ("Loading theme at %s", path);
1507
1508         /* Load html files */
1509         file = g_build_filename (data->basedir, "Content.html", NULL);
1510         g_file_get_contents (file, &data->content_html, &data->content_len, NULL);
1511         g_free (file);
1512
1513         /* Fallback to legacy html files */
1514         if (data->content_html == NULL) {
1515                 DEBUG ("  fallback to legacy theme");
1516
1517                 file = g_build_filename (data->basedir, "Incoming", "Content.html", NULL);
1518                 g_file_get_contents (file, &data->in_content_html, &data->in_content_len, NULL);
1519                 g_free (file);
1520
1521                 file = g_build_filename (data->basedir, "Incoming", "NextContent.html", NULL);
1522                 g_file_get_contents (file, &data->in_nextcontent_html, &data->in_nextcontent_len, NULL);
1523                 g_free (file);
1524
1525                 file = g_build_filename (data->basedir, "Incoming", "Context.html", NULL);
1526                 g_file_get_contents (file, &data->in_context_html, &data->in_context_len, NULL);
1527                 g_free (file);
1528
1529                 file = g_build_filename (data->basedir, "Incoming", "NextContext.html", NULL);
1530                 g_file_get_contents (file, &data->in_nextcontext_html, &data->in_nextcontext_len, NULL);
1531                 g_free (file);
1532
1533                 file = g_build_filename (data->basedir, "Outgoing", "Content.html", NULL);
1534                 g_file_get_contents (file, &data->out_content_html, &data->out_content_len, NULL);
1535                 g_free (file);
1536
1537                 file = g_build_filename (data->basedir, "Outgoing", "NextContent.html", NULL);
1538                 g_file_get_contents (file, &data->out_nextcontent_html, &data->out_nextcontent_len, NULL);
1539                 g_free (file);
1540
1541                 file = g_build_filename (data->basedir, "Outgoing", "Context.html", NULL);
1542                 g_file_get_contents (file, &data->out_context_html, &data->out_context_len, NULL);
1543                 g_free (file);
1544
1545                 file = g_build_filename (data->basedir, "Outgoing", "NextContext.html", NULL);
1546                 g_file_get_contents (file, &data->out_nextcontext_html, &data->out_nextcontext_len, NULL);
1547                 g_free (file);
1548
1549                 file = g_build_filename (data->basedir, "Status.html", NULL);
1550                 g_file_get_contents (file, &data->status_html, &data->status_len, NULL);
1551                 g_free (file);
1552         }
1553
1554         file = g_build_filename (data->basedir, "Footer.html", NULL);
1555         g_file_get_contents (file, &footer_html, &footer_len, NULL);
1556         g_free (file);
1557
1558         file = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1559         if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1560                 data->default_incoming_avatar_filename = file;
1561         } else {
1562                 g_free (file);
1563         }
1564
1565         file = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1566         if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1567                 data->default_outgoing_avatar_filename = file;
1568         } else {
1569                 g_free (file);
1570         }
1571
1572         css_path = g_build_filename (data->basedir, "main.css", NULL);
1573
1574         /* There is 2 formats for Template.html: The old one has 4 parameters,
1575          * the new one has 5 parameters. */
1576         file = g_build_filename (data->basedir, "Template.html", NULL);
1577         if (g_file_get_contents (file, &template_html, &template_len, NULL)) {
1578                 strv = g_strsplit (template_html, "%@", -1);
1579                 len = g_strv_length (strv);
1580         }
1581         g_free (file);
1582
1583         if (len != 5 && len != 6) {
1584                 /* Either the theme has no template or it don't have the good
1585                  * number of parameters. Fallback to use our own template. */
1586                 g_free (template_html);
1587                 g_strfreev (strv);
1588
1589                 file = empathy_file_lookup ("Template.html", "data");
1590                 g_file_get_contents (file, &template_html, &template_len, NULL);
1591                 g_free (file);
1592                 strv = g_strsplit (template_html, "%@", -1);
1593                 len = g_strv_length (strv);
1594         }
1595
1596         /* Replace %@ with the needed information in the template html. */
1597         string = g_string_sized_new (template_len);
1598         g_string_append (string, strv[i++]);
1599         g_string_append (string, data->basedir);
1600         g_string_append (string, strv[i++]);
1601         if (len == 6) {
1602                 const gchar *variant;
1603
1604                 /* We include main.css by default */
1605                 g_string_append_printf (string, "@import url(\"%s\");", css_path);
1606                 g_string_append (string, strv[i++]);
1607                 variant = tp_asv_get_string (data->info, "DefaultVariant");
1608                 if (variant) {
1609                         g_string_append (string, "Variants/");
1610                         g_string_append (string, variant);
1611                         g_string_append (string, ".css");
1612                 }
1613         } else {
1614                 /* FIXME: We should set main.css OR the variant css */
1615                 g_string_append (string, css_path);
1616         }
1617         g_string_append (string, strv[i++]);
1618         g_string_append (string, ""); /* We don't want header */
1619         g_string_append (string, strv[i++]);
1620         /* FIXME: We should replace adium %macros% in footer */
1621         if (footer_html) {
1622                 g_string_append (string, footer_html);
1623         }
1624         g_string_append (string, strv[i++]);
1625         data->template_html = g_string_free (string, FALSE);
1626
1627         g_free (footer_html);
1628         g_free (template_html);
1629         g_free (css_path);
1630         g_strfreev (strv);
1631
1632         return data;
1633 }
1634
1635 EmpathyAdiumData  *
1636 empathy_adium_data_new (const gchar *path)
1637 {
1638         EmpathyAdiumData *data;
1639         GHashTable *info;
1640
1641         info = empathy_adium_info_new (path);
1642         data = empathy_adium_data_new_with_info (path, info);
1643         g_hash_table_unref (info);
1644
1645         return data;
1646 }
1647
1648 EmpathyAdiumData  *
1649 empathy_adium_data_ref (EmpathyAdiumData *data)
1650 {
1651         g_return_val_if_fail (data != NULL, NULL);
1652
1653         g_atomic_int_inc (&data->ref_count);
1654
1655         return data;
1656 }
1657
1658 void
1659 empathy_adium_data_unref (EmpathyAdiumData *data)
1660 {
1661         g_return_if_fail (data != NULL);
1662
1663         if (g_atomic_int_dec_and_test (&data->ref_count)) {
1664                 g_free (data->path);
1665                 g_free (data->basedir);
1666                 g_free (data->default_avatar_filename);
1667                 g_free (data->default_incoming_avatar_filename);
1668                 g_free (data->default_outgoing_avatar_filename);
1669                 g_free (data->template_html);
1670                 g_free (data->content_html);
1671                 g_hash_table_unref (data->info);
1672
1673                 g_free (data->in_content_html);
1674                 g_free (data->in_nextcontent_html);
1675                 g_free (data->in_context_html);
1676                 g_free (data->in_nextcontext_html);
1677                 g_free (data->out_content_html);
1678                 g_free (data->out_nextcontent_html);
1679                 g_free (data->out_context_html);
1680                 g_free (data->out_nextcontext_html);
1681                 g_free (data->status_html);
1682
1683                 g_slice_free (EmpathyAdiumData, data);
1684         }
1685 }
1686
1687 GHashTable *
1688 empathy_adium_data_get_info (EmpathyAdiumData *data)
1689 {
1690         g_return_val_if_fail (data != NULL, NULL);
1691
1692         return data->info;
1693 }
1694
1695 const gchar *
1696 empathy_adium_data_get_path (EmpathyAdiumData *data)
1697 {
1698         g_return_val_if_fail (data != NULL, NULL);
1699
1700         return data->path;
1701 }
1702