2 * Copyright (C) 2008-2012 Collabora Ltd.
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 * Authors: Xavier Claessens <xclaesse@gmail.com>
24 #include <glib/gi18n-lib.h>
26 #include <webkit/webkit.h>
27 #include <telepathy-glib/dbus.h>
28 #include <telepathy-glib/util.h>
30 #include <pango/pango.h>
33 #include <libempathy/empathy-gsettings.h>
34 #include <libempathy/empathy-time.h>
35 #include <libempathy/empathy-utils.h>
37 #include "empathy-theme-adium.h"
38 #include "empathy-smiley-manager.h"
39 #include "empathy-ui-utils.h"
40 #include "empathy-plist.h"
41 #include "empathy-images.h"
42 #include "empathy-webkit-utils.h"
44 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
45 #include <libempathy/empathy-debug.h>
47 #define BORING_DPI_DEFAULT 96
49 /* "Join" consecutive messages with timestamps within five minutes */
50 #define MESSAGE_JOIN_PERIOD 5*60
52 struct _EmpathyThemeAdiumPriv
54 EmpathyAdiumData *data;
55 EmpathySmileyManager *smiley_manager;
56 EmpathyContact *last_contact;
57 gint64 last_timestamp;
58 gboolean last_is_backlog;
60 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
62 /* Queue of guint32 of pending message id to remove unread
63 * marker for when we lose focus. */
64 GQueue acked_messages;
65 GtkWidget *inspector_window;
67 GSettings *gsettings_chat;
68 GSettings *gsettings_desktop;
71 gboolean has_unread_message;
72 gboolean allow_scrolling;
74 gboolean in_construction;
75 gboolean show_avatars;
78 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);
113 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
114 const gchar *variant);
123 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
124 WEBKIT_TYPE_WEB_VIEW,
125 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
126 theme_adium_iface_init));
140 gboolean should_highlight;
144 queue_item (GQueue *queue,
148 gboolean should_highlight)
150 QueuedItem *item = g_slice_new0 (QueuedItem);
154 item->msg = g_object_ref (msg);
155 item->str = g_strdup (str);
156 item->should_highlight = should_highlight;
158 g_queue_push_tail (queue, item);
164 free_queued_item (QueuedItem *item)
166 tp_clear_object (&item->msg);
169 g_slice_free (QueuedItem, item);
173 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *self)
175 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
176 gboolean enable_webkit_developer_tools;
178 enable_webkit_developer_tools = g_settings_get_boolean (
179 self->priv->gsettings_chat,
180 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
182 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
183 "enable-developer-extras", enable_webkit_developer_tools, NULL);
187 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
191 EmpathyThemeAdium *self = user_data;
193 theme_adium_update_enable_webkit_developer_tools (self);
197 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
198 WebKitWebFrame *web_frame,
199 WebKitNetworkRequest *request,
200 WebKitWebNavigationAction *action,
201 WebKitWebPolicyDecision *decision,
206 /* Only call url_show on clicks */
207 if (webkit_web_navigation_action_get_reason (action) !=
208 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
210 webkit_web_policy_decision_use (decision);
214 uri = webkit_network_request_get_uri (request);
215 empathy_url_show (GTK_WIDGET (view), uri);
217 webkit_web_policy_decision_ignore (decision);
221 /* Replace each %@ in format with string passed in args */
223 string_with_format (const gchar *format,
224 const gchar *first_string,
231 va_start (args, first_string);
232 result = g_string_sized_new (strlen (format));
233 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
237 next = strstr (format, "%@");
241 g_string_append_len (result, format, next - format);
242 g_string_append (result, str);
245 g_string_append (result, format);
248 return g_string_free (result, FALSE);
252 theme_adium_load_template (EmpathyThemeAdium *self)
258 self->priv->pages_loading++;
259 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
261 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
262 self->priv->variant);
264 template = string_with_format (self->priv->data->template_html,
267 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
268 template, basedir_uri);
270 g_free (basedir_uri);
271 g_free (variant_path);
276 theme_adium_parse_body (EmpathyThemeAdium *self,
280 EmpathyStringParser *parsers;
283 /* Check if we have to parse smileys */
284 parsers = empathy_webkit_get_string_parser (
285 g_settings_get_boolean (self->priv->gsettings_chat,
286 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
288 /* Parse text and construct string with links and smileys replaced
289 * by html tags. Also escape text to make sure html code is
290 * displayed verbatim. */
291 string = g_string_sized_new (strlen (text));
293 /* wrap this in HTML that allows us to find the message for later
295 if (!tp_str_empty (token))
296 g_string_append_printf (string,
297 "<span id=\"message-token-%s\">",
300 empathy_string_parser_substr (text, -1, parsers, string);
302 if (!tp_str_empty (token))
303 g_string_append (string, "</span>");
305 /* Wrap body in order to make tabs and multiple spaces displayed
306 * properly. See bug #625745. */
307 g_string_prepend (string, "<div style=\"display: inline; "
308 "white-space: pre-wrap\"'>");
309 g_string_append (string, "</div>");
311 return g_string_free (string, FALSE);
315 escape_and_append_len (GString *string, const gchar *str, gint len)
317 while (str != NULL && *str != '\0' && len != 0)
323 g_string_append (string, "\\\\");
327 g_string_append (string, "\\\"");
330 /* Remove end of lines */
333 g_string_append_c (string, *str);
341 /* If *str starts with match, returns TRUE and move pointer to the end */
343 theme_adium_match (const gchar **str,
348 len = strlen (match);
349 if (strncmp (*str, match, len) == 0)
358 /* Like theme_adium_match() but also return the X part if match is
361 theme_adium_match_with_format (const gchar **str,
365 const gchar *cur = *str;
368 if (!theme_adium_match (&cur, match))
373 end = strstr (cur, "}%");
377 *format = g_strndup (cur , end - cur);
382 /* List of colors used by %senderColor%. Copied from
383 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
385 static gchar *colors[] = {
386 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
387 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
388 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
389 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
390 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
391 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
392 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
393 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
394 "lightblue", "lightcoral",
395 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
396 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
397 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
398 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
399 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
400 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
401 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
402 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
403 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
404 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
409 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
411 /* Convert from NSDateFormatter
412 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
413 * to strftime supported by g_date_time_format.
414 * FIXME: table is incomplete, doc of g_date_time_format has a table of
416 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
417 * in 2.29.x we have to explictely request padding with %0x */
418 static const gchar *convert_table[] = {
420 "A", NULL, // 0~86399999 (Millisecond of Day)
422 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
423 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
424 "cc", "%u", // 1~7 (Day of Week)
425 "c", "%u", // 1~7 (Day of Week)
427 "dd", "%d", // 1~31 (0 padded Day of Month)
428 "d", "%d", // 1~31 (0 padded Day of Month)
429 "D", "%j", // 1~366 (0 padded Day of Year)
431 "e", "%u", // 1~7 (0 padded Day of Week)
432 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
433 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
434 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
435 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
437 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
439 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
440 "GGGG", NULL, // Before Christ/Anno Domini
441 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
442 "GG", NULL, // BC/AD (Era Designator Abbreviated)
443 "G", NULL, // BC/AD (Era Designator Abbreviated)
445 "h", "%I", // 1~12 (0 padded Hour (12hr))
446 "H", "%H", // 0~23 (0 padded Hour (24hr))
448 "k", NULL, // 1~24 (0 padded Hour (24hr)
449 "K", NULL, // 0~11 (0 padded Hour (12hr))
451 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
452 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
453 "LL", "%m", // 1~12 (0 padded Month)
454 "L", "%m", // 1~12 (0 padded Month)
456 "m", "%M", // 0~59 (0 padded Minute)
457 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
458 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
459 "MM", "%m", // 1~12 (0 padded Month)
460 "M", "%m", // 1~12 (0 padded Month)
462 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
463 "qqq", NULL, // Q1/Q2/Q3/Q4
464 "qq", NULL, // 1~4 (0 padded Quarter)
465 "q", NULL, // 1~4 (0 padded Quarter)
466 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
467 "QQQ", NULL, // Q1/Q2/Q3/Q4
468 "QQ", NULL, // 1~4 (0 padded Quarter)
469 "Q", NULL, // 1~4 (0 padded Quarter)
471 "s", "%S", // 0~59 (0 padded Second)
472 "S", NULL, // (rounded Sub-Second)
474 "u", "%Y", // (0 padded Year)
476 "vvvv", "%Z", // (General GMT Timezone Name)
477 "vvv", "%Z", // (General GMT Timezone Abbreviation)
478 "vv", "%Z", // (General GMT Timezone Abbreviation)
479 "v", "%Z", // (General GMT Timezone Abbreviation)
481 "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)
482 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
484 "yyyy", "%Y", // (Full Year)
485 "yyy", "%y", // (2 Digits Year)
486 "yy", "%y", // (2 Digits Year)
487 "y", "%Y", // (Full Year)
488 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
489 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
490 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
491 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
493 "zzzz", NULL, // (Specific GMT Timezone Name)
494 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
495 "zz", NULL, // (Specific GMT Timezone Abbreviation)
496 "z", NULL, // (Specific GMT Timezone Abbreviation)
497 "Z", "%z", // +0000 (RFC 822 Timezone)
506 str = g_hash_table_lookup (data->date_format_cache, nsdate);
511 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
512 * by corresponding strftime tag. */
513 string = g_string_sized_new (strlen (nsdate));
514 for (i = 0; nsdate[i] != '\0'; i++)
516 gboolean found = FALSE;
518 /* even indexes are NSDateFormatter tag, odd indexes are the
519 * corresponding strftime tag */
520 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
522 if (g_str_has_prefix (nsdate + i, convert_table[j]))
531 /* If we don't have a replacement, just ignore that tag */
532 if (convert_table[j + 1] != NULL)
533 g_string_append (string, convert_table[j + 1]);
535 i += strlen (convert_table[j]) - 1;
539 g_string_append_c (string, nsdate[i]);
543 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
545 /* The cache takes ownership of string->str */
546 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
547 return g_string_free (string, FALSE);
552 theme_adium_append_html (EmpathyThemeAdium *self,
555 const gchar *message,
556 const gchar *avatar_filename,
558 const gchar *contact_id,
559 const gchar *service_name,
560 const gchar *message_classes,
566 const gchar *cur = NULL;
569 /* Make some search-and-replace in the html code */
570 string = g_string_sized_new (strlen (html) + strlen (message));
571 g_string_append_printf (string, "%s(\"", func);
573 for (cur = html; *cur != '\0'; cur++)
575 const gchar *replace = NULL;
576 gchar *dup_replace = NULL;
577 gchar *format = NULL;
579 /* Those are all well known keywords that needs replacement in
580 * html files. Please keep them in the same order than the adium
581 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
582 if (theme_adium_match (&cur, "%userIconPath%"))
584 replace = avatar_filename;
586 else if (theme_adium_match (&cur, "%senderScreenName%"))
588 replace = contact_id;
590 else if (theme_adium_match (&cur, "%sender%"))
594 else if (theme_adium_match (&cur, "%senderColor%"))
596 /* A color derived from the user's name.
597 * FIXME: If a colon separated list of HTML colors is at
598 * Incoming/SenderColors.txt it will be used instead of
599 * the default colors.
602 /* Ensure we always use the same color when sending messages
608 else if (contact_id != NULL)
610 guint hash = g_str_hash (contact_id);
611 replace = colors[hash % G_N_ELEMENTS (colors)];
614 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
616 /* FIXME: The path to the status icon of the sender
617 * (available, away, etc...)
620 else if (theme_adium_match (&cur, "%messageDirection%"))
622 /* FIXME: The text direction of the message
623 * (either rtl or ltr)
626 else if (theme_adium_match (&cur, "%senderDisplayName%"))
628 /* FIXME: The serverside (remotely set) name of the
629 * sender, such as an MSN display name.
631 * We don't have access to that yet so we use
632 * local alias instead.
636 else if (theme_adium_match (&cur, "%senderPrefix%"))
638 /* FIXME: If we supported IRC user mode flags, this
639 * would be replaced with @ if the user is an op, + if
640 * the user has voice, etc. as per
641 * http://hg.adium.im/adium/rev/b586b027de42. But we
642 * don't, so for now we just strip it. */
644 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
647 /* FIXME: This keyword is used to represent the
648 * highlight background color. "X" is the opacity of the
649 * background, ranges from 0 to 1 and can be any decimal
653 else if (theme_adium_match (&cur, "%message%"))
657 else if (theme_adium_match (&cur, "%time%") ||
658 theme_adium_match_with_format (&cur, "%time{", &format))
660 const gchar *strftime_format;
662 strftime_format = nsdate_to_strftime (self->priv->data, format);
664 dup_replace = empathy_time_to_string_local (timestamp,
665 strftime_format ? strftime_format :
666 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
668 dup_replace = empathy_time_to_string_local (timestamp,
669 strftime_format ? strftime_format :
670 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
672 replace = dup_replace;
674 else if (theme_adium_match (&cur, "%shortTime%"))
676 dup_replace = empathy_time_to_string_local (timestamp,
677 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
678 replace = dup_replace;
680 else if (theme_adium_match (&cur, "%service%"))
682 replace = service_name;
684 else if (theme_adium_match (&cur, "%variant%"))
686 /* FIXME: The name of the active message style variant,
687 * with all spaces replaced with an underscore.
688 * A variant named "Alternating Messages - Blue Red"
689 * will become "Alternating_Messages_-_Blue_Red".
692 else if (theme_adium_match (&cur, "%userIcons%"))
694 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
696 else if (theme_adium_match (&cur, "%messageClasses%"))
698 replace = message_classes;
700 else if (theme_adium_match (&cur, "%status%"))
702 /* FIXME: A description of the status event. This is
703 * neither in the user's local language nor expected to
704 * be displayed; it may be useful to use a different div
705 * class to present different types of status messages.
706 * The following is a list of some of the more important
707 * status messages; your message style should be able to
708 * handle being shown a status message not in this list,
709 * as even at present the list is incomplete and is
710 * certain to become out of date in the future:
719 * contact_joined (group chats)
723 * encryption (all OTR messages use this status)
724 * purple (all IRC topic and join/part messages use this status)
725 * fileTransferStarted
726 * fileTransferCompleted
731 escape_and_append_len (string, cur, 1);
735 /* Here we have a replacement to make */
736 escape_and_append_len (string, replace, -1);
738 g_free (dup_replace);
741 g_string_append (string, "\")");
743 script = g_string_free (string, FALSE);
744 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
749 theme_adium_append_event_escaped (EmpathyChatView *view,
750 const gchar *escaped)
752 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
754 theme_adium_append_html (self, "appendMessage",
755 self->priv->data->status_html, escaped, NULL, NULL, NULL,
756 NULL, "event", empathy_time_get_current (), FALSE, FALSE);
758 /* There is no last contact */
759 if (self->priv->last_contact)
761 g_object_unref (self->priv->last_contact);
762 self->priv->last_contact = NULL;
767 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
768 WebKitDOMNodeList *nodes)
772 /* Remove focus and firstFocus class */
773 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
775 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
776 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
778 gchar **classes, **iter;
779 GString *new_class_name;
780 gboolean first = TRUE;
785 class_name = webkit_dom_html_element_get_class_name (element);
786 classes = g_strsplit (class_name, " ", -1);
787 new_class_name = g_string_sized_new (strlen (class_name));
789 for (iter = classes; *iter != NULL; iter++)
791 if (tp_strdiff (*iter, "focus") &&
792 tp_strdiff (*iter, "firstFocus"))
795 g_string_append_c (new_class_name, ' ');
797 g_string_append (new_class_name, *iter);
802 webkit_dom_html_element_set_class_name (element, new_class_name->str);
805 g_strfreev (classes);
806 g_string_free (new_class_name, TRUE);
811 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
813 WebKitDOMDocument *dom;
814 WebKitDOMNodeList *nodes;
815 GError *error = NULL;
817 if (!self->priv->has_unread_message)
820 self->priv->has_unread_message = FALSE;
822 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
826 /* Get all nodes with focus class */
827 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
831 DEBUG ("Error getting focus nodes: %s",
832 error ? error->message : "No error");
833 g_clear_error (&error);
837 theme_adium_remove_focus_marks (self, nodes);
841 theme_adium_append_message (EmpathyChatView *view,
843 gboolean should_highlight)
845 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
846 EmpathyContact *sender;
849 gchar *body_escaped, *name_escaped;
851 const gchar *contact_id;
852 EmpathyAvatar *avatar;
853 const gchar *avatar_filename = NULL;
855 const gchar *html = NULL;
857 const gchar *service_name;
858 GString *message_classes = NULL;
860 gboolean consecutive;
863 if (self->priv->pages_loading != 0)
865 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
870 /* Get information */
871 sender = empathy_message_get_sender (msg);
872 account = empathy_contact_get_account (sender);
873 service_name = empathy_protocol_name_to_display_name
874 (tp_account_get_protocol (account));
875 if (service_name == NULL)
876 service_name = tp_account_get_protocol (account);
877 timestamp = empathy_message_get_timestamp (msg);
878 body_escaped = theme_adium_parse_body (self,
879 empathy_message_get_body (msg),
880 empathy_message_get_token (msg));
881 name = empathy_contact_get_logged_alias (sender);
882 contact_id = empathy_contact_get_id (sender);
883 action = (empathy_message_get_tptype (msg) ==
884 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
886 name_escaped = g_markup_escape_text (name, -1);
888 /* If this is a /me probably */
893 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
895 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
896 "<span class='actionMessageBody'>%s</span>",
897 name_escaped, body_escaped);
901 str = g_strdup_printf ("*%s*", body_escaped);
904 g_free (body_escaped);
908 /* Get the avatar filename, or a fallback */
909 avatar = empathy_contact_get_avatar (sender);
911 avatar_filename = avatar->filename;
913 if (!avatar_filename)
915 if (empathy_contact_is_user (sender))
916 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
918 avatar_filename = self->priv->data->default_incoming_avatar_filename;
920 if (!avatar_filename)
922 if (!self->priv->data->default_avatar_filename)
923 self->priv->data->default_avatar_filename =
924 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
925 GTK_ICON_SIZE_DIALOG);
927 avatar_filename = self->priv->data->default_avatar_filename;
931 /* We want to join this message with the last one if
932 * - senders are the same contact,
933 * - last message was recieved recently,
934 * - last message and this message both are/aren't backlog, and
935 * - DisableCombineConsecutive is not set in theme's settings */
936 is_backlog = empathy_message_is_backlog (msg);
937 consecutive = empathy_contact_equal (self->priv->last_contact, sender) &&
938 (timestamp - self->priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
939 (is_backlog == self->priv->last_is_backlog) &&
940 !tp_asv_get_boolean (self->priv->data->info,
941 "DisableCombineConsecutive", NULL);
943 /* Define message classes */
944 message_classes = g_string_new ("message");
945 if (!self->priv->has_focus && !is_backlog)
947 if (!self->priv->has_unread_message)
949 g_string_append (message_classes, " firstFocus");
950 self->priv->has_unread_message = TRUE;
952 g_string_append (message_classes, " focus");
956 g_string_append (message_classes, " history");
959 g_string_append (message_classes, " consecutive");
961 if (empathy_contact_is_user (sender))
962 g_string_append (message_classes, " outgoing");
964 g_string_append (message_classes, " incoming");
966 if (should_highlight)
967 g_string_append (message_classes, " mention");
969 if (empathy_message_get_tptype (msg) ==
970 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
971 g_string_append (message_classes, " autoreply");
974 g_string_append (message_classes, " action");
976 /* FIXME: other classes:
977 * status - the message is a status change
978 * event - the message is a notification of something happening
979 * (for example, encryption being turned on)
980 * %status% - See %status% in theme_adium_append_html ()
983 /* This is slightly a hack, but it's the only way to add
984 * arbitrary data to messages in the HTML. We add another
985 * class called "x-empathy-message-id-*" to the message. This
986 * way, we can remove the unread marker for this specific
988 tp_msg = empathy_message_get_tp_message (msg);
994 id = tp_message_get_pending_message_id (tp_msg, &valid);
996 g_string_append_printf (message_classes,
997 " x-empathy-message-id-%u", id);
1000 /* Define javascript function to use */
1002 func = self->priv->allow_scrolling ? "appendNextMessage" :
1003 "appendNextMessageNoScroll";
1005 func = self->priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
1007 if (empathy_contact_is_user (sender))
1012 html = consecutive ? self->priv->data->out_nextcontext_html :
1013 self->priv->data->out_context_html;
1016 html = consecutive ? self->priv->data->out_nextcontent_html :
1017 self->priv->data->out_content_html;
1019 /* remove all the unread marks when we are sending a message */
1020 theme_adium_remove_all_focus_marks (self);
1027 html = consecutive ? self->priv->data->in_nextcontext_html :
1028 self->priv->data->in_context_html;
1031 html = consecutive ? self->priv->data->in_nextcontent_html :
1032 self->priv->data->in_content_html;
1035 theme_adium_append_html (self, func, html, body_escaped,
1036 avatar_filename, name_escaped, contact_id,
1037 service_name, message_classes->str,
1038 timestamp, is_backlog, empathy_contact_is_user (sender));
1040 /* Keep the sender of the last displayed message */
1041 if (self->priv->last_contact)
1042 g_object_unref (self->priv->last_contact);
1044 self->priv->last_contact = g_object_ref (sender);
1045 self->priv->last_timestamp = timestamp;
1046 self->priv->last_is_backlog = is_backlog;
1048 g_free (body_escaped);
1049 g_free (name_escaped);
1050 g_string_free (message_classes, TRUE);
1054 theme_adium_append_event (EmpathyChatView *view,
1057 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1060 if (self->priv->pages_loading != 0)
1062 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
1066 str_escaped = g_markup_escape_text (str, -1);
1067 theme_adium_append_event_escaped (view, str_escaped);
1068 g_free (str_escaped);
1072 theme_adium_append_event_markup (EmpathyChatView *view,
1073 const gchar *markup_text,
1074 const gchar *fallback_text)
1076 theme_adium_append_event_escaped (view, markup_text);
1080 theme_adium_edit_message (EmpathyChatView *view,
1081 EmpathyMessage *message)
1083 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1084 WebKitDOMDocument *doc;
1085 WebKitDOMElement *span;
1086 gchar *id, *parsed_body;
1087 gchar *tooltip, *timestamp;
1088 GtkIconInfo *icon_info;
1089 GError *error = NULL;
1091 if (self->priv->pages_loading != 0)
1093 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1097 id = g_strdup_printf ("message-token-%s",
1098 empathy_message_get_supersedes (message));
1099 /* we don't pass a token here, because doing so will return another
1100 * <span> element, and we don't want nested <span> elements */
1101 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1102 empathy_message_get_body (message), NULL);
1104 /* find the element */
1105 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1106 span = webkit_dom_document_get_element_by_id (doc, id);
1110 DEBUG ("Failed to find id '%s'", id);
1114 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1116 DEBUG ("Not a HTML element");
1120 /* update the HTML */
1121 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1122 parsed_body, &error);
1126 DEBUG ("Error setting new inner-HTML: %s", error->message);
1127 g_error_free (error);
1132 timestamp = empathy_time_to_string_local (
1133 empathy_message_get_timestamp (message),
1135 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1137 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1143 /* mark this message as edited */
1144 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1145 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1147 if (icon_info != NULL)
1149 /* set the icon as a background image using CSS
1150 * FIXME: the icon won't update in response to theme changes */
1151 gchar *style = g_strdup_printf (
1152 "background-image:url('%s');"
1153 "background-repeat:no-repeat;"
1154 "background-position:left center;"
1155 "padding-left:19px;", /* 16px icon + 3px padding */
1156 gtk_icon_info_get_filename (icon_info));
1158 webkit_dom_element_set_attribute (span, "style", style, &error);
1162 DEBUG ("Error setting element style: %s",
1164 g_clear_error (&error);
1169 gtk_icon_info_free (icon_info);
1175 DEBUG ("Could not find message to edit with: %s",
1176 empathy_message_get_body (message));
1180 g_free (parsed_body);
1184 theme_adium_scroll (EmpathyChatView *view,
1185 gboolean allow_scrolling)
1187 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1189 self->priv->allow_scrolling = allow_scrolling;
1191 if (allow_scrolling)
1192 empathy_chat_view_scroll_down (view);
1196 theme_adium_scroll_down (EmpathyChatView *view)
1198 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1202 theme_adium_get_has_selection (EmpathyChatView *view)
1204 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1208 theme_adium_clear (EmpathyChatView *view)
1210 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1212 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1214 /* Clear last contact to avoid trying to add a 'joined'
1215 * message when we don't have an insertion point. */
1216 if (self->priv->last_contact)
1218 g_object_unref (self->priv->last_contact);
1219 self->priv->last_contact = NULL;
1224 theme_adium_find_previous (EmpathyChatView *view,
1225 const gchar *search_criteria,
1226 gboolean new_search,
1227 gboolean match_case)
1229 /* FIXME: Doesn't respect new_search */
1230 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1231 search_criteria, match_case, FALSE, TRUE);
1235 theme_adium_find_next (EmpathyChatView *view,
1236 const gchar *search_criteria,
1237 gboolean new_search,
1238 gboolean match_case)
1240 /* FIXME: Doesn't respect new_search */
1241 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1242 search_criteria, match_case, TRUE, TRUE);
1246 theme_adium_find_abilities (EmpathyChatView *view,
1247 const gchar *search_criteria,
1248 gboolean match_case,
1249 gboolean *can_do_previous,
1250 gboolean *can_do_next)
1252 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1253 * find_next and find_previous to work around this problem. */
1254 if (can_do_previous)
1255 *can_do_previous = TRUE;
1257 *can_do_next = TRUE;
1261 theme_adium_highlight (EmpathyChatView *view,
1263 gboolean match_case)
1265 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1266 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1267 text, match_case, 0);
1268 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1273 theme_adium_copy_clipboard (EmpathyChatView *view)
1275 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1279 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1282 WebKitDOMDocument *dom;
1283 WebKitDOMNodeList *nodes;
1285 GError *error = NULL;
1287 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1291 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1293 /* Get all nodes with focus class */
1294 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1299 DEBUG ("Error getting focus nodes: %s",
1300 error ? error->message : "No error");
1301 g_clear_error (&error);
1305 theme_adium_remove_focus_marks (self, nodes);
1309 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1312 EmpathyThemeAdium *self = user_data;
1313 guint32 id = GPOINTER_TO_UINT (data);
1315 theme_adium_remove_mark_from_message (self, id);
1319 theme_adium_focus_toggled (EmpathyChatView *view,
1322 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1324 self->priv->has_focus = has_focus;
1325 if (!self->priv->has_focus)
1327 /* We've lost focus, so let's make sure all the acked
1328 * messages have lost their unread marker. */
1329 g_queue_foreach (&self->priv->acked_messages,
1330 theme_adium_remove_acked_message_unread_mark_foreach, view);
1331 g_queue_clear (&self->priv->acked_messages);
1333 self->priv->has_unread_message = FALSE;
1338 theme_adium_message_acknowledged (EmpathyChatView *view,
1339 EmpathyMessage *message)
1341 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1346 tp_msg = empathy_message_get_tp_message (message);
1351 id = tp_message_get_pending_message_id (tp_msg, &valid);
1354 g_warning ("Acknoledged message doesn't have a pending ID");
1358 /* We only want to actually remove the unread marker if the
1359 * view doesn't have focus. If we did it all the time we would
1360 * never see the unread markers, ever! So, we'll queue these
1361 * up, and when we lose focus, we'll remove the markers. */
1362 if (self->priv->has_focus)
1364 g_queue_push_tail (&self->priv->acked_messages,
1365 GUINT_TO_POINTER (id));
1369 theme_adium_remove_mark_from_message (self, id);
1373 theme_adium_button_press_event (GtkWidget *widget,
1374 GdkEventButton *event)
1376 if (event->button == 3)
1378 gboolean developer_tools_enabled;
1381 G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1382 "enable-developer-extras", &developer_tools_enabled, NULL);
1384 /* We currently have no way to add an inspector menu
1385 * item ourselves, so we disable our customized menu
1386 * if the developer extras are enabled. */
1387 if (!developer_tools_enabled)
1389 empathy_webkit_context_menu_for_event (
1390 WEBKIT_WEB_VIEW (widget), event,
1391 EMPATHY_WEBKIT_MENU_CLEAR);
1396 return GTK_WIDGET_CLASS (
1397 empathy_theme_adium_parent_class)->button_press_event (widget, event);
1401 theme_adium_set_show_avatars (EmpathyChatView *view,
1402 gboolean show_avatars)
1404 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1406 self->priv->show_avatars = show_avatars;
1410 theme_adium_iface_init (EmpathyChatViewIface *iface)
1412 iface->append_message = theme_adium_append_message;
1413 iface->append_event = theme_adium_append_event;
1414 iface->append_event_markup = theme_adium_append_event_markup;
1415 iface->edit_message = theme_adium_edit_message;
1416 iface->scroll = theme_adium_scroll;
1417 iface->scroll_down = theme_adium_scroll_down;
1418 iface->get_has_selection = theme_adium_get_has_selection;
1419 iface->clear = theme_adium_clear;
1420 iface->find_previous = theme_adium_find_previous;
1421 iface->find_next = theme_adium_find_next;
1422 iface->find_abilities = theme_adium_find_abilities;
1423 iface->highlight = theme_adium_highlight;
1424 iface->copy_clipboard = theme_adium_copy_clipboard;
1425 iface->focus_toggled = theme_adium_focus_toggled;
1426 iface->message_acknowledged = theme_adium_message_acknowledged;
1427 iface->set_show_avatars = theme_adium_set_show_avatars;
1431 theme_adium_load_finished_cb (WebKitWebView *view,
1432 WebKitWebFrame *frame,
1435 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1436 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1439 DEBUG ("Page loaded");
1440 self->priv->pages_loading--;
1442 if (self->priv->pages_loading != 0)
1445 /* Display queued messages */
1446 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1448 QueuedItem *item = l->data;
1452 case QUEUED_MESSAGE:
1453 theme_adium_append_message (chat_view, item->msg,
1454 item->should_highlight);
1458 theme_adium_edit_message (chat_view, item->msg);
1462 theme_adium_append_event (chat_view, item->str);
1466 free_queued_item (item);
1469 g_queue_clear (&self->priv->message_queue);
1473 theme_adium_finalize (GObject *object)
1475 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1477 empathy_adium_data_unref (self->priv->data);
1479 g_object_unref (self->priv->gsettings_chat);
1480 g_object_unref (self->priv->gsettings_desktop);
1482 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1486 theme_adium_dispose (GObject *object)
1488 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1490 if (self->priv->smiley_manager)
1492 g_object_unref (self->priv->smiley_manager);
1493 self->priv->smiley_manager = NULL;
1496 if (self->priv->last_contact)
1498 g_object_unref (self->priv->last_contact);
1499 self->priv->last_contact = NULL;
1502 if (self->priv->inspector_window)
1504 gtk_widget_destroy (self->priv->inspector_window);
1505 self->priv->inspector_window = NULL;
1508 if (self->priv->acked_messages.length > 0)
1510 g_queue_clear (&self->priv->acked_messages);
1513 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1517 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1518 EmpathyThemeAdium *self)
1520 if (self->priv->inspector_window)
1522 gtk_widget_show_all (self->priv->inspector_window);
1529 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1530 EmpathyThemeAdium *self)
1532 if (self->priv->inspector_window)
1534 gtk_widget_hide (self->priv->inspector_window);
1540 static WebKitWebView *
1541 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1542 WebKitWebView *web_view,
1543 EmpathyThemeAdium *self)
1545 GtkWidget *scrolled_window;
1546 GtkWidget *inspector_web_view;
1548 if (!self->priv->inspector_window)
1550 /* Create main window */
1551 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1553 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1556 g_signal_connect (self->priv->inspector_window, "delete-event",
1557 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1559 /* Pack a scrolled window */
1560 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1562 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1563 GTK_POLICY_AUTOMATIC,
1564 GTK_POLICY_AUTOMATIC);
1565 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1567 gtk_widget_show (scrolled_window);
1569 /* Pack a webview in the scrolled window. That webview will be
1570 * used to render the inspector tool. */
1571 inspector_web_view = webkit_web_view_new ();
1572 gtk_container_add (GTK_CONTAINER (scrolled_window),
1573 inspector_web_view);
1574 gtk_widget_show (scrolled_window);
1576 return WEBKIT_WEB_VIEW (inspector_web_view);
1583 theme_adium_constructed (GObject *object)
1585 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1586 const gchar *font_family = NULL;
1588 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1589 WebKitWebInspector *webkit_inspector;
1591 /* Set default settings */
1592 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1593 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1595 if (font_family && font_size)
1597 g_object_set (webkit_web_view_get_settings (webkit_view),
1598 "default-font-family", font_family,
1599 "default-font-size", font_size,
1604 empathy_webkit_bind_font_setting (webkit_view,
1605 self->priv->gsettings_desktop,
1606 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1609 /* Setup webkit inspector */
1610 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1611 g_signal_connect (webkit_inspector, "inspect-web-view",
1612 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1613 g_signal_connect (webkit_inspector, "show-window",
1614 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1615 g_signal_connect (webkit_inspector, "close-window",
1616 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1619 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1621 self->priv->in_construction = FALSE;
1625 theme_adium_get_property (GObject *object,
1630 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1634 case PROP_ADIUM_DATA:
1635 g_value_set_boxed (value, self->priv->data);
1638 g_value_set_string (value, self->priv->variant);
1641 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1647 theme_adium_set_property (GObject *object,
1649 const GValue *value,
1652 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1656 case PROP_ADIUM_DATA:
1657 g_assert (self->priv->data == NULL);
1658 self->priv->data = g_value_dup_boxed (value);
1661 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1664 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1670 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1672 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1673 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1675 object_class->finalize = theme_adium_finalize;
1676 object_class->dispose = theme_adium_dispose;
1677 object_class->constructed = theme_adium_constructed;
1678 object_class->get_property = theme_adium_get_property;
1679 object_class->set_property = theme_adium_set_property;
1681 widget_class->button_press_event = theme_adium_button_press_event;
1683 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1684 g_param_spec_boxed ("adium-data",
1686 "Data for the adium theme",
1687 EMPATHY_TYPE_ADIUM_DATA,
1688 G_PARAM_CONSTRUCT_ONLY |
1690 G_PARAM_STATIC_STRINGS));
1692 g_object_class_install_property (object_class, PROP_VARIANT,
1693 g_param_spec_string ("variant",
1694 "The theme variant",
1695 "Variant name for the theme",
1699 G_PARAM_STATIC_STRINGS));
1701 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1705 empathy_theme_adium_init (EmpathyThemeAdium *self)
1707 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1708 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1710 self->priv->in_construction = TRUE;
1711 g_queue_init (&self->priv->message_queue);
1712 self->priv->allow_scrolling = TRUE;
1713 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1715 /* Show avatars by default. */
1716 self->priv->show_avatars = TRUE;
1718 g_signal_connect (self, "load-finished",
1719 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1720 g_signal_connect (self, "navigation-policy-decision-requested",
1721 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1723 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1724 self->priv->gsettings_desktop = g_settings_new (
1725 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1727 g_signal_connect (self->priv->gsettings_chat,
1728 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1729 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1732 theme_adium_update_enable_webkit_developer_tools (self);
1736 empathy_theme_adium_new (EmpathyAdiumData *data,
1737 const gchar *variant)
1739 g_return_val_if_fail (data != NULL, NULL);
1741 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1748 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1749 const gchar *variant)
1751 gchar *variant_path;
1754 if (!tp_strdiff (self->priv->variant, variant))
1757 g_free (self->priv->variant);
1758 self->priv->variant = g_strdup (variant);
1760 if (self->priv->in_construction)
1763 DEBUG ("Update view with variant: '%s'", variant);
1764 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1765 self->priv->variant);
1766 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1769 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1771 g_free (variant_path);
1774 g_object_notify (G_OBJECT (self), "variant");
1778 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1780 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1781 WebKitWebInspector *inspector;
1783 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1784 "enable-developer-extras", TRUE, NULL);
1786 inspector = webkit_web_view_get_inspector (web_view);
1787 webkit_web_inspector_show (inspector);
1791 empathy_adium_path_is_valid (const gchar *path)
1799 /* The theme is not valid if there is no Info.plist */
1800 file = g_build_filename (path, "Contents", "Info.plist",
1802 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1808 /* We ship a default Template.html as fallback if there is any problem
1809 * with the one inside the theme. The only other required file is
1810 * Content.html OR Incoming/Content.html*/
1811 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1813 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1818 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1819 "Content.html", NULL);
1820 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1828 empathy_adium_info_new (const gchar *path)
1832 GHashTable *info = NULL;
1834 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1836 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1837 value = empathy_plist_parse_from_file (file);
1843 info = g_value_dup_boxed (value);
1844 tp_g_value_slice_free (value);
1846 /* Insert the theme's path into the hash table,
1847 * keys have to be dupped */
1848 tp_asv_set_string (info, g_strdup ("path"), path);
1854 adium_info_get_version (GHashTable *info)
1856 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1859 static const gchar *
1860 adium_info_get_no_variant_name (GHashTable *info)
1862 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1863 return name ? name : _("Normal");
1867 adium_info_dup_path_for_variant (GHashTable *info,
1868 const gchar *variant)
1870 guint version = adium_info_get_version (info);
1871 const gchar *no_variant = adium_info_get_no_variant_name (info);
1872 GPtrArray *variants;
1875 if (version <= 2 && !tp_strdiff (variant, no_variant))
1876 return g_strdup ("main.css");
1878 variants = empathy_adium_info_get_available_variants (info);
1879 if (variants->len == 0)
1880 return g_strdup ("main.css");
1882 /* Verify the variant exists, fallback to the first one */
1883 for (i = 0; i < variants->len; i++)
1885 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1889 if (i == variants->len)
1891 DEBUG ("Variant %s does not exist", variant);
1892 variant = g_ptr_array_index (variants, 0);
1895 return g_strdup_printf ("Variants/%s.css", variant);
1900 empathy_adium_info_get_default_variant (GHashTable *info)
1902 if (adium_info_get_version (info) <= 2)
1903 return adium_info_get_no_variant_name (info);
1905 return tp_asv_get_string (info, "DefaultVariant");
1909 empathy_adium_info_get_available_variants (GHashTable *info)
1911 GPtrArray *variants;
1916 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1917 if (variants != NULL)
1920 variants = g_ptr_array_new_with_free_func (g_free);
1921 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1922 G_TYPE_PTR_ARRAY, variants);
1924 path = tp_asv_get_string (info, "path");
1925 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1926 dir = g_dir_open (dirpath, 0, NULL);
1931 for (name = g_dir_read_name (dir);
1933 name = g_dir_read_name (dir))
1935 gchar *display_name;
1937 if (!g_str_has_suffix (name, ".css"))
1940 display_name = g_strdup (name);
1941 strstr (display_name, ".css")[0] = '\0';
1942 g_ptr_array_add (variants, display_name);
1949 if (adium_info_get_version (info) <= 2)
1950 g_ptr_array_add (variants,
1951 g_strdup (adium_info_get_no_variant_name (info)));
1957 empathy_adium_data_get_type (void)
1959 static GType type_id = 0;
1963 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1964 (GBoxedCopyFunc) empathy_adium_data_ref,
1965 (GBoxedFreeFunc) empathy_adium_data_unref);
1972 empathy_adium_data_new_with_info (const gchar *path,
1975 EmpathyAdiumData *data;
1976 gchar *template_html = NULL;
1977 gchar *footer_html = NULL;
1980 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1982 data = g_slice_new0 (EmpathyAdiumData);
1983 data->ref_count = 1;
1984 data->path = g_strdup (path);
1985 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1986 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1987 data->info = g_hash_table_ref (info);
1988 data->version = adium_info_get_version (info);
1989 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1990 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1991 g_str_equal, g_free, g_free);
1993 DEBUG ("Loading theme at %s", path);
1995 #define LOAD(path, var) \
1996 tmp = g_build_filename (data->basedir, path, NULL); \
1997 g_file_get_contents (tmp, &var, NULL, NULL); \
2000 #define LOAD_CONST(path, var) \
2003 LOAD (path, content); \
2004 if (content != NULL) { \
2005 g_ptr_array_add (data->strings_to_free, content); \
2010 /* Load html files */
2011 LOAD_CONST ("Content.html", data->content_html);
2012 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2013 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2014 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2015 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2016 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2017 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2018 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2019 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2020 LOAD_CONST ("Status.html", data->status_html);
2021 LOAD ("Template.html", template_html);
2022 LOAD ("Footer.html", footer_html);
2027 /* HTML fallbacks: If we have at least content OR in_content, then
2028 * everything else gets a fallback */
2030 #define FALLBACK(html, fallback) \
2031 if (html == NULL) { \
2035 /* in_nextcontent -> in_content -> content */
2036 FALLBACK (data->in_content_html, data->content_html);
2037 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2039 /* context -> content */
2040 FALLBACK (data->in_context_html, data->in_content_html);
2041 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2042 FALLBACK (data->out_context_html, data->out_content_html);
2043 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2046 FALLBACK (data->out_content_html, data->in_content_html);
2047 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2048 FALLBACK (data->out_context_html, data->in_context_html);
2049 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2051 /* status -> in_content */
2052 FALLBACK (data->status_html, data->in_content_html);
2056 /* template -> empathy's template */
2057 data->custom_template = (template_html != NULL);
2058 if (template_html == NULL)
2060 GError *error = NULL;
2062 tmp = empathy_file_lookup ("Template.html", "data");
2064 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2065 g_warning ("couldn't load Empathy's default theme "
2066 "template: %s", error->message);
2067 g_return_val_if_reached (data);
2073 /* Default avatar */
2074 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2075 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2077 data->default_incoming_avatar_filename = tmp;
2084 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2085 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2087 data->default_outgoing_avatar_filename = tmp;
2094 /* Old custom templates had only 4 parameters.
2095 * New templates have 5 parameters */
2096 if (data->version <= 2 && data->custom_template)
2098 tmp = string_with_format (template_html,
2100 "%@", /* Leave variant unset */
2101 "", /* The header */
2102 footer_html ? footer_html : "",
2107 tmp = string_with_format (template_html,
2109 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2110 "%@", /* Leave variant unset */
2111 "", /* The header */
2112 footer_html ? footer_html : "",
2115 g_ptr_array_add (data->strings_to_free, tmp);
2116 data->template_html = tmp;
2118 g_free (template_html);
2119 g_free (footer_html);
2125 empathy_adium_data_new (const gchar *path)
2127 EmpathyAdiumData *data;
2130 info = empathy_adium_info_new (path);
2131 data = empathy_adium_data_new_with_info (path, info);
2132 g_hash_table_unref (info);
2138 empathy_adium_data_ref (EmpathyAdiumData *data)
2140 g_return_val_if_fail (data != NULL, NULL);
2142 g_atomic_int_inc (&data->ref_count);
2148 empathy_adium_data_unref (EmpathyAdiumData *data)
2150 g_return_if_fail (data != NULL);
2152 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2153 g_free (data->path);
2154 g_free (data->basedir);
2155 g_free (data->default_avatar_filename);
2156 g_free (data->default_incoming_avatar_filename);
2157 g_free (data->default_outgoing_avatar_filename);
2158 g_hash_table_unref (data->info);
2159 g_ptr_array_unref (data->strings_to_free);
2160 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2162 g_slice_free (EmpathyAdiumData, data);
2167 empathy_adium_data_get_info (EmpathyAdiumData *data)
2169 g_return_val_if_fail (data != NULL, NULL);
2175 empathy_adium_data_get_path (EmpathyAdiumData *data)
2177 g_return_val_if_fail (data != NULL, NULL);