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 gchar * adium_info_dup_path_for_variant (GHashTable *info,
112 const gchar *variant);
121 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
122 WEBKIT_TYPE_WEB_VIEW)
136 gboolean should_highlight;
140 queue_item (GQueue *queue,
144 gboolean should_highlight)
146 QueuedItem *item = g_slice_new0 (QueuedItem);
150 item->msg = g_object_ref (msg);
151 item->str = g_strdup (str);
152 item->should_highlight = should_highlight;
154 g_queue_push_tail (queue, item);
160 free_queued_item (QueuedItem *item)
162 tp_clear_object (&item->msg);
165 g_slice_free (QueuedItem, item);
169 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *self)
171 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
172 gboolean enable_webkit_developer_tools;
174 enable_webkit_developer_tools = g_settings_get_boolean (
175 self->priv->gsettings_chat,
176 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
178 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
179 "enable-developer-extras", enable_webkit_developer_tools, NULL);
183 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
187 EmpathyThemeAdium *self = user_data;
189 theme_adium_update_enable_webkit_developer_tools (self);
193 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
194 WebKitWebFrame *web_frame,
195 WebKitNetworkRequest *request,
196 WebKitWebNavigationAction *action,
197 WebKitWebPolicyDecision *decision,
202 /* Only call url_show on clicks */
203 if (webkit_web_navigation_action_get_reason (action) !=
204 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
206 webkit_web_policy_decision_use (decision);
210 uri = webkit_network_request_get_uri (request);
211 empathy_url_show (GTK_WIDGET (view), uri);
213 webkit_web_policy_decision_ignore (decision);
217 /* Replace each %@ in format with string passed in args */
219 string_with_format (const gchar *format,
220 const gchar *first_string,
227 va_start (args, first_string);
228 result = g_string_sized_new (strlen (format));
229 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
233 next = strstr (format, "%@");
237 g_string_append_len (result, format, next - format);
238 g_string_append (result, str);
241 g_string_append (result, format);
244 return g_string_free (result, FALSE);
248 theme_adium_load_template (EmpathyThemeAdium *self)
254 self->priv->pages_loading++;
255 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
257 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
258 self->priv->variant);
260 template = string_with_format (self->priv->data->template_html,
263 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
264 template, basedir_uri);
266 g_free (basedir_uri);
267 g_free (variant_path);
272 theme_adium_parse_body (EmpathyThemeAdium *self,
276 EmpathyStringParser *parsers;
279 /* Check if we have to parse smileys */
280 parsers = empathy_webkit_get_string_parser (
281 g_settings_get_boolean (self->priv->gsettings_chat,
282 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
284 /* Parse text and construct string with links and smileys replaced
285 * by html tags. Also escape text to make sure html code is
286 * displayed verbatim. */
287 string = g_string_sized_new (strlen (text));
289 /* wrap this in HTML that allows us to find the message for later
291 if (!tp_str_empty (token))
292 g_string_append_printf (string,
293 "<span id=\"message-token-%s\">",
296 empathy_string_parser_substr (text, -1, parsers, string);
298 if (!tp_str_empty (token))
299 g_string_append (string, "</span>");
301 /* Wrap body in order to make tabs and multiple spaces displayed
302 * properly. See bug #625745. */
303 g_string_prepend (string, "<div style=\"display: inline; "
304 "white-space: pre-wrap\"'>");
305 g_string_append (string, "</div>");
307 return g_string_free (string, FALSE);
311 escape_and_append_len (GString *string, const gchar *str, gint len)
313 while (str != NULL && *str != '\0' && len != 0)
319 g_string_append (string, "\\\\");
323 g_string_append (string, "\\\"");
326 /* Remove end of lines */
329 g_string_append_c (string, *str);
337 /* If *str starts with match, returns TRUE and move pointer to the end */
339 theme_adium_match (const gchar **str,
344 len = strlen (match);
345 if (strncmp (*str, match, len) == 0)
354 /* Like theme_adium_match() but also return the X part if match is
357 theme_adium_match_with_format (const gchar **str,
361 const gchar *cur = *str;
364 if (!theme_adium_match (&cur, match))
369 end = strstr (cur, "}%");
373 *format = g_strndup (cur , end - cur);
378 /* List of colors used by %senderColor%. Copied from
379 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
381 static gchar *colors[] = {
382 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
383 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
384 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
385 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
386 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
387 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
388 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
389 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
390 "lightblue", "lightcoral",
391 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
392 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
393 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
394 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
395 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
396 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
397 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
398 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
399 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
400 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
405 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
407 /* Convert from NSDateFormatter
408 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
409 * to strftime supported by g_date_time_format.
410 * FIXME: table is incomplete, doc of g_date_time_format has a table of
412 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
413 * in 2.29.x we have to explictely request padding with %0x */
414 static const gchar *convert_table[] = {
416 "A", NULL, // 0~86399999 (Millisecond of Day)
418 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
419 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
420 "cc", "%u", // 1~7 (Day of Week)
421 "c", "%u", // 1~7 (Day of Week)
423 "dd", "%d", // 1~31 (0 padded Day of Month)
424 "d", "%d", // 1~31 (0 padded Day of Month)
425 "D", "%j", // 1~366 (0 padded Day of Year)
427 "e", "%u", // 1~7 (0 padded Day of Week)
428 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
429 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
430 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
431 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
433 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
435 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
436 "GGGG", NULL, // Before Christ/Anno Domini
437 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
438 "GG", NULL, // BC/AD (Era Designator Abbreviated)
439 "G", NULL, // BC/AD (Era Designator Abbreviated)
441 "h", "%I", // 1~12 (0 padded Hour (12hr))
442 "H", "%H", // 0~23 (0 padded Hour (24hr))
444 "k", NULL, // 1~24 (0 padded Hour (24hr)
445 "K", NULL, // 0~11 (0 padded Hour (12hr))
447 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
448 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
449 "LL", "%m", // 1~12 (0 padded Month)
450 "L", "%m", // 1~12 (0 padded Month)
452 "m", "%M", // 0~59 (0 padded Minute)
453 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
454 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
455 "MM", "%m", // 1~12 (0 padded Month)
456 "M", "%m", // 1~12 (0 padded Month)
458 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
459 "qqq", NULL, // Q1/Q2/Q3/Q4
460 "qq", NULL, // 1~4 (0 padded Quarter)
461 "q", NULL, // 1~4 (0 padded Quarter)
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)
467 "s", "%S", // 0~59 (0 padded Second)
468 "S", NULL, // (rounded Sub-Second)
470 "u", "%Y", // (0 padded Year)
472 "vvvv", "%Z", // (General GMT Timezone Name)
473 "vvv", "%Z", // (General GMT Timezone Abbreviation)
474 "vv", "%Z", // (General GMT Timezone Abbreviation)
475 "v", "%Z", // (General GMT Timezone Abbreviation)
477 "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)
478 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
480 "yyyy", "%Y", // (Full Year)
481 "yyy", "%y", // (2 Digits Year)
482 "yy", "%y", // (2 Digits Year)
483 "y", "%Y", // (Full Year)
484 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
485 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
486 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
487 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
489 "zzzz", NULL, // (Specific GMT Timezone Name)
490 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
491 "zz", NULL, // (Specific GMT Timezone Abbreviation)
492 "z", NULL, // (Specific GMT Timezone Abbreviation)
493 "Z", "%z", // +0000 (RFC 822 Timezone)
502 str = g_hash_table_lookup (data->date_format_cache, nsdate);
507 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
508 * by corresponding strftime tag. */
509 string = g_string_sized_new (strlen (nsdate));
510 for (i = 0; nsdate[i] != '\0'; i++)
512 gboolean found = FALSE;
514 /* even indexes are NSDateFormatter tag, odd indexes are the
515 * corresponding strftime tag */
516 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
518 if (g_str_has_prefix (nsdate + i, convert_table[j]))
527 /* If we don't have a replacement, just ignore that tag */
528 if (convert_table[j + 1] != NULL)
529 g_string_append (string, convert_table[j + 1]);
531 i += strlen (convert_table[j]) - 1;
535 g_string_append_c (string, nsdate[i]);
539 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
541 /* The cache takes ownership of string->str */
542 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
543 return g_string_free (string, FALSE);
548 theme_adium_append_html (EmpathyThemeAdium *self,
551 const gchar *message,
552 const gchar *avatar_filename,
554 const gchar *contact_id,
555 const gchar *service_name,
556 const gchar *message_classes,
562 const gchar *cur = NULL;
565 /* Make some search-and-replace in the html code */
566 string = g_string_sized_new (strlen (html) + strlen (message));
567 g_string_append_printf (string, "%s(\"", func);
569 for (cur = html; *cur != '\0'; cur++)
571 const gchar *replace = NULL;
572 gchar *dup_replace = NULL;
573 gchar *format = NULL;
575 /* Those are all well known keywords that needs replacement in
576 * html files. Please keep them in the same order than the adium
577 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
578 if (theme_adium_match (&cur, "%userIconPath%"))
580 replace = avatar_filename;
582 else if (theme_adium_match (&cur, "%senderScreenName%"))
584 replace = contact_id;
586 else if (theme_adium_match (&cur, "%sender%"))
590 else if (theme_adium_match (&cur, "%senderColor%"))
592 /* A color derived from the user's name.
593 * FIXME: If a colon separated list of HTML colors is at
594 * Incoming/SenderColors.txt it will be used instead of
595 * the default colors.
598 /* Ensure we always use the same color when sending messages
604 else if (contact_id != NULL)
606 guint hash = g_str_hash (contact_id);
607 replace = colors[hash % G_N_ELEMENTS (colors)];
610 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
612 /* FIXME: The path to the status icon of the sender
613 * (available, away, etc...)
616 else if (theme_adium_match (&cur, "%messageDirection%"))
618 /* FIXME: The text direction of the message
619 * (either rtl or ltr)
622 else if (theme_adium_match (&cur, "%senderDisplayName%"))
624 /* FIXME: The serverside (remotely set) name of the
625 * sender, such as an MSN display name.
627 * We don't have access to that yet so we use
628 * local alias instead.
632 else if (theme_adium_match (&cur, "%senderPrefix%"))
634 /* FIXME: If we supported IRC user mode flags, this
635 * would be replaced with @ if the user is an op, + if
636 * the user has voice, etc. as per
637 * http://hg.adium.im/adium/rev/b586b027de42. But we
638 * don't, so for now we just strip it. */
640 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
643 /* FIXME: This keyword is used to represent the
644 * highlight background color. "X" is the opacity of the
645 * background, ranges from 0 to 1 and can be any decimal
649 else if (theme_adium_match (&cur, "%message%"))
653 else if (theme_adium_match (&cur, "%time%") ||
654 theme_adium_match_with_format (&cur, "%time{", &format))
656 const gchar *strftime_format;
658 strftime_format = nsdate_to_strftime (self->priv->data, format);
660 dup_replace = empathy_time_to_string_local (timestamp,
661 strftime_format ? strftime_format :
662 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
664 dup_replace = empathy_time_to_string_local (timestamp,
665 strftime_format ? strftime_format :
666 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
668 replace = dup_replace;
670 else if (theme_adium_match (&cur, "%shortTime%"))
672 dup_replace = empathy_time_to_string_local (timestamp,
673 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
674 replace = dup_replace;
676 else if (theme_adium_match (&cur, "%service%"))
678 replace = service_name;
680 else if (theme_adium_match (&cur, "%variant%"))
682 /* FIXME: The name of the active message style variant,
683 * with all spaces replaced with an underscore.
684 * A variant named "Alternating Messages - Blue Red"
685 * will become "Alternating_Messages_-_Blue_Red".
688 else if (theme_adium_match (&cur, "%userIcons%"))
690 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
692 else if (theme_adium_match (&cur, "%messageClasses%"))
694 replace = message_classes;
696 else if (theme_adium_match (&cur, "%status%"))
698 /* FIXME: A description of the status event. This is
699 * neither in the user's local language nor expected to
700 * be displayed; it may be useful to use a different div
701 * class to present different types of status messages.
702 * The following is a list of some of the more important
703 * status messages; your message style should be able to
704 * handle being shown a status message not in this list,
705 * as even at present the list is incomplete and is
706 * certain to become out of date in the future:
715 * contact_joined (group chats)
719 * encryption (all OTR messages use this status)
720 * purple (all IRC topic and join/part messages use this status)
721 * fileTransferStarted
722 * fileTransferCompleted
727 escape_and_append_len (string, cur, 1);
731 /* Here we have a replacement to make */
732 escape_and_append_len (string, replace, -1);
734 g_free (dup_replace);
737 g_string_append (string, "\")");
739 script = g_string_free (string, FALSE);
740 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
745 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
746 const gchar *escaped)
748 theme_adium_append_html (self, "appendMessage",
749 self->priv->data->status_html, escaped, NULL, NULL, NULL,
750 NULL, "event", empathy_time_get_current (), FALSE, FALSE);
752 /* There is no last contact */
753 if (self->priv->last_contact)
755 g_object_unref (self->priv->last_contact);
756 self->priv->last_contact = NULL;
761 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
762 WebKitDOMNodeList *nodes)
766 /* Remove focus and firstFocus class */
767 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
769 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
770 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
772 gchar **classes, **iter;
773 GString *new_class_name;
774 gboolean first = TRUE;
779 class_name = webkit_dom_html_element_get_class_name (element);
780 classes = g_strsplit (class_name, " ", -1);
781 new_class_name = g_string_sized_new (strlen (class_name));
783 for (iter = classes; *iter != NULL; iter++)
785 if (tp_strdiff (*iter, "focus") &&
786 tp_strdiff (*iter, "firstFocus"))
789 g_string_append_c (new_class_name, ' ');
791 g_string_append (new_class_name, *iter);
796 webkit_dom_html_element_set_class_name (element, new_class_name->str);
799 g_strfreev (classes);
800 g_string_free (new_class_name, TRUE);
805 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
807 WebKitDOMDocument *dom;
808 WebKitDOMNodeList *nodes;
809 GError *error = NULL;
811 if (!self->priv->has_unread_message)
814 self->priv->has_unread_message = FALSE;
816 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
820 /* Get all nodes with focus class */
821 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
825 DEBUG ("Error getting focus nodes: %s",
826 error ? error->message : "No error");
827 g_clear_error (&error);
831 theme_adium_remove_focus_marks (self, nodes);
835 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
837 gboolean should_highlight)
839 EmpathyContact *sender;
842 gchar *body_escaped, *name_escaped;
844 const gchar *contact_id;
845 EmpathyAvatar *avatar;
846 const gchar *avatar_filename = NULL;
848 const gchar *html = NULL;
850 const gchar *service_name;
851 GString *message_classes = NULL;
853 gboolean consecutive;
856 if (self->priv->pages_loading != 0)
858 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
863 /* Get information */
864 sender = empathy_message_get_sender (msg);
865 account = empathy_contact_get_account (sender);
866 service_name = empathy_protocol_name_to_display_name
867 (tp_account_get_protocol_name (account));
868 if (service_name == NULL)
869 service_name = tp_account_get_protocol_name (account);
870 timestamp = empathy_message_get_timestamp (msg);
871 body_escaped = theme_adium_parse_body (self,
872 empathy_message_get_body (msg),
873 empathy_message_get_token (msg));
874 name = empathy_contact_get_logged_alias (sender);
875 contact_id = empathy_contact_get_id (sender);
876 action = (empathy_message_get_tptype (msg) ==
877 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
879 name_escaped = g_markup_escape_text (name, -1);
881 /* If this is a /me probably */
886 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
888 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
889 "<span class='actionMessageBody'>%s</span>",
890 name_escaped, body_escaped);
894 str = g_strdup_printf ("*%s*", body_escaped);
897 g_free (body_escaped);
901 /* Get the avatar filename, or a fallback */
902 avatar = empathy_contact_get_avatar (sender);
904 avatar_filename = avatar->filename;
906 if (!avatar_filename)
908 if (empathy_contact_is_user (sender))
909 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
911 avatar_filename = self->priv->data->default_incoming_avatar_filename;
913 if (!avatar_filename)
915 if (!self->priv->data->default_avatar_filename)
916 self->priv->data->default_avatar_filename =
917 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
918 GTK_ICON_SIZE_DIALOG);
920 avatar_filename = self->priv->data->default_avatar_filename;
924 /* We want to join this message with the last one if
925 * - senders are the same contact,
926 * - last message was recieved recently,
927 * - last message and this message both are/aren't backlog, and
928 * - DisableCombineConsecutive is not set in theme's settings */
929 is_backlog = empathy_message_is_backlog (msg);
930 consecutive = empathy_contact_equal (self->priv->last_contact, sender) &&
931 (timestamp - self->priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
932 (is_backlog == self->priv->last_is_backlog) &&
933 !tp_asv_get_boolean (self->priv->data->info,
934 "DisableCombineConsecutive", NULL);
936 /* Define message classes */
937 message_classes = g_string_new ("message");
938 if (!self->priv->has_focus && !is_backlog)
940 if (!self->priv->has_unread_message)
942 g_string_append (message_classes, " firstFocus");
943 self->priv->has_unread_message = TRUE;
945 g_string_append (message_classes, " focus");
949 g_string_append (message_classes, " history");
952 g_string_append (message_classes, " consecutive");
954 if (empathy_contact_is_user (sender))
955 g_string_append (message_classes, " outgoing");
957 g_string_append (message_classes, " incoming");
959 if (should_highlight)
960 g_string_append (message_classes, " mention");
962 if (empathy_message_get_tptype (msg) ==
963 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
964 g_string_append (message_classes, " autoreply");
967 g_string_append (message_classes, " action");
969 /* FIXME: other classes:
970 * status - the message is a status change
971 * event - the message is a notification of something happening
972 * (for example, encryption being turned on)
973 * %status% - See %status% in theme_adium_append_html ()
976 /* This is slightly a hack, but it's the only way to add
977 * arbitrary data to messages in the HTML. We add another
978 * class called "x-empathy-message-id-*" to the message. This
979 * way, we can remove the unread marker for this specific
981 tp_msg = empathy_message_get_tp_message (msg);
987 id = tp_message_get_pending_message_id (tp_msg, &valid);
989 g_string_append_printf (message_classes,
990 " x-empathy-message-id-%u", id);
993 /* Define javascript function to use */
995 func = self->priv->allow_scrolling ? "appendNextMessage" :
996 "appendNextMessageNoScroll";
998 func = self->priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
1000 if (empathy_contact_is_user (sender))
1005 html = consecutive ? self->priv->data->out_nextcontext_html :
1006 self->priv->data->out_context_html;
1009 html = consecutive ? self->priv->data->out_nextcontent_html :
1010 self->priv->data->out_content_html;
1012 /* remove all the unread marks when we are sending a message */
1013 theme_adium_remove_all_focus_marks (self);
1020 html = consecutive ? self->priv->data->in_nextcontext_html :
1021 self->priv->data->in_context_html;
1024 html = consecutive ? self->priv->data->in_nextcontent_html :
1025 self->priv->data->in_content_html;
1028 theme_adium_append_html (self, func, html, body_escaped,
1029 avatar_filename, name_escaped, contact_id,
1030 service_name, message_classes->str,
1031 timestamp, is_backlog, empathy_contact_is_user (sender));
1033 /* Keep the sender of the last displayed message */
1034 if (self->priv->last_contact)
1035 g_object_unref (self->priv->last_contact);
1037 self->priv->last_contact = g_object_ref (sender);
1038 self->priv->last_timestamp = timestamp;
1039 self->priv->last_is_backlog = is_backlog;
1041 g_free (body_escaped);
1042 g_free (name_escaped);
1043 g_string_free (message_classes, TRUE);
1047 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1052 if (self->priv->pages_loading != 0)
1054 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
1058 str_escaped = g_markup_escape_text (str, -1);
1059 theme_adium_append_event_escaped (self, str_escaped);
1060 g_free (str_escaped);
1064 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1065 const gchar *markup_text,
1066 const gchar *fallback_text)
1068 theme_adium_append_event_escaped (self, markup_text);
1072 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1073 EmpathyMessage *message)
1075 WebKitDOMDocument *doc;
1076 WebKitDOMElement *span;
1077 gchar *id, *parsed_body;
1078 gchar *tooltip, *timestamp;
1079 GtkIconInfo *icon_info;
1080 GError *error = NULL;
1082 if (self->priv->pages_loading != 0)
1084 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1088 id = g_strdup_printf ("message-token-%s",
1089 empathy_message_get_supersedes (message));
1090 /* we don't pass a token here, because doing so will return another
1091 * <span> element, and we don't want nested <span> elements */
1092 parsed_body = theme_adium_parse_body (self,
1093 empathy_message_get_body (message), NULL);
1095 /* find the element */
1096 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1097 span = webkit_dom_document_get_element_by_id (doc, id);
1101 DEBUG ("Failed to find id '%s'", id);
1105 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1107 DEBUG ("Not a HTML element");
1111 /* update the HTML */
1112 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1113 parsed_body, &error);
1117 DEBUG ("Error setting new inner-HTML: %s", error->message);
1118 g_error_free (error);
1123 timestamp = empathy_time_to_string_local (
1124 empathy_message_get_timestamp (message),
1126 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1128 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1134 /* mark this message as edited */
1135 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1136 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1138 if (icon_info != NULL)
1140 /* set the icon as a background image using CSS
1141 * FIXME: the icon won't update in response to theme changes */
1142 gchar *style = g_strdup_printf (
1143 "background-image:url('%s');"
1144 "background-repeat:no-repeat;"
1145 "background-position:left center;"
1146 "padding-left:19px;", /* 16px icon + 3px padding */
1147 gtk_icon_info_get_filename (icon_info));
1149 webkit_dom_element_set_attribute (span, "style", style, &error);
1153 DEBUG ("Error setting element style: %s",
1155 g_clear_error (&error);
1160 gtk_icon_info_free (icon_info);
1166 DEBUG ("Could not find message to edit with: %s",
1167 empathy_message_get_body (message));
1171 g_free (parsed_body);
1175 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1176 gboolean allow_scrolling)
1178 self->priv->allow_scrolling = allow_scrolling;
1180 if (allow_scrolling)
1181 empathy_theme_adium_scroll_down (self);
1185 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1187 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1191 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1193 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1197 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1199 theme_adium_load_template (self);
1201 /* Clear last contact to avoid trying to add a 'joined'
1202 * message when we don't have an insertion point. */
1203 if (self->priv->last_contact)
1205 g_object_unref (self->priv->last_contact);
1206 self->priv->last_contact = NULL;
1211 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1212 const gchar *search_criteria,
1213 gboolean new_search,
1214 gboolean match_case)
1216 /* FIXME: Doesn't respect new_search */
1217 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1218 search_criteria, match_case, FALSE, TRUE);
1222 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1223 const gchar *search_criteria,
1224 gboolean new_search,
1225 gboolean match_case)
1227 /* FIXME: Doesn't respect new_search */
1228 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1229 search_criteria, match_case, TRUE, TRUE);
1233 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1234 const gchar *search_criteria,
1235 gboolean match_case,
1236 gboolean *can_do_previous,
1237 gboolean *can_do_next)
1239 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1240 * find_next and find_previous to work around this problem. */
1241 if (can_do_previous)
1242 *can_do_previous = TRUE;
1244 *can_do_next = TRUE;
1248 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1250 gboolean match_case)
1252 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1253 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1254 text, match_case, 0);
1255 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1260 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1262 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1266 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1269 WebKitDOMDocument *dom;
1270 WebKitDOMNodeList *nodes;
1272 GError *error = NULL;
1274 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1278 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1280 /* Get all nodes with focus class */
1281 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1286 DEBUG ("Error getting focus nodes: %s",
1287 error ? error->message : "No error");
1288 g_clear_error (&error);
1292 theme_adium_remove_focus_marks (self, nodes);
1296 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1299 EmpathyThemeAdium *self = user_data;
1300 guint32 id = GPOINTER_TO_UINT (data);
1302 theme_adium_remove_mark_from_message (self, id);
1306 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1309 self->priv->has_focus = has_focus;
1310 if (!self->priv->has_focus)
1312 /* We've lost focus, so let's make sure all the acked
1313 * messages have lost their unread marker. */
1314 g_queue_foreach (&self->priv->acked_messages,
1315 theme_adium_remove_acked_message_unread_mark_foreach, self);
1316 g_queue_clear (&self->priv->acked_messages);
1318 self->priv->has_unread_message = FALSE;
1323 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1324 EmpathyMessage *message)
1330 tp_msg = empathy_message_get_tp_message (message);
1335 id = tp_message_get_pending_message_id (tp_msg, &valid);
1338 g_warning ("Acknoledged message doesn't have a pending ID");
1342 /* We only want to actually remove the unread marker if the
1343 * view doesn't have focus. If we did it all the time we would
1344 * never see the unread markers, ever! So, we'll queue these
1345 * up, and when we lose focus, we'll remove the markers. */
1346 if (self->priv->has_focus)
1348 g_queue_push_tail (&self->priv->acked_messages,
1349 GUINT_TO_POINTER (id));
1353 theme_adium_remove_mark_from_message (self, id);
1357 theme_adium_button_press_event (GtkWidget *widget,
1358 GdkEventButton *event)
1360 if (event->button == 3)
1362 gboolean developer_tools_enabled;
1365 G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1366 "enable-developer-extras", &developer_tools_enabled, NULL);
1368 /* We currently have no way to add an inspector menu
1369 * item ourselves, so we disable our customized menu
1370 * if the developer extras are enabled. */
1371 if (!developer_tools_enabled)
1373 empathy_webkit_context_menu_for_event (
1374 WEBKIT_WEB_VIEW (widget), event,
1375 EMPATHY_WEBKIT_MENU_CLEAR);
1380 return GTK_WIDGET_CLASS (
1381 empathy_theme_adium_parent_class)->button_press_event (widget, event);
1385 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1386 gboolean show_avatars)
1388 self->priv->show_avatars = show_avatars;
1392 theme_adium_load_finished_cb (WebKitWebView *view,
1393 WebKitWebFrame *frame,
1396 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1399 DEBUG ("Page loaded");
1400 self->priv->pages_loading--;
1402 if (self->priv->pages_loading != 0)
1405 /* Display queued messages */
1406 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1408 QueuedItem *item = l->data;
1412 case QUEUED_MESSAGE:
1413 empathy_theme_adium_append_message (self, item->msg,
1414 item->should_highlight);
1418 empathy_theme_adium_edit_message (self, item->msg);
1422 empathy_theme_adium_append_event (self, item->str);
1426 free_queued_item (item);
1429 g_queue_clear (&self->priv->message_queue);
1433 theme_adium_finalize (GObject *object)
1435 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1437 empathy_adium_data_unref (self->priv->data);
1439 g_object_unref (self->priv->gsettings_chat);
1440 g_object_unref (self->priv->gsettings_desktop);
1442 g_free (self->priv->variant);
1444 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1448 theme_adium_dispose (GObject *object)
1450 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1452 if (self->priv->smiley_manager)
1454 g_object_unref (self->priv->smiley_manager);
1455 self->priv->smiley_manager = NULL;
1458 if (self->priv->last_contact)
1460 g_object_unref (self->priv->last_contact);
1461 self->priv->last_contact = NULL;
1464 if (self->priv->inspector_window)
1466 gtk_widget_destroy (self->priv->inspector_window);
1467 self->priv->inspector_window = NULL;
1470 if (self->priv->acked_messages.length > 0)
1472 g_queue_clear (&self->priv->acked_messages);
1475 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1479 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1480 EmpathyThemeAdium *self)
1482 if (self->priv->inspector_window)
1484 gtk_widget_show_all (self->priv->inspector_window);
1491 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1492 EmpathyThemeAdium *self)
1494 if (self->priv->inspector_window)
1496 gtk_widget_hide (self->priv->inspector_window);
1502 static WebKitWebView *
1503 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1504 WebKitWebView *web_view,
1505 EmpathyThemeAdium *self)
1507 GtkWidget *scrolled_window;
1508 GtkWidget *inspector_web_view;
1510 if (!self->priv->inspector_window)
1512 /* Create main window */
1513 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1515 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1518 g_signal_connect (self->priv->inspector_window, "delete-event",
1519 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1521 /* Pack a scrolled window */
1522 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1524 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1525 GTK_POLICY_AUTOMATIC,
1526 GTK_POLICY_AUTOMATIC);
1527 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1529 gtk_widget_show (scrolled_window);
1531 /* Pack a webview in the scrolled window. That webview will be
1532 * used to render the inspector tool. */
1533 inspector_web_view = webkit_web_view_new ();
1534 gtk_container_add (GTK_CONTAINER (scrolled_window),
1535 inspector_web_view);
1536 gtk_widget_show (scrolled_window);
1538 return WEBKIT_WEB_VIEW (inspector_web_view);
1545 theme_adium_constructed (GObject *object)
1547 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1548 const gchar *font_family = NULL;
1550 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1551 WebKitWebInspector *webkit_inspector;
1553 /* Set default settings */
1554 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1555 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1557 if (font_family && font_size)
1559 g_object_set (webkit_web_view_get_settings (webkit_view),
1560 "default-font-family", font_family,
1561 "default-font-size", font_size,
1566 empathy_webkit_bind_font_setting (webkit_view,
1567 self->priv->gsettings_desktop,
1568 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1571 /* Setup webkit inspector */
1572 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1573 g_signal_connect (webkit_inspector, "inspect-web-view",
1574 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1575 g_signal_connect (webkit_inspector, "show-window",
1576 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1577 g_signal_connect (webkit_inspector, "close-window",
1578 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1581 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1583 self->priv->in_construction = FALSE;
1587 theme_adium_get_property (GObject *object,
1592 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1596 case PROP_ADIUM_DATA:
1597 g_value_set_boxed (value, self->priv->data);
1600 g_value_set_string (value, self->priv->variant);
1603 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1609 theme_adium_set_property (GObject *object,
1611 const GValue *value,
1614 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1618 case PROP_ADIUM_DATA:
1619 g_assert (self->priv->data == NULL);
1620 self->priv->data = g_value_dup_boxed (value);
1623 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1626 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1632 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1634 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1635 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1637 object_class->finalize = theme_adium_finalize;
1638 object_class->dispose = theme_adium_dispose;
1639 object_class->constructed = theme_adium_constructed;
1640 object_class->get_property = theme_adium_get_property;
1641 object_class->set_property = theme_adium_set_property;
1643 widget_class->button_press_event = theme_adium_button_press_event;
1645 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1646 g_param_spec_boxed ("adium-data",
1648 "Data for the adium theme",
1649 EMPATHY_TYPE_ADIUM_DATA,
1650 G_PARAM_CONSTRUCT_ONLY |
1652 G_PARAM_STATIC_STRINGS));
1654 g_object_class_install_property (object_class, PROP_VARIANT,
1655 g_param_spec_string ("variant",
1656 "The theme variant",
1657 "Variant name for the theme",
1661 G_PARAM_STATIC_STRINGS));
1663 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1667 empathy_theme_adium_init (EmpathyThemeAdium *self)
1669 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1670 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1672 self->priv->in_construction = TRUE;
1673 g_queue_init (&self->priv->message_queue);
1674 self->priv->allow_scrolling = TRUE;
1675 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1677 /* Show avatars by default. */
1678 self->priv->show_avatars = TRUE;
1680 g_signal_connect (self, "load-finished",
1681 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1682 g_signal_connect (self, "navigation-policy-decision-requested",
1683 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1685 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1686 self->priv->gsettings_desktop = g_settings_new (
1687 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1689 g_signal_connect (self->priv->gsettings_chat,
1690 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1691 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1694 theme_adium_update_enable_webkit_developer_tools (self);
1698 empathy_theme_adium_new (EmpathyAdiumData *data,
1699 const gchar *variant)
1701 g_return_val_if_fail (data != NULL, NULL);
1703 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1710 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1711 const gchar *variant)
1713 gchar *variant_path;
1716 if (!tp_strdiff (self->priv->variant, variant))
1719 g_free (self->priv->variant);
1720 self->priv->variant = g_strdup (variant);
1722 if (self->priv->in_construction)
1725 DEBUG ("Update view with variant: '%s'", variant);
1726 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1727 self->priv->variant);
1728 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1731 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1733 g_free (variant_path);
1736 g_object_notify (G_OBJECT (self), "variant");
1740 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1742 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1743 WebKitWebInspector *inspector;
1745 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1746 "enable-developer-extras", TRUE, NULL);
1748 inspector = webkit_web_view_get_inspector (web_view);
1749 webkit_web_inspector_show (inspector);
1753 empathy_adium_path_is_valid (const gchar *path)
1763 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1764 tmp = g_strsplit (path, "/", 0);
1771 dir = tmp[g_strv_length (tmp) - 1];
1773 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1781 /* The theme is not valid if there is no Info.plist */
1782 file = g_build_filename (path, "Contents", "Info.plist",
1784 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1790 /* We ship a default Template.html as fallback if there is any problem
1791 * with the one inside the theme. The only other required file is
1792 * Content.html OR Incoming/Content.html*/
1793 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1795 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1800 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1801 "Content.html", NULL);
1802 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1810 empathy_adium_info_new (const gchar *path)
1814 GHashTable *info = NULL;
1816 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1818 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1819 value = empathy_plist_parse_from_file (file);
1825 info = g_value_dup_boxed (value);
1826 tp_g_value_slice_free (value);
1828 /* Insert the theme's path into the hash table,
1829 * keys have to be dupped */
1830 tp_asv_set_string (info, g_strdup ("path"), path);
1836 adium_info_get_version (GHashTable *info)
1838 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1841 static const gchar *
1842 adium_info_get_no_variant_name (GHashTable *info)
1844 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1845 return name ? name : _("Normal");
1849 adium_info_dup_path_for_variant (GHashTable *info,
1850 const gchar *variant)
1852 guint version = adium_info_get_version (info);
1853 const gchar *no_variant = adium_info_get_no_variant_name (info);
1854 GPtrArray *variants;
1857 if (version <= 2 && !tp_strdiff (variant, no_variant))
1858 return g_strdup ("main.css");
1860 variants = empathy_adium_info_get_available_variants (info);
1861 if (variants->len == 0)
1862 return g_strdup ("main.css");
1864 /* Verify the variant exists, fallback to the first one */
1865 for (i = 0; i < variants->len; i++)
1867 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1871 if (i == variants->len)
1873 DEBUG ("Variant %s does not exist", variant);
1874 variant = g_ptr_array_index (variants, 0);
1877 return g_strdup_printf ("Variants/%s.css", variant);
1882 empathy_adium_info_get_default_variant (GHashTable *info)
1884 if (adium_info_get_version (info) <= 2)
1885 return adium_info_get_no_variant_name (info);
1887 return tp_asv_get_string (info, "DefaultVariant");
1891 empathy_adium_info_get_available_variants (GHashTable *info)
1893 GPtrArray *variants;
1898 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1899 if (variants != NULL)
1902 variants = g_ptr_array_new_with_free_func (g_free);
1903 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1904 G_TYPE_PTR_ARRAY, variants);
1906 path = tp_asv_get_string (info, "path");
1907 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1908 dir = g_dir_open (dirpath, 0, NULL);
1913 for (name = g_dir_read_name (dir);
1915 name = g_dir_read_name (dir))
1917 gchar *display_name;
1919 if (!g_str_has_suffix (name, ".css"))
1922 display_name = g_strdup (name);
1923 strstr (display_name, ".css")[0] = '\0';
1924 g_ptr_array_add (variants, display_name);
1931 if (adium_info_get_version (info) <= 2)
1932 g_ptr_array_add (variants,
1933 g_strdup (adium_info_get_no_variant_name (info)));
1939 empathy_adium_data_get_type (void)
1941 static GType type_id = 0;
1945 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1946 (GBoxedCopyFunc) empathy_adium_data_ref,
1947 (GBoxedFreeFunc) empathy_adium_data_unref);
1954 empathy_adium_data_new_with_info (const gchar *path,
1957 EmpathyAdiumData *data;
1958 gchar *template_html = NULL;
1959 gchar *footer_html = NULL;
1962 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1964 data = g_slice_new0 (EmpathyAdiumData);
1965 data->ref_count = 1;
1966 data->path = g_strdup (path);
1967 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1968 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1969 data->info = g_hash_table_ref (info);
1970 data->version = adium_info_get_version (info);
1971 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1972 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1973 g_str_equal, g_free, g_free);
1975 DEBUG ("Loading theme at %s", path);
1977 #define LOAD(path, var) \
1978 tmp = g_build_filename (data->basedir, path, NULL); \
1979 g_file_get_contents (tmp, &var, NULL, NULL); \
1982 #define LOAD_CONST(path, var) \
1985 LOAD (path, content); \
1986 if (content != NULL) { \
1987 g_ptr_array_add (data->strings_to_free, content); \
1992 /* Load html files */
1993 LOAD_CONST ("Content.html", data->content_html);
1994 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1995 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1996 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1997 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1998 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1999 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2000 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2001 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2002 LOAD_CONST ("Status.html", data->status_html);
2003 LOAD ("Template.html", template_html);
2004 LOAD ("Footer.html", footer_html);
2009 /* HTML fallbacks: If we have at least content OR in_content, then
2010 * everything else gets a fallback */
2012 #define FALLBACK(html, fallback) \
2013 if (html == NULL) { \
2017 /* in_nextcontent -> in_content -> content */
2018 FALLBACK (data->in_content_html, data->content_html);
2019 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2021 /* context -> content */
2022 FALLBACK (data->in_context_html, data->in_content_html);
2023 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2024 FALLBACK (data->out_context_html, data->out_content_html);
2025 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2028 FALLBACK (data->out_content_html, data->in_content_html);
2029 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2030 FALLBACK (data->out_context_html, data->in_context_html);
2031 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2033 /* status -> in_content */
2034 FALLBACK (data->status_html, data->in_content_html);
2038 /* template -> empathy's template */
2039 data->custom_template = (template_html != NULL);
2040 if (template_html == NULL)
2042 GError *error = NULL;
2044 tmp = empathy_file_lookup ("Template.html", "data");
2046 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2047 g_warning ("couldn't load Empathy's default theme "
2048 "template: %s", error->message);
2049 g_return_val_if_reached (data);
2055 /* Default avatar */
2056 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2057 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2059 data->default_incoming_avatar_filename = tmp;
2066 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2067 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2069 data->default_outgoing_avatar_filename = tmp;
2076 /* Old custom templates had only 4 parameters.
2077 * New templates have 5 parameters */
2078 if (data->version <= 2 && data->custom_template)
2080 tmp = string_with_format (template_html,
2082 "%@", /* Leave variant unset */
2083 "", /* The header */
2084 footer_html ? footer_html : "",
2089 tmp = string_with_format (template_html,
2091 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2092 "%@", /* Leave variant unset */
2093 "", /* The header */
2094 footer_html ? footer_html : "",
2097 g_ptr_array_add (data->strings_to_free, tmp);
2098 data->template_html = tmp;
2100 g_free (template_html);
2101 g_free (footer_html);
2107 empathy_adium_data_new (const gchar *path)
2109 EmpathyAdiumData *data;
2112 info = empathy_adium_info_new (path);
2113 data = empathy_adium_data_new_with_info (path, info);
2114 g_hash_table_unref (info);
2120 empathy_adium_data_ref (EmpathyAdiumData *data)
2122 g_return_val_if_fail (data != NULL, NULL);
2124 g_atomic_int_inc (&data->ref_count);
2130 empathy_adium_data_unref (EmpathyAdiumData *data)
2132 g_return_if_fail (data != NULL);
2134 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2135 g_free (data->path);
2136 g_free (data->basedir);
2137 g_free (data->default_avatar_filename);
2138 g_free (data->default_incoming_avatar_filename);
2139 g_free (data->default_outgoing_avatar_filename);
2140 g_hash_table_unref (data->info);
2141 g_ptr_array_unref (data->strings_to_free);
2142 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2144 g_slice_free (EmpathyAdiumData, data);
2149 empathy_adium_data_get_info (EmpathyAdiumData *data)
2151 g_return_val_if_fail (data != NULL, NULL);
2157 empathy_adium_data_get_path (EmpathyAdiumData *data)
2159 g_return_val_if_fail (data != NULL, NULL);