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;
785 gchar *body_escaped, *name_escaped;
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 name_escaped = g_markup_escape_text (name, -1);
821 /* If this is a /me probably */
825 if (priv->data->version >= 4 || !priv->data->custom_template) {
826 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
827 "<span class='actionMessageBody'>%s</span>",
828 name_escaped, body_escaped);
830 str = g_strdup_printf ("*%s*", body_escaped);
832 g_free (body_escaped);
836 /* Get the avatar filename, or a fallback */
837 avatar = empathy_contact_get_avatar (sender);
839 avatar_filename = avatar->filename;
841 if (!avatar_filename) {
842 if (empathy_contact_is_user (sender)) {
843 avatar_filename = priv->data->default_outgoing_avatar_filename;
845 avatar_filename = priv->data->default_incoming_avatar_filename;
847 if (!avatar_filename) {
848 if (!priv->data->default_avatar_filename) {
849 priv->data->default_avatar_filename =
850 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
851 GTK_ICON_SIZE_DIALOG);
853 avatar_filename = priv->data->default_avatar_filename;
857 /* We want to join this message with the last one if
858 * - senders are the same contact,
859 * - last message was recieved recently,
860 * - last message and this message both are/aren't backlog, and
861 * - DisableCombineConsecutive is not set in theme's settings */
862 is_backlog = empathy_message_is_backlog (msg);
863 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
864 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
865 (is_backlog == priv->last_is_backlog) &&
866 !tp_asv_get_boolean (priv->data->info,
867 "DisableCombineConsecutive", NULL);
869 /* Define message classes */
870 message_classes = g_string_new ("message");
871 if (!priv->has_focus && !is_backlog) {
872 if (!priv->has_unread_message) {
873 g_string_append (message_classes, " firstFocus");
874 priv->has_unread_message = TRUE;
876 g_string_append (message_classes, " focus");
879 g_string_append (message_classes, " history");
882 g_string_append (message_classes, " consecutive");
884 if (empathy_contact_is_user (sender)) {
885 g_string_append (message_classes, " outgoing");
887 g_string_append (message_classes, " incoming");
889 if (empathy_message_should_highlight (msg)) {
890 g_string_append (message_classes, " mention");
892 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
893 g_string_append (message_classes, " autoreply");
896 g_string_append (message_classes, " action");
898 /* FIXME: other classes:
899 * status - the message is a status change
900 * event - the message is a notification of something happening
901 * (for example, encryption being turned on)
902 * %status% - See %status% in theme_adium_append_html ()
905 /* This is slightly a hack, but it's the only way to add
906 * arbitrary data to messages in the HTML. We add another
907 * class called "x-empathy-message-id-*" to the message. This
908 * way, we can remove the unread marker for this specific
910 tp_msg = empathy_message_get_tp_message (msg);
911 if (tp_msg != NULL) {
915 id = tp_message_get_pending_message_id (tp_msg, &valid);
917 g_string_append_printf (message_classes,
918 " x-empathy-message-id-%u", id);
922 /* Define javascript function to use */
924 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
926 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
929 if (empathy_contact_is_user (sender)) {
933 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
936 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
939 /* remove all the unread marks when we are sending a message */
940 theme_adium_remove_all_focus_marks (theme);
945 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
948 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
952 theme_adium_append_html (theme, func, html, body_escaped,
953 avatar_filename, name_escaped, contact_id,
954 service_name, message_classes->str,
955 timestamp, is_backlog, empathy_contact_is_user (sender));
957 /* Keep the sender of the last displayed message */
958 if (priv->last_contact) {
959 g_object_unref (priv->last_contact);
961 priv->last_contact = g_object_ref (sender);
962 priv->last_timestamp = timestamp;
963 priv->last_is_backlog = is_backlog;
965 g_free (body_escaped);
966 g_free (name_escaped);
967 g_string_free (message_classes, TRUE);
971 theme_adium_append_event (EmpathyChatView *view,
974 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
977 if (priv->pages_loading != 0) {
978 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str);
982 str_escaped = g_markup_escape_text (str, -1);
983 theme_adium_append_event_escaped (view, str_escaped);
984 g_free (str_escaped);
988 theme_adium_append_event_markup (EmpathyChatView *view,
989 const gchar *markup_text,
990 const gchar *fallback_text)
992 theme_adium_append_event_escaped (view, markup_text);
996 theme_adium_edit_message (EmpathyChatView *view,
997 EmpathyMessage *message)
999 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1000 WebKitDOMDocument *doc;
1001 WebKitDOMElement *span;
1002 gchar *id, *parsed_body;
1003 gchar *tooltip, *timestamp;
1004 GtkIconInfo *icon_info;
1005 GError *error = NULL;
1007 if (priv->pages_loading != 0) {
1008 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL);
1012 id = g_strdup_printf ("message-token-%s",
1013 empathy_message_get_supersedes (message));
1014 /* we don't pass a token here, because doing so will return another
1015 * <span> element, and we don't want nested <span> elements */
1016 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1017 empathy_message_get_body (message), NULL);
1019 /* find the element */
1020 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1021 span = webkit_dom_document_get_element_by_id (doc, id);
1024 DEBUG ("Failed to find id '%s'", id);
1028 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1029 DEBUG ("Not a HTML element");
1033 /* update the HTML */
1034 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1035 parsed_body, &error);
1037 if (error != NULL) {
1038 DEBUG ("Error setting new inner-HTML: %s", error->message);
1039 g_error_free (error);
1044 timestamp = empathy_time_to_string_local (
1045 empathy_message_get_timestamp (message),
1047 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1049 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1055 /* mark this message as edited */
1056 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1057 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1059 if (icon_info != NULL) {
1060 /* set the icon as a background image using CSS
1061 * FIXME: the icon won't update in response to theme changes */
1062 gchar *style = g_strdup_printf (
1063 "background-image:url('%s');"
1064 "background-repeat:no-repeat;"
1065 "background-position:left center;"
1066 "padding-left:19px;", /* 16px icon + 3px padding */
1067 gtk_icon_info_get_filename (icon_info));
1069 webkit_dom_element_set_attribute (span, "style", style, &error);
1071 if (error != NULL) {
1072 DEBUG ("Error setting element style: %s",
1074 g_clear_error (&error);
1079 gtk_icon_info_free (icon_info);
1085 DEBUG ("Could not find message to edit with: %s",
1086 empathy_message_get_body (message));
1090 g_free (parsed_body);
1094 theme_adium_scroll (EmpathyChatView *view,
1095 gboolean allow_scrolling)
1097 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1099 priv->allow_scrolling = allow_scrolling;
1100 if (allow_scrolling) {
1101 empathy_chat_view_scroll_down (view);
1106 theme_adium_scroll_down (EmpathyChatView *view)
1108 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1112 theme_adium_get_has_selection (EmpathyChatView *view)
1114 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1118 theme_adium_clear (EmpathyChatView *view)
1120 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1122 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1124 /* Clear last contact to avoid trying to add a 'joined'
1125 * message when we don't have an insertion point. */
1126 if (priv->last_contact) {
1127 g_object_unref (priv->last_contact);
1128 priv->last_contact = NULL;
1133 theme_adium_find_previous (EmpathyChatView *view,
1134 const gchar *search_criteria,
1135 gboolean new_search,
1136 gboolean match_case)
1138 /* FIXME: Doesn't respect new_search */
1139 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1140 search_criteria, match_case,
1145 theme_adium_find_next (EmpathyChatView *view,
1146 const gchar *search_criteria,
1147 gboolean new_search,
1148 gboolean match_case)
1150 /* FIXME: Doesn't respect new_search */
1151 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1152 search_criteria, match_case,
1157 theme_adium_find_abilities (EmpathyChatView *view,
1158 const gchar *search_criteria,
1159 gboolean match_case,
1160 gboolean *can_do_previous,
1161 gboolean *can_do_next)
1163 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1164 * find_next and find_previous to work around this problem. */
1165 if (can_do_previous)
1166 *can_do_previous = TRUE;
1168 *can_do_next = TRUE;
1172 theme_adium_highlight (EmpathyChatView *view,
1174 gboolean match_case)
1176 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1177 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1178 text, match_case, 0);
1179 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1184 theme_adium_copy_clipboard (EmpathyChatView *view)
1186 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1190 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1193 WebKitDOMDocument *dom;
1194 WebKitDOMNodeList *nodes;
1196 GError *error = NULL;
1198 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1203 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1205 /* Get all nodes with focus class */
1206 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1209 if (nodes == NULL) {
1210 DEBUG ("Error getting focus nodes: %s",
1211 error ? error->message : "No error");
1212 g_clear_error (&error);
1216 theme_adium_remove_focus_marks (self, nodes);
1220 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1223 EmpathyThemeAdium *self = user_data;
1224 guint32 id = GPOINTER_TO_UINT (data);
1226 theme_adium_remove_mark_from_message (self, id);
1230 theme_adium_focus_toggled (EmpathyChatView *view,
1233 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1235 priv->has_focus = has_focus;
1236 if (!priv->has_focus) {
1237 /* We've lost focus, so let's make sure all the acked
1238 * messages have lost their unread marker. */
1239 g_queue_foreach (&priv->acked_messages,
1240 theme_adium_remove_acked_message_unread_mark_foreach,
1242 g_queue_clear (&priv->acked_messages);
1244 priv->has_unread_message = FALSE;
1249 theme_adium_message_acknowledged (EmpathyChatView *view,
1250 EmpathyMessage *message)
1252 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1253 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1258 tp_msg = empathy_message_get_tp_message (message);
1260 if (tp_msg == NULL) {
1264 id = tp_message_get_pending_message_id (tp_msg, &valid);
1266 g_warning ("Acknoledged message doesn't have a pending ID");
1270 /* We only want to actually remove the unread marker if the
1271 * view doesn't have focus. If we did it all the time we would
1272 * never see the unread markers, ever! So, we'll queue these
1273 * up, and when we lose focus, we'll remove the markers. */
1274 if (priv->has_focus) {
1275 g_queue_push_tail (&priv->acked_messages,
1276 GUINT_TO_POINTER (id));
1280 theme_adium_remove_mark_from_message (self, id);
1284 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1286 if (event->button == 3) {
1287 gboolean developer_tools_enabled;
1289 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1290 "enable-developer-extras", &developer_tools_enabled, NULL);
1292 /* We currently have no way to add an inspector menu
1293 * item ourselves, so we disable our customized menu
1294 * if the developer extras are enabled. */
1295 if (!developer_tools_enabled) {
1296 empathy_webkit_context_menu_for_event (
1297 WEBKIT_WEB_VIEW (widget), event,
1298 EMPATHY_WEBKIT_MENU_CLEAR);
1303 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1307 theme_adium_iface_init (EmpathyChatViewIface *iface)
1309 iface->append_message = theme_adium_append_message;
1310 iface->append_event = theme_adium_append_event;
1311 iface->append_event_markup = theme_adium_append_event_markup;
1312 iface->edit_message = theme_adium_edit_message;
1313 iface->scroll = theme_adium_scroll;
1314 iface->scroll_down = theme_adium_scroll_down;
1315 iface->get_has_selection = theme_adium_get_has_selection;
1316 iface->clear = theme_adium_clear;
1317 iface->find_previous = theme_adium_find_previous;
1318 iface->find_next = theme_adium_find_next;
1319 iface->find_abilities = theme_adium_find_abilities;
1320 iface->highlight = theme_adium_highlight;
1321 iface->copy_clipboard = theme_adium_copy_clipboard;
1322 iface->focus_toggled = theme_adium_focus_toggled;
1323 iface->message_acknowledged = theme_adium_message_acknowledged;
1327 theme_adium_load_finished_cb (WebKitWebView *view,
1328 WebKitWebFrame *frame,
1331 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1332 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1335 DEBUG ("Page loaded");
1336 priv->pages_loading--;
1338 if (priv->pages_loading != 0)
1341 /* Display queued messages */
1342 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1343 QueuedItem *item = l->data;
1347 case QUEUED_MESSAGE:
1348 theme_adium_append_message (chat_view, item->msg);
1352 theme_adium_edit_message (chat_view, item->msg);
1356 theme_adium_append_event (chat_view, item->str);
1360 free_queued_item (item);
1363 g_queue_clear (&priv->message_queue);
1367 theme_adium_finalize (GObject *object)
1369 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1371 empathy_adium_data_unref (priv->data);
1373 g_object_unref (priv->gsettings_chat);
1374 g_object_unref (priv->gsettings_desktop);
1376 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1380 theme_adium_dispose (GObject *object)
1382 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1384 if (priv->smiley_manager) {
1385 g_object_unref (priv->smiley_manager);
1386 priv->smiley_manager = NULL;
1389 if (priv->last_contact) {
1390 g_object_unref (priv->last_contact);
1391 priv->last_contact = NULL;
1394 if (priv->inspector_window) {
1395 gtk_widget_destroy (priv->inspector_window);
1396 priv->inspector_window = NULL;
1399 if (priv->acked_messages.length > 0) {
1400 g_queue_clear (&priv->acked_messages);
1403 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1407 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1408 EmpathyThemeAdium *theme)
1410 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1412 if (priv->inspector_window) {
1413 gtk_widget_show_all (priv->inspector_window);
1420 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1421 EmpathyThemeAdium *theme)
1423 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1425 if (priv->inspector_window) {
1426 gtk_widget_hide (priv->inspector_window);
1432 static WebKitWebView *
1433 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1434 WebKitWebView *web_view,
1435 EmpathyThemeAdium *theme)
1437 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1438 GtkWidget *scrolled_window;
1439 GtkWidget *inspector_web_view;
1441 if (!priv->inspector_window) {
1442 /* Create main window */
1443 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1444 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1446 g_signal_connect (priv->inspector_window, "delete-event",
1447 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1449 /* Pack a scrolled window */
1450 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1451 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1452 GTK_POLICY_AUTOMATIC,
1453 GTK_POLICY_AUTOMATIC);
1454 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1456 gtk_widget_show (scrolled_window);
1458 /* Pack a webview in the scrolled window. That webview will be
1459 * used to render the inspector tool. */
1460 inspector_web_view = webkit_web_view_new ();
1461 gtk_container_add (GTK_CONTAINER (scrolled_window),
1462 inspector_web_view);
1463 gtk_widget_show (scrolled_window);
1465 return WEBKIT_WEB_VIEW (inspector_web_view);
1472 theme_adium_constructed (GObject *object)
1474 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1475 const gchar *font_family = NULL;
1477 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1478 WebKitWebInspector *webkit_inspector;
1480 /* Set default settings */
1481 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1482 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1484 if (font_family && font_size) {
1485 g_object_set (webkit_web_view_get_settings (webkit_view),
1486 "default-font-family", font_family,
1487 "default-font-size", font_size,
1490 empathy_webkit_bind_font_setting (webkit_view,
1491 priv->gsettings_desktop,
1492 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1495 /* Setup webkit inspector */
1496 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1497 g_signal_connect (webkit_inspector, "inspect-web-view",
1498 G_CALLBACK (theme_adium_inspect_web_view_cb),
1500 g_signal_connect (webkit_inspector, "show-window",
1501 G_CALLBACK (theme_adium_inspector_show_window_cb),
1503 g_signal_connect (webkit_inspector, "close-window",
1504 G_CALLBACK (theme_adium_inspector_close_window_cb),
1508 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1510 priv->in_construction = FALSE;
1514 theme_adium_get_property (GObject *object,
1519 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1522 case PROP_ADIUM_DATA:
1523 g_value_set_boxed (value, priv->data);
1526 g_value_set_string (value, priv->variant);
1529 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1535 theme_adium_set_property (GObject *object,
1537 const GValue *value,
1540 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1541 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1544 case PROP_ADIUM_DATA:
1545 g_assert (priv->data == NULL);
1546 priv->data = g_value_dup_boxed (value);
1549 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1552 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1558 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1560 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1561 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1563 object_class->finalize = theme_adium_finalize;
1564 object_class->dispose = theme_adium_dispose;
1565 object_class->constructed = theme_adium_constructed;
1566 object_class->get_property = theme_adium_get_property;
1567 object_class->set_property = theme_adium_set_property;
1569 widget_class->button_press_event = theme_adium_button_press_event;
1571 g_object_class_install_property (object_class,
1573 g_param_spec_boxed ("adium-data",
1575 "Data for the adium theme",
1576 EMPATHY_TYPE_ADIUM_DATA,
1577 G_PARAM_CONSTRUCT_ONLY |
1579 G_PARAM_STATIC_STRINGS));
1580 g_object_class_install_property (object_class,
1582 g_param_spec_string ("variant",
1583 "The theme variant",
1584 "Variant name for the theme",
1588 G_PARAM_STATIC_STRINGS));
1590 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1594 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1596 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1597 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1601 priv->in_construction = TRUE;
1602 g_queue_init (&priv->message_queue);
1603 priv->allow_scrolling = TRUE;
1604 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1606 g_signal_connect (theme, "load-finished",
1607 G_CALLBACK (theme_adium_load_finished_cb),
1609 g_signal_connect (theme, "navigation-policy-decision-requested",
1610 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1613 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1614 priv->gsettings_desktop = g_settings_new (
1615 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1617 g_signal_connect (priv->gsettings_chat,
1618 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1619 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1622 theme_adium_update_enable_webkit_developer_tools (theme);
1626 empathy_theme_adium_new (EmpathyAdiumData *data,
1627 const gchar *variant)
1629 g_return_val_if_fail (data != NULL, NULL);
1631 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1638 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1639 const gchar *variant)
1641 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1642 gchar *variant_path;
1645 if (!tp_strdiff (priv->variant, variant)) {
1649 g_free (priv->variant);
1650 priv->variant = g_strdup (variant);
1652 if (priv->in_construction) {
1656 DEBUG ("Update view with variant: '%s'", variant);
1657 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1659 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1661 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1663 g_free (variant_path);
1666 g_object_notify (G_OBJECT (theme), "variant");
1670 empathy_theme_adium_show_inspector (EmpathyThemeAdium *theme)
1672 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
1673 WebKitWebInspector *inspector;
1675 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1676 "enable-developer-extras", TRUE,
1679 inspector = webkit_web_view_get_inspector (web_view);
1680 webkit_web_inspector_show (inspector);
1684 empathy_adium_path_is_valid (const gchar *path)
1689 /* The theme is not valid if there is no Info.plist */
1690 file = g_build_filename (path, "Contents", "Info.plist",
1692 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1698 /* We ship a default Template.html as fallback if there is any problem
1699 * with the one inside the theme. The only other required file is
1700 * Content.html OR Incoming/Content.html*/
1701 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1703 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1707 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1708 "Content.html", NULL);
1709 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1717 empathy_adium_info_new (const gchar *path)
1721 GHashTable *info = NULL;
1723 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1725 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1726 value = empathy_plist_parse_from_file (file);
1732 info = g_value_dup_boxed (value);
1733 tp_g_value_slice_free (value);
1735 /* Insert the theme's path into the hash table,
1736 * keys have to be dupped */
1737 tp_asv_set_string (info, g_strdup ("path"), path);
1743 adium_info_get_version (GHashTable *info)
1745 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1748 static const gchar *
1749 adium_info_get_no_variant_name (GHashTable *info)
1751 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1752 return name ? name : _("Normal");
1756 adium_info_dup_path_for_variant (GHashTable *info,
1757 const gchar *variant)
1759 guint version = adium_info_get_version (info);
1760 const gchar *no_variant = adium_info_get_no_variant_name (info);
1761 GPtrArray *variants;
1764 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1765 return g_strdup ("main.css");
1768 variants = empathy_adium_info_get_available_variants (info);
1769 if (variants->len == 0)
1770 return g_strdup ("main.css");
1772 /* Verify the variant exists, fallback to the first one */
1773 for (i = 0; i < variants->len; i++) {
1774 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1778 if (i == variants->len) {
1779 DEBUG ("Variant %s does not exist", variant);
1780 variant = g_ptr_array_index (variants, 0);
1783 return g_strdup_printf ("Variants/%s.css", variant);
1788 empathy_adium_info_get_default_variant (GHashTable *info)
1790 if (adium_info_get_version (info) <= 2) {
1791 return adium_info_get_no_variant_name (info);
1794 return tp_asv_get_string (info, "DefaultVariant");
1798 empathy_adium_info_get_available_variants (GHashTable *info)
1800 GPtrArray *variants;
1805 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1806 if (variants != NULL) {
1810 variants = g_ptr_array_new_with_free_func (g_free);
1811 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1812 G_TYPE_PTR_ARRAY, variants);
1814 path = tp_asv_get_string (info, "path");
1815 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1816 dir = g_dir_open (dirpath, 0, NULL);
1820 for (name = g_dir_read_name (dir);
1822 name = g_dir_read_name (dir)) {
1823 gchar *display_name;
1825 if (!g_str_has_suffix (name, ".css")) {
1829 display_name = g_strdup (name);
1830 strstr (display_name, ".css")[0] = '\0';
1831 g_ptr_array_add (variants, display_name);
1837 if (adium_info_get_version (info) <= 2) {
1838 g_ptr_array_add (variants,
1839 g_strdup (adium_info_get_no_variant_name (info)));
1846 empathy_adium_data_get_type (void)
1848 static GType type_id = 0;
1852 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1853 (GBoxedCopyFunc) empathy_adium_data_ref,
1854 (GBoxedFreeFunc) empathy_adium_data_unref);
1861 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1863 EmpathyAdiumData *data;
1864 gchar *template_html = NULL;
1865 gchar *footer_html = NULL;
1868 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1870 data = g_slice_new0 (EmpathyAdiumData);
1871 data->ref_count = 1;
1872 data->path = g_strdup (path);
1873 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1874 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1875 data->info = g_hash_table_ref (info);
1876 data->version = adium_info_get_version (info);
1877 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1878 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1879 g_str_equal, g_free, g_free);
1881 DEBUG ("Loading theme at %s", path);
1883 #define LOAD(path, var) \
1884 tmp = g_build_filename (data->basedir, path, NULL); \
1885 g_file_get_contents (tmp, &var, NULL, NULL); \
1888 #define LOAD_CONST(path, var) \
1891 LOAD (path, content); \
1892 if (content != NULL) { \
1893 g_ptr_array_add (data->strings_to_free, content); \
1898 /* Load html files */
1899 LOAD_CONST ("Content.html", data->content_html);
1900 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1901 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1902 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1903 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1904 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1905 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1906 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1907 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1908 LOAD_CONST ("Status.html", data->status_html);
1909 LOAD ("Template.html", template_html);
1910 LOAD ("Footer.html", footer_html);
1915 /* HTML fallbacks: If we have at least content OR in_content, then
1916 * everything else gets a fallback */
1918 #define FALLBACK(html, fallback) \
1919 if (html == NULL) { \
1923 /* in_nextcontent -> in_content -> content */
1924 FALLBACK (data->in_content_html, data->content_html);
1925 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1927 /* context -> content */
1928 FALLBACK (data->in_context_html, data->in_content_html);
1929 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1930 FALLBACK (data->out_context_html, data->out_content_html);
1931 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1934 FALLBACK (data->out_content_html, data->in_content_html);
1935 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1936 FALLBACK (data->out_context_html, data->in_context_html);
1937 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1939 /* status -> in_content */
1940 FALLBACK (data->status_html, data->in_content_html);
1944 /* template -> empathy's template */
1945 data->custom_template = (template_html != NULL);
1946 if (template_html == NULL) {
1947 GError *error = NULL;
1949 tmp = empathy_file_lookup ("Template.html", "data");
1951 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
1952 g_warning ("couldn't load Empathy's default theme "
1953 "template: %s", error->message);
1954 g_return_val_if_reached (data);
1960 /* Default avatar */
1961 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1962 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1963 data->default_incoming_avatar_filename = tmp;
1967 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1968 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1969 data->default_outgoing_avatar_filename = tmp;
1974 /* Old custom templates had only 4 parameters.
1975 * New templates have 5 parameters */
1976 if (data->version <= 2 && data->custom_template) {
1977 tmp = string_with_format (template_html,
1979 "%@", /* Leave variant unset */
1980 "", /* The header */
1981 footer_html ? footer_html : "",
1984 tmp = string_with_format (template_html,
1986 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1987 "%@", /* Leave variant unset */
1988 "", /* The header */
1989 footer_html ? footer_html : "",
1992 g_ptr_array_add (data->strings_to_free, tmp);
1993 data->template_html = tmp;
1995 g_free (template_html);
1996 g_free (footer_html);
2002 empathy_adium_data_new (const gchar *path)
2004 EmpathyAdiumData *data;
2007 info = empathy_adium_info_new (path);
2008 data = empathy_adium_data_new_with_info (path, info);
2009 g_hash_table_unref (info);
2015 empathy_adium_data_ref (EmpathyAdiumData *data)
2017 g_return_val_if_fail (data != NULL, NULL);
2019 g_atomic_int_inc (&data->ref_count);
2025 empathy_adium_data_unref (EmpathyAdiumData *data)
2027 g_return_if_fail (data != NULL);
2029 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2030 g_free (data->path);
2031 g_free (data->basedir);
2032 g_free (data->default_avatar_filename);
2033 g_free (data->default_incoming_avatar_filename);
2034 g_free (data->default_outgoing_avatar_filename);
2035 g_hash_table_unref (data->info);
2036 g_ptr_array_unref (data->strings_to_free);
2037 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2039 g_slice_free (EmpathyAdiumData, data);
2044 empathy_adium_data_get_info (EmpathyAdiumData *data)
2046 g_return_val_if_fail (data != NULL, NULL);
2052 empathy_adium_data_get_path (EmpathyAdiumData *data)
2054 g_return_val_if_fail (data != NULL, NULL);