1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2008-2009 Collabora Ltd.
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.
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.
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
19 * Authors: Xavier Claessens <xclaesse@gmail.com>
25 #include <glib/gi18n-lib.h>
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
31 #include <pango/pango.h>
34 #include <libempathy/empathy-gsettings.h>
35 #include <libempathy/empathy-time.h>
36 #include <libempathy/empathy-utils.h>
38 #include "empathy-theme-adium.h"
39 #include "empathy-smiley-manager.h"
40 #include "empathy-ui-utils.h"
41 #include "empathy-plist.h"
42 #include "empathy-images.h"
43 #include "empathy-webkit-utils.h"
45 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
46 #include <libempathy/empathy-debug.h>
48 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
50 #define BORING_DPI_DEFAULT 96
52 /* "Join" consecutive messages with timestamps within five minutes */
53 #define MESSAGE_JOIN_PERIOD 5*60
56 EmpathyAdiumData *data;
57 EmpathySmileyManager *smiley_manager;
58 EmpathyContact *last_contact;
59 gint64 last_timestamp;
60 gboolean last_is_backlog;
62 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
64 /* Queue of guint32 of pending message id to remove unread
65 * marker for when we lose focus. */
66 GQueue acked_messages;
67 GtkWidget *inspector_window;
69 GSettings *gsettings_chat;
70 GSettings *gsettings_desktop;
73 gboolean has_unread_message;
74 gboolean allow_scrolling;
76 gboolean in_construction;
77 } EmpathyThemeAdiumPriv;
79 struct _EmpathyAdiumData {
83 gchar *default_avatar_filename;
84 gchar *default_incoming_avatar_filename;
85 gchar *default_outgoing_avatar_filename;
88 gboolean custom_template;
89 /* gchar* -> gchar* both owned */
90 GHashTable *date_format_cache;
93 const gchar *template_html;
94 const gchar *content_html;
95 const gchar *in_content_html;
96 const gchar *in_context_html;
97 const gchar *in_nextcontent_html;
98 const gchar *in_nextcontext_html;
99 const gchar *out_content_html;
100 const gchar *out_context_html;
101 const gchar *out_nextcontent_html;
102 const gchar *out_nextcontext_html;
103 const gchar *status_html;
105 /* Above html strings are pointers to strings stored in this array.
106 * We do this because of fallbacks, some htmls could be pointing the
108 GPtrArray *strings_to_free;
111 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
112 static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
120 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
121 WEBKIT_TYPE_WEB_VIEW,
122 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
123 theme_adium_iface_init));
138 queue_item (GQueue *queue,
143 QueuedItem *item = g_slice_new0 (QueuedItem);
147 item->msg = g_object_ref (msg);
148 item->str = g_strdup (str);
150 g_queue_push_tail (queue, item);
156 free_queued_item (QueuedItem *item)
158 tp_clear_object (&item->msg);
161 g_slice_free (QueuedItem, item);
165 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
167 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
168 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
169 gboolean enable_webkit_developer_tools;
171 enable_webkit_developer_tools = g_settings_get_boolean (
172 priv->gsettings_chat,
173 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
175 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
176 "enable-developer-extras",
177 enable_webkit_developer_tools,
182 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
186 EmpathyThemeAdium *theme = user_data;
188 theme_adium_update_enable_webkit_developer_tools (theme);
192 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
193 WebKitWebFrame *web_frame,
194 WebKitNetworkRequest *request,
195 WebKitWebNavigationAction *action,
196 WebKitWebPolicyDecision *decision,
201 /* Only call url_show on clicks */
202 if (webkit_web_navigation_action_get_reason (action) !=
203 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
204 webkit_web_policy_decision_use (decision);
208 uri = webkit_network_request_get_uri (request);
209 empathy_url_show (GTK_WIDGET (view), uri);
211 webkit_web_policy_decision_ignore (decision);
215 /* Replace each %@ in format with string passed in args */
217 string_with_format (const gchar *format,
218 const gchar *first_string,
225 va_start (args, first_string);
226 result = g_string_sized_new (strlen (format));
227 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
230 next = strstr (format, "%@");
235 g_string_append_len (result, format, next - format);
236 g_string_append (result, str);
239 g_string_append (result, format);
242 return g_string_free (result, FALSE);
246 theme_adium_load_template (EmpathyThemeAdium *theme)
248 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
253 priv->pages_loading++;
254 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
255 variant_path = adium_info_dup_path_for_variant (priv->data->info,
257 template = string_with_format (priv->data->template_html,
259 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
260 template, basedir_uri);
261 g_free (basedir_uri);
262 g_free (variant_path);
267 theme_adium_parse_body (EmpathyThemeAdium *self,
271 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
272 EmpathyStringParser *parsers;
275 /* Check if we have to parse smileys */
276 parsers = empathy_webkit_get_string_parser (
277 g_settings_get_boolean (priv->gsettings_chat,
278 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
280 /* Parse text and construct string with links and smileys replaced
281 * by html tags. Also escape text to make sure html code is
282 * displayed verbatim. */
283 string = g_string_sized_new (strlen (text));
285 /* wrap this in HTML that allows us to find the message for later
287 if (!tp_str_empty (token))
288 g_string_append_printf (string,
289 "<span id=\"message-token-%s\">",
292 empathy_string_parser_substr (text, -1, parsers, string);
294 if (!tp_str_empty (token))
295 g_string_append (string, "</span>");
297 /* Wrap body in order to make tabs and multiple spaces displayed
298 * properly. See bug #625745. */
299 g_string_prepend (string, "<div style=\"display: inline; "
300 "white-space: pre-wrap\"'>");
301 g_string_append (string, "</div>");
303 return g_string_free (string, FALSE);
307 escape_and_append_len (GString *string, const gchar *str, gint len)
309 while (str != NULL && *str != '\0' && len != 0) {
313 g_string_append (string, "\\\\");
317 g_string_append (string, "\\\"");
320 /* Remove end of lines */
323 g_string_append_c (string, *str);
331 /* If *str starts with match, returns TRUE and move pointer to the end */
333 theme_adium_match (const gchar **str,
338 len = strlen (match);
339 if (strncmp (*str, match, len) == 0) {
347 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
349 theme_adium_match_with_format (const gchar **str,
353 const gchar *cur = *str;
356 if (!theme_adium_match (&cur, match)) {
361 end = strstr (cur, "}%");
366 *format = g_strndup (cur , end - cur);
371 /* List of colors used by %senderColor%. Copied from
372 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
374 static gchar *colors[] = {
375 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
376 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
377 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
378 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
379 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
380 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
381 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
382 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
383 "lightblue", "lightcoral",
384 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
385 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
386 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
387 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
388 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
389 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
390 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
391 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
392 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
393 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
398 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
400 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
401 * to strftime supported by g_date_time_format.
402 * FIXME: table is incomplete, doc of g_date_time_format has a table of
404 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
405 * in 2.29.x we have to explictely request padding with %0x */
406 static const gchar *convert_table[] = {
408 "A", NULL, // 0~86399999 (Millisecond of Day)
410 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
411 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
412 "cc", "%u", // 1~7 (Day of Week)
413 "c", "%u", // 1~7 (Day of Week)
415 "dd", "%d", // 1~31 (0 padded Day of Month)
416 "d", "%d", // 1~31 (0 padded Day of Month)
417 "D", "%j", // 1~366 (0 padded Day of Year)
419 "e", "%u", // 1~7 (0 padded Day of Week)
420 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
421 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
422 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
423 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
425 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
427 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
428 "GGGG", NULL, // Before Christ/Anno Domini
429 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
430 "GG", NULL, // BC/AD (Era Designator Abbreviated)
431 "G", NULL, // BC/AD (Era Designator Abbreviated)
433 "h", "%I", // 1~12 (0 padded Hour (12hr))
434 "H", "%H", // 0~23 (0 padded Hour (24hr))
436 "k", NULL, // 1~24 (0 padded Hour (24hr)
437 "K", NULL, // 0~11 (0 padded Hour (12hr))
439 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
440 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
441 "LL", "%m", // 1~12 (0 padded Month)
442 "L", "%m", // 1~12 (0 padded Month)
444 "m", "%M", // 0~59 (0 padded Minute)
445 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
446 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
447 "MM", "%m", // 1~12 (0 padded Month)
448 "M", "%m", // 1~12 (0 padded Month)
450 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
451 "qqq", NULL, // Q1/Q2/Q3/Q4
452 "qq", NULL, // 1~4 (0 padded Quarter)
453 "q", NULL, // 1~4 (0 padded Quarter)
454 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
455 "QQQ", NULL, // Q1/Q2/Q3/Q4
456 "QQ", NULL, // 1~4 (0 padded Quarter)
457 "Q", NULL, // 1~4 (0 padded Quarter)
459 "s", "%S", // 0~59 (0 padded Second)
460 "S", NULL, // (rounded Sub-Second)
462 "u", "%Y", // (0 padded Year)
464 "vvvv", "%Z", // (General GMT Timezone Name)
465 "vvv", "%Z", // (General GMT Timezone Abbreviation)
466 "vv", "%Z", // (General GMT Timezone Abbreviation)
467 "v", "%Z", // (General GMT Timezone Abbreviation)
469 "w", "%W", // 1~53 (0 padded Week of Year, 1st day of week = Sunday, NB, 1st week of year starts from the last Sunday of last year)
470 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
472 "yyyy", "%Y", // (Full Year)
473 "yyy", "%y", // (2 Digits Year)
474 "yy", "%y", // (2 Digits Year)
475 "y", "%Y", // (Full Year)
476 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
477 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
478 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
479 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
481 "zzzz", NULL, // (Specific GMT Timezone Name)
482 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
483 "zz", NULL, // (Specific GMT Timezone Abbreviation)
484 "z", NULL, // (Specific GMT Timezone Abbreviation)
485 "Z", "%z", // +0000 (RFC 822 Timezone)
491 if (nsdate == NULL) {
495 str = g_hash_table_lookup (data->date_format_cache, nsdate);
500 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
501 * by corresponding strftime tag. */
502 string = g_string_sized_new (strlen (nsdate));
503 for (i = 0; nsdate[i] != '\0'; i++) {
504 gboolean found = FALSE;
506 /* even indexes are NSDateFormatter tag, odd indexes are the
507 * corresponding strftime tag */
508 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
509 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
515 /* If we don't have a replacement, just ignore that tag */
516 if (convert_table[j + 1] != NULL) {
517 g_string_append (string, convert_table[j + 1]);
519 i += strlen (convert_table[j]) - 1;
521 g_string_append_c (string, nsdate[i]);
525 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
527 /* The cache takes ownership of string->str */
528 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
529 return g_string_free (string, FALSE);
534 theme_adium_append_html (EmpathyThemeAdium *theme,
537 const gchar *message,
538 const gchar *avatar_filename,
540 const gchar *contact_id,
541 const gchar *service_name,
542 const gchar *message_classes,
546 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
548 const gchar *cur = NULL;
551 /* Make some search-and-replace in the html code */
552 string = g_string_sized_new (strlen (html) + strlen (message));
553 g_string_append_printf (string, "%s(\"", func);
554 for (cur = html; *cur != '\0'; cur++) {
555 const gchar *replace = NULL;
556 gchar *dup_replace = NULL;
557 gchar *format = NULL;
559 /* Those are all well known keywords that needs replacement in
560 * html files. Please keep them in the same order than the adium
561 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
562 if (theme_adium_match (&cur, "%userIconPath%")) {
563 replace = avatar_filename;
564 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
565 replace = contact_id;
566 } else if (theme_adium_match (&cur, "%sender%")) {
568 } else if (theme_adium_match (&cur, "%senderColor%")) {
569 /* A color derived from the user's name.
570 * FIXME: If a colon separated list of HTML colors is at
571 * Incoming/SenderColors.txt it will be used instead of
572 * the default colors.
574 if (contact_id != NULL) {
575 guint hash = g_str_hash (contact_id);
576 replace = colors[hash % G_N_ELEMENTS (colors)];
578 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
579 /* FIXME: The path to the status icon of the sender
580 * (available, away, etc...)
582 } else if (theme_adium_match (&cur, "%messageDirection%")) {
583 /* FIXME: The text direction of the message
584 * (either rtl or ltr)
586 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
587 /* FIXME: The serverside (remotely set) name of the
588 * sender, such as an MSN display name.
590 * We don't have access to that yet so we use
591 * local alias instead.
594 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
595 /* FIXME: This keyword is used to represent the
596 * highlight background color. "X" is the opacity of the
597 * background, ranges from 0 to 1 and can be any decimal
600 } else if (theme_adium_match (&cur, "%message%")) {
602 } else if (theme_adium_match (&cur, "%time%") ||
603 theme_adium_match_with_format (&cur, "%time{", &format)) {
604 const gchar *strftime_format;
606 strftime_format = nsdate_to_strftime (priv->data, format);
608 dup_replace = empathy_time_to_string_local (timestamp,
609 strftime_format ? strftime_format :
610 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
612 dup_replace = empathy_time_to_string_local (timestamp,
613 strftime_format ? strftime_format :
614 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
616 replace = dup_replace;
617 } else if (theme_adium_match (&cur, "%shortTime%")) {
618 dup_replace = empathy_time_to_string_local (timestamp,
619 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
620 replace = dup_replace;
621 } else if (theme_adium_match (&cur, "%service%")) {
622 replace = service_name;
623 } else if (theme_adium_match (&cur, "%variant%")) {
624 /* FIXME: The name of the active message style variant,
625 * with all spaces replaced with an underscore.
626 * A variant named "Alternating Messages - Blue Red"
627 * will become "Alternating_Messages_-_Blue_Red".
629 } else if (theme_adium_match (&cur, "%userIcons%")) {
630 /* FIXME: mus t be "hideIcons" if use preference is set
632 replace = "showIcons";
633 } else if (theme_adium_match (&cur, "%messageClasses%")) {
634 replace = message_classes;
635 } else if (theme_adium_match (&cur, "%status%")) {
636 /* FIXME: A description of the status event. This is
637 * neither in the user's local language nor expected to
638 * be displayed; it may be useful to use a different div
639 * class to present different types of status messages.
640 * The following is a list of some of the more important
641 * status messages; your message style should be able to
642 * handle being shown a status message not in this list,
643 * as even at present the list is incomplete and is
644 * certain to become out of date in the future:
653 * contact_joined (group chats)
657 * encryption (all OTR messages use this status)
658 * purple (all IRC topic and join/part messages use this status)
659 * fileTransferStarted
660 * fileTransferCompleted
663 escape_and_append_len (string, cur, 1);
667 /* Here we have a replacement to make */
668 escape_and_append_len (string, replace, -1);
670 g_free (dup_replace);
673 g_string_append (string, "\")");
675 script = g_string_free (string, FALSE);
676 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
681 theme_adium_append_event_escaped (EmpathyChatView *view,
682 const gchar *escaped)
684 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
685 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
687 theme_adium_append_html (theme, "appendMessage",
688 priv->data->status_html, escaped, NULL, NULL, NULL,
690 empathy_time_get_current (), FALSE);
692 /* There is no last contact */
693 if (priv->last_contact) {
694 g_object_unref (priv->last_contact);
695 priv->last_contact = NULL;
700 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
701 WebKitDOMNodeList *nodes)
705 /* Remove focus and firstFocus class */
706 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
707 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
708 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
710 gchar **classes, **iter;
711 GString *new_class_name;
712 gboolean first = TRUE;
714 if (element == NULL) {
718 class_name = webkit_dom_html_element_get_class_name (element);
719 classes = g_strsplit (class_name, " ", -1);
720 new_class_name = g_string_sized_new (strlen (class_name));
721 for (iter = classes; *iter != NULL; iter++) {
722 if (tp_strdiff (*iter, "focus") &&
723 tp_strdiff (*iter, "firstFocus")) {
725 g_string_append_c (new_class_name, ' ');
727 g_string_append (new_class_name, *iter);
732 webkit_dom_html_element_set_class_name (element, new_class_name->str);
735 g_strfreev (classes);
736 g_string_free (new_class_name, TRUE);
741 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
743 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
744 WebKitDOMDocument *dom;
745 WebKitDOMNodeList *nodes;
746 GError *error = NULL;
748 if (!priv->has_unread_message)
751 priv->has_unread_message = FALSE;
753 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
758 /* Get all nodes with focus class */
759 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
761 DEBUG ("Error getting focus nodes: %s",
762 error ? error->message : "No error");
763 g_clear_error (&error);
767 theme_adium_remove_focus_marks (theme, nodes);
771 theme_adium_append_message (EmpathyChatView *view,
774 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
775 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
776 EmpathyContact *sender;
781 const gchar *contact_id;
782 EmpathyAvatar *avatar;
783 const gchar *avatar_filename = NULL;
785 const gchar *html = NULL;
787 const gchar *service_name;
788 GString *message_classes = NULL;
790 gboolean consecutive;
793 if (priv->pages_loading != 0) {
794 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL);
798 /* Get information */
799 sender = empathy_message_get_sender (msg);
800 account = empathy_contact_get_account (sender);
801 service_name = empathy_protocol_name_to_display_name
802 (tp_account_get_protocol (account));
803 if (service_name == NULL)
804 service_name = tp_account_get_protocol (account);
805 timestamp = empathy_message_get_timestamp (msg);
806 body_escaped = theme_adium_parse_body (theme,
807 empathy_message_get_body (msg),
808 empathy_message_get_token (msg));
809 name = empathy_contact_get_logged_alias (sender);
810 contact_id = empathy_contact_get_id (sender);
811 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
813 /* If this is a /me probably */
817 if (priv->data->version >= 4 || !priv->data->custom_template) {
818 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
819 "<span class='actionMessageBody'>%s</span>",
822 str = g_strdup_printf ("*%s*", body_escaped);
824 g_free (body_escaped);
828 /* Get the avatar filename, or a fallback */
829 avatar = empathy_contact_get_avatar (sender);
831 avatar_filename = avatar->filename;
833 if (!avatar_filename) {
834 if (empathy_contact_is_user (sender)) {
835 avatar_filename = priv->data->default_outgoing_avatar_filename;
837 avatar_filename = priv->data->default_incoming_avatar_filename;
839 if (!avatar_filename) {
840 if (!priv->data->default_avatar_filename) {
841 priv->data->default_avatar_filename =
842 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
843 GTK_ICON_SIZE_DIALOG);
845 avatar_filename = priv->data->default_avatar_filename;
849 /* We want to join this message with the last one if
850 * - senders are the same contact,
851 * - last message was recieved recently,
852 * - last message and this message both are/aren't backlog, and
853 * - DisableCombineConsecutive is not set in theme's settings */
854 is_backlog = empathy_message_is_backlog (msg);
855 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
856 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
857 (is_backlog == priv->last_is_backlog) &&
858 !tp_asv_get_boolean (priv->data->info,
859 "DisableCombineConsecutive", NULL);
861 /* Define message classes */
862 message_classes = g_string_new ("message");
863 if (!priv->has_focus && !is_backlog) {
864 if (!priv->has_unread_message) {
865 g_string_append (message_classes, " firstFocus");
866 priv->has_unread_message = TRUE;
868 g_string_append (message_classes, " focus");
871 g_string_append (message_classes, " history");
874 g_string_append (message_classes, " consecutive");
876 if (empathy_contact_is_user (sender)) {
877 g_string_append (message_classes, " outgoing");
879 g_string_append (message_classes, " incoming");
881 if (empathy_message_should_highlight (msg)) {
882 g_string_append (message_classes, " mention");
884 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
885 g_string_append (message_classes, " autoreply");
888 g_string_append (message_classes, " action");
890 /* FIXME: other classes:
891 * status - the message is a status change
892 * event - the message is a notification of something happening
893 * (for example, encryption being turned on)
894 * %status% - See %status% in theme_adium_append_html ()
897 /* This is slightly a hack, but it's the only way to add
898 * arbitrary data to messages in the HTML. We add another
899 * class called "x-empathy-message-id-*" to the message. This
900 * way, we can remove the unread marker for this specific
902 tp_msg = empathy_message_get_tp_message (msg);
903 if (tp_msg != NULL) {
907 id = tp_message_get_pending_message_id (tp_msg, &valid);
909 g_string_append_printf (message_classes,
910 " x-empathy-message-id-%u", id);
914 /* Define javascript function to use */
916 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
918 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
921 if (empathy_contact_is_user (sender)) {
925 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
928 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
931 /* remove all the unread marks when we are sending a message */
932 theme_adium_remove_all_focus_marks (theme);
937 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
940 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
944 theme_adium_append_html (theme, func, html, body_escaped,
945 avatar_filename, name, contact_id,
946 service_name, message_classes->str,
947 timestamp, is_backlog);
949 /* Keep the sender of the last displayed message */
950 if (priv->last_contact) {
951 g_object_unref (priv->last_contact);
953 priv->last_contact = g_object_ref (sender);
954 priv->last_timestamp = timestamp;
955 priv->last_is_backlog = is_backlog;
957 g_free (body_escaped);
958 g_string_free (message_classes, TRUE);
962 theme_adium_append_event (EmpathyChatView *view,
965 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
968 if (priv->pages_loading != 0) {
969 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str);
973 str_escaped = g_markup_escape_text (str, -1);
974 theme_adium_append_event_escaped (view, str_escaped);
975 g_free (str_escaped);
979 theme_adium_edit_message (EmpathyChatView *view,
980 EmpathyMessage *message)
982 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
983 WebKitDOMDocument *doc;
984 WebKitDOMElement *span;
985 gchar *id, *parsed_body;
986 gchar *tooltip, *timestamp;
987 GtkIconInfo *icon_info;
988 GError *error = NULL;
990 if (priv->pages_loading != 0) {
991 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL);
995 id = g_strdup_printf ("message-token-%s",
996 empathy_message_get_supersedes (message));
997 /* we don't pass a token here, because doing so will return another
998 * <span> element, and we don't want nested <span> elements */
999 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1000 empathy_message_get_body (message), NULL);
1002 /* find the element */
1003 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1004 span = webkit_dom_document_get_element_by_id (doc, id);
1007 DEBUG ("Failed to find id '%s'", id);
1011 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1012 DEBUG ("Not a HTML element");
1016 /* update the HTML */
1017 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1018 parsed_body, &error);
1020 if (error != NULL) {
1021 DEBUG ("Error setting new inner-HTML: %s", error->message);
1022 g_error_free (error);
1027 timestamp = empathy_time_to_string_local (
1028 empathy_message_get_timestamp (message),
1030 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1032 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1038 /* mark this message as edited */
1039 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1040 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1042 if (icon_info != NULL) {
1043 /* set the icon as a background image using CSS
1044 * FIXME: the icon won't update in response to theme changes */
1045 gchar *style = g_strdup_printf (
1046 "background-image:url('%s');"
1047 "background-repeat:no-repeat;"
1048 "background-position:left center;"
1049 "padding-left:19px;", /* 16px icon + 3px padding */
1050 gtk_icon_info_get_filename (icon_info));
1052 webkit_dom_element_set_attribute (span, "style", style, &error);
1054 if (error != NULL) {
1055 DEBUG ("Error setting element style: %s",
1057 g_clear_error (&error);
1062 gtk_icon_info_free (icon_info);
1068 DEBUG ("Could not find message to edit with: %s",
1069 empathy_message_get_body (message));
1073 g_free (parsed_body);
1077 theme_adium_scroll (EmpathyChatView *view,
1078 gboolean allow_scrolling)
1080 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1082 priv->allow_scrolling = allow_scrolling;
1083 if (allow_scrolling) {
1084 empathy_chat_view_scroll_down (view);
1089 theme_adium_scroll_down (EmpathyChatView *view)
1091 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1095 theme_adium_get_has_selection (EmpathyChatView *view)
1097 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1101 theme_adium_clear (EmpathyChatView *view)
1103 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1105 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1107 /* Clear last contact to avoid trying to add a 'joined'
1108 * message when we don't have an insertion point. */
1109 if (priv->last_contact) {
1110 g_object_unref (priv->last_contact);
1111 priv->last_contact = NULL;
1116 theme_adium_find_previous (EmpathyChatView *view,
1117 const gchar *search_criteria,
1118 gboolean new_search,
1119 gboolean match_case)
1121 /* FIXME: Doesn't respect new_search */
1122 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1123 search_criteria, match_case,
1128 theme_adium_find_next (EmpathyChatView *view,
1129 const gchar *search_criteria,
1130 gboolean new_search,
1131 gboolean match_case)
1133 /* FIXME: Doesn't respect new_search */
1134 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1135 search_criteria, match_case,
1140 theme_adium_find_abilities (EmpathyChatView *view,
1141 const gchar *search_criteria,
1142 gboolean match_case,
1143 gboolean *can_do_previous,
1144 gboolean *can_do_next)
1146 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1147 * find_next and find_previous to work around this problem. */
1148 if (can_do_previous)
1149 *can_do_previous = TRUE;
1151 *can_do_next = TRUE;
1155 theme_adium_highlight (EmpathyChatView *view,
1157 gboolean match_case)
1159 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1160 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1161 text, match_case, 0);
1162 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1167 theme_adium_copy_clipboard (EmpathyChatView *view)
1169 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1173 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1176 WebKitDOMDocument *dom;
1177 WebKitDOMNodeList *nodes;
1179 GError *error = NULL;
1181 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1186 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1188 /* Get all nodes with focus class */
1189 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1192 if (nodes == NULL) {
1193 DEBUG ("Error getting focus nodes: %s",
1194 error ? error->message : "No error");
1195 g_clear_error (&error);
1199 theme_adium_remove_focus_marks (self, nodes);
1203 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1206 EmpathyThemeAdium *self = user_data;
1207 guint32 id = GPOINTER_TO_UINT (data);
1209 theme_adium_remove_mark_from_message (self, id);
1213 theme_adium_focus_toggled (EmpathyChatView *view,
1216 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1218 priv->has_focus = has_focus;
1219 if (!priv->has_focus) {
1220 /* We've lost focus, so let's make sure all the acked
1221 * messages have lost their unread marker. */
1222 g_queue_foreach (&priv->acked_messages,
1223 theme_adium_remove_acked_message_unread_mark_foreach,
1225 g_queue_clear (&priv->acked_messages);
1227 priv->has_unread_message = FALSE;
1232 theme_adium_message_acknowledged (EmpathyChatView *view,
1233 EmpathyMessage *message)
1235 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1236 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1241 tp_msg = empathy_message_get_tp_message (message);
1243 if (tp_msg == NULL) {
1247 id = tp_message_get_pending_message_id (tp_msg, &valid);
1249 g_warning ("Acknoledged message doesn't have a pending ID");
1253 /* We only want to actually remove the unread marker if the
1254 * view doesn't have focus. If we did it all the time we would
1255 * never see the unread markers, ever! So, we'll queue these
1256 * up, and when we lose focus, we'll remove the markers. */
1257 if (priv->has_focus) {
1258 g_queue_push_tail (&priv->acked_messages,
1259 GUINT_TO_POINTER (id));
1263 theme_adium_remove_mark_from_message (self, id);
1267 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1269 if (event->button == 3) {
1270 gboolean developer_tools_enabled;
1272 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1273 "enable-developer-extras", &developer_tools_enabled, NULL);
1275 /* We currently have no way to add an inspector menu
1276 * item ourselves, so we disable our customized menu
1277 * if the developer extras are enabled. */
1278 if (!developer_tools_enabled) {
1279 empathy_webkit_context_menu_for_event (
1280 WEBKIT_WEB_VIEW (widget), event,
1281 EMPATHY_WEBKIT_MENU_CLEAR);
1286 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1290 theme_adium_iface_init (EmpathyChatViewIface *iface)
1292 iface->append_message = theme_adium_append_message;
1293 iface->append_event = theme_adium_append_event;
1294 iface->edit_message = theme_adium_edit_message;
1295 iface->scroll = theme_adium_scroll;
1296 iface->scroll_down = theme_adium_scroll_down;
1297 iface->get_has_selection = theme_adium_get_has_selection;
1298 iface->clear = theme_adium_clear;
1299 iface->find_previous = theme_adium_find_previous;
1300 iface->find_next = theme_adium_find_next;
1301 iface->find_abilities = theme_adium_find_abilities;
1302 iface->highlight = theme_adium_highlight;
1303 iface->copy_clipboard = theme_adium_copy_clipboard;
1304 iface->focus_toggled = theme_adium_focus_toggled;
1305 iface->message_acknowledged = theme_adium_message_acknowledged;
1309 theme_adium_load_finished_cb (WebKitWebView *view,
1310 WebKitWebFrame *frame,
1313 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1314 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1317 DEBUG ("Page loaded");
1318 priv->pages_loading--;
1320 if (priv->pages_loading != 0)
1323 /* Display queued messages */
1324 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1325 QueuedItem *item = l->data;
1329 case QUEUED_MESSAGE:
1330 theme_adium_append_message (chat_view, item->msg);
1334 theme_adium_edit_message (chat_view, item->msg);
1338 theme_adium_append_event (chat_view, item->str);
1342 free_queued_item (item);
1345 g_queue_clear (&priv->message_queue);
1349 theme_adium_finalize (GObject *object)
1351 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1353 empathy_adium_data_unref (priv->data);
1355 g_object_unref (priv->gsettings_chat);
1356 g_object_unref (priv->gsettings_desktop);
1358 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1362 theme_adium_dispose (GObject *object)
1364 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1366 if (priv->smiley_manager) {
1367 g_object_unref (priv->smiley_manager);
1368 priv->smiley_manager = NULL;
1371 if (priv->last_contact) {
1372 g_object_unref (priv->last_contact);
1373 priv->last_contact = NULL;
1376 if (priv->inspector_window) {
1377 gtk_widget_destroy (priv->inspector_window);
1378 priv->inspector_window = NULL;
1381 if (priv->acked_messages.length > 0) {
1382 g_queue_clear (&priv->acked_messages);
1385 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1389 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1390 EmpathyThemeAdium *theme)
1392 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1394 if (priv->inspector_window) {
1395 gtk_widget_show_all (priv->inspector_window);
1402 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1403 EmpathyThemeAdium *theme)
1405 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1407 if (priv->inspector_window) {
1408 gtk_widget_hide (priv->inspector_window);
1414 static WebKitWebView *
1415 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1416 WebKitWebView *web_view,
1417 EmpathyThemeAdium *theme)
1419 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1420 GtkWidget *scrolled_window;
1421 GtkWidget *inspector_web_view;
1423 if (!priv->inspector_window) {
1424 /* Create main window */
1425 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1426 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1428 g_signal_connect (priv->inspector_window, "delete-event",
1429 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1431 /* Pack a scrolled window */
1432 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1433 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1434 GTK_POLICY_AUTOMATIC,
1435 GTK_POLICY_AUTOMATIC);
1436 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1438 gtk_widget_show (scrolled_window);
1440 /* Pack a webview in the scrolled window. That webview will be
1441 * used to render the inspector tool. */
1442 inspector_web_view = webkit_web_view_new ();
1443 gtk_container_add (GTK_CONTAINER (scrolled_window),
1444 inspector_web_view);
1445 gtk_widget_show (scrolled_window);
1447 return WEBKIT_WEB_VIEW (inspector_web_view);
1454 theme_adium_constructed (GObject *object)
1456 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1457 const gchar *font_family = NULL;
1459 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1460 WebKitWebInspector *webkit_inspector;
1462 /* Set default settings */
1463 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1464 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1466 if (font_family && font_size) {
1467 g_object_set (webkit_web_view_get_settings (webkit_view),
1468 "default-font-family", font_family,
1469 "default-font-size", font_size,
1472 empathy_webkit_bind_font_setting (webkit_view,
1473 priv->gsettings_desktop,
1474 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1477 /* Setup webkit inspector */
1478 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1479 g_signal_connect (webkit_inspector, "inspect-web-view",
1480 G_CALLBACK (theme_adium_inspect_web_view_cb),
1482 g_signal_connect (webkit_inspector, "show-window",
1483 G_CALLBACK (theme_adium_inspector_show_window_cb),
1485 g_signal_connect (webkit_inspector, "close-window",
1486 G_CALLBACK (theme_adium_inspector_close_window_cb),
1490 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1492 priv->in_construction = FALSE;
1496 theme_adium_get_property (GObject *object,
1501 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1504 case PROP_ADIUM_DATA:
1505 g_value_set_boxed (value, priv->data);
1508 g_value_set_string (value, priv->variant);
1511 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1517 theme_adium_set_property (GObject *object,
1519 const GValue *value,
1522 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1523 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1526 case PROP_ADIUM_DATA:
1527 g_assert (priv->data == NULL);
1528 priv->data = g_value_dup_boxed (value);
1531 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1534 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1540 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1542 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1543 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1545 object_class->finalize = theme_adium_finalize;
1546 object_class->dispose = theme_adium_dispose;
1547 object_class->constructed = theme_adium_constructed;
1548 object_class->get_property = theme_adium_get_property;
1549 object_class->set_property = theme_adium_set_property;
1551 widget_class->button_press_event = theme_adium_button_press_event;
1553 g_object_class_install_property (object_class,
1555 g_param_spec_boxed ("adium-data",
1557 "Data for the adium theme",
1558 EMPATHY_TYPE_ADIUM_DATA,
1559 G_PARAM_CONSTRUCT_ONLY |
1561 G_PARAM_STATIC_STRINGS));
1562 g_object_class_install_property (object_class,
1564 g_param_spec_string ("variant",
1565 "The theme variant",
1566 "Variant name for the theme",
1570 G_PARAM_STATIC_STRINGS));
1572 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1576 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1578 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1579 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1583 priv->in_construction = TRUE;
1584 g_queue_init (&priv->message_queue);
1585 priv->allow_scrolling = TRUE;
1586 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1588 g_signal_connect (theme, "load-finished",
1589 G_CALLBACK (theme_adium_load_finished_cb),
1591 g_signal_connect (theme, "navigation-policy-decision-requested",
1592 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1595 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1596 priv->gsettings_desktop = g_settings_new (
1597 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1599 g_signal_connect (priv->gsettings_chat,
1600 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1601 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1604 theme_adium_update_enable_webkit_developer_tools (theme);
1608 empathy_theme_adium_new (EmpathyAdiumData *data,
1609 const gchar *variant)
1611 g_return_val_if_fail (data != NULL, NULL);
1613 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1620 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1621 const gchar *variant)
1623 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1624 gchar *variant_path;
1627 if (!tp_strdiff (priv->variant, variant)) {
1631 g_free (priv->variant);
1632 priv->variant = g_strdup (variant);
1634 if (priv->in_construction) {
1638 DEBUG ("Update view with variant: '%s'", variant);
1639 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1641 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1643 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1645 g_free (variant_path);
1648 g_object_notify (G_OBJECT (theme), "variant");
1652 empathy_adium_path_is_valid (const gchar *path)
1657 /* The theme is not valid if there is no Info.plist */
1658 file = g_build_filename (path, "Contents", "Info.plist",
1660 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1666 /* We ship a default Template.html as fallback if there is any problem
1667 * with the one inside the theme. The only other required file is
1668 * Content.html OR Incoming/Content.html*/
1669 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1671 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1675 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1676 "Content.html", NULL);
1677 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1685 empathy_adium_info_new (const gchar *path)
1689 GHashTable *info = NULL;
1691 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1693 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1694 value = empathy_plist_parse_from_file (file);
1700 info = g_value_dup_boxed (value);
1701 tp_g_value_slice_free (value);
1703 /* Insert the theme's path into the hash table,
1704 * keys have to be dupped */
1705 tp_asv_set_string (info, g_strdup ("path"), path);
1711 adium_info_get_version (GHashTable *info)
1713 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1716 static const gchar *
1717 adium_info_get_no_variant_name (GHashTable *info)
1719 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1720 return name ? name : _("Normal");
1724 adium_info_dup_path_for_variant (GHashTable *info,
1725 const gchar *variant)
1727 guint version = adium_info_get_version (info);
1728 const gchar *no_variant = adium_info_get_no_variant_name (info);
1729 GPtrArray *variants;
1732 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1733 return g_strdup ("main.css");
1736 /* Verify the variant exists, fallback to the first one */
1737 variants = empathy_adium_info_get_available_variants (info);
1738 for (i = 0; i < variants->len; i++) {
1739 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1743 if (i == variants->len) {
1744 DEBUG ("Variant %s does not exist", variant);
1745 variant = g_ptr_array_index (variants, 0);
1748 return g_strdup_printf ("Variants/%s.css", variant);
1753 empathy_adium_info_get_default_variant (GHashTable *info)
1755 if (adium_info_get_version (info) <= 2) {
1756 return adium_info_get_no_variant_name (info);
1759 return tp_asv_get_string (info, "DefaultVariant");
1763 empathy_adium_info_get_available_variants (GHashTable *info)
1765 GPtrArray *variants;
1770 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1771 if (variants != NULL) {
1775 variants = g_ptr_array_new_with_free_func (g_free);
1776 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1777 G_TYPE_PTR_ARRAY, variants);
1779 path = tp_asv_get_string (info, "path");
1780 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1781 dir = g_dir_open (dirpath, 0, NULL);
1785 for (name = g_dir_read_name (dir);
1787 name = g_dir_read_name (dir)) {
1788 gchar *display_name;
1790 if (!g_str_has_suffix (name, ".css")) {
1794 display_name = g_strdup (name);
1795 strstr (display_name, ".css")[0] = '\0';
1796 g_ptr_array_add (variants, display_name);
1802 if (adium_info_get_version (info) <= 2) {
1803 g_ptr_array_add (variants,
1804 g_strdup (adium_info_get_no_variant_name (info)));
1811 empathy_adium_data_get_type (void)
1813 static GType type_id = 0;
1817 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1818 (GBoxedCopyFunc) empathy_adium_data_ref,
1819 (GBoxedFreeFunc) empathy_adium_data_unref);
1826 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1828 EmpathyAdiumData *data;
1829 gchar *template_html = NULL;
1830 gchar *footer_html = NULL;
1833 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1835 data = g_slice_new0 (EmpathyAdiumData);
1836 data->ref_count = 1;
1837 data->path = g_strdup (path);
1838 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1839 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1840 data->info = g_hash_table_ref (info);
1841 data->version = adium_info_get_version (info);
1842 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1843 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1844 g_str_equal, g_free, g_free);
1846 DEBUG ("Loading theme at %s", path);
1848 #define LOAD(path, var) \
1849 tmp = g_build_filename (data->basedir, path, NULL); \
1850 g_file_get_contents (tmp, &var, NULL, NULL); \
1853 #define LOAD_CONST(path, var) \
1856 LOAD (path, content); \
1857 if (content != NULL) { \
1858 g_ptr_array_add (data->strings_to_free, content); \
1863 /* Load html files */
1864 LOAD_CONST ("Content.html", data->content_html);
1865 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1866 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1867 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1868 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1869 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1870 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1871 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1872 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1873 LOAD_CONST ("Status.html", data->status_html);
1874 LOAD ("Template.html", template_html);
1875 LOAD ("Footer.html", footer_html);
1880 /* HTML fallbacks: If we have at least content OR in_content, then
1881 * everything else gets a fallback */
1883 #define FALLBACK(html, fallback) \
1884 if (html == NULL) { \
1888 /* in_nextcontent -> in_content -> content */
1889 FALLBACK (data->in_content_html, data->content_html);
1890 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1892 /* context -> content */
1893 FALLBACK (data->in_context_html, data->in_content_html);
1894 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1895 FALLBACK (data->out_context_html, data->out_content_html);
1896 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1899 FALLBACK (data->out_content_html, data->in_content_html);
1900 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1901 FALLBACK (data->out_context_html, data->in_context_html);
1902 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1904 /* status -> in_content */
1905 FALLBACK (data->status_html, data->in_content_html);
1909 /* template -> empathy's template */
1910 data->custom_template = (template_html != NULL);
1911 if (template_html == NULL) {
1912 tmp = empathy_file_lookup ("Template.html", "data");
1913 g_file_get_contents (tmp, &template_html, NULL, NULL);
1917 /* Default avatar */
1918 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1919 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1920 data->default_incoming_avatar_filename = tmp;
1924 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1925 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1926 data->default_outgoing_avatar_filename = tmp;
1931 /* Old custom templates had only 4 parameters.
1932 * New templates have 5 parameters */
1933 if (data->version <= 2 && data->custom_template) {
1934 tmp = string_with_format (template_html,
1936 "%@", /* Leave variant unset */
1937 "", /* The header */
1938 footer_html ? footer_html : "",
1941 tmp = string_with_format (template_html,
1943 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1944 "%@", /* Leave variant unset */
1945 "", /* The header */
1946 footer_html ? footer_html : "",
1949 g_ptr_array_add (data->strings_to_free, tmp);
1950 data->template_html = tmp;
1952 g_free (template_html);
1953 g_free (footer_html);
1959 empathy_adium_data_new (const gchar *path)
1961 EmpathyAdiumData *data;
1964 info = empathy_adium_info_new (path);
1965 data = empathy_adium_data_new_with_info (path, info);
1966 g_hash_table_unref (info);
1972 empathy_adium_data_ref (EmpathyAdiumData *data)
1974 g_return_val_if_fail (data != NULL, NULL);
1976 g_atomic_int_inc (&data->ref_count);
1982 empathy_adium_data_unref (EmpathyAdiumData *data)
1984 g_return_if_fail (data != NULL);
1986 if (g_atomic_int_dec_and_test (&data->ref_count)) {
1987 g_free (data->path);
1988 g_free (data->basedir);
1989 g_free (data->default_avatar_filename);
1990 g_free (data->default_incoming_avatar_filename);
1991 g_free (data->default_outgoing_avatar_filename);
1992 g_hash_table_unref (data->info);
1993 g_ptr_array_unref (data->strings_to_free);
1994 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
1996 g_slice_free (EmpathyAdiumData, data);
2001 empathy_adium_data_get_info (EmpathyAdiumData *data)
2003 g_return_val_if_fail (data != NULL, NULL);
2009 empathy_adium_data_get_path (EmpathyAdiumData *data)
2011 g_return_val_if_fail (data != NULL, NULL);