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