]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-theme-adium.c
empathy-message.c: remove a trailing space
[empathy.git] / libempathy-gtk / empathy-theme-adium.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2008-2009 Collabora Ltd.
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  *
19  * Authors: Xavier Claessens <xclaesse@gmail.com>
20  */
21
22 #include "config.h"
23
24 #include <string.h>
25 #include <glib/gi18n.h>
26
27 #include <webkit/webkitnetworkrequest.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
30
31
32 #include <libempathy/empathy-time.h>
33 #include <libempathy/empathy-utils.h>
34 #include <libmissioncontrol/mc-profile.h>
35
36 #include "empathy-theme-adium.h"
37 #include "empathy-smiley-manager.h"
38 #include "empathy-conf.h"
39 #include "empathy-ui-utils.h"
40 #include "empathy-plist.h"
41
42 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
43 #include <libempathy/empathy-debug.h>
44
45 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
46
47 /* "Join" consecutive messages with timestamps within five minutes */
48 #define MESSAGE_JOIN_PERIOD 5*60
49
50 typedef struct {
51         EmpathyAdiumData     *data;
52         EmpathySmileyManager *smiley_manager;
53         EmpathyContact       *last_contact;
54         time_t                last_timestamp;
55         gboolean              last_is_backlog;
56         gboolean              page_loaded;
57         GList                *message_queue;
58 } EmpathyThemeAdiumPriv;
59
60 struct _EmpathyAdiumData {
61         guint  ref_count;
62         gchar *path;
63         gchar *basedir;
64         gchar *default_avatar_filename;
65         gchar *default_incoming_avatar_filename;
66         gchar *default_outgoing_avatar_filename;
67         gchar *template_html;
68         gchar *in_content_html;
69         gsize  in_content_len;
70         gchar *in_context_html;
71         gsize  in_context_len;
72         gchar *in_nextcontent_html;
73         gsize  in_nextcontent_len;
74         gchar *in_nextcontext_html;
75         gsize  in_nextcontext_len;
76         gchar *out_content_html;
77         gsize  out_content_len;
78         gchar *out_context_html;
79         gsize  out_context_len;
80         gchar *out_nextcontent_html;
81         gsize  out_nextcontent_len;
82         gchar *out_nextcontext_html;
83         gsize  out_nextcontext_len;
84         gchar *status_html;
85         gsize  status_len;
86         GHashTable *info;
87 };
88
89 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
90
91 enum {
92         PROP_0,
93         PROP_ADIUM_DATA,
94 };
95
96 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
97                          WEBKIT_TYPE_WEB_VIEW,
98                          G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
99                                                 theme_adium_iface_init));
100
101 static WebKitNavigationResponse
102 theme_adium_navigation_requested_cb (WebKitWebView        *view,
103                                      WebKitWebFrame       *frame,
104                                      WebKitNetworkRequest *request,
105                                      gpointer              user_data)
106 {
107         const gchar *uri;
108
109         uri = webkit_network_request_get_uri (request);
110         empathy_url_show (GTK_WIDGET (view), uri);
111
112         return WEBKIT_NAVIGATION_RESPONSE_IGNORE;
113 }
114
115 static void
116 theme_adium_populate_popup_cb (WebKitWebView *view,
117                                GtkMenu       *menu,
118                                gpointer       user_data)
119 {
120         GtkWidget *item;
121
122         /* Remove default menu items */
123         gtk_container_foreach (GTK_CONTAINER (menu),
124                 (GtkCallback) gtk_widget_destroy, NULL);
125
126         /* Select all item */
127         item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
128         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
129         gtk_widget_show (item);
130
131         g_signal_connect_swapped (item, "activate",
132                                   G_CALLBACK (webkit_web_view_select_all),
133                                   view);
134
135         /* Copy menu item */
136         if (webkit_web_view_can_copy_clipboard (view)) {
137                 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
138                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
139                 gtk_widget_show (item);
140
141                 g_signal_connect_swapped (item, "activate",
142                                           G_CALLBACK (webkit_web_view_copy_clipboard),
143                                           view);
144         }
145
146         /* Clear menu item */
147         item = gtk_separator_menu_item_new ();
148         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
149         gtk_widget_show (item);
150
151         item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
152         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
153         gtk_widget_show (item);
154
155         g_signal_connect_swapped (item, "activate",
156                                   G_CALLBACK (empathy_chat_view_clear),
157                                   view);
158
159         /* FIXME: Add open_link and copy_link when those bugs are fixed:
160          * https://bugs.webkit.org/show_bug.cgi?id=16092
161          * https://bugs.webkit.org/show_bug.cgi?id=16562
162          */
163 }
164
165 static gchar *
166 theme_adium_parse_body (EmpathyThemeAdium *theme,
167                         const gchar       *text)
168 {
169         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
170         gboolean               use_smileys = FALSE;
171         GSList                *smileys, *l;
172         GString               *string;
173         gint                   i;
174         GRegex                *uri_regex;
175         GMatchInfo            *match_info;
176         gboolean               match;
177         gchar                 *ret = NULL;
178         gint                   prev;
179
180         empathy_conf_get_bool (empathy_conf_get (),
181                                EMPATHY_PREFS_CHAT_SHOW_SMILEYS,
182                                &use_smileys);
183
184         if (use_smileys) {
185                 /* Replace smileys by a <img/> tag */
186                 string = g_string_sized_new (strlen (text));
187                 smileys = empathy_smiley_manager_parse (priv->smiley_manager, text);
188                 for (l = smileys; l; l = l->next) {
189                         EmpathySmiley *smiley;
190
191                         smiley = l->data;
192                         if (smiley->path) {
193                                 g_string_append_printf (string,
194                                                         "<abbr title='%s'><img src=\"%s\"/ alt=\"%s\"/></abbr>",
195                                                         smiley->str, smiley->path, smiley->str);
196                         } else {
197                                 gchar *str;
198
199                                 str = g_markup_escape_text (smiley->str, -1);
200                                 g_string_append (string, str);
201                                 g_free (str);
202                         }
203                         empathy_smiley_free (smiley);
204                 }
205                 g_slist_free (smileys);
206
207                 g_free (ret);
208                 text = ret = g_string_free (string, FALSE);
209         }
210
211         /* Add <a href></a> arround links */
212         uri_regex = empathy_uri_regex_dup_singleton ();
213         match = g_regex_match (uri_regex, text, 0, &match_info);
214         if (match) {
215                 gint last = 0;
216                 gint s = 0, e = 0;
217
218                 string = g_string_sized_new (strlen (text));
219                 do {
220                         g_match_info_fetch_pos (match_info, 0, &s, &e);
221
222                         if (s > last) {
223                                 /* Append the text between last link (or the
224                                  * start of the message) and this link */
225                                 g_string_append_len (string, text + last, s - last);
226                         }
227
228                         /* Append the link inside <a href=""></a> tag */
229                         g_string_append (string, "<a href=\"");
230                         g_string_append_len (string, text + s, e - s);
231                         g_string_append (string, "\">");
232                         g_string_append_len (string, text + s, e - s);
233                         g_string_append (string, "</a>");
234
235                         last = e;
236                 } while (g_match_info_next (match_info, NULL));
237
238                 if (e < strlen (text)) {
239                         /* Append the text after the last link */
240                         g_string_append_len (string, text + e, strlen (text) - e);
241                 }
242
243                 g_free (ret);
244                 text = ret = g_string_free (string, FALSE);
245         }
246         g_match_info_free (match_info);
247         g_regex_unref (uri_regex);
248
249         /* Replace \n by <br/> */
250         string = NULL;
251         prev = 0;
252         for (i = 0; text[i] != '\0'; i++) {
253                 if (text[i] == '\n') {
254                         if (!string ) {
255                                 string = g_string_sized_new (strlen (text));
256                         }
257                         g_string_append_len (string, text + prev, i - prev);
258                         g_string_append (string, "<br/>");
259                         prev = i + 1;
260                 }
261         }
262         if (string) {
263                 g_string_append (string, text + prev);
264                 g_free (ret);
265                 text = ret = g_string_free (string, FALSE);
266         }
267
268         return ret;
269 }
270
271 static void
272 escape_and_append_len (GString *string, const gchar *str, gint len)
273 {
274         while (*str != '\0' && len != 0) {
275                 switch (*str) {
276                 case '\\':
277                         /* \ becomes \\ */
278                         g_string_append (string, "\\\\");
279                         break;
280                 case '\"':
281                         /* " becomes \" */
282                         g_string_append (string, "\\\"");
283                         break;
284                 case '\n':
285                         /* Remove end of lines */
286                         break;
287                 default:
288                         g_string_append_c (string, *str);
289                 }
290
291                 str++;
292                 len--;
293         }
294 }
295
296 static gboolean
297 theme_adium_match (const gchar **str, const gchar *match)
298 {
299         gint len;
300
301         len = strlen (match);
302         if (strncmp (*str, match, len) == 0) {
303                 *str += len - 1;
304                 return TRUE;
305         }
306
307         return FALSE;
308 }
309
310 static void
311 theme_adium_append_html (EmpathyThemeAdium *theme,
312                          const gchar       *func,
313                          const gchar       *html, gsize len,
314                          const gchar       *message,
315                          const gchar       *avatar_filename,
316                          const gchar       *name,
317                          const gchar       *contact_id,
318                          const gchar       *service_name,
319                          const gchar       *message_classes,
320                          time_t             timestamp)
321 {
322         GString     *string;
323         const gchar *cur = NULL;
324         gchar       *script;
325
326         /* Make some search-and-replace in the html code */
327         string = g_string_sized_new (len + strlen (message));
328         g_string_append_printf (string, "%s(\"", func);
329         for (cur = html; *cur != '\0'; cur++) {
330                 const gchar *replace = NULL;
331                 gchar       *dup_replace = NULL;
332
333                 if (theme_adium_match (&cur, "%message%")) {
334                         replace = message;
335                 } else if (theme_adium_match (&cur, "%messageClasses%")) {
336                         replace = message_classes;
337                 } else if (theme_adium_match (&cur, "%userIconPath%")) {
338                         replace = avatar_filename;
339                 } else if (theme_adium_match (&cur, "%sender%")) {
340                         replace = name;
341                 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
342                         replace = contact_id;
343                 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
344                         /* %senderDisplayName% -
345                          * "The serverside (remotely set) name of the sender,
346                          *  such as an MSN display name."
347                          *
348                          * We don't have access to that yet so we use local
349                          * alias instead.*/
350                         replace = name;
351                 } else if (theme_adium_match (&cur, "%service%")) {
352                         replace = service_name;
353                 } else if (theme_adium_match (&cur, "%shortTime%")) {
354                         dup_replace = empathy_time_to_string_local (timestamp,
355                                 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
356                         replace = dup_replace;
357                 } else if (theme_adium_match (&cur, "%time")) {
358                         gchar *format = NULL;
359                         gchar *end;
360
361                         /* Time can be in 2 formats:
362                          * %time% or %time{strftime format}%
363                          * Extract the time format if provided. */
364                         if (cur[1] == '{') {
365                                 cur += 2;
366                                 end = strstr (cur, "}%");
367                                 if (!end) {
368                                         /* Invalid string */
369                                         continue;
370                                 }
371                                 format = g_strndup (cur, end - cur);
372                                 cur = end + 1;
373                         } else {
374                                 cur++;
375                         }
376
377                         dup_replace = empathy_time_to_string_local (timestamp,
378                                 format ? format : EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
379                         replace = dup_replace;
380                         g_free (format);
381                 } else {
382                         escape_and_append_len (string, cur, 1);
383                         continue;
384                 }
385
386                 /* Here we have a replacement to make */
387                 escape_and_append_len (string, replace, -1);
388                 g_free (dup_replace);
389         }
390         g_string_append (string, "\")");
391
392         script = g_string_free (string, FALSE);
393         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
394         g_free (script);
395 }
396
397 static void
398 theme_adium_append_message (EmpathyChatView *view,
399                             EmpathyMessage  *msg)
400 {
401         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
402         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
403         EmpathyContact        *sender;
404         EmpathyAccount        *account;
405         McProfile             *account_profile;
406         gchar                 *dup_body = NULL;
407         const gchar           *body;
408         const gchar           *name;
409         const gchar           *contact_id;
410         EmpathyAvatar         *avatar;
411         const gchar           *avatar_filename = NULL;
412         time_t                 timestamp;
413         gchar                 *html = NULL;
414         gsize                  len = 0;
415         const gchar           *func;
416         const gchar           *service_name;
417         GString               *message_classes = NULL;
418         gboolean              is_backlog;
419
420         if (!priv->page_loaded) {
421                 priv->message_queue = g_list_prepend (priv->message_queue,
422                                                       g_object_ref (msg));
423                 return;
424         }
425
426         /* Get information */
427         sender = empathy_message_get_sender (msg);
428         account = empathy_contact_get_account (sender);
429         account_profile = empathy_account_get_profile (account);
430         service_name = mc_profile_get_display_name (account_profile);
431         timestamp = empathy_message_get_timestamp (msg);
432         body = empathy_message_get_body (msg);
433         dup_body = theme_adium_parse_body (theme, body);
434         if (dup_body) {
435                 body = dup_body;
436         }
437         name = empathy_contact_get_name (sender);
438         contact_id = empathy_contact_get_id (sender);
439
440         /* If this is a /me, append an event */
441         if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION) {
442                 gchar *str;
443
444                 str = g_strdup_printf ("%s %s", name, body);
445                 empathy_chat_view_append_event (view, str);
446                 g_free (str);
447                 g_free (dup_body);
448                 return;
449         }
450
451         /* Get the avatar filename, or a fallback */
452         avatar = empathy_contact_get_avatar (sender);
453         if (avatar) {
454                 avatar_filename = avatar->filename;
455         }
456         if (!avatar_filename) {
457                 if (empathy_contact_is_user (sender)) {
458                         avatar_filename = priv->data->default_outgoing_avatar_filename;
459                 } else {
460                         avatar_filename = priv->data->default_incoming_avatar_filename;
461                 }
462                 if (!avatar_filename) {
463                         if (!priv->data->default_avatar_filename) {
464                                 priv->data->default_avatar_filename =
465                                         empathy_filename_from_icon_name ("stock_person",
466                                                                          GTK_ICON_SIZE_DIALOG);
467                         }
468                         avatar_filename = priv->data->default_avatar_filename;
469                 }
470         }
471
472         is_backlog = empathy_message_is_backlog (msg);
473
474         /* Get the right html/func to add the message */
475         func = "appendMessage";
476
477         message_classes = g_string_new ("message");
478
479         /* eventually append the "history" class */
480         if (is_backlog) {
481                 g_string_append (message_classes, " history");
482         }
483
484         /* check the sender of the message and append the appropriate class */
485         if (empathy_contact_is_user (sender)) {
486                 g_string_append (message_classes, " outgoing");
487         }
488         else {
489                 g_string_append (message_classes, " incoming");
490         }
491
492         /*
493          * To mimick Adium's behavior, we only want to join messages
494          * sent by the same contact within a 5 minute time frame.
495          */
496         if (empathy_contact_equal (priv->last_contact, sender) &&
497             (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
498             (is_backlog == priv->last_is_backlog)) {
499                 /* the messages can be appended */
500                 func = "appendNextMessage";
501                 g_string_append (message_classes, " consecutive");
502
503                 /* check who is the sender of the message to use the correct html file */
504                 if (empathy_contact_is_user (sender)) {
505                         /* check if this is a backlog message and use NextContext.html */
506                         if (is_backlog) {
507                                 html = priv->data->out_nextcontext_html;
508                                 len = priv->data->out_nextcontext_len;
509                         }
510
511                         /*
512                          * html is null if this is not a backlog message or
513                          * if we have to fallback (NextContext.html missing).
514                          * use NextContent.html
515                          */
516                         if (html == NULL) {
517                                 html = priv->data->out_nextcontent_html;
518                                 len = priv->data->out_nextcontent_len;
519                         }
520                 }
521                 else {
522                         if (is_backlog) {
523                                 html = priv->data->in_nextcontext_html;
524                                 len = priv->data->in_nextcontext_len;
525                         }
526
527                         if (html == NULL) {
528                                 html = priv->data->in_nextcontent_html;
529                                 len = priv->data->in_nextcontent_len;
530                         }
531                 }
532         }
533
534         /*
535          * we have html == NULL here if:
536          * 1. the message didn't have to be appended because
537          *    the sender was different or the timestamp was too far
538          * 2. NextContent.html file does not exist, so we must
539          *    not forget to fallback to the correct Content.html
540          */
541         if (html == NULL) {
542                 if (empathy_contact_is_user (sender)) {
543                         if (is_backlog) {
544                                 html = priv->data->out_context_html;
545                                 len = priv->data->out_context_len;
546                         }
547
548                         if (html == NULL) {
549                                 html = priv->data->out_content_html;
550                                 len = priv->data->out_content_len;
551                         }
552                 }
553                 else {
554                         if (is_backlog) {
555                                 html = priv->data->in_context_html;
556                                 len = priv->data->in_context_len;
557                         }
558
559                         if (html == NULL) {
560                                 html = priv->data->in_content_html;
561                                 len = priv->data->in_content_len;
562                         }
563                 }
564         }
565
566         theme_adium_append_html (theme, func, html, len, body, avatar_filename,
567                                  name, contact_id, service_name, message_classes->str,
568                                  timestamp);
569
570         /* Keep the sender of the last displayed message */
571         if (priv->last_contact) {
572                 g_object_unref (priv->last_contact);
573         }
574         priv->last_contact = g_object_ref (sender);
575         priv->last_timestamp = timestamp;
576         priv->last_is_backlog = is_backlog;
577
578         g_free (dup_body);
579         g_string_free (message_classes, TRUE);
580 }
581
582 static void
583 theme_adium_append_event (EmpathyChatView *view,
584                           const gchar     *str)
585 {
586         EmpathyThemeAdium     *theme = EMPATHY_THEME_ADIUM (view);
587         EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
588
589         if (priv->data->status_html) {
590                 theme_adium_append_html (theme, "appendMessage",
591                                          priv->data->status_html,
592                                          priv->data->status_len,
593                                          str, NULL, NULL, NULL, NULL, "event",
594                                          empathy_time_get_current ());
595         }
596
597         /* There is no last contact */
598         if (priv->last_contact) {
599                 g_object_unref (priv->last_contact);
600                 priv->last_contact = NULL;
601         }
602 }
603
604 static void
605 theme_adium_scroll (EmpathyChatView *view,
606                     gboolean         allow_scrolling)
607 {
608         /* FIXME: Is it possible? I guess we need a js function, but I don't
609          * see any... */
610 }
611
612 static void
613 theme_adium_scroll_down (EmpathyChatView *view)
614 {
615         webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "scrollToBottom()");
616 }
617
618 static gboolean
619 theme_adium_get_has_selection (EmpathyChatView *view)
620 {
621         return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
622 }
623
624 static void
625 theme_adium_clear (EmpathyChatView *view)
626 {
627         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
628         gchar *basedir_uri;
629
630         priv->page_loaded = FALSE;
631         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
632         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view),
633                                           priv->data->template_html,
634                                           basedir_uri);
635         g_free (basedir_uri);
636
637         /* Clear last contact to avoid trying to add a 'joined'
638          * message when we don't have an insertion point. */
639         if (priv->last_contact) {
640                 g_object_unref (priv->last_contact);
641                 priv->last_contact = NULL;
642         }
643 }
644
645 static gboolean
646 theme_adium_find_previous (EmpathyChatView *view,
647                            const gchar     *search_criteria,
648                            gboolean         new_search)
649 {
650         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
651                                             search_criteria, FALSE,
652                                             FALSE, TRUE);
653 }
654
655 static gboolean
656 theme_adium_find_next (EmpathyChatView *view,
657                        const gchar     *search_criteria,
658                        gboolean         new_search)
659 {
660         return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
661                                             search_criteria, FALSE,
662                                             TRUE, TRUE);
663 }
664
665 static void
666 theme_adium_find_abilities (EmpathyChatView *view,
667                             const gchar    *search_criteria,
668                             gboolean       *can_do_previous,
669                             gboolean       *can_do_next)
670 {
671         /* FIXME: Does webkit provide an API for that? We have wrap=true in
672          * find_next and find_previous to work around this problem. */
673         if (can_do_previous)
674                 *can_do_previous = TRUE;
675         if (can_do_next)
676                 *can_do_next = TRUE;
677 }
678
679 static void
680 theme_adium_highlight (EmpathyChatView *view,
681                        const gchar     *text)
682 {
683         webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
684         webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
685                                            text, FALSE, 0);
686         webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
687                                                     TRUE);
688 }
689
690 static void
691 theme_adium_copy_clipboard (EmpathyChatView *view)
692 {
693         webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
694 }
695
696 static void
697 theme_adium_iface_init (EmpathyChatViewIface *iface)
698 {
699         iface->append_message = theme_adium_append_message;
700         iface->append_event = theme_adium_append_event;
701         iface->scroll = theme_adium_scroll;
702         iface->scroll_down = theme_adium_scroll_down;
703         iface->get_has_selection = theme_adium_get_has_selection;
704         iface->clear = theme_adium_clear;
705         iface->find_previous = theme_adium_find_previous;
706         iface->find_next = theme_adium_find_next;
707         iface->find_abilities = theme_adium_find_abilities;
708         iface->highlight = theme_adium_highlight;
709         iface->copy_clipboard = theme_adium_copy_clipboard;
710 }
711
712 static void
713 theme_adium_load_finished_cb (WebKitWebView  *view,
714                               WebKitWebFrame *frame,
715                               gpointer        user_data)
716 {
717         EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
718         EmpathyChatView       *chat_view = EMPATHY_CHAT_VIEW (view);
719
720         DEBUG ("Page loaded");
721         priv->page_loaded = TRUE;
722
723         /* Display queued messages */
724         priv->message_queue = g_list_reverse (priv->message_queue);
725         while (priv->message_queue) {
726                 EmpathyMessage *message = priv->message_queue->data;
727
728                 theme_adium_append_message (chat_view, message);
729                 priv->message_queue = g_list_remove (priv->message_queue, message);
730                 g_object_unref (message);
731         }
732 }
733
734 static void
735 theme_adium_finalize (GObject *object)
736 {
737         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
738
739         empathy_adium_data_unref (priv->data);
740
741         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
742 }
743
744 static void
745 theme_adium_dispose (GObject *object)
746 {
747         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
748
749         if (priv->smiley_manager) {
750                 g_object_unref (priv->smiley_manager);
751                 priv->smiley_manager = NULL;
752         }
753
754         if (priv->last_contact) {
755                 g_object_unref (priv->last_contact);
756                 priv->last_contact = NULL;
757         }
758
759         G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
760 }
761
762 static void
763 theme_adium_constructed (GObject *object)
764 {
765         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
766         gchar                 *basedir_uri;
767         const gchar           *font_family = NULL;
768         gint                   font_size = 0;
769         WebKitWebSettings     *webkit_settings;
770
771         /* Set default settings */
772         font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
773         font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
774         webkit_settings = webkit_web_settings_new ();
775         if (font_family) {
776                 g_object_set (G_OBJECT (webkit_settings), "default-font-family", font_family, NULL);
777         }
778         if (font_size) {
779                 g_object_set (G_OBJECT (webkit_settings), "default-font-size", font_size, NULL);
780         }
781         webkit_web_view_set_settings (WEBKIT_WEB_VIEW (object), webkit_settings);
782
783         /* Load template */
784         basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
785         webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
786                                           priv->data->template_html,
787                                           basedir_uri);
788
789         g_object_unref (webkit_settings);
790         g_free (basedir_uri);
791 }
792
793 static void
794 theme_adium_get_property (GObject    *object,
795                           guint       param_id,
796                           GValue     *value,
797                           GParamSpec *pspec)
798 {
799         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
800
801         switch (param_id) {
802         case PROP_ADIUM_DATA:
803                 g_value_set_boxed (value, priv->data);
804                 break;
805         default:
806                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
807                 break;
808         };
809 }
810
811 static void
812 theme_adium_set_property (GObject      *object,
813                           guint         param_id,
814                           const GValue *value,
815                           GParamSpec   *pspec)
816 {
817         EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
818
819         switch (param_id) {
820         case PROP_ADIUM_DATA:
821                 g_assert (priv->data == NULL);
822                 priv->data = g_value_dup_boxed (value);
823                 break;
824         default:
825                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
826                 break;
827         };
828 }
829
830 static void
831 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
832 {
833         GObjectClass *object_class = G_OBJECT_CLASS (klass);
834
835         object_class->finalize = theme_adium_finalize;
836         object_class->dispose = theme_adium_dispose;
837         object_class->constructed = theme_adium_constructed;
838         object_class->get_property = theme_adium_get_property;
839         object_class->set_property = theme_adium_set_property;
840
841         g_object_class_install_property (object_class,
842                                          PROP_ADIUM_DATA,
843                                          g_param_spec_boxed ("adium-data",
844                                                              "The theme data",
845                                                              "Data for the adium theme",
846                                                               EMPATHY_TYPE_ADIUM_DATA,
847                                                               G_PARAM_CONSTRUCT_ONLY |
848                                                               G_PARAM_READWRITE |
849                                                               G_PARAM_STATIC_STRINGS));
850
851
852         g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
853 }
854
855 static void
856 empathy_theme_adium_init (EmpathyThemeAdium *theme)
857 {
858         EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
859                 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
860
861         theme->priv = priv;
862
863         priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
864
865         g_signal_connect (theme, "load-finished",
866                           G_CALLBACK (theme_adium_load_finished_cb),
867                           NULL);
868         g_signal_connect (theme, "navigation-requested",
869                           G_CALLBACK (theme_adium_navigation_requested_cb),
870                           NULL);
871         g_signal_connect (theme, "populate-popup",
872                           G_CALLBACK (theme_adium_populate_popup_cb),
873                           NULL);
874 }
875
876 EmpathyThemeAdium *
877 empathy_theme_adium_new (EmpathyAdiumData *data)
878 {
879         g_return_val_if_fail (data != NULL, NULL);
880
881         return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
882                              "adium-data", data,
883                              NULL);
884 }
885
886 gboolean
887 empathy_adium_path_is_valid (const gchar *path)
888 {
889         gboolean ret;
890         gchar   *file;
891
892         /* The theme is not valid if there is no Info.plist */
893         file = g_build_filename (path, "Contents", "Info.plist",
894                                  NULL);
895         ret = g_file_test (file, G_FILE_TEST_EXISTS);
896         g_free (file);
897
898         if (ret == FALSE)
899                 return ret;
900
901         /* We ship a default Template.html as fallback if there is any problem
902          * with the one inside the theme. The only other required file is
903          * Content.html for incoming messages (outgoing fallback to use
904          * incoming). */
905         file = g_build_filename (path, "Contents", "Resources", "Incoming",
906                                  "Content.html", NULL);
907         ret = g_file_test (file, G_FILE_TEST_EXISTS);
908         g_free (file);
909
910         return ret;
911 }
912
913 GHashTable *
914 empathy_adium_info_new (const gchar *path)
915 {
916         gchar *file;
917         GValue *value;
918         GHashTable *info = NULL;
919
920         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
921
922         file = g_build_filename (path, "Contents", "Info.plist", NULL);
923         value = empathy_plist_parse_from_file (file);
924         g_free (file);
925
926         if (value == NULL)
927                 return NULL;
928
929         info = g_value_dup_boxed (value);
930         tp_g_value_slice_free (value);
931
932         /* Insert the theme's path into the hash table,
933          * keys have to be dupped */
934         tp_asv_set_string (info, g_strdup ("path"), path);
935
936         return info;
937 }
938
939 GType
940 empathy_adium_data_get_type (void)
941 {
942   static GType type_id = 0;
943
944   if (!type_id)
945     {
946       type_id = g_boxed_type_register_static ("EmpathyAdiumData",
947           (GBoxedCopyFunc) empathy_adium_data_ref,
948           (GBoxedFreeFunc) empathy_adium_data_unref);
949     }
950
951   return type_id;
952 }
953
954 EmpathyAdiumData  *
955 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
956 {
957         EmpathyAdiumData *data;
958         gchar            *file;
959         gchar            *template_html = NULL;
960         gsize             template_len;
961         gchar            *footer_html = NULL;
962         gsize             footer_len;
963         GString          *string;
964         gchar           **strv = NULL;
965         gchar            *css_path;
966         guint             len = 0;
967         guint             i = 0;
968
969         g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
970
971         data = g_slice_new0 (EmpathyAdiumData);
972         data->ref_count = 1;
973         data->path = g_strdup (path);
974         data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
975                 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
976         data->info = g_hash_table_ref (info);
977
978         /* Load html files */
979         file = g_build_filename (data->basedir, "Incoming", "Content.html", NULL);
980         g_file_get_contents (file, &data->in_content_html, &data->in_content_len, NULL);
981         g_free (file);
982
983         file = g_build_filename (data->basedir, "Incoming", "NextContent.html", NULL);
984         g_file_get_contents (file, &data->in_nextcontent_html, &data->in_nextcontent_len, NULL);
985         g_free (file);
986
987         file = g_build_filename (data->basedir, "Incoming", "Context.html", NULL);
988         g_file_get_contents (file, &data->in_context_html, &data->in_context_len, NULL);
989         g_free (file);
990
991         file = g_build_filename (data->basedir, "Incoming", "NextContext.html", NULL);
992         g_file_get_contents (file, &data->in_nextcontext_html, &data->in_nextcontext_len, NULL);
993         g_free (file);
994
995         file = g_build_filename (data->basedir, "Outgoing", "Content.html", NULL);
996         g_file_get_contents (file, &data->out_content_html, &data->out_content_len, NULL);
997         g_free (file);
998
999         file = g_build_filename (data->basedir, "Outgoing", "NextContent.html", NULL);
1000         g_file_get_contents (file, &data->out_nextcontent_html, &data->out_nextcontent_len, NULL);
1001         g_free (file);
1002
1003         file = g_build_filename (data->basedir, "Outgoing", "Context.html", NULL);
1004         g_file_get_contents (file, &data->out_context_html, &data->out_context_len, NULL);
1005         g_free (file);
1006
1007         file = g_build_filename (data->basedir, "Outgoing", "NextContext.html", NULL);
1008         g_file_get_contents (file, &data->out_nextcontext_html, &data->out_nextcontext_len, NULL);
1009         g_free (file);
1010
1011         file = g_build_filename (data->basedir, "Status.html", NULL);
1012         g_file_get_contents (file, &data->status_html, &data->status_len, NULL);
1013         g_free (file);
1014
1015         file = g_build_filename (data->basedir, "Footer.html", NULL);
1016         g_file_get_contents (file, &footer_html, &footer_len, NULL);
1017         g_free (file);
1018
1019         file = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1020         if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1021                 data->default_incoming_avatar_filename = file;
1022         } else {
1023                 g_free (file);
1024         }
1025
1026         file = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1027         if (g_file_test (file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1028                 data->default_outgoing_avatar_filename = file;
1029         } else {
1030                 g_free (file);
1031         }
1032
1033         css_path = g_build_filename (data->basedir, "main.css", NULL);
1034
1035         /* There is 2 formats for Template.html: The old one has 4 parameters,
1036          * the new one has 5 parameters. */
1037         file = g_build_filename (data->basedir, "Template.html", NULL);
1038         if (g_file_get_contents (file, &template_html, &template_len, NULL)) {
1039                 strv = g_strsplit (template_html, "%@", -1);
1040                 len = g_strv_length (strv);
1041         }
1042         g_free (file);
1043
1044         if (len != 5 && len != 6) {
1045                 /* Either the theme has no template or it don't have the good
1046                  * number of parameters. Fallback to use our own template. */
1047                 g_free (template_html);
1048                 g_strfreev (strv);
1049
1050                 file = empathy_file_lookup ("Template.html", "data");
1051                 g_file_get_contents (file, &template_html, &template_len, NULL);
1052                 g_free (file);
1053                 strv = g_strsplit (template_html, "%@", -1);
1054                 len = g_strv_length (strv);
1055         }
1056
1057         /* Replace %@ with the needed information in the template html. */
1058         string = g_string_sized_new (template_len);
1059         g_string_append (string, strv[i++]);
1060         g_string_append (string, data->basedir);
1061         g_string_append (string, strv[i++]);
1062         if (len == 6) {
1063                 const gchar *variant;
1064
1065                 /* We include main.css by default */
1066                 g_string_append_printf (string, "@import url(\"%s\");", css_path);
1067                 g_string_append (string, strv[i++]);
1068                 variant = tp_asv_get_string (data->info, "DefaultVariant");
1069                 if (variant) {
1070                         g_string_append (string, "Variants/");
1071                         g_string_append (string, variant);
1072                         g_string_append (string, ".css");
1073                 }
1074         } else {
1075                 /* FIXME: We should set main.css OR the variant css */
1076                 g_string_append (string, css_path);
1077         }
1078         g_string_append (string, strv[i++]);
1079         g_string_append (string, ""); /* We don't want header */
1080         g_string_append (string, strv[i++]);
1081         /* FIXME: We should replace adium %macros% in footer */
1082         if (footer_html) {
1083                 g_string_append (string, footer_html);
1084         }
1085         g_string_append (string, strv[i++]);
1086         data->template_html = g_string_free (string, FALSE);
1087
1088         g_free (footer_html);
1089         g_free (template_html);
1090         g_free (css_path);
1091         g_strfreev (strv);
1092
1093         return data;
1094 }
1095
1096 EmpathyAdiumData  *
1097 empathy_adium_data_new (const gchar *path)
1098 {
1099         EmpathyAdiumData *data;
1100         GHashTable *info;
1101
1102         info = empathy_adium_info_new (path);
1103         data = empathy_adium_data_new_with_info (path, info);
1104         g_hash_table_unref (info);
1105
1106         return data;
1107 }
1108
1109 EmpathyAdiumData  *
1110 empathy_adium_data_ref (EmpathyAdiumData *data)
1111 {
1112         g_return_val_if_fail (data != NULL, NULL);
1113
1114         g_atomic_int_inc (&data->ref_count);
1115
1116         return data;
1117 }
1118
1119 void
1120 empathy_adium_data_unref (EmpathyAdiumData *data)
1121 {
1122         g_return_if_fail (data != NULL);
1123
1124         if (g_atomic_int_dec_and_test (&data->ref_count)) {
1125                 g_free (data->path);
1126                 g_free (data->basedir);
1127                 g_free (data->template_html);
1128                 g_free (data->in_content_html);
1129                 g_free (data->in_nextcontent_html);
1130                 g_free (data->in_context_html);
1131                 g_free (data->in_nextcontext_html);
1132                 g_free (data->out_content_html);
1133                 g_free (data->out_nextcontent_html);
1134                 g_free (data->out_context_html);
1135                 g_free (data->out_nextcontext_html);
1136                 g_free (data->default_avatar_filename);
1137                 g_free (data->default_incoming_avatar_filename);
1138                 g_free (data->default_outgoing_avatar_filename);
1139                 g_free (data->status_html);
1140                 g_hash_table_unref (data->info);
1141                 g_slice_free (EmpathyAdiumData, data);
1142         }
1143 }
1144
1145 GHashTable *
1146 empathy_adium_data_get_info (EmpathyAdiumData *data)
1147 {
1148         g_return_val_if_fail (data != NULL, NULL);
1149
1150         return data->info;
1151 }
1152
1153 const gchar *
1154 empathy_adium_data_get_path (EmpathyAdiumData *data)
1155 {
1156         g_return_val_if_fail (data != NULL, NULL);
1157
1158         return data->path;
1159 }
1160