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));
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 *theme)
170 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
171 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
172 gboolean enable_webkit_developer_tools;
174 enable_webkit_developer_tools = g_settings_get_boolean (
175 priv->gsettings_chat,
176 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
178 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
179 "enable-developer-extras",
180 enable_webkit_developer_tools,
185 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
189 EmpathyThemeAdium *theme = user_data;
191 theme_adium_update_enable_webkit_developer_tools (theme);
195 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
196 WebKitWebFrame *web_frame,
197 WebKitNetworkRequest *request,
198 WebKitWebNavigationAction *action,
199 WebKitWebPolicyDecision *decision,
204 /* Only call url_show on clicks */
205 if (webkit_web_navigation_action_get_reason (action) !=
206 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
207 webkit_web_policy_decision_use (decision);
211 uri = webkit_network_request_get_uri (request);
212 empathy_url_show (GTK_WIDGET (view), uri);
214 webkit_web_policy_decision_ignore (decision);
218 /* Replace each %@ in format with string passed in args */
220 string_with_format (const gchar *format,
221 const gchar *first_string,
228 va_start (args, first_string);
229 result = g_string_sized_new (strlen (format));
230 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
233 next = strstr (format, "%@");
238 g_string_append_len (result, format, next - format);
239 g_string_append (result, str);
242 g_string_append (result, format);
245 return g_string_free (result, FALSE);
249 theme_adium_load_template (EmpathyThemeAdium *theme)
251 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
256 priv->pages_loading++;
257 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
258 variant_path = adium_info_dup_path_for_variant (priv->data->info,
260 template = string_with_format (priv->data->template_html,
262 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
263 template, basedir_uri);
264 g_free (basedir_uri);
265 g_free (variant_path);
270 theme_adium_parse_body (EmpathyThemeAdium *self,
274 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
275 EmpathyStringParser *parsers;
278 /* Check if we have to parse smileys */
279 parsers = empathy_webkit_get_string_parser (
280 g_settings_get_boolean (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) {
316 g_string_append (string, "\\\\");
320 g_string_append (string, "\\\"");
323 /* Remove end of lines */
326 g_string_append_c (string, *str);
334 /* If *str starts with match, returns TRUE and move pointer to the end */
336 theme_adium_match (const gchar **str,
341 len = strlen (match);
342 if (strncmp (*str, match, len) == 0) {
350 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
352 theme_adium_match_with_format (const gchar **str,
356 const gchar *cur = *str;
359 if (!theme_adium_match (&cur, match)) {
364 end = strstr (cur, "}%");
369 *format = g_strndup (cur , end - cur);
374 /* List of colors used by %senderColor%. Copied from
375 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
377 static gchar *colors[] = {
378 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
379 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
380 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
381 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
382 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
383 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
384 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
385 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
386 "lightblue", "lightcoral",
387 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
388 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
389 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
390 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
391 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
392 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
393 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
394 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
395 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
396 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
401 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
403 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
404 * to strftime supported by g_date_time_format.
405 * FIXME: table is incomplete, doc of g_date_time_format has a table of
407 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
408 * in 2.29.x we have to explictely request padding with %0x */
409 static const gchar *convert_table[] = {
411 "A", NULL, // 0~86399999 (Millisecond of Day)
413 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
414 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
415 "cc", "%u", // 1~7 (Day of Week)
416 "c", "%u", // 1~7 (Day of Week)
418 "dd", "%d", // 1~31 (0 padded Day of Month)
419 "d", "%d", // 1~31 (0 padded Day of Month)
420 "D", "%j", // 1~366 (0 padded Day of Year)
422 "e", "%u", // 1~7 (0 padded Day of Week)
423 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
424 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
425 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
426 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
428 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
430 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
431 "GGGG", NULL, // Before Christ/Anno Domini
432 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
433 "GG", NULL, // BC/AD (Era Designator Abbreviated)
434 "G", NULL, // BC/AD (Era Designator Abbreviated)
436 "h", "%I", // 1~12 (0 padded Hour (12hr))
437 "H", "%H", // 0~23 (0 padded Hour (24hr))
439 "k", NULL, // 1~24 (0 padded Hour (24hr)
440 "K", NULL, // 0~11 (0 padded Hour (12hr))
442 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
443 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
444 "LL", "%m", // 1~12 (0 padded Month)
445 "L", "%m", // 1~12 (0 padded Month)
447 "m", "%M", // 0~59 (0 padded Minute)
448 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
449 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
450 "MM", "%m", // 1~12 (0 padded Month)
451 "M", "%m", // 1~12 (0 padded Month)
453 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
454 "qqq", NULL, // Q1/Q2/Q3/Q4
455 "qq", NULL, // 1~4 (0 padded Quarter)
456 "q", NULL, // 1~4 (0 padded Quarter)
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)
462 "s", "%S", // 0~59 (0 padded Second)
463 "S", NULL, // (rounded Sub-Second)
465 "u", "%Y", // (0 padded Year)
467 "vvvv", "%Z", // (General GMT Timezone Name)
468 "vvv", "%Z", // (General GMT Timezone Abbreviation)
469 "vv", "%Z", // (General GMT Timezone Abbreviation)
470 "v", "%Z", // (General GMT Timezone Abbreviation)
472 "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)
473 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
475 "yyyy", "%Y", // (Full Year)
476 "yyy", "%y", // (2 Digits Year)
477 "yy", "%y", // (2 Digits Year)
478 "y", "%Y", // (Full Year)
479 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
480 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
481 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
482 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
484 "zzzz", NULL, // (Specific GMT Timezone Name)
485 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
486 "zz", NULL, // (Specific GMT Timezone Abbreviation)
487 "z", NULL, // (Specific GMT Timezone Abbreviation)
488 "Z", "%z", // +0000 (RFC 822 Timezone)
494 if (nsdate == NULL) {
498 str = g_hash_table_lookup (data->date_format_cache, nsdate);
503 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
504 * by corresponding strftime tag. */
505 string = g_string_sized_new (strlen (nsdate));
506 for (i = 0; nsdate[i] != '\0'; i++) {
507 gboolean found = FALSE;
509 /* even indexes are NSDateFormatter tag, odd indexes are the
510 * corresponding strftime tag */
511 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
512 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
518 /* If we don't have a replacement, just ignore that tag */
519 if (convert_table[j + 1] != NULL) {
520 g_string_append (string, convert_table[j + 1]);
522 i += strlen (convert_table[j]) - 1;
524 g_string_append_c (string, nsdate[i]);
528 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
530 /* The cache takes ownership of string->str */
531 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
532 return g_string_free (string, FALSE);
537 theme_adium_append_html (EmpathyThemeAdium *theme,
540 const gchar *message,
541 const gchar *avatar_filename,
543 const gchar *contact_id,
544 const gchar *service_name,
545 const gchar *message_classes,
550 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
552 const gchar *cur = NULL;
555 /* Make some search-and-replace in the html code */
556 string = g_string_sized_new (strlen (html) + strlen (message));
557 g_string_append_printf (string, "%s(\"", func);
558 for (cur = html; *cur != '\0'; cur++) {
559 const gchar *replace = NULL;
560 gchar *dup_replace = NULL;
561 gchar *format = NULL;
563 /* Those are all well known keywords that needs replacement in
564 * html files. Please keep them in the same order than the adium
565 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
566 if (theme_adium_match (&cur, "%userIconPath%")) {
567 replace = avatar_filename;
568 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
569 replace = contact_id;
570 } else if (theme_adium_match (&cur, "%sender%")) {
572 } else if (theme_adium_match (&cur, "%senderColor%")) {
573 /* A color derived from the user's name.
574 * FIXME: If a colon separated list of HTML colors is at
575 * Incoming/SenderColors.txt it will be used instead of
576 * the default colors.
579 /* Ensure we always use the same color when sending messages
583 } else if (contact_id != NULL) {
584 guint hash = g_str_hash (contact_id);
585 replace = colors[hash % G_N_ELEMENTS (colors)];
587 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
588 /* FIXME: The path to the status icon of the sender
589 * (available, away, etc...)
591 } else if (theme_adium_match (&cur, "%messageDirection%")) {
592 /* FIXME: The text direction of the message
593 * (either rtl or ltr)
595 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
596 /* FIXME: The serverside (remotely set) name of the
597 * sender, such as an MSN display name.
599 * We don't have access to that yet so we use
600 * local alias instead.
603 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
604 /* FIXME: This keyword is used to represent the
605 * highlight background color. "X" is the opacity of the
606 * background, ranges from 0 to 1 and can be any decimal
609 } else if (theme_adium_match (&cur, "%message%")) {
611 } else if (theme_adium_match (&cur, "%time%") ||
612 theme_adium_match_with_format (&cur, "%time{", &format)) {
613 const gchar *strftime_format;
615 strftime_format = nsdate_to_strftime (priv->data, format);
617 dup_replace = empathy_time_to_string_local (timestamp,
618 strftime_format ? strftime_format :
619 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
621 dup_replace = empathy_time_to_string_local (timestamp,
622 strftime_format ? strftime_format :
623 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
625 replace = dup_replace;
626 } else if (theme_adium_match (&cur, "%shortTime%")) {
627 dup_replace = empathy_time_to_string_local (timestamp,
628 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
629 replace = dup_replace;
630 } else if (theme_adium_match (&cur, "%service%")) {
631 replace = service_name;
632 } else if (theme_adium_match (&cur, "%variant%")) {
633 /* FIXME: The name of the active message style variant,
634 * with all spaces replaced with an underscore.
635 * A variant named "Alternating Messages - Blue Red"
636 * will become "Alternating_Messages_-_Blue_Red".
638 } else if (theme_adium_match (&cur, "%userIcons%")) {
639 /* FIXME: mus t be "hideIcons" if use preference is set
641 replace = "showIcons";
642 } else if (theme_adium_match (&cur, "%messageClasses%")) {
643 replace = message_classes;
644 } else if (theme_adium_match (&cur, "%status%")) {
645 /* FIXME: A description of the status event. This is
646 * neither in the user's local language nor expected to
647 * be displayed; it may be useful to use a different div
648 * class to present different types of status messages.
649 * The following is a list of some of the more important
650 * status messages; your message style should be able to
651 * handle being shown a status message not in this list,
652 * as even at present the list is incomplete and is
653 * certain to become out of date in the future:
662 * contact_joined (group chats)
666 * encryption (all OTR messages use this status)
667 * purple (all IRC topic and join/part messages use this status)
668 * fileTransferStarted
669 * fileTransferCompleted
672 escape_and_append_len (string, cur, 1);
676 /* Here we have a replacement to make */
677 escape_and_append_len (string, replace, -1);
679 g_free (dup_replace);
682 g_string_append (string, "\")");
684 script = g_string_free (string, FALSE);
685 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
690 theme_adium_append_event_escaped (EmpathyChatView *view,
691 const gchar *escaped)
693 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
694 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
696 theme_adium_append_html (theme, "appendMessage",
697 priv->data->status_html, escaped, NULL, NULL, NULL,
699 empathy_time_get_current (), FALSE, FALSE);
701 /* There is no last contact */
702 if (priv->last_contact) {
703 g_object_unref (priv->last_contact);
704 priv->last_contact = NULL;
709 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
710 WebKitDOMNodeList *nodes)
714 /* Remove focus and firstFocus class */
715 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
716 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
717 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
719 gchar **classes, **iter;
720 GString *new_class_name;
721 gboolean first = TRUE;
723 if (element == NULL) {
727 class_name = webkit_dom_html_element_get_class_name (element);
728 classes = g_strsplit (class_name, " ", -1);
729 new_class_name = g_string_sized_new (strlen (class_name));
730 for (iter = classes; *iter != NULL; iter++) {
731 if (tp_strdiff (*iter, "focus") &&
732 tp_strdiff (*iter, "firstFocus")) {
734 g_string_append_c (new_class_name, ' ');
736 g_string_append (new_class_name, *iter);
741 webkit_dom_html_element_set_class_name (element, new_class_name->str);
744 g_strfreev (classes);
745 g_string_free (new_class_name, TRUE);
750 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
752 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
753 WebKitDOMDocument *dom;
754 WebKitDOMNodeList *nodes;
755 GError *error = NULL;
757 if (!priv->has_unread_message)
760 priv->has_unread_message = FALSE;
762 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
767 /* Get all nodes with focus class */
768 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
770 DEBUG ("Error getting focus nodes: %s",
771 error ? error->message : "No error");
772 g_clear_error (&error);
776 theme_adium_remove_focus_marks (theme, nodes);
780 theme_adium_append_message (EmpathyChatView *view,
782 gboolean should_highlight)
784 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
785 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
786 EmpathyContact *sender;
789 gchar *body_escaped, *name_escaped;
791 const gchar *contact_id;
792 EmpathyAvatar *avatar;
793 const gchar *avatar_filename = NULL;
795 const gchar *html = NULL;
797 const gchar *service_name;
798 GString *message_classes = NULL;
800 gboolean consecutive;
803 if (priv->pages_loading != 0) {
804 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL, should_highlight);
808 /* Get information */
809 sender = empathy_message_get_sender (msg);
810 account = empathy_contact_get_account (sender);
811 service_name = empathy_protocol_name_to_display_name
812 (tp_account_get_protocol (account));
813 if (service_name == NULL)
814 service_name = tp_account_get_protocol (account);
815 timestamp = empathy_message_get_timestamp (msg);
816 body_escaped = theme_adium_parse_body (theme,
817 empathy_message_get_body (msg),
818 empathy_message_get_token (msg));
819 name = empathy_contact_get_logged_alias (sender);
820 contact_id = empathy_contact_get_id (sender);
821 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
823 name_escaped = g_markup_escape_text (name, -1);
825 /* If this is a /me probably */
829 if (priv->data->version >= 4 || !priv->data->custom_template) {
830 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
831 "<span class='actionMessageBody'>%s</span>",
832 name_escaped, body_escaped);
834 str = g_strdup_printf ("*%s*", body_escaped);
836 g_free (body_escaped);
840 /* Get the avatar filename, or a fallback */
841 avatar = empathy_contact_get_avatar (sender);
843 avatar_filename = avatar->filename;
845 if (!avatar_filename) {
846 if (empathy_contact_is_user (sender)) {
847 avatar_filename = priv->data->default_outgoing_avatar_filename;
849 avatar_filename = priv->data->default_incoming_avatar_filename;
851 if (!avatar_filename) {
852 if (!priv->data->default_avatar_filename) {
853 priv->data->default_avatar_filename =
854 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
855 GTK_ICON_SIZE_DIALOG);
857 avatar_filename = priv->data->default_avatar_filename;
861 /* We want to join this message with the last one if
862 * - senders are the same contact,
863 * - last message was recieved recently,
864 * - last message and this message both are/aren't backlog, and
865 * - DisableCombineConsecutive is not set in theme's settings */
866 is_backlog = empathy_message_is_backlog (msg);
867 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
868 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
869 (is_backlog == priv->last_is_backlog) &&
870 !tp_asv_get_boolean (priv->data->info,
871 "DisableCombineConsecutive", NULL);
873 /* Define message classes */
874 message_classes = g_string_new ("message");
875 if (!priv->has_focus && !is_backlog) {
876 if (!priv->has_unread_message) {
877 g_string_append (message_classes, " firstFocus");
878 priv->has_unread_message = TRUE;
880 g_string_append (message_classes, " focus");
883 g_string_append (message_classes, " history");
886 g_string_append (message_classes, " consecutive");
888 if (empathy_contact_is_user (sender)) {
889 g_string_append (message_classes, " outgoing");
891 g_string_append (message_classes, " incoming");
893 if (should_highlight) {
894 g_string_append (message_classes, " mention");
896 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
897 g_string_append (message_classes, " autoreply");
900 g_string_append (message_classes, " action");
902 /* FIXME: other classes:
903 * status - the message is a status change
904 * event - the message is a notification of something happening
905 * (for example, encryption being turned on)
906 * %status% - See %status% in theme_adium_append_html ()
909 /* This is slightly a hack, but it's the only way to add
910 * arbitrary data to messages in the HTML. We add another
911 * class called "x-empathy-message-id-*" to the message. This
912 * way, we can remove the unread marker for this specific
914 tp_msg = empathy_message_get_tp_message (msg);
915 if (tp_msg != NULL) {
919 id = tp_message_get_pending_message_id (tp_msg, &valid);
921 g_string_append_printf (message_classes,
922 " x-empathy-message-id-%u", id);
926 /* Define javascript function to use */
928 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
930 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
933 if (empathy_contact_is_user (sender)) {
937 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
940 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
943 /* remove all the unread marks when we are sending a message */
944 theme_adium_remove_all_focus_marks (theme);
949 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
952 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
956 theme_adium_append_html (theme, func, html, body_escaped,
957 avatar_filename, name_escaped, contact_id,
958 service_name, message_classes->str,
959 timestamp, is_backlog, empathy_contact_is_user (sender));
961 /* Keep the sender of the last displayed message */
962 if (priv->last_contact) {
963 g_object_unref (priv->last_contact);
965 priv->last_contact = g_object_ref (sender);
966 priv->last_timestamp = timestamp;
967 priv->last_is_backlog = is_backlog;
969 g_free (body_escaped);
970 g_free (name_escaped);
971 g_string_free (message_classes, TRUE);
975 theme_adium_append_event (EmpathyChatView *view,
978 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
981 if (priv->pages_loading != 0) {
982 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
986 str_escaped = g_markup_escape_text (str, -1);
987 theme_adium_append_event_escaped (view, str_escaped);
988 g_free (str_escaped);
992 theme_adium_append_event_markup (EmpathyChatView *view,
993 const gchar *markup_text,
994 const gchar *fallback_text)
996 theme_adium_append_event_escaped (view, markup_text);
1000 theme_adium_edit_message (EmpathyChatView *view,
1001 EmpathyMessage *message)
1003 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1004 WebKitDOMDocument *doc;
1005 WebKitDOMElement *span;
1006 gchar *id, *parsed_body;
1007 gchar *tooltip, *timestamp;
1008 GtkIconInfo *icon_info;
1009 GError *error = NULL;
1011 if (priv->pages_loading != 0) {
1012 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1016 id = g_strdup_printf ("message-token-%s",
1017 empathy_message_get_supersedes (message));
1018 /* we don't pass a token here, because doing so will return another
1019 * <span> element, and we don't want nested <span> elements */
1020 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1021 empathy_message_get_body (message), NULL);
1023 /* find the element */
1024 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1025 span = webkit_dom_document_get_element_by_id (doc, id);
1028 DEBUG ("Failed to find id '%s'", id);
1032 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1033 DEBUG ("Not a HTML element");
1037 /* update the HTML */
1038 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1039 parsed_body, &error);
1041 if (error != NULL) {
1042 DEBUG ("Error setting new inner-HTML: %s", error->message);
1043 g_error_free (error);
1048 timestamp = empathy_time_to_string_local (
1049 empathy_message_get_timestamp (message),
1051 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1053 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1059 /* mark this message as edited */
1060 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1061 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1063 if (icon_info != NULL) {
1064 /* set the icon as a background image using CSS
1065 * FIXME: the icon won't update in response to theme changes */
1066 gchar *style = g_strdup_printf (
1067 "background-image:url('%s');"
1068 "background-repeat:no-repeat;"
1069 "background-position:left center;"
1070 "padding-left:19px;", /* 16px icon + 3px padding */
1071 gtk_icon_info_get_filename (icon_info));
1073 webkit_dom_element_set_attribute (span, "style", style, &error);
1075 if (error != NULL) {
1076 DEBUG ("Error setting element style: %s",
1078 g_clear_error (&error);
1083 gtk_icon_info_free (icon_info);
1089 DEBUG ("Could not find message to edit with: %s",
1090 empathy_message_get_body (message));
1094 g_free (parsed_body);
1098 theme_adium_scroll (EmpathyChatView *view,
1099 gboolean allow_scrolling)
1101 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1103 priv->allow_scrolling = allow_scrolling;
1104 if (allow_scrolling) {
1105 empathy_chat_view_scroll_down (view);
1110 theme_adium_scroll_down (EmpathyChatView *view)
1112 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1116 theme_adium_get_has_selection (EmpathyChatView *view)
1118 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1122 theme_adium_clear (EmpathyChatView *view)
1124 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1126 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1128 /* Clear last contact to avoid trying to add a 'joined'
1129 * message when we don't have an insertion point. */
1130 if (priv->last_contact) {
1131 g_object_unref (priv->last_contact);
1132 priv->last_contact = NULL;
1137 theme_adium_find_previous (EmpathyChatView *view,
1138 const gchar *search_criteria,
1139 gboolean new_search,
1140 gboolean match_case)
1142 /* FIXME: Doesn't respect new_search */
1143 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1144 search_criteria, match_case,
1149 theme_adium_find_next (EmpathyChatView *view,
1150 const gchar *search_criteria,
1151 gboolean new_search,
1152 gboolean match_case)
1154 /* FIXME: Doesn't respect new_search */
1155 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1156 search_criteria, match_case,
1161 theme_adium_find_abilities (EmpathyChatView *view,
1162 const gchar *search_criteria,
1163 gboolean match_case,
1164 gboolean *can_do_previous,
1165 gboolean *can_do_next)
1167 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1168 * find_next and find_previous to work around this problem. */
1169 if (can_do_previous)
1170 *can_do_previous = TRUE;
1172 *can_do_next = TRUE;
1176 theme_adium_highlight (EmpathyChatView *view,
1178 gboolean match_case)
1180 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1181 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1182 text, match_case, 0);
1183 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1188 theme_adium_copy_clipboard (EmpathyChatView *view)
1190 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1194 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1197 WebKitDOMDocument *dom;
1198 WebKitDOMNodeList *nodes;
1200 GError *error = NULL;
1202 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1207 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1209 /* Get all nodes with focus class */
1210 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1213 if (nodes == NULL) {
1214 DEBUG ("Error getting focus nodes: %s",
1215 error ? error->message : "No error");
1216 g_clear_error (&error);
1220 theme_adium_remove_focus_marks (self, nodes);
1224 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1227 EmpathyThemeAdium *self = user_data;
1228 guint32 id = GPOINTER_TO_UINT (data);
1230 theme_adium_remove_mark_from_message (self, id);
1234 theme_adium_focus_toggled (EmpathyChatView *view,
1237 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1239 priv->has_focus = has_focus;
1240 if (!priv->has_focus) {
1241 /* We've lost focus, so let's make sure all the acked
1242 * messages have lost their unread marker. */
1243 g_queue_foreach (&priv->acked_messages,
1244 theme_adium_remove_acked_message_unread_mark_foreach,
1246 g_queue_clear (&priv->acked_messages);
1248 priv->has_unread_message = FALSE;
1253 theme_adium_message_acknowledged (EmpathyChatView *view,
1254 EmpathyMessage *message)
1256 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1257 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1262 tp_msg = empathy_message_get_tp_message (message);
1264 if (tp_msg == NULL) {
1268 id = tp_message_get_pending_message_id (tp_msg, &valid);
1270 g_warning ("Acknoledged message doesn't have a pending ID");
1274 /* We only want to actually remove the unread marker if the
1275 * view doesn't have focus. If we did it all the time we would
1276 * never see the unread markers, ever! So, we'll queue these
1277 * up, and when we lose focus, we'll remove the markers. */
1278 if (priv->has_focus) {
1279 g_queue_push_tail (&priv->acked_messages,
1280 GUINT_TO_POINTER (id));
1284 theme_adium_remove_mark_from_message (self, id);
1288 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1290 if (event->button == 3) {
1291 gboolean developer_tools_enabled;
1293 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1294 "enable-developer-extras", &developer_tools_enabled, NULL);
1296 /* We currently have no way to add an inspector menu
1297 * item ourselves, so we disable our customized menu
1298 * if the developer extras are enabled. */
1299 if (!developer_tools_enabled) {
1300 empathy_webkit_context_menu_for_event (
1301 WEBKIT_WEB_VIEW (widget), event,
1302 EMPATHY_WEBKIT_MENU_CLEAR);
1307 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1311 theme_adium_iface_init (EmpathyChatViewIface *iface)
1313 iface->append_message = theme_adium_append_message;
1314 iface->append_event = theme_adium_append_event;
1315 iface->append_event_markup = theme_adium_append_event_markup;
1316 iface->edit_message = theme_adium_edit_message;
1317 iface->scroll = theme_adium_scroll;
1318 iface->scroll_down = theme_adium_scroll_down;
1319 iface->get_has_selection = theme_adium_get_has_selection;
1320 iface->clear = theme_adium_clear;
1321 iface->find_previous = theme_adium_find_previous;
1322 iface->find_next = theme_adium_find_next;
1323 iface->find_abilities = theme_adium_find_abilities;
1324 iface->highlight = theme_adium_highlight;
1325 iface->copy_clipboard = theme_adium_copy_clipboard;
1326 iface->focus_toggled = theme_adium_focus_toggled;
1327 iface->message_acknowledged = theme_adium_message_acknowledged;
1331 theme_adium_load_finished_cb (WebKitWebView *view,
1332 WebKitWebFrame *frame,
1335 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1336 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1339 DEBUG ("Page loaded");
1340 priv->pages_loading--;
1342 if (priv->pages_loading != 0)
1345 /* Display queued messages */
1346 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1347 QueuedItem *item = l->data;
1351 case QUEUED_MESSAGE:
1352 theme_adium_append_message (chat_view, item->msg,
1353 item->should_highlight);
1357 theme_adium_edit_message (chat_view, item->msg);
1361 theme_adium_append_event (chat_view, item->str);
1365 free_queued_item (item);
1368 g_queue_clear (&priv->message_queue);
1372 theme_adium_finalize (GObject *object)
1374 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1376 empathy_adium_data_unref (priv->data);
1378 g_object_unref (priv->gsettings_chat);
1379 g_object_unref (priv->gsettings_desktop);
1381 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1385 theme_adium_dispose (GObject *object)
1387 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1389 if (priv->smiley_manager) {
1390 g_object_unref (priv->smiley_manager);
1391 priv->smiley_manager = NULL;
1394 if (priv->last_contact) {
1395 g_object_unref (priv->last_contact);
1396 priv->last_contact = NULL;
1399 if (priv->inspector_window) {
1400 gtk_widget_destroy (priv->inspector_window);
1401 priv->inspector_window = NULL;
1404 if (priv->acked_messages.length > 0) {
1405 g_queue_clear (&priv->acked_messages);
1408 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1412 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1413 EmpathyThemeAdium *theme)
1415 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1417 if (priv->inspector_window) {
1418 gtk_widget_show_all (priv->inspector_window);
1425 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1426 EmpathyThemeAdium *theme)
1428 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1430 if (priv->inspector_window) {
1431 gtk_widget_hide (priv->inspector_window);
1437 static WebKitWebView *
1438 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1439 WebKitWebView *web_view,
1440 EmpathyThemeAdium *theme)
1442 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1443 GtkWidget *scrolled_window;
1444 GtkWidget *inspector_web_view;
1446 if (!priv->inspector_window) {
1447 /* Create main window */
1448 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1449 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1451 g_signal_connect (priv->inspector_window, "delete-event",
1452 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1454 /* Pack a scrolled window */
1455 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1456 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1457 GTK_POLICY_AUTOMATIC,
1458 GTK_POLICY_AUTOMATIC);
1459 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1461 gtk_widget_show (scrolled_window);
1463 /* Pack a webview in the scrolled window. That webview will be
1464 * used to render the inspector tool. */
1465 inspector_web_view = webkit_web_view_new ();
1466 gtk_container_add (GTK_CONTAINER (scrolled_window),
1467 inspector_web_view);
1468 gtk_widget_show (scrolled_window);
1470 return WEBKIT_WEB_VIEW (inspector_web_view);
1477 theme_adium_constructed (GObject *object)
1479 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1480 const gchar *font_family = NULL;
1482 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1483 WebKitWebInspector *webkit_inspector;
1485 /* Set default settings */
1486 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1487 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1489 if (font_family && font_size) {
1490 g_object_set (webkit_web_view_get_settings (webkit_view),
1491 "default-font-family", font_family,
1492 "default-font-size", font_size,
1495 empathy_webkit_bind_font_setting (webkit_view,
1496 priv->gsettings_desktop,
1497 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1500 /* Setup webkit inspector */
1501 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1502 g_signal_connect (webkit_inspector, "inspect-web-view",
1503 G_CALLBACK (theme_adium_inspect_web_view_cb),
1505 g_signal_connect (webkit_inspector, "show-window",
1506 G_CALLBACK (theme_adium_inspector_show_window_cb),
1508 g_signal_connect (webkit_inspector, "close-window",
1509 G_CALLBACK (theme_adium_inspector_close_window_cb),
1513 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1515 priv->in_construction = FALSE;
1519 theme_adium_get_property (GObject *object,
1524 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1527 case PROP_ADIUM_DATA:
1528 g_value_set_boxed (value, priv->data);
1531 g_value_set_string (value, priv->variant);
1534 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1540 theme_adium_set_property (GObject *object,
1542 const GValue *value,
1545 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1546 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1549 case PROP_ADIUM_DATA:
1550 g_assert (priv->data == NULL);
1551 priv->data = g_value_dup_boxed (value);
1554 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1557 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1563 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1565 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1566 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1568 object_class->finalize = theme_adium_finalize;
1569 object_class->dispose = theme_adium_dispose;
1570 object_class->constructed = theme_adium_constructed;
1571 object_class->get_property = theme_adium_get_property;
1572 object_class->set_property = theme_adium_set_property;
1574 widget_class->button_press_event = theme_adium_button_press_event;
1576 g_object_class_install_property (object_class,
1578 g_param_spec_boxed ("adium-data",
1580 "Data for the adium theme",
1581 EMPATHY_TYPE_ADIUM_DATA,
1582 G_PARAM_CONSTRUCT_ONLY |
1584 G_PARAM_STATIC_STRINGS));
1585 g_object_class_install_property (object_class,
1587 g_param_spec_string ("variant",
1588 "The theme variant",
1589 "Variant name for the theme",
1593 G_PARAM_STATIC_STRINGS));
1595 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1599 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1601 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1602 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1606 priv->in_construction = TRUE;
1607 g_queue_init (&priv->message_queue);
1608 priv->allow_scrolling = TRUE;
1609 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1611 g_signal_connect (theme, "load-finished",
1612 G_CALLBACK (theme_adium_load_finished_cb),
1614 g_signal_connect (theme, "navigation-policy-decision-requested",
1615 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1618 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1619 priv->gsettings_desktop = g_settings_new (
1620 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1622 g_signal_connect (priv->gsettings_chat,
1623 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1624 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1627 theme_adium_update_enable_webkit_developer_tools (theme);
1631 empathy_theme_adium_new (EmpathyAdiumData *data,
1632 const gchar *variant)
1634 g_return_val_if_fail (data != NULL, NULL);
1636 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1643 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1644 const gchar *variant)
1646 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1647 gchar *variant_path;
1650 if (!tp_strdiff (priv->variant, variant)) {
1654 g_free (priv->variant);
1655 priv->variant = g_strdup (variant);
1657 if (priv->in_construction) {
1661 DEBUG ("Update view with variant: '%s'", variant);
1662 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1664 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1666 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1668 g_free (variant_path);
1671 g_object_notify (G_OBJECT (theme), "variant");
1675 empathy_theme_adium_show_inspector (EmpathyThemeAdium *theme)
1677 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
1678 WebKitWebInspector *inspector;
1680 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1681 "enable-developer-extras", TRUE,
1684 inspector = webkit_web_view_get_inspector (web_view);
1685 webkit_web_inspector_show (inspector);
1689 empathy_adium_path_is_valid (const gchar *path)
1694 /* The theme is not valid if there is no Info.plist */
1695 file = g_build_filename (path, "Contents", "Info.plist",
1697 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1703 /* We ship a default Template.html as fallback if there is any problem
1704 * with the one inside the theme. The only other required file is
1705 * Content.html OR Incoming/Content.html*/
1706 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1708 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1712 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1713 "Content.html", NULL);
1714 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1722 empathy_adium_info_new (const gchar *path)
1726 GHashTable *info = NULL;
1728 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1730 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1731 value = empathy_plist_parse_from_file (file);
1737 info = g_value_dup_boxed (value);
1738 tp_g_value_slice_free (value);
1740 /* Insert the theme's path into the hash table,
1741 * keys have to be dupped */
1742 tp_asv_set_string (info, g_strdup ("path"), path);
1748 adium_info_get_version (GHashTable *info)
1750 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1753 static const gchar *
1754 adium_info_get_no_variant_name (GHashTable *info)
1756 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1757 return name ? name : _("Normal");
1761 adium_info_dup_path_for_variant (GHashTable *info,
1762 const gchar *variant)
1764 guint version = adium_info_get_version (info);
1765 const gchar *no_variant = adium_info_get_no_variant_name (info);
1766 GPtrArray *variants;
1769 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1770 return g_strdup ("main.css");
1773 variants = empathy_adium_info_get_available_variants (info);
1774 if (variants->len == 0)
1775 return g_strdup ("main.css");
1777 /* Verify the variant exists, fallback to the first one */
1778 for (i = 0; i < variants->len; i++) {
1779 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1783 if (i == variants->len) {
1784 DEBUG ("Variant %s does not exist", variant);
1785 variant = g_ptr_array_index (variants, 0);
1788 return g_strdup_printf ("Variants/%s.css", variant);
1793 empathy_adium_info_get_default_variant (GHashTable *info)
1795 if (adium_info_get_version (info) <= 2) {
1796 return adium_info_get_no_variant_name (info);
1799 return tp_asv_get_string (info, "DefaultVariant");
1803 empathy_adium_info_get_available_variants (GHashTable *info)
1805 GPtrArray *variants;
1810 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1811 if (variants != NULL) {
1815 variants = g_ptr_array_new_with_free_func (g_free);
1816 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1817 G_TYPE_PTR_ARRAY, variants);
1819 path = tp_asv_get_string (info, "path");
1820 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1821 dir = g_dir_open (dirpath, 0, NULL);
1825 for (name = g_dir_read_name (dir);
1827 name = g_dir_read_name (dir)) {
1828 gchar *display_name;
1830 if (!g_str_has_suffix (name, ".css")) {
1834 display_name = g_strdup (name);
1835 strstr (display_name, ".css")[0] = '\0';
1836 g_ptr_array_add (variants, display_name);
1842 if (adium_info_get_version (info) <= 2) {
1843 g_ptr_array_add (variants,
1844 g_strdup (adium_info_get_no_variant_name (info)));
1851 empathy_adium_data_get_type (void)
1853 static GType type_id = 0;
1857 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1858 (GBoxedCopyFunc) empathy_adium_data_ref,
1859 (GBoxedFreeFunc) empathy_adium_data_unref);
1866 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1868 EmpathyAdiumData *data;
1869 gchar *template_html = NULL;
1870 gchar *footer_html = NULL;
1873 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1875 data = g_slice_new0 (EmpathyAdiumData);
1876 data->ref_count = 1;
1877 data->path = g_strdup (path);
1878 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1879 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1880 data->info = g_hash_table_ref (info);
1881 data->version = adium_info_get_version (info);
1882 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1883 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1884 g_str_equal, g_free, g_free);
1886 DEBUG ("Loading theme at %s", path);
1888 #define LOAD(path, var) \
1889 tmp = g_build_filename (data->basedir, path, NULL); \
1890 g_file_get_contents (tmp, &var, NULL, NULL); \
1893 #define LOAD_CONST(path, var) \
1896 LOAD (path, content); \
1897 if (content != NULL) { \
1898 g_ptr_array_add (data->strings_to_free, content); \
1903 /* Load html files */
1904 LOAD_CONST ("Content.html", data->content_html);
1905 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1906 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1907 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1908 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1909 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1910 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1911 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1912 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1913 LOAD_CONST ("Status.html", data->status_html);
1914 LOAD ("Template.html", template_html);
1915 LOAD ("Footer.html", footer_html);
1920 /* HTML fallbacks: If we have at least content OR in_content, then
1921 * everything else gets a fallback */
1923 #define FALLBACK(html, fallback) \
1924 if (html == NULL) { \
1928 /* in_nextcontent -> in_content -> content */
1929 FALLBACK (data->in_content_html, data->content_html);
1930 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1932 /* context -> content */
1933 FALLBACK (data->in_context_html, data->in_content_html);
1934 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1935 FALLBACK (data->out_context_html, data->out_content_html);
1936 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1939 FALLBACK (data->out_content_html, data->in_content_html);
1940 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1941 FALLBACK (data->out_context_html, data->in_context_html);
1942 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1944 /* status -> in_content */
1945 FALLBACK (data->status_html, data->in_content_html);
1949 /* template -> empathy's template */
1950 data->custom_template = (template_html != NULL);
1951 if (template_html == NULL) {
1952 GError *error = NULL;
1954 tmp = empathy_file_lookup ("Template.html", "data");
1956 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
1957 g_warning ("couldn't load Empathy's default theme "
1958 "template: %s", error->message);
1959 g_return_val_if_reached (data);
1965 /* Default avatar */
1966 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1967 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1968 data->default_incoming_avatar_filename = tmp;
1972 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1973 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1974 data->default_outgoing_avatar_filename = tmp;
1979 /* Old custom templates had only 4 parameters.
1980 * New templates have 5 parameters */
1981 if (data->version <= 2 && data->custom_template) {
1982 tmp = string_with_format (template_html,
1984 "%@", /* Leave variant unset */
1985 "", /* The header */
1986 footer_html ? footer_html : "",
1989 tmp = string_with_format (template_html,
1991 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1992 "%@", /* Leave variant unset */
1993 "", /* The header */
1994 footer_html ? footer_html : "",
1997 g_ptr_array_add (data->strings_to_free, tmp);
1998 data->template_html = tmp;
2000 g_free (template_html);
2001 g_free (footer_html);
2007 empathy_adium_data_new (const gchar *path)
2009 EmpathyAdiumData *data;
2012 info = empathy_adium_info_new (path);
2013 data = empathy_adium_data_new_with_info (path, info);
2014 g_hash_table_unref (info);
2020 empathy_adium_data_ref (EmpathyAdiumData *data)
2022 g_return_val_if_fail (data != NULL, NULL);
2024 g_atomic_int_inc (&data->ref_count);
2030 empathy_adium_data_unref (EmpathyAdiumData *data)
2032 g_return_if_fail (data != NULL);
2034 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2035 g_free (data->path);
2036 g_free (data->basedir);
2037 g_free (data->default_avatar_filename);
2038 g_free (data->default_incoming_avatar_filename);
2039 g_free (data->default_outgoing_avatar_filename);
2040 g_hash_table_unref (data->info);
2041 g_ptr_array_unref (data->strings_to_free);
2042 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2044 g_slice_free (EmpathyAdiumData, data);
2049 empathy_adium_data_get_info (EmpathyAdiumData *data)
2051 g_return_val_if_fail (data != NULL, NULL);
2057 empathy_adium_data_get_path (EmpathyAdiumData *data)
2059 g_return_val_if_fail (data != NULL, NULL);