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/telepathy-glib.h>
29 #include <pango/pango.h>
32 #include <libempathy/empathy-gsettings.h>
33 #include <libempathy/empathy-time.h>
34 #include <libempathy/empathy-utils.h>
36 #include "empathy-theme-adium.h"
37 #include "empathy-smiley-manager.h"
38 #include "empathy-ui-utils.h"
39 #include "empathy-plist.h"
40 #include "empathy-images.h"
41 #include "empathy-webkit-utils.h"
43 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
44 #include <libempathy/empathy-debug.h>
46 #define BORING_DPI_DEFAULT 96
48 /* "Join" consecutive messages with timestamps within five minutes */
49 #define MESSAGE_JOIN_PERIOD 5*60
51 struct _EmpathyThemeAdiumPriv
53 EmpathyAdiumData *data;
54 EmpathySmileyManager *smiley_manager;
55 EmpathyContact *last_contact;
56 gint64 last_timestamp;
57 gboolean last_is_backlog;
59 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
61 /* Queue of guint32 of pending message id to remove unread
62 * marker for when we lose focus. */
63 GQueue acked_messages;
64 GtkWidget *inspector_window;
66 GSettings *gsettings_chat;
67 GSettings *gsettings_desktop;
70 gboolean has_unread_message;
71 gboolean allow_scrolling;
73 gboolean in_construction;
74 gboolean show_avatars;
77 struct _EmpathyAdiumData
82 gchar *default_avatar_filename;
83 gchar *default_incoming_avatar_filename;
84 gchar *default_outgoing_avatar_filename;
87 gboolean custom_template;
88 /* gchar* -> gchar* both owned */
89 GHashTable *date_format_cache;
92 const gchar *template_html;
93 const gchar *content_html;
94 const gchar *in_content_html;
95 const gchar *in_context_html;
96 const gchar *in_nextcontent_html;
97 const gchar *in_nextcontext_html;
98 const gchar *out_content_html;
99 const gchar *out_context_html;
100 const gchar *out_nextcontent_html;
101 const gchar *out_nextcontext_html;
102 const gchar *status_html;
104 /* Above html strings are pointers to strings stored in this array.
105 * We do this because of fallbacks, some htmls could be pointing the
107 GPtrArray *strings_to_free;
110 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
111 const gchar *variant);
120 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
121 WEBKIT_TYPE_WEB_VIEW)
135 gboolean should_highlight;
139 queue_item (GQueue *queue,
143 gboolean should_highlight)
145 QueuedItem *item = g_slice_new0 (QueuedItem);
149 item->msg = g_object_ref (msg);
150 item->str = g_strdup (str);
151 item->should_highlight = should_highlight;
153 g_queue_push_tail (queue, item);
159 free_queued_item (QueuedItem *item)
161 tp_clear_object (&item->msg);
164 g_slice_free (QueuedItem, item);
168 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *self)
170 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
171 gboolean enable_webkit_developer_tools;
173 enable_webkit_developer_tools = g_settings_get_boolean (
174 self->priv->gsettings_chat,
175 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
177 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
178 "enable-developer-extras", enable_webkit_developer_tools, NULL);
182 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
186 EmpathyThemeAdium *self = user_data;
188 theme_adium_update_enable_webkit_developer_tools (self);
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)
205 webkit_web_policy_decision_use (decision);
209 uri = webkit_network_request_get_uri (request);
210 empathy_url_show (GTK_WIDGET (view), uri);
212 webkit_web_policy_decision_ignore (decision);
216 /* Replace each %@ in format with string passed in args */
218 string_with_format (const gchar *format,
219 const gchar *first_string,
226 va_start (args, first_string);
227 result = g_string_sized_new (strlen (format));
228 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
232 next = strstr (format, "%@");
236 g_string_append_len (result, format, next - format);
237 g_string_append (result, str);
240 g_string_append (result, format);
243 return g_string_free (result, FALSE);
247 theme_adium_load_template (EmpathyThemeAdium *self)
253 self->priv->pages_loading++;
254 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
256 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
257 self->priv->variant);
259 template = string_with_format (self->priv->data->template_html,
262 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
263 template, basedir_uri);
265 g_free (basedir_uri);
266 g_free (variant_path);
271 theme_adium_parse_body (EmpathyThemeAdium *self,
275 EmpathyStringParser *parsers;
278 /* Check if we have to parse smileys */
279 parsers = empathy_webkit_get_string_parser (
280 g_settings_get_boolean (self->priv->gsettings_chat,
281 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
283 /* Parse text and construct string with links and smileys replaced
284 * by html tags. Also escape text to make sure html code is
285 * displayed verbatim. */
286 string = g_string_sized_new (strlen (text));
288 /* wrap this in HTML that allows us to find the message for later
290 if (!tp_str_empty (token))
291 g_string_append_printf (string,
292 "<span id=\"message-token-%s\">",
295 empathy_string_parser_substr (text, -1, parsers, string);
297 if (!tp_str_empty (token))
298 g_string_append (string, "</span>");
300 /* Wrap body in order to make tabs and multiple spaces displayed
301 * properly. See bug #625745. */
302 g_string_prepend (string, "<div style=\"display: inline; "
303 "white-space: pre-wrap\"'>");
304 g_string_append (string, "</div>");
306 return g_string_free (string, FALSE);
310 escape_and_append_len (GString *string, const gchar *str, gint len)
312 while (str != NULL && *str != '\0' && len != 0)
318 g_string_append (string, "\\\\");
322 g_string_append (string, "\\\"");
325 /* Remove end of lines */
328 g_string_append_c (string, *str);
336 /* If *str starts with match, returns TRUE and move pointer to the end */
338 theme_adium_match (const gchar **str,
343 len = strlen (match);
344 if (strncmp (*str, match, len) == 0)
353 /* Like theme_adium_match() but also return the X part if match is
356 theme_adium_match_with_format (const gchar **str,
360 const gchar *cur = *str;
363 if (!theme_adium_match (&cur, match))
368 end = strstr (cur, "}%");
372 *format = g_strndup (cur , end - cur);
377 /* List of colors used by %senderColor%. Copied from
378 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
380 static gchar *colors[] = {
381 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
382 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
383 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
384 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
385 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
386 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
387 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
388 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
389 "lightblue", "lightcoral",
390 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
391 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
392 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
393 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
394 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
395 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
396 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
397 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
398 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
399 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
404 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
406 /* Convert from NSDateFormatter
407 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
408 * to strftime supported by g_date_time_format.
409 * FIXME: table is incomplete, doc of g_date_time_format has a table of
411 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
412 * in 2.29.x we have to explictely request padding with %0x */
413 static const gchar *convert_table[] = {
415 "A", NULL, // 0~86399999 (Millisecond of Day)
417 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
418 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
419 "cc", "%u", // 1~7 (Day of Week)
420 "c", "%u", // 1~7 (Day of Week)
422 "dd", "%d", // 1~31 (0 padded Day of Month)
423 "d", "%d", // 1~31 (0 padded Day of Month)
424 "D", "%j", // 1~366 (0 padded Day of Year)
426 "e", "%u", // 1~7 (0 padded Day of Week)
427 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
428 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
429 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
430 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
432 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
434 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
435 "GGGG", NULL, // Before Christ/Anno Domini
436 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
437 "GG", NULL, // BC/AD (Era Designator Abbreviated)
438 "G", NULL, // BC/AD (Era Designator Abbreviated)
440 "h", "%I", // 1~12 (0 padded Hour (12hr))
441 "H", "%H", // 0~23 (0 padded Hour (24hr))
443 "k", NULL, // 1~24 (0 padded Hour (24hr)
444 "K", NULL, // 0~11 (0 padded Hour (12hr))
446 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
447 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
448 "LL", "%m", // 1~12 (0 padded Month)
449 "L", "%m", // 1~12 (0 padded Month)
451 "m", "%M", // 0~59 (0 padded Minute)
452 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
453 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
454 "MM", "%m", // 1~12 (0 padded Month)
455 "M", "%m", // 1~12 (0 padded Month)
457 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
458 "qqq", NULL, // Q1/Q2/Q3/Q4
459 "qq", NULL, // 1~4 (0 padded Quarter)
460 "q", NULL, // 1~4 (0 padded Quarter)
461 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
462 "QQQ", NULL, // Q1/Q2/Q3/Q4
463 "QQ", NULL, // 1~4 (0 padded Quarter)
464 "Q", NULL, // 1~4 (0 padded Quarter)
466 "s", "%S", // 0~59 (0 padded Second)
467 "S", NULL, // (rounded Sub-Second)
469 "u", "%Y", // (0 padded Year)
471 "vvvv", "%Z", // (General GMT Timezone Name)
472 "vvv", "%Z", // (General GMT Timezone Abbreviation)
473 "vv", "%Z", // (General GMT Timezone Abbreviation)
474 "v", "%Z", // (General GMT Timezone Abbreviation)
476 "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)
477 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
479 "yyyy", "%Y", // (Full Year)
480 "yyy", "%y", // (2 Digits Year)
481 "yy", "%y", // (2 Digits Year)
482 "y", "%Y", // (Full Year)
483 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
484 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
485 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
486 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
488 "zzzz", NULL, // (Specific GMT Timezone Name)
489 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
490 "zz", NULL, // (Specific GMT Timezone Abbreviation)
491 "z", NULL, // (Specific GMT Timezone Abbreviation)
492 "Z", "%z", // +0000 (RFC 822 Timezone)
501 str = g_hash_table_lookup (data->date_format_cache, nsdate);
506 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
507 * by corresponding strftime tag. */
508 string = g_string_sized_new (strlen (nsdate));
509 for (i = 0; nsdate[i] != '\0'; i++)
511 gboolean found = FALSE;
513 /* even indexes are NSDateFormatter tag, odd indexes are the
514 * corresponding strftime tag */
515 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
517 if (g_str_has_prefix (nsdate + i, convert_table[j]))
526 /* If we don't have a replacement, just ignore that tag */
527 if (convert_table[j + 1] != NULL)
528 g_string_append (string, convert_table[j + 1]);
530 i += strlen (convert_table[j]) - 1;
534 g_string_append_c (string, nsdate[i]);
538 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
540 /* The cache takes ownership of string->str */
541 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
542 return g_string_free (string, FALSE);
547 theme_adium_append_html (EmpathyThemeAdium *self,
550 const gchar *message,
551 const gchar *avatar_filename,
553 const gchar *contact_id,
554 const gchar *service_name,
555 const gchar *message_classes,
561 const gchar *cur = NULL;
564 /* Make some search-and-replace in the html code */
565 string = g_string_sized_new (strlen (html) + strlen (message));
566 g_string_append_printf (string, "%s(\"", func);
568 for (cur = html; *cur != '\0'; cur++)
570 const gchar *replace = NULL;
571 gchar *dup_replace = NULL;
572 gchar *format = NULL;
574 /* Those are all well known keywords that needs replacement in
575 * html files. Please keep them in the same order than the adium
576 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
577 if (theme_adium_match (&cur, "%userIconPath%"))
579 replace = avatar_filename;
581 else if (theme_adium_match (&cur, "%senderScreenName%"))
583 replace = contact_id;
585 else if (theme_adium_match (&cur, "%sender%"))
589 else if (theme_adium_match (&cur, "%senderColor%"))
591 /* A color derived from the user's name.
592 * FIXME: If a colon separated list of HTML colors is at
593 * Incoming/SenderColors.txt it will be used instead of
594 * the default colors.
597 /* Ensure we always use the same color when sending messages
603 else if (contact_id != NULL)
605 guint hash = g_str_hash (contact_id);
606 replace = colors[hash % G_N_ELEMENTS (colors)];
609 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
611 /* FIXME: The path to the status icon of the sender
612 * (available, away, etc...)
615 else if (theme_adium_match (&cur, "%messageDirection%"))
617 /* FIXME: The text direction of the message
618 * (either rtl or ltr)
621 else if (theme_adium_match (&cur, "%senderDisplayName%"))
623 /* FIXME: The serverside (remotely set) name of the
624 * sender, such as an MSN display name.
626 * We don't have access to that yet so we use
627 * local alias instead.
631 else if (theme_adium_match (&cur, "%senderPrefix%"))
633 /* FIXME: If we supported IRC user mode flags, this
634 * would be replaced with @ if the user is an op, + if
635 * the user has voice, etc. as per
636 * http://hg.adium.im/adium/rev/b586b027de42. But we
637 * don't, so for now we just strip it. */
639 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
642 /* FIXME: This keyword is used to represent the
643 * highlight background color. "X" is the opacity of the
644 * background, ranges from 0 to 1 and can be any decimal
648 else if (theme_adium_match (&cur, "%message%"))
652 else if (theme_adium_match (&cur, "%time%") ||
653 theme_adium_match_with_format (&cur, "%time{", &format))
655 const gchar *strftime_format;
657 strftime_format = nsdate_to_strftime (self->priv->data, format);
659 dup_replace = empathy_time_to_string_local (timestamp,
660 strftime_format ? strftime_format :
661 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
663 dup_replace = empathy_time_to_string_local (timestamp,
664 strftime_format ? strftime_format :
665 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
667 replace = dup_replace;
669 else if (theme_adium_match (&cur, "%shortTime%"))
671 dup_replace = empathy_time_to_string_local (timestamp,
672 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
673 replace = dup_replace;
675 else if (theme_adium_match (&cur, "%service%"))
677 replace = service_name;
679 else if (theme_adium_match (&cur, "%variant%"))
681 /* FIXME: The name of the active message style variant,
682 * with all spaces replaced with an underscore.
683 * A variant named "Alternating Messages - Blue Red"
684 * will become "Alternating_Messages_-_Blue_Red".
687 else if (theme_adium_match (&cur, "%userIcons%"))
689 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
691 else if (theme_adium_match (&cur, "%messageClasses%"))
693 replace = message_classes;
695 else if (theme_adium_match (&cur, "%status%"))
697 /* FIXME: A description of the status event. This is
698 * neither in the user's local language nor expected to
699 * be displayed; it may be useful to use a different div
700 * class to present different types of status messages.
701 * The following is a list of some of the more important
702 * status messages; your message style should be able to
703 * handle being shown a status message not in this list,
704 * as even at present the list is incomplete and is
705 * certain to become out of date in the future:
714 * contact_joined (group chats)
718 * encryption (all OTR messages use this status)
719 * purple (all IRC topic and join/part messages use this status)
720 * fileTransferStarted
721 * fileTransferCompleted
726 escape_and_append_len (string, cur, 1);
730 /* Here we have a replacement to make */
731 escape_and_append_len (string, replace, -1);
733 g_free (dup_replace);
736 g_string_append (string, "\")");
738 script = g_string_free (string, FALSE);
739 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
744 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
745 const gchar *escaped)
747 theme_adium_append_html (self, "appendMessage",
748 self->priv->data->status_html, escaped, NULL, NULL, NULL,
749 NULL, "event", empathy_time_get_current (), FALSE, FALSE);
751 /* There is no last contact */
752 if (self->priv->last_contact)
754 g_object_unref (self->priv->last_contact);
755 self->priv->last_contact = NULL;
760 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
761 WebKitDOMNodeList *nodes)
765 /* Remove focus and firstFocus class */
766 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
768 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
769 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
771 gchar **classes, **iter;
772 GString *new_class_name;
773 gboolean first = TRUE;
778 class_name = webkit_dom_html_element_get_class_name (element);
779 classes = g_strsplit (class_name, " ", -1);
780 new_class_name = g_string_sized_new (strlen (class_name));
782 for (iter = classes; *iter != NULL; iter++)
784 if (tp_strdiff (*iter, "focus") &&
785 tp_strdiff (*iter, "firstFocus"))
788 g_string_append_c (new_class_name, ' ');
790 g_string_append (new_class_name, *iter);
795 webkit_dom_html_element_set_class_name (element, new_class_name->str);
798 g_strfreev (classes);
799 g_string_free (new_class_name, TRUE);
804 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
806 WebKitDOMDocument *dom;
807 WebKitDOMNodeList *nodes;
808 GError *error = NULL;
810 if (!self->priv->has_unread_message)
813 self->priv->has_unread_message = FALSE;
815 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
819 /* Get all nodes with focus class */
820 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
824 DEBUG ("Error getting focus nodes: %s",
825 error ? error->message : "No error");
826 g_clear_error (&error);
830 theme_adium_remove_focus_marks (self, nodes);
834 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
836 gboolean should_highlight)
838 EmpathyContact *sender;
841 gchar *body_escaped, *name_escaped;
843 const gchar *contact_id;
844 EmpathyAvatar *avatar;
845 const gchar *avatar_filename = NULL;
847 const gchar *html = NULL;
849 const gchar *service_name;
850 GString *message_classes = NULL;
852 gboolean consecutive;
855 if (self->priv->pages_loading != 0)
857 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
862 /* Get information */
863 sender = empathy_message_get_sender (msg);
864 account = empathy_contact_get_account (sender);
865 service_name = empathy_protocol_name_to_display_name
866 (tp_account_get_protocol_name (account));
867 if (service_name == NULL)
868 service_name = tp_account_get_protocol_name (account);
869 timestamp = empathy_message_get_timestamp (msg);
870 body_escaped = theme_adium_parse_body (self,
871 empathy_message_get_body (msg),
872 empathy_message_get_token (msg));
873 name = empathy_contact_get_logged_alias (sender);
874 contact_id = empathy_contact_get_id (sender);
875 action = (empathy_message_get_tptype (msg) ==
876 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
878 name_escaped = g_markup_escape_text (name, -1);
880 /* If this is a /me probably */
885 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
887 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
888 "<span class='actionMessageBody'>%s</span>",
889 name_escaped, body_escaped);
893 str = g_strdup_printf ("*%s*", body_escaped);
896 g_free (body_escaped);
900 /* Get the avatar filename, or a fallback */
901 avatar = empathy_contact_get_avatar (sender);
903 avatar_filename = avatar->filename;
905 if (!avatar_filename)
907 if (empathy_contact_is_user (sender))
908 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
910 avatar_filename = self->priv->data->default_incoming_avatar_filename;
912 if (!avatar_filename)
914 if (!self->priv->data->default_avatar_filename)
915 self->priv->data->default_avatar_filename =
916 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
917 GTK_ICON_SIZE_DIALOG);
919 avatar_filename = self->priv->data->default_avatar_filename;
923 /* We want to join this message with the last one if
924 * - senders are the same contact,
925 * - last message was recieved recently,
926 * - last message and this message both are/aren't backlog, and
927 * - DisableCombineConsecutive is not set in theme's settings */
928 is_backlog = empathy_message_is_backlog (msg);
929 consecutive = empathy_contact_equal (self->priv->last_contact, sender) &&
930 (timestamp - self->priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
931 (is_backlog == self->priv->last_is_backlog) &&
932 !tp_asv_get_boolean (self->priv->data->info,
933 "DisableCombineConsecutive", NULL);
935 /* Define message classes */
936 message_classes = g_string_new ("message");
937 if (!self->priv->has_focus && !is_backlog)
939 if (!self->priv->has_unread_message)
941 g_string_append (message_classes, " firstFocus");
942 self->priv->has_unread_message = TRUE;
944 g_string_append (message_classes, " focus");
948 g_string_append (message_classes, " history");
951 g_string_append (message_classes, " consecutive");
953 if (empathy_contact_is_user (sender))
954 g_string_append (message_classes, " outgoing");
956 g_string_append (message_classes, " incoming");
958 if (should_highlight)
959 g_string_append (message_classes, " mention");
961 if (empathy_message_get_tptype (msg) ==
962 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
963 g_string_append (message_classes, " autoreply");
966 g_string_append (message_classes, " action");
968 /* FIXME: other classes:
969 * status - the message is a status change
970 * event - the message is a notification of something happening
971 * (for example, encryption being turned on)
972 * %status% - See %status% in theme_adium_append_html ()
975 /* This is slightly a hack, but it's the only way to add
976 * arbitrary data to messages in the HTML. We add another
977 * class called "x-empathy-message-id-*" to the message. This
978 * way, we can remove the unread marker for this specific
980 tp_msg = empathy_message_get_tp_message (msg);
986 id = tp_message_get_pending_message_id (tp_msg, &valid);
988 g_string_append_printf (message_classes,
989 " x-empathy-message-id-%u", id);
992 /* Define javascript function to use */
994 func = self->priv->allow_scrolling ? "appendNextMessage" :
995 "appendNextMessageNoScroll";
997 func = self->priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
999 if (empathy_contact_is_user (sender))
1004 html = consecutive ? self->priv->data->out_nextcontext_html :
1005 self->priv->data->out_context_html;
1008 html = consecutive ? self->priv->data->out_nextcontent_html :
1009 self->priv->data->out_content_html;
1011 /* remove all the unread marks when we are sending a message */
1012 theme_adium_remove_all_focus_marks (self);
1019 html = consecutive ? self->priv->data->in_nextcontext_html :
1020 self->priv->data->in_context_html;
1023 html = consecutive ? self->priv->data->in_nextcontent_html :
1024 self->priv->data->in_content_html;
1027 theme_adium_append_html (self, func, html, body_escaped,
1028 avatar_filename, name_escaped, contact_id,
1029 service_name, message_classes->str,
1030 timestamp, is_backlog, empathy_contact_is_user (sender));
1032 /* Keep the sender of the last displayed message */
1033 if (self->priv->last_contact)
1034 g_object_unref (self->priv->last_contact);
1036 self->priv->last_contact = g_object_ref (sender);
1037 self->priv->last_timestamp = timestamp;
1038 self->priv->last_is_backlog = is_backlog;
1040 g_free (body_escaped);
1041 g_free (name_escaped);
1042 g_string_free (message_classes, TRUE);
1046 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1051 if (self->priv->pages_loading != 0)
1053 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
1057 str_escaped = g_markup_escape_text (str, -1);
1058 theme_adium_append_event_escaped (self, str_escaped);
1059 g_free (str_escaped);
1063 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1064 const gchar *markup_text,
1065 const gchar *fallback_text)
1067 theme_adium_append_event_escaped (self, markup_text);
1071 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1072 EmpathyMessage *message)
1074 WebKitDOMDocument *doc;
1075 WebKitDOMElement *span;
1076 gchar *id, *parsed_body;
1077 gchar *tooltip, *timestamp;
1078 GtkIconInfo *icon_info;
1079 GError *error = NULL;
1081 if (self->priv->pages_loading != 0)
1083 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1087 id = g_strdup_printf ("message-token-%s",
1088 empathy_message_get_supersedes (message));
1089 /* we don't pass a token here, because doing so will return another
1090 * <span> element, and we don't want nested <span> elements */
1091 parsed_body = theme_adium_parse_body (self,
1092 empathy_message_get_body (message), NULL);
1094 /* find the element */
1095 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1096 span = webkit_dom_document_get_element_by_id (doc, id);
1100 DEBUG ("Failed to find id '%s'", id);
1104 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1106 DEBUG ("Not a HTML element");
1110 /* update the HTML */
1111 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1112 parsed_body, &error);
1116 DEBUG ("Error setting new inner-HTML: %s", error->message);
1117 g_error_free (error);
1122 timestamp = empathy_time_to_string_local (
1123 empathy_message_get_timestamp (message),
1125 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1127 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1133 /* mark this message as edited */
1134 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1135 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1137 if (icon_info != NULL)
1139 /* set the icon as a background image using CSS
1140 * FIXME: the icon won't update in response to theme changes */
1141 gchar *style = g_strdup_printf (
1142 "background-image:url('%s');"
1143 "background-repeat:no-repeat;"
1144 "background-position:left center;"
1145 "padding-left:19px;", /* 16px icon + 3px padding */
1146 gtk_icon_info_get_filename (icon_info));
1148 webkit_dom_element_set_attribute (span, "style", style, &error);
1152 DEBUG ("Error setting element style: %s",
1154 g_clear_error (&error);
1159 gtk_icon_info_free (icon_info);
1165 DEBUG ("Could not find message to edit with: %s",
1166 empathy_message_get_body (message));
1170 g_free (parsed_body);
1174 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1175 gboolean allow_scrolling)
1177 self->priv->allow_scrolling = allow_scrolling;
1179 if (allow_scrolling)
1180 empathy_theme_adium_scroll_down (self);
1184 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1186 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1190 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1192 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1196 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1198 theme_adium_load_template (self);
1200 /* Clear last contact to avoid trying to add a 'joined'
1201 * message when we don't have an insertion point. */
1202 if (self->priv->last_contact)
1204 g_object_unref (self->priv->last_contact);
1205 self->priv->last_contact = NULL;
1210 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1211 const gchar *search_criteria,
1212 gboolean new_search,
1213 gboolean match_case)
1215 /* FIXME: Doesn't respect new_search */
1216 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1217 search_criteria, match_case, FALSE, TRUE);
1221 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1222 const gchar *search_criteria,
1223 gboolean new_search,
1224 gboolean match_case)
1226 /* FIXME: Doesn't respect new_search */
1227 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1228 search_criteria, match_case, TRUE, TRUE);
1232 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1233 const gchar *search_criteria,
1234 gboolean match_case,
1235 gboolean *can_do_previous,
1236 gboolean *can_do_next)
1238 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1239 * find_next and find_previous to work around this problem. */
1240 if (can_do_previous)
1241 *can_do_previous = TRUE;
1243 *can_do_next = TRUE;
1247 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1249 gboolean match_case)
1251 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1252 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1253 text, match_case, 0);
1254 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1259 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1261 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1265 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1268 WebKitDOMDocument *dom;
1269 WebKitDOMNodeList *nodes;
1271 GError *error = NULL;
1273 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1277 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1279 /* Get all nodes with focus class */
1280 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1285 DEBUG ("Error getting focus nodes: %s",
1286 error ? error->message : "No error");
1287 g_clear_error (&error);
1291 theme_adium_remove_focus_marks (self, nodes);
1295 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1298 EmpathyThemeAdium *self = user_data;
1299 guint32 id = GPOINTER_TO_UINT (data);
1301 theme_adium_remove_mark_from_message (self, id);
1305 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1308 self->priv->has_focus = has_focus;
1309 if (!self->priv->has_focus)
1311 /* We've lost focus, so let's make sure all the acked
1312 * messages have lost their unread marker. */
1313 g_queue_foreach (&self->priv->acked_messages,
1314 theme_adium_remove_acked_message_unread_mark_foreach, self);
1315 g_queue_clear (&self->priv->acked_messages);
1317 self->priv->has_unread_message = FALSE;
1322 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1323 EmpathyMessage *message)
1329 tp_msg = empathy_message_get_tp_message (message);
1334 id = tp_message_get_pending_message_id (tp_msg, &valid);
1337 g_warning ("Acknoledged message doesn't have a pending ID");
1341 /* We only want to actually remove the unread marker if the
1342 * view doesn't have focus. If we did it all the time we would
1343 * never see the unread markers, ever! So, we'll queue these
1344 * up, and when we lose focus, we'll remove the markers. */
1345 if (self->priv->has_focus)
1347 g_queue_push_tail (&self->priv->acked_messages,
1348 GUINT_TO_POINTER (id));
1352 theme_adium_remove_mark_from_message (self, id);
1356 theme_adium_button_press_event (GtkWidget *widget,
1357 GdkEventButton *event)
1359 if (event->button == 3)
1361 gboolean developer_tools_enabled;
1364 G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1365 "enable-developer-extras", &developer_tools_enabled, NULL);
1367 /* We currently have no way to add an inspector menu
1368 * item ourselves, so we disable our customized menu
1369 * if the developer extras are enabled. */
1370 if (!developer_tools_enabled)
1372 empathy_webkit_context_menu_for_event (
1373 WEBKIT_WEB_VIEW (widget), event,
1374 EMPATHY_WEBKIT_MENU_CLEAR);
1379 return GTK_WIDGET_CLASS (
1380 empathy_theme_adium_parent_class)->button_press_event (widget, event);
1384 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1385 gboolean show_avatars)
1387 self->priv->show_avatars = show_avatars;
1391 theme_adium_load_finished_cb (WebKitWebView *view,
1392 WebKitWebFrame *frame,
1395 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1398 DEBUG ("Page loaded");
1399 self->priv->pages_loading--;
1401 if (self->priv->pages_loading != 0)
1404 /* Display queued messages */
1405 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1407 QueuedItem *item = l->data;
1411 case QUEUED_MESSAGE:
1412 empathy_theme_adium_append_message (self, item->msg,
1413 item->should_highlight);
1417 empathy_theme_adium_edit_message (self, item->msg);
1421 empathy_theme_adium_append_event (self, item->str);
1425 free_queued_item (item);
1428 g_queue_clear (&self->priv->message_queue);
1432 theme_adium_finalize (GObject *object)
1434 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1436 empathy_adium_data_unref (self->priv->data);
1438 g_object_unref (self->priv->gsettings_chat);
1439 g_object_unref (self->priv->gsettings_desktop);
1441 g_free (self->priv->variant);
1443 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1447 theme_adium_dispose (GObject *object)
1449 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1451 if (self->priv->smiley_manager)
1453 g_object_unref (self->priv->smiley_manager);
1454 self->priv->smiley_manager = NULL;
1457 if (self->priv->last_contact)
1459 g_object_unref (self->priv->last_contact);
1460 self->priv->last_contact = NULL;
1463 if (self->priv->inspector_window)
1465 gtk_widget_destroy (self->priv->inspector_window);
1466 self->priv->inspector_window = NULL;
1469 if (self->priv->acked_messages.length > 0)
1471 g_queue_clear (&self->priv->acked_messages);
1474 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1478 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1479 EmpathyThemeAdium *self)
1481 if (self->priv->inspector_window)
1483 gtk_widget_show_all (self->priv->inspector_window);
1490 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1491 EmpathyThemeAdium *self)
1493 if (self->priv->inspector_window)
1495 gtk_widget_hide (self->priv->inspector_window);
1501 static WebKitWebView *
1502 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1503 WebKitWebView *web_view,
1504 EmpathyThemeAdium *self)
1506 GtkWidget *scrolled_window;
1507 GtkWidget *inspector_web_view;
1509 if (!self->priv->inspector_window)
1511 /* Create main window */
1512 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1514 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1517 g_signal_connect (self->priv->inspector_window, "delete-event",
1518 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1520 /* Pack a scrolled window */
1521 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1523 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1524 GTK_POLICY_AUTOMATIC,
1525 GTK_POLICY_AUTOMATIC);
1526 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1528 gtk_widget_show (scrolled_window);
1530 /* Pack a webview in the scrolled window. That webview will be
1531 * used to render the inspector tool. */
1532 inspector_web_view = webkit_web_view_new ();
1533 gtk_container_add (GTK_CONTAINER (scrolled_window),
1534 inspector_web_view);
1535 gtk_widget_show (scrolled_window);
1537 return WEBKIT_WEB_VIEW (inspector_web_view);
1544 theme_adium_constructed (GObject *object)
1546 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1547 const gchar *font_family = NULL;
1549 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1550 WebKitWebInspector *webkit_inspector;
1552 /* Set default settings */
1553 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1554 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1556 if (font_family && font_size)
1558 g_object_set (webkit_web_view_get_settings (webkit_view),
1559 "default-font-family", font_family,
1560 "default-font-size", font_size,
1565 empathy_webkit_bind_font_setting (webkit_view,
1566 self->priv->gsettings_desktop,
1567 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1570 /* Setup webkit inspector */
1571 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1572 g_signal_connect (webkit_inspector, "inspect-web-view",
1573 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1574 g_signal_connect (webkit_inspector, "show-window",
1575 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1576 g_signal_connect (webkit_inspector, "close-window",
1577 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1580 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1582 self->priv->in_construction = FALSE;
1586 theme_adium_get_property (GObject *object,
1591 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1595 case PROP_ADIUM_DATA:
1596 g_value_set_boxed (value, self->priv->data);
1599 g_value_set_string (value, self->priv->variant);
1602 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1608 theme_adium_set_property (GObject *object,
1610 const GValue *value,
1613 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1617 case PROP_ADIUM_DATA:
1618 g_assert (self->priv->data == NULL);
1619 self->priv->data = g_value_dup_boxed (value);
1622 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1625 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1631 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1633 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1634 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1636 object_class->finalize = theme_adium_finalize;
1637 object_class->dispose = theme_adium_dispose;
1638 object_class->constructed = theme_adium_constructed;
1639 object_class->get_property = theme_adium_get_property;
1640 object_class->set_property = theme_adium_set_property;
1642 widget_class->button_press_event = theme_adium_button_press_event;
1644 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1645 g_param_spec_boxed ("adium-data",
1647 "Data for the adium theme",
1648 EMPATHY_TYPE_ADIUM_DATA,
1649 G_PARAM_CONSTRUCT_ONLY |
1651 G_PARAM_STATIC_STRINGS));
1653 g_object_class_install_property (object_class, PROP_VARIANT,
1654 g_param_spec_string ("variant",
1655 "The theme variant",
1656 "Variant name for the theme",
1660 G_PARAM_STATIC_STRINGS));
1662 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1666 empathy_theme_adium_init (EmpathyThemeAdium *self)
1668 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1669 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1671 self->priv->in_construction = TRUE;
1672 g_queue_init (&self->priv->message_queue);
1673 self->priv->allow_scrolling = TRUE;
1674 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1676 /* Show avatars by default. */
1677 self->priv->show_avatars = TRUE;
1679 g_signal_connect (self, "load-finished",
1680 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1681 g_signal_connect (self, "navigation-policy-decision-requested",
1682 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1684 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1685 self->priv->gsettings_desktop = g_settings_new (
1686 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1688 g_signal_connect (self->priv->gsettings_chat,
1689 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1690 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1693 theme_adium_update_enable_webkit_developer_tools (self);
1697 empathy_theme_adium_new (EmpathyAdiumData *data,
1698 const gchar *variant)
1700 g_return_val_if_fail (data != NULL, NULL);
1702 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1709 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1710 const gchar *variant)
1712 gchar *variant_path;
1715 if (!tp_strdiff (self->priv->variant, variant))
1718 g_free (self->priv->variant);
1719 self->priv->variant = g_strdup (variant);
1721 if (self->priv->in_construction)
1724 DEBUG ("Update view with variant: '%s'", variant);
1725 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1726 self->priv->variant);
1727 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1730 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1732 g_free (variant_path);
1735 g_object_notify (G_OBJECT (self), "variant");
1739 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1741 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1742 WebKitWebInspector *inspector;
1744 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1745 "enable-developer-extras", TRUE, NULL);
1747 inspector = webkit_web_view_get_inspector (web_view);
1748 webkit_web_inspector_show (inspector);
1752 empathy_adium_path_is_valid (const gchar *path)
1762 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1763 tmp = g_strsplit (path, "/", 0);
1767 dir = tmp[g_strv_length (tmp) - 1];
1769 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1777 /* The theme is not valid if there is no Info.plist */
1778 file = g_build_filename (path, "Contents", "Info.plist",
1780 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1786 /* We ship a default Template.html as fallback if there is any problem
1787 * with the one inside the theme. The only other required file is
1788 * Content.html OR Incoming/Content.html*/
1789 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1791 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1796 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1797 "Content.html", NULL);
1798 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1806 empathy_adium_info_new (const gchar *path)
1810 GHashTable *info = NULL;
1812 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1814 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1815 value = empathy_plist_parse_from_file (file);
1821 info = g_value_dup_boxed (value);
1822 tp_g_value_slice_free (value);
1824 /* Insert the theme's path into the hash table,
1825 * keys have to be dupped */
1826 tp_asv_set_string (info, g_strdup ("path"), path);
1832 adium_info_get_version (GHashTable *info)
1834 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1837 static const gchar *
1838 adium_info_get_no_variant_name (GHashTable *info)
1840 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1841 return name ? name : _("Normal");
1845 adium_info_dup_path_for_variant (GHashTable *info,
1846 const gchar *variant)
1848 guint version = adium_info_get_version (info);
1849 const gchar *no_variant = adium_info_get_no_variant_name (info);
1850 GPtrArray *variants;
1853 if (version <= 2 && !tp_strdiff (variant, no_variant))
1854 return g_strdup ("main.css");
1856 variants = empathy_adium_info_get_available_variants (info);
1857 if (variants->len == 0)
1858 return g_strdup ("main.css");
1860 /* Verify the variant exists, fallback to the first one */
1861 for (i = 0; i < variants->len; i++)
1863 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1867 if (i == variants->len)
1869 DEBUG ("Variant %s does not exist", variant);
1870 variant = g_ptr_array_index (variants, 0);
1873 return g_strdup_printf ("Variants/%s.css", variant);
1878 empathy_adium_info_get_default_variant (GHashTable *info)
1880 if (adium_info_get_version (info) <= 2)
1881 return adium_info_get_no_variant_name (info);
1883 return tp_asv_get_string (info, "DefaultVariant");
1887 empathy_adium_info_get_available_variants (GHashTable *info)
1889 GPtrArray *variants;
1894 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1895 if (variants != NULL)
1898 variants = g_ptr_array_new_with_free_func (g_free);
1899 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1900 G_TYPE_PTR_ARRAY, variants);
1902 path = tp_asv_get_string (info, "path");
1903 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1904 dir = g_dir_open (dirpath, 0, NULL);
1909 for (name = g_dir_read_name (dir);
1911 name = g_dir_read_name (dir))
1913 gchar *display_name;
1915 if (!g_str_has_suffix (name, ".css"))
1918 display_name = g_strdup (name);
1919 strstr (display_name, ".css")[0] = '\0';
1920 g_ptr_array_add (variants, display_name);
1927 if (adium_info_get_version (info) <= 2)
1928 g_ptr_array_add (variants,
1929 g_strdup (adium_info_get_no_variant_name (info)));
1935 empathy_adium_data_get_type (void)
1937 static GType type_id = 0;
1941 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1942 (GBoxedCopyFunc) empathy_adium_data_ref,
1943 (GBoxedFreeFunc) empathy_adium_data_unref);
1950 empathy_adium_data_new_with_info (const gchar *path,
1953 EmpathyAdiumData *data;
1954 gchar *template_html = NULL;
1955 gchar *footer_html = NULL;
1958 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1960 data = g_slice_new0 (EmpathyAdiumData);
1961 data->ref_count = 1;
1962 data->path = g_strdup (path);
1963 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1964 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1965 data->info = g_hash_table_ref (info);
1966 data->version = adium_info_get_version (info);
1967 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1968 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1969 g_str_equal, g_free, g_free);
1971 DEBUG ("Loading theme at %s", path);
1973 #define LOAD(path, var) \
1974 tmp = g_build_filename (data->basedir, path, NULL); \
1975 g_file_get_contents (tmp, &var, NULL, NULL); \
1978 #define LOAD_CONST(path, var) \
1981 LOAD (path, content); \
1982 if (content != NULL) { \
1983 g_ptr_array_add (data->strings_to_free, content); \
1988 /* Load html files */
1989 LOAD_CONST ("Content.html", data->content_html);
1990 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1991 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1992 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1993 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1994 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1995 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1996 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1997 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1998 LOAD_CONST ("Status.html", data->status_html);
1999 LOAD ("Template.html", template_html);
2000 LOAD ("Footer.html", footer_html);
2005 /* HTML fallbacks: If we have at least content OR in_content, then
2006 * everything else gets a fallback */
2008 #define FALLBACK(html, fallback) \
2009 if (html == NULL) { \
2013 /* in_nextcontent -> in_content -> content */
2014 FALLBACK (data->in_content_html, data->content_html);
2015 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2017 /* context -> content */
2018 FALLBACK (data->in_context_html, data->in_content_html);
2019 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2020 FALLBACK (data->out_context_html, data->out_content_html);
2021 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2024 FALLBACK (data->out_content_html, data->in_content_html);
2025 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2026 FALLBACK (data->out_context_html, data->in_context_html);
2027 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2029 /* status -> in_content */
2030 FALLBACK (data->status_html, data->in_content_html);
2034 /* template -> empathy's template */
2035 data->custom_template = (template_html != NULL);
2036 if (template_html == NULL)
2038 GError *error = NULL;
2040 tmp = empathy_file_lookup ("Template.html", "data");
2042 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2043 g_warning ("couldn't load Empathy's default theme "
2044 "template: %s", error->message);
2045 g_return_val_if_reached (data);
2051 /* Default avatar */
2052 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2053 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2055 data->default_incoming_avatar_filename = tmp;
2062 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2063 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2065 data->default_outgoing_avatar_filename = tmp;
2072 /* Old custom templates had only 4 parameters.
2073 * New templates have 5 parameters */
2074 if (data->version <= 2 && data->custom_template)
2076 tmp = string_with_format (template_html,
2078 "%@", /* Leave variant unset */
2079 "", /* The header */
2080 footer_html ? footer_html : "",
2085 tmp = string_with_format (template_html,
2087 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2088 "%@", /* Leave variant unset */
2089 "", /* The header */
2090 footer_html ? footer_html : "",
2093 g_ptr_array_add (data->strings_to_free, tmp);
2094 data->template_html = tmp;
2096 g_free (template_html);
2097 g_free (footer_html);
2103 empathy_adium_data_new (const gchar *path)
2105 EmpathyAdiumData *data;
2108 info = empathy_adium_info_new (path);
2109 data = empathy_adium_data_new_with_info (path, info);
2110 g_hash_table_unref (info);
2116 empathy_adium_data_ref (EmpathyAdiumData *data)
2118 g_return_val_if_fail (data != NULL, NULL);
2120 g_atomic_int_inc (&data->ref_count);
2126 empathy_adium_data_unref (EmpathyAdiumData *data)
2128 g_return_if_fail (data != NULL);
2130 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2131 g_free (data->path);
2132 g_free (data->basedir);
2133 g_free (data->default_avatar_filename);
2134 g_free (data->default_incoming_avatar_filename);
2135 g_free (data->default_outgoing_avatar_filename);
2136 g_hash_table_unref (data->info);
2137 g_ptr_array_unref (data->strings_to_free);
2138 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2140 g_slice_free (EmpathyAdiumData, data);
2145 empathy_adium_data_get_info (EmpathyAdiumData *data)
2147 g_return_val_if_fail (data != NULL, NULL);
2153 empathy_adium_data_get_path (EmpathyAdiumData *data)
2155 g_return_val_if_fail (data != NULL, NULL);