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,
547 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
549 const gchar *cur = NULL;
552 /* Make some search-and-replace in the html code */
553 string = g_string_sized_new (strlen (html) + strlen (message));
554 g_string_append_printf (string, "%s(\"", func);
555 for (cur = html; *cur != '\0'; cur++) {
556 const gchar *replace = NULL;
557 gchar *dup_replace = NULL;
558 gchar *format = NULL;
560 /* Those are all well known keywords that needs replacement in
561 * html files. Please keep them in the same order than the adium
562 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
563 if (theme_adium_match (&cur, "%userIconPath%")) {
564 replace = avatar_filename;
565 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
566 replace = contact_id;
567 } else if (theme_adium_match (&cur, "%sender%")) {
569 } else if (theme_adium_match (&cur, "%senderColor%")) {
570 /* A color derived from the user's name.
571 * FIXME: If a colon separated list of HTML colors is at
572 * Incoming/SenderColors.txt it will be used instead of
573 * the default colors.
576 /* Ensure we always use the same color when sending messages
580 } else if (contact_id != NULL) {
581 guint hash = g_str_hash (contact_id);
582 replace = colors[hash % G_N_ELEMENTS (colors)];
584 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
585 /* FIXME: The path to the status icon of the sender
586 * (available, away, etc...)
588 } else if (theme_adium_match (&cur, "%messageDirection%")) {
589 /* FIXME: The text direction of the message
590 * (either rtl or ltr)
592 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
593 /* FIXME: The serverside (remotely set) name of the
594 * sender, such as an MSN display name.
596 * We don't have access to that yet so we use
597 * local alias instead.
600 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
601 /* FIXME: This keyword is used to represent the
602 * highlight background color. "X" is the opacity of the
603 * background, ranges from 0 to 1 and can be any decimal
606 } else if (theme_adium_match (&cur, "%message%")) {
608 } else if (theme_adium_match (&cur, "%time%") ||
609 theme_adium_match_with_format (&cur, "%time{", &format)) {
610 const gchar *strftime_format;
612 strftime_format = nsdate_to_strftime (priv->data, format);
614 dup_replace = empathy_time_to_string_local (timestamp,
615 strftime_format ? strftime_format :
616 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
618 dup_replace = empathy_time_to_string_local (timestamp,
619 strftime_format ? strftime_format :
620 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
622 replace = dup_replace;
623 } else if (theme_adium_match (&cur, "%shortTime%")) {
624 dup_replace = empathy_time_to_string_local (timestamp,
625 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
626 replace = dup_replace;
627 } else if (theme_adium_match (&cur, "%service%")) {
628 replace = service_name;
629 } else if (theme_adium_match (&cur, "%variant%")) {
630 /* FIXME: The name of the active message style variant,
631 * with all spaces replaced with an underscore.
632 * A variant named "Alternating Messages - Blue Red"
633 * will become "Alternating_Messages_-_Blue_Red".
635 } else if (theme_adium_match (&cur, "%userIcons%")) {
636 /* FIXME: mus t be "hideIcons" if use preference is set
638 replace = "showIcons";
639 } else if (theme_adium_match (&cur, "%messageClasses%")) {
640 replace = message_classes;
641 } else if (theme_adium_match (&cur, "%status%")) {
642 /* FIXME: A description of the status event. This is
643 * neither in the user's local language nor expected to
644 * be displayed; it may be useful to use a different div
645 * class to present different types of status messages.
646 * The following is a list of some of the more important
647 * status messages; your message style should be able to
648 * handle being shown a status message not in this list,
649 * as even at present the list is incomplete and is
650 * certain to become out of date in the future:
659 * contact_joined (group chats)
663 * encryption (all OTR messages use this status)
664 * purple (all IRC topic and join/part messages use this status)
665 * fileTransferStarted
666 * fileTransferCompleted
669 escape_and_append_len (string, cur, 1);
673 /* Here we have a replacement to make */
674 escape_and_append_len (string, replace, -1);
676 g_free (dup_replace);
679 g_string_append (string, "\")");
681 script = g_string_free (string, FALSE);
682 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
687 theme_adium_append_event_escaped (EmpathyChatView *view,
688 const gchar *escaped)
690 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
691 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
693 theme_adium_append_html (theme, "appendMessage",
694 priv->data->status_html, escaped, NULL, NULL, NULL,
696 empathy_time_get_current (), FALSE, FALSE);
698 /* There is no last contact */
699 if (priv->last_contact) {
700 g_object_unref (priv->last_contact);
701 priv->last_contact = NULL;
706 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
707 WebKitDOMNodeList *nodes)
711 /* Remove focus and firstFocus class */
712 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
713 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
714 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
716 gchar **classes, **iter;
717 GString *new_class_name;
718 gboolean first = TRUE;
720 if (element == NULL) {
724 class_name = webkit_dom_html_element_get_class_name (element);
725 classes = g_strsplit (class_name, " ", -1);
726 new_class_name = g_string_sized_new (strlen (class_name));
727 for (iter = classes; *iter != NULL; iter++) {
728 if (tp_strdiff (*iter, "focus") &&
729 tp_strdiff (*iter, "firstFocus")) {
731 g_string_append_c (new_class_name, ' ');
733 g_string_append (new_class_name, *iter);
738 webkit_dom_html_element_set_class_name (element, new_class_name->str);
741 g_strfreev (classes);
742 g_string_free (new_class_name, TRUE);
747 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
749 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
750 WebKitDOMDocument *dom;
751 WebKitDOMNodeList *nodes;
752 GError *error = NULL;
754 if (!priv->has_unread_message)
757 priv->has_unread_message = FALSE;
759 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
764 /* Get all nodes with focus class */
765 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
767 DEBUG ("Error getting focus nodes: %s",
768 error ? error->message : "No error");
769 g_clear_error (&error);
773 theme_adium_remove_focus_marks (theme, nodes);
777 theme_adium_append_message (EmpathyChatView *view,
780 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
781 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
782 EmpathyContact *sender;
787 const gchar *contact_id;
788 EmpathyAvatar *avatar;
789 const gchar *avatar_filename = NULL;
791 const gchar *html = NULL;
793 const gchar *service_name;
794 GString *message_classes = NULL;
796 gboolean consecutive;
799 if (priv->pages_loading != 0) {
800 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL);
804 /* Get information */
805 sender = empathy_message_get_sender (msg);
806 account = empathy_contact_get_account (sender);
807 service_name = empathy_protocol_name_to_display_name
808 (tp_account_get_protocol (account));
809 if (service_name == NULL)
810 service_name = tp_account_get_protocol (account);
811 timestamp = empathy_message_get_timestamp (msg);
812 body_escaped = theme_adium_parse_body (theme,
813 empathy_message_get_body (msg),
814 empathy_message_get_token (msg));
815 name = empathy_contact_get_logged_alias (sender);
816 contact_id = empathy_contact_get_id (sender);
817 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
819 /* If this is a /me probably */
823 if (priv->data->version >= 4 || !priv->data->custom_template) {
824 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
825 "<span class='actionMessageBody'>%s</span>",
828 str = g_strdup_printf ("*%s*", body_escaped);
830 g_free (body_escaped);
834 /* Get the avatar filename, or a fallback */
835 avatar = empathy_contact_get_avatar (sender);
837 avatar_filename = avatar->filename;
839 if (!avatar_filename) {
840 if (empathy_contact_is_user (sender)) {
841 avatar_filename = priv->data->default_outgoing_avatar_filename;
843 avatar_filename = priv->data->default_incoming_avatar_filename;
845 if (!avatar_filename) {
846 if (!priv->data->default_avatar_filename) {
847 priv->data->default_avatar_filename =
848 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
849 GTK_ICON_SIZE_DIALOG);
851 avatar_filename = priv->data->default_avatar_filename;
855 /* We want to join this message with the last one if
856 * - senders are the same contact,
857 * - last message was recieved recently,
858 * - last message and this message both are/aren't backlog, and
859 * - DisableCombineConsecutive is not set in theme's settings */
860 is_backlog = empathy_message_is_backlog (msg);
861 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
862 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
863 (is_backlog == priv->last_is_backlog) &&
864 !tp_asv_get_boolean (priv->data->info,
865 "DisableCombineConsecutive", NULL);
867 /* Define message classes */
868 message_classes = g_string_new ("message");
869 if (!priv->has_focus && !is_backlog) {
870 if (!priv->has_unread_message) {
871 g_string_append (message_classes, " firstFocus");
872 priv->has_unread_message = TRUE;
874 g_string_append (message_classes, " focus");
877 g_string_append (message_classes, " history");
880 g_string_append (message_classes, " consecutive");
882 if (empathy_contact_is_user (sender)) {
883 g_string_append (message_classes, " outgoing");
885 g_string_append (message_classes, " incoming");
887 if (empathy_message_should_highlight (msg)) {
888 g_string_append (message_classes, " mention");
890 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
891 g_string_append (message_classes, " autoreply");
894 g_string_append (message_classes, " action");
896 /* FIXME: other classes:
897 * status - the message is a status change
898 * event - the message is a notification of something happening
899 * (for example, encryption being turned on)
900 * %status% - See %status% in theme_adium_append_html ()
903 /* This is slightly a hack, but it's the only way to add
904 * arbitrary data to messages in the HTML. We add another
905 * class called "x-empathy-message-id-*" to the message. This
906 * way, we can remove the unread marker for this specific
908 tp_msg = empathy_message_get_tp_message (msg);
909 if (tp_msg != NULL) {
913 id = tp_message_get_pending_message_id (tp_msg, &valid);
915 g_string_append_printf (message_classes,
916 " x-empathy-message-id-%u", id);
920 /* Define javascript function to use */
922 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
924 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
927 if (empathy_contact_is_user (sender)) {
931 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
934 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
937 /* remove all the unread marks when we are sending a message */
938 theme_adium_remove_all_focus_marks (theme);
943 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
946 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
950 theme_adium_append_html (theme, func, html, body_escaped,
951 avatar_filename, name, contact_id,
952 service_name, message_classes->str,
953 timestamp, is_backlog, empathy_contact_is_user (sender));
955 /* Keep the sender of the last displayed message */
956 if (priv->last_contact) {
957 g_object_unref (priv->last_contact);
959 priv->last_contact = g_object_ref (sender);
960 priv->last_timestamp = timestamp;
961 priv->last_is_backlog = is_backlog;
963 g_free (body_escaped);
964 g_string_free (message_classes, TRUE);
968 theme_adium_append_event (EmpathyChatView *view,
971 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
974 if (priv->pages_loading != 0) {
975 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str);
979 str_escaped = g_markup_escape_text (str, -1);
980 theme_adium_append_event_escaped (view, str_escaped);
981 g_free (str_escaped);
985 theme_adium_edit_message (EmpathyChatView *view,
986 EmpathyMessage *message)
988 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
989 WebKitDOMDocument *doc;
990 WebKitDOMElement *span;
991 gchar *id, *parsed_body;
992 gchar *tooltip, *timestamp;
993 GtkIconInfo *icon_info;
994 GError *error = NULL;
996 if (priv->pages_loading != 0) {
997 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL);
1001 id = g_strdup_printf ("message-token-%s",
1002 empathy_message_get_supersedes (message));
1003 /* we don't pass a token here, because doing so will return another
1004 * <span> element, and we don't want nested <span> elements */
1005 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1006 empathy_message_get_body (message), NULL);
1008 /* find the element */
1009 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1010 span = webkit_dom_document_get_element_by_id (doc, id);
1013 DEBUG ("Failed to find id '%s'", id);
1017 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1018 DEBUG ("Not a HTML element");
1022 /* update the HTML */
1023 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1024 parsed_body, &error);
1026 if (error != NULL) {
1027 DEBUG ("Error setting new inner-HTML: %s", error->message);
1028 g_error_free (error);
1033 timestamp = empathy_time_to_string_local (
1034 empathy_message_get_timestamp (message),
1036 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1038 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1044 /* mark this message as edited */
1045 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1046 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1048 if (icon_info != NULL) {
1049 /* set the icon as a background image using CSS
1050 * FIXME: the icon won't update in response to theme changes */
1051 gchar *style = g_strdup_printf (
1052 "background-image:url('%s');"
1053 "background-repeat:no-repeat;"
1054 "background-position:left center;"
1055 "padding-left:19px;", /* 16px icon + 3px padding */
1056 gtk_icon_info_get_filename (icon_info));
1058 webkit_dom_element_set_attribute (span, "style", style, &error);
1060 if (error != NULL) {
1061 DEBUG ("Error setting element style: %s",
1063 g_clear_error (&error);
1068 gtk_icon_info_free (icon_info);
1074 DEBUG ("Could not find message to edit with: %s",
1075 empathy_message_get_body (message));
1079 g_free (parsed_body);
1083 theme_adium_scroll (EmpathyChatView *view,
1084 gboolean allow_scrolling)
1086 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1088 priv->allow_scrolling = allow_scrolling;
1089 if (allow_scrolling) {
1090 empathy_chat_view_scroll_down (view);
1095 theme_adium_scroll_down (EmpathyChatView *view)
1097 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1101 theme_adium_get_has_selection (EmpathyChatView *view)
1103 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1107 theme_adium_clear (EmpathyChatView *view)
1109 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1111 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1113 /* Clear last contact to avoid trying to add a 'joined'
1114 * message when we don't have an insertion point. */
1115 if (priv->last_contact) {
1116 g_object_unref (priv->last_contact);
1117 priv->last_contact = NULL;
1122 theme_adium_find_previous (EmpathyChatView *view,
1123 const gchar *search_criteria,
1124 gboolean new_search,
1125 gboolean match_case)
1127 /* FIXME: Doesn't respect new_search */
1128 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1129 search_criteria, match_case,
1134 theme_adium_find_next (EmpathyChatView *view,
1135 const gchar *search_criteria,
1136 gboolean new_search,
1137 gboolean match_case)
1139 /* FIXME: Doesn't respect new_search */
1140 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1141 search_criteria, match_case,
1146 theme_adium_find_abilities (EmpathyChatView *view,
1147 const gchar *search_criteria,
1148 gboolean match_case,
1149 gboolean *can_do_previous,
1150 gboolean *can_do_next)
1152 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1153 * find_next and find_previous to work around this problem. */
1154 if (can_do_previous)
1155 *can_do_previous = TRUE;
1157 *can_do_next = TRUE;
1161 theme_adium_highlight (EmpathyChatView *view,
1163 gboolean match_case)
1165 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1166 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1167 text, match_case, 0);
1168 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1173 theme_adium_copy_clipboard (EmpathyChatView *view)
1175 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1179 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1182 WebKitDOMDocument *dom;
1183 WebKitDOMNodeList *nodes;
1185 GError *error = NULL;
1187 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1192 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1194 /* Get all nodes with focus class */
1195 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1198 if (nodes == NULL) {
1199 DEBUG ("Error getting focus nodes: %s",
1200 error ? error->message : "No error");
1201 g_clear_error (&error);
1205 theme_adium_remove_focus_marks (self, nodes);
1209 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1212 EmpathyThemeAdium *self = user_data;
1213 guint32 id = GPOINTER_TO_UINT (data);
1215 theme_adium_remove_mark_from_message (self, id);
1219 theme_adium_focus_toggled (EmpathyChatView *view,
1222 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1224 priv->has_focus = has_focus;
1225 if (!priv->has_focus) {
1226 /* We've lost focus, so let's make sure all the acked
1227 * messages have lost their unread marker. */
1228 g_queue_foreach (&priv->acked_messages,
1229 theme_adium_remove_acked_message_unread_mark_foreach,
1231 g_queue_clear (&priv->acked_messages);
1233 priv->has_unread_message = FALSE;
1238 theme_adium_message_acknowledged (EmpathyChatView *view,
1239 EmpathyMessage *message)
1241 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1242 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1247 tp_msg = empathy_message_get_tp_message (message);
1249 if (tp_msg == NULL) {
1253 id = tp_message_get_pending_message_id (tp_msg, &valid);
1255 g_warning ("Acknoledged message doesn't have a pending ID");
1259 /* We only want to actually remove the unread marker if the
1260 * view doesn't have focus. If we did it all the time we would
1261 * never see the unread markers, ever! So, we'll queue these
1262 * up, and when we lose focus, we'll remove the markers. */
1263 if (priv->has_focus) {
1264 g_queue_push_tail (&priv->acked_messages,
1265 GUINT_TO_POINTER (id));
1269 theme_adium_remove_mark_from_message (self, id);
1273 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1275 if (event->button == 3) {
1276 gboolean developer_tools_enabled;
1278 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1279 "enable-developer-extras", &developer_tools_enabled, NULL);
1281 /* We currently have no way to add an inspector menu
1282 * item ourselves, so we disable our customized menu
1283 * if the developer extras are enabled. */
1284 if (!developer_tools_enabled) {
1285 empathy_webkit_context_menu_for_event (
1286 WEBKIT_WEB_VIEW (widget), event,
1287 EMPATHY_WEBKIT_MENU_CLEAR);
1292 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1296 theme_adium_iface_init (EmpathyChatViewIface *iface)
1298 iface->append_message = theme_adium_append_message;
1299 iface->append_event = theme_adium_append_event;
1300 iface->edit_message = theme_adium_edit_message;
1301 iface->scroll = theme_adium_scroll;
1302 iface->scroll_down = theme_adium_scroll_down;
1303 iface->get_has_selection = theme_adium_get_has_selection;
1304 iface->clear = theme_adium_clear;
1305 iface->find_previous = theme_adium_find_previous;
1306 iface->find_next = theme_adium_find_next;
1307 iface->find_abilities = theme_adium_find_abilities;
1308 iface->highlight = theme_adium_highlight;
1309 iface->copy_clipboard = theme_adium_copy_clipboard;
1310 iface->focus_toggled = theme_adium_focus_toggled;
1311 iface->message_acknowledged = theme_adium_message_acknowledged;
1315 theme_adium_load_finished_cb (WebKitWebView *view,
1316 WebKitWebFrame *frame,
1319 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1320 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1323 DEBUG ("Page loaded");
1324 priv->pages_loading--;
1326 if (priv->pages_loading != 0)
1329 /* Display queued messages */
1330 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1331 QueuedItem *item = l->data;
1335 case QUEUED_MESSAGE:
1336 theme_adium_append_message (chat_view, item->msg);
1340 theme_adium_edit_message (chat_view, item->msg);
1344 theme_adium_append_event (chat_view, item->str);
1348 free_queued_item (item);
1351 g_queue_clear (&priv->message_queue);
1355 theme_adium_finalize (GObject *object)
1357 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1359 empathy_adium_data_unref (priv->data);
1361 g_object_unref (priv->gsettings_chat);
1362 g_object_unref (priv->gsettings_desktop);
1364 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1368 theme_adium_dispose (GObject *object)
1370 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1372 if (priv->smiley_manager) {
1373 g_object_unref (priv->smiley_manager);
1374 priv->smiley_manager = NULL;
1377 if (priv->last_contact) {
1378 g_object_unref (priv->last_contact);
1379 priv->last_contact = NULL;
1382 if (priv->inspector_window) {
1383 gtk_widget_destroy (priv->inspector_window);
1384 priv->inspector_window = NULL;
1387 if (priv->acked_messages.length > 0) {
1388 g_queue_clear (&priv->acked_messages);
1391 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1395 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1396 EmpathyThemeAdium *theme)
1398 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1400 if (priv->inspector_window) {
1401 gtk_widget_show_all (priv->inspector_window);
1408 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1409 EmpathyThemeAdium *theme)
1411 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1413 if (priv->inspector_window) {
1414 gtk_widget_hide (priv->inspector_window);
1420 static WebKitWebView *
1421 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1422 WebKitWebView *web_view,
1423 EmpathyThemeAdium *theme)
1425 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1426 GtkWidget *scrolled_window;
1427 GtkWidget *inspector_web_view;
1429 if (!priv->inspector_window) {
1430 /* Create main window */
1431 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1432 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1434 g_signal_connect (priv->inspector_window, "delete-event",
1435 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1437 /* Pack a scrolled window */
1438 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1439 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1440 GTK_POLICY_AUTOMATIC,
1441 GTK_POLICY_AUTOMATIC);
1442 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1444 gtk_widget_show (scrolled_window);
1446 /* Pack a webview in the scrolled window. That webview will be
1447 * used to render the inspector tool. */
1448 inspector_web_view = webkit_web_view_new ();
1449 gtk_container_add (GTK_CONTAINER (scrolled_window),
1450 inspector_web_view);
1451 gtk_widget_show (scrolled_window);
1453 return WEBKIT_WEB_VIEW (inspector_web_view);
1460 theme_adium_constructed (GObject *object)
1462 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1463 const gchar *font_family = NULL;
1465 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1466 WebKitWebInspector *webkit_inspector;
1468 /* Set default settings */
1469 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1470 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1472 if (font_family && font_size) {
1473 g_object_set (webkit_web_view_get_settings (webkit_view),
1474 "default-font-family", font_family,
1475 "default-font-size", font_size,
1478 empathy_webkit_bind_font_setting (webkit_view,
1479 priv->gsettings_desktop,
1480 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1483 /* Setup webkit inspector */
1484 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1485 g_signal_connect (webkit_inspector, "inspect-web-view",
1486 G_CALLBACK (theme_adium_inspect_web_view_cb),
1488 g_signal_connect (webkit_inspector, "show-window",
1489 G_CALLBACK (theme_adium_inspector_show_window_cb),
1491 g_signal_connect (webkit_inspector, "close-window",
1492 G_CALLBACK (theme_adium_inspector_close_window_cb),
1496 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1498 priv->in_construction = FALSE;
1502 theme_adium_get_property (GObject *object,
1507 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1510 case PROP_ADIUM_DATA:
1511 g_value_set_boxed (value, priv->data);
1514 g_value_set_string (value, priv->variant);
1517 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1523 theme_adium_set_property (GObject *object,
1525 const GValue *value,
1528 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1529 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1532 case PROP_ADIUM_DATA:
1533 g_assert (priv->data == NULL);
1534 priv->data = g_value_dup_boxed (value);
1537 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1540 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1546 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1548 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1549 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1551 object_class->finalize = theme_adium_finalize;
1552 object_class->dispose = theme_adium_dispose;
1553 object_class->constructed = theme_adium_constructed;
1554 object_class->get_property = theme_adium_get_property;
1555 object_class->set_property = theme_adium_set_property;
1557 widget_class->button_press_event = theme_adium_button_press_event;
1559 g_object_class_install_property (object_class,
1561 g_param_spec_boxed ("adium-data",
1563 "Data for the adium theme",
1564 EMPATHY_TYPE_ADIUM_DATA,
1565 G_PARAM_CONSTRUCT_ONLY |
1567 G_PARAM_STATIC_STRINGS));
1568 g_object_class_install_property (object_class,
1570 g_param_spec_string ("variant",
1571 "The theme variant",
1572 "Variant name for the theme",
1576 G_PARAM_STATIC_STRINGS));
1578 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1582 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1584 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1585 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1589 priv->in_construction = TRUE;
1590 g_queue_init (&priv->message_queue);
1591 priv->allow_scrolling = TRUE;
1592 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1594 g_signal_connect (theme, "load-finished",
1595 G_CALLBACK (theme_adium_load_finished_cb),
1597 g_signal_connect (theme, "navigation-policy-decision-requested",
1598 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1601 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1602 priv->gsettings_desktop = g_settings_new (
1603 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1605 g_signal_connect (priv->gsettings_chat,
1606 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1607 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1610 theme_adium_update_enable_webkit_developer_tools (theme);
1614 empathy_theme_adium_new (EmpathyAdiumData *data,
1615 const gchar *variant)
1617 g_return_val_if_fail (data != NULL, NULL);
1619 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1626 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1627 const gchar *variant)
1629 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1630 gchar *variant_path;
1633 if (!tp_strdiff (priv->variant, variant)) {
1637 g_free (priv->variant);
1638 priv->variant = g_strdup (variant);
1640 if (priv->in_construction) {
1644 DEBUG ("Update view with variant: '%s'", variant);
1645 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1647 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1649 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1651 g_free (variant_path);
1654 g_object_notify (G_OBJECT (theme), "variant");
1658 empathy_adium_path_is_valid (const gchar *path)
1663 /* The theme is not valid if there is no Info.plist */
1664 file = g_build_filename (path, "Contents", "Info.plist",
1666 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1672 /* We ship a default Template.html as fallback if there is any problem
1673 * with the one inside the theme. The only other required file is
1674 * Content.html OR Incoming/Content.html*/
1675 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1677 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1681 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1682 "Content.html", NULL);
1683 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1691 empathy_adium_info_new (const gchar *path)
1695 GHashTable *info = NULL;
1697 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1699 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1700 value = empathy_plist_parse_from_file (file);
1706 info = g_value_dup_boxed (value);
1707 tp_g_value_slice_free (value);
1709 /* Insert the theme's path into the hash table,
1710 * keys have to be dupped */
1711 tp_asv_set_string (info, g_strdup ("path"), path);
1717 adium_info_get_version (GHashTable *info)
1719 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1722 static const gchar *
1723 adium_info_get_no_variant_name (GHashTable *info)
1725 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1726 return name ? name : _("Normal");
1730 adium_info_dup_path_for_variant (GHashTable *info,
1731 const gchar *variant)
1733 guint version = adium_info_get_version (info);
1734 const gchar *no_variant = adium_info_get_no_variant_name (info);
1735 GPtrArray *variants;
1738 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1739 return g_strdup ("main.css");
1742 /* Verify the variant exists, fallback to the first one */
1743 variants = empathy_adium_info_get_available_variants (info);
1744 for (i = 0; i < variants->len; i++) {
1745 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1749 if (i == variants->len) {
1750 DEBUG ("Variant %s does not exist", variant);
1751 variant = g_ptr_array_index (variants, 0);
1754 return g_strdup_printf ("Variants/%s.css", variant);
1759 empathy_adium_info_get_default_variant (GHashTable *info)
1761 if (adium_info_get_version (info) <= 2) {
1762 return adium_info_get_no_variant_name (info);
1765 return tp_asv_get_string (info, "DefaultVariant");
1769 empathy_adium_info_get_available_variants (GHashTable *info)
1771 GPtrArray *variants;
1776 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1777 if (variants != NULL) {
1781 variants = g_ptr_array_new_with_free_func (g_free);
1782 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1783 G_TYPE_PTR_ARRAY, variants);
1785 path = tp_asv_get_string (info, "path");
1786 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1787 dir = g_dir_open (dirpath, 0, NULL);
1791 for (name = g_dir_read_name (dir);
1793 name = g_dir_read_name (dir)) {
1794 gchar *display_name;
1796 if (!g_str_has_suffix (name, ".css")) {
1800 display_name = g_strdup (name);
1801 strstr (display_name, ".css")[0] = '\0';
1802 g_ptr_array_add (variants, display_name);
1808 if (adium_info_get_version (info) <= 2) {
1809 g_ptr_array_add (variants,
1810 g_strdup (adium_info_get_no_variant_name (info)));
1817 empathy_adium_data_get_type (void)
1819 static GType type_id = 0;
1823 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1824 (GBoxedCopyFunc) empathy_adium_data_ref,
1825 (GBoxedFreeFunc) empathy_adium_data_unref);
1832 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1834 EmpathyAdiumData *data;
1835 gchar *template_html = NULL;
1836 gchar *footer_html = NULL;
1839 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1841 data = g_slice_new0 (EmpathyAdiumData);
1842 data->ref_count = 1;
1843 data->path = g_strdup (path);
1844 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1845 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1846 data->info = g_hash_table_ref (info);
1847 data->version = adium_info_get_version (info);
1848 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1849 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1850 g_str_equal, g_free, g_free);
1852 DEBUG ("Loading theme at %s", path);
1854 #define LOAD(path, var) \
1855 tmp = g_build_filename (data->basedir, path, NULL); \
1856 g_file_get_contents (tmp, &var, NULL, NULL); \
1859 #define LOAD_CONST(path, var) \
1862 LOAD (path, content); \
1863 if (content != NULL) { \
1864 g_ptr_array_add (data->strings_to_free, content); \
1869 /* Load html files */
1870 LOAD_CONST ("Content.html", data->content_html);
1871 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1872 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1873 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1874 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1875 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1876 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1877 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1878 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1879 LOAD_CONST ("Status.html", data->status_html);
1880 LOAD ("Template.html", template_html);
1881 LOAD ("Footer.html", footer_html);
1886 /* HTML fallbacks: If we have at least content OR in_content, then
1887 * everything else gets a fallback */
1889 #define FALLBACK(html, fallback) \
1890 if (html == NULL) { \
1894 /* in_nextcontent -> in_content -> content */
1895 FALLBACK (data->in_content_html, data->content_html);
1896 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1898 /* context -> content */
1899 FALLBACK (data->in_context_html, data->in_content_html);
1900 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1901 FALLBACK (data->out_context_html, data->out_content_html);
1902 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1905 FALLBACK (data->out_content_html, data->in_content_html);
1906 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1907 FALLBACK (data->out_context_html, data->in_context_html);
1908 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1910 /* status -> in_content */
1911 FALLBACK (data->status_html, data->in_content_html);
1915 /* template -> empathy's template */
1916 data->custom_template = (template_html != NULL);
1917 if (template_html == NULL) {
1918 tmp = empathy_file_lookup ("Template.html", "data");
1919 g_file_get_contents (tmp, &template_html, NULL, NULL);
1923 /* Default avatar */
1924 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1925 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1926 data->default_incoming_avatar_filename = tmp;
1930 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1931 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1932 data->default_outgoing_avatar_filename = tmp;
1937 /* Old custom templates had only 4 parameters.
1938 * New templates have 5 parameters */
1939 if (data->version <= 2 && data->custom_template) {
1940 tmp = string_with_format (template_html,
1942 "%@", /* Leave variant unset */
1943 "", /* The header */
1944 footer_html ? footer_html : "",
1947 tmp = string_with_format (template_html,
1949 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1950 "%@", /* Leave variant unset */
1951 "", /* The header */
1952 footer_html ? footer_html : "",
1955 g_ptr_array_add (data->strings_to_free, tmp);
1956 data->template_html = tmp;
1958 g_free (template_html);
1959 g_free (footer_html);
1965 empathy_adium_data_new (const gchar *path)
1967 EmpathyAdiumData *data;
1970 info = empathy_adium_info_new (path);
1971 data = empathy_adium_data_new_with_info (path, info);
1972 g_hash_table_unref (info);
1978 empathy_adium_data_ref (EmpathyAdiumData *data)
1980 g_return_val_if_fail (data != NULL, NULL);
1982 g_atomic_int_inc (&data->ref_count);
1988 empathy_adium_data_unref (EmpathyAdiumData *data)
1990 g_return_if_fail (data != NULL);
1992 if (g_atomic_int_dec_and_test (&data->ref_count)) {
1993 g_free (data->path);
1994 g_free (data->basedir);
1995 g_free (data->default_avatar_filename);
1996 g_free (data->default_incoming_avatar_filename);
1997 g_free (data->default_outgoing_avatar_filename);
1998 g_hash_table_unref (data->info);
1999 g_ptr_array_unref (data->strings_to_free);
2000 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2002 g_slice_free (EmpathyAdiumData, data);
2007 empathy_adium_data_get_info (EmpathyAdiumData *data)
2009 g_return_val_if_fail (data != NULL, NULL);
2015 empathy_adium_data_get_path (EmpathyAdiumData *data)
2017 g_return_val_if_fail (data != NULL, NULL);