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