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