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