1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2008-2009 Collabora Ltd.
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 * Authors: Xavier Claessens <xclaesse@gmail.com>
25 #include <glib/gi18n-lib.h>
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
31 #include <pango/pango.h>
34 #include <libempathy/empathy-gsettings.h>
35 #include <libempathy/empathy-time.h>
36 #include <libempathy/empathy-utils.h>
38 #include "empathy-theme-adium.h"
39 #include "empathy-smiley-manager.h"
40 #include "empathy-ui-utils.h"
41 #include "empathy-plist.h"
42 #include "empathy-images.h"
43 #include "empathy-webkit-utils.h"
45 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
46 #include <libempathy/empathy-debug.h>
48 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
50 #define BORING_DPI_DEFAULT 96
52 /* "Join" consecutive messages with timestamps within five minutes */
53 #define MESSAGE_JOIN_PERIOD 5*60
56 EmpathyAdiumData *data;
57 EmpathySmileyManager *smiley_manager;
58 EmpathyContact *last_contact;
59 gint64 last_timestamp;
60 gboolean last_is_backlog;
62 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
64 /* Queue of guint32 of pending message id to remove unread
65 * marker for when we lose focus. */
66 GQueue acked_messages;
67 GtkWidget *inspector_window;
69 GSettings *gsettings_chat;
70 GSettings *gsettings_desktop;
73 gboolean has_unread_message;
74 gboolean allow_scrolling;
76 gboolean in_construction;
77 } EmpathyThemeAdiumPriv;
79 struct _EmpathyAdiumData {
83 gchar *default_avatar_filename;
84 gchar *default_incoming_avatar_filename;
85 gchar *default_outgoing_avatar_filename;
88 gboolean custom_template;
89 /* gchar* -> gchar* both owned */
90 GHashTable *date_format_cache;
93 const gchar *template_html;
94 const gchar *content_html;
95 const gchar *in_content_html;
96 const gchar *in_context_html;
97 const gchar *in_nextcontent_html;
98 const gchar *in_nextcontext_html;
99 const gchar *out_content_html;
100 const gchar *out_context_html;
101 const gchar *out_nextcontent_html;
102 const gchar *out_nextcontext_html;
103 const gchar *status_html;
105 /* Above html strings are pointers to strings stored in this array.
106 * We do this because of fallbacks, some htmls could be pointing the
108 GPtrArray *strings_to_free;
111 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
112 static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
120 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
121 WEBKIT_TYPE_WEB_VIEW,
122 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
123 theme_adium_iface_init));
138 queue_item (GQueue *queue,
143 QueuedItem *item = g_slice_new0 (QueuedItem);
147 item->msg = g_object_ref (msg);
148 item->str = g_strdup (str);
150 g_queue_push_tail (queue, item);
156 free_queued_item (QueuedItem *item)
158 tp_clear_object (&item->msg);
161 g_slice_free (QueuedItem, item);
165 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
167 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
168 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
169 gboolean enable_webkit_developer_tools;
171 enable_webkit_developer_tools = g_settings_get_boolean (
172 priv->gsettings_chat,
173 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
175 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
176 "enable-developer-extras",
177 enable_webkit_developer_tools,
182 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
186 EmpathyThemeAdium *theme = user_data;
188 theme_adium_update_enable_webkit_developer_tools (theme);
192 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
193 WebKitWebFrame *web_frame,
194 WebKitNetworkRequest *request,
195 WebKitWebNavigationAction *action,
196 WebKitWebPolicyDecision *decision,
201 /* Only call url_show on clicks */
202 if (webkit_web_navigation_action_get_reason (action) !=
203 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
204 webkit_web_policy_decision_use (decision);
208 uri = webkit_network_request_get_uri (request);
209 empathy_url_show (GTK_WIDGET (view), uri);
211 webkit_web_policy_decision_ignore (decision);
215 /* Replace each %@ in format with string passed in args */
217 string_with_format (const gchar *format,
218 const gchar *first_string,
225 va_start (args, first_string);
226 result = g_string_sized_new (strlen (format));
227 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
230 next = strstr (format, "%@");
235 g_string_append_len (result, format, next - format);
236 g_string_append (result, str);
239 g_string_append (result, format);
242 return g_string_free (result, FALSE);
246 theme_adium_load_template (EmpathyThemeAdium *theme)
248 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
253 priv->pages_loading++;
254 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
255 variant_path = adium_info_dup_path_for_variant (priv->data->info,
257 template = string_with_format (priv->data->template_html,
259 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
260 template, basedir_uri);
261 g_free (basedir_uri);
262 g_free (variant_path);
267 theme_adium_parse_body (EmpathyThemeAdium *self,
271 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
272 EmpathyStringParser *parsers;
275 /* Check if we have to parse smileys */
276 parsers = empathy_webkit_get_string_parser (
277 g_settings_get_boolean (priv->gsettings_chat,
278 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
280 /* Parse text and construct string with links and smileys replaced
281 * by html tags. Also escape text to make sure html code is
282 * displayed verbatim. */
283 string = g_string_sized_new (strlen (text));
285 /* wrap this in HTML that allows us to find the message for later
287 if (!tp_str_empty (token))
288 g_string_append_printf (string,
289 "<span id=\"message-token-%s\">",
292 empathy_string_parser_substr (text, -1, parsers, string);
294 if (!tp_str_empty (token))
295 g_string_append (string, "</span>");
297 /* Wrap body in order to make tabs and multiple spaces displayed
298 * properly. See bug #625745. */
299 g_string_prepend (string, "<div style=\"display: inline; "
300 "white-space: pre-wrap\"'>");
301 g_string_append (string, "</div>");
303 return g_string_free (string, FALSE);
307 escape_and_append_len (GString *string, const gchar *str, gint len)
309 while (str != NULL && *str != '\0' && len != 0) {
313 g_string_append (string, "\\\\");
317 g_string_append (string, "\\\"");
320 /* Remove end of lines */
323 g_string_append_c (string, *str);
331 /* If *str starts with match, returns TRUE and move pointer to the end */
333 theme_adium_match (const gchar **str,
338 len = strlen (match);
339 if (strncmp (*str, match, len) == 0) {
347 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
349 theme_adium_match_with_format (const gchar **str,
353 const gchar *cur = *str;
356 if (!theme_adium_match (&cur, match)) {
361 end = strstr (cur, "}%");
366 *format = g_strndup (cur , end - cur);
371 /* List of colors used by %senderColor%. Copied from
372 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
374 static gchar *colors[] = {
375 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
376 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
377 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
378 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
379 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
380 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
381 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
382 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
383 "lightblue", "lightcoral",
384 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
385 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
386 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
387 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
388 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
389 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
390 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
391 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
392 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
393 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
398 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
400 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
401 * to strftime supported by g_date_time_format.
402 * FIXME: table is incomplete, doc of g_date_time_format has a table of
404 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
405 * in 2.29.x we have to explictely request padding with %0x */
406 static const gchar *convert_table[] = {
408 "A", NULL, // 0~86399999 (Millisecond of Day)
410 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
411 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
412 "cc", "%u", // 1~7 (Day of Week)
413 "c", "%u", // 1~7 (Day of Week)
415 "dd", "%d", // 1~31 (0 padded Day of Month)
416 "d", "%d", // 1~31 (0 padded Day of Month)
417 "D", "%j", // 1~366 (0 padded Day of Year)
419 "e", "%u", // 1~7 (0 padded Day of Week)
420 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
421 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
422 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
423 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
425 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
427 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
428 "GGGG", NULL, // Before Christ/Anno Domini
429 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
430 "GG", NULL, // BC/AD (Era Designator Abbreviated)
431 "G", NULL, // BC/AD (Era Designator Abbreviated)
433 "h", "%I", // 1~12 (0 padded Hour (12hr))
434 "H", "%H", // 0~23 (0 padded Hour (24hr))
436 "k", NULL, // 1~24 (0 padded Hour (24hr)
437 "K", NULL, // 0~11 (0 padded Hour (12hr))
439 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
440 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
441 "LL", "%m", // 1~12 (0 padded Month)
442 "L", "%m", // 1~12 (0 padded Month)
444 "m", "%M", // 0~59 (0 padded Minute)
445 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
446 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
447 "MM", "%m", // 1~12 (0 padded Month)
448 "M", "%m", // 1~12 (0 padded Month)
450 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
451 "qqq", NULL, // Q1/Q2/Q3/Q4
452 "qq", NULL, // 1~4 (0 padded Quarter)
453 "q", NULL, // 1~4 (0 padded Quarter)
454 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
455 "QQQ", NULL, // Q1/Q2/Q3/Q4
456 "QQ", NULL, // 1~4 (0 padded Quarter)
457 "Q", NULL, // 1~4 (0 padded Quarter)
459 "s", "%S", // 0~59 (0 padded Second)
460 "S", NULL, // (rounded Sub-Second)
462 "u", "%Y", // (0 padded Year)
464 "vvvv", "%Z", // (General GMT Timezone Name)
465 "vvv", "%Z", // (General GMT Timezone Abbreviation)
466 "vv", "%Z", // (General GMT Timezone Abbreviation)
467 "v", "%Z", // (General GMT Timezone Abbreviation)
469 "w", "%W", // 1~53 (0 padded Week of Year, 1st day of week = Sunday, NB, 1st week of year starts from the last Sunday of last year)
470 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
472 "yyyy", "%Y", // (Full Year)
473 "yyy", "%y", // (2 Digits Year)
474 "yy", "%y", // (2 Digits Year)
475 "y", "%Y", // (Full Year)
476 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
477 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
478 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
479 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
481 "zzzz", NULL, // (Specific GMT Timezone Name)
482 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
483 "zz", NULL, // (Specific GMT Timezone Abbreviation)
484 "z", NULL, // (Specific GMT Timezone Abbreviation)
485 "Z", "%z", // +0000 (RFC 822 Timezone)
491 if (nsdate == NULL) {
495 str = g_hash_table_lookup (data->date_format_cache, nsdate);
500 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
501 * by corresponding strftime tag. */
502 string = g_string_sized_new (strlen (nsdate));
503 for (i = 0; nsdate[i] != '\0'; i++) {
504 gboolean found = FALSE;
506 /* even indexes are NSDateFormatter tag, odd indexes are the
507 * corresponding strftime tag */
508 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
509 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
515 /* If we don't have a replacement, just ignore that tag */
516 if (convert_table[j + 1] != NULL) {
517 g_string_append (string, convert_table[j + 1]);
519 i += strlen (convert_table[j]) - 1;
521 g_string_append_c (string, nsdate[i]);
525 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
527 /* The cache takes ownership of string->str */
528 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
529 return g_string_free (string, FALSE);
534 theme_adium_append_html (EmpathyThemeAdium *theme,
537 const gchar *message,
538 const gchar *avatar_filename,
540 const gchar *contact_id,
541 const gchar *service_name,
542 const gchar *message_classes,
547 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
549 const gchar *cur = NULL;
552 /* Make some search-and-replace in the html code */
553 string = g_string_sized_new (strlen (html) + strlen (message));
554 g_string_append_printf (string, "%s(\"", func);
555 for (cur = html; *cur != '\0'; cur++) {
556 const gchar *replace = NULL;
557 gchar *dup_replace = NULL;
558 gchar *format = NULL;
560 /* Those are all well known keywords that needs replacement in
561 * html files. Please keep them in the same order than the adium
562 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
563 if (theme_adium_match (&cur, "%userIconPath%")) {
564 replace = avatar_filename;
565 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
566 replace = contact_id;
567 } else if (theme_adium_match (&cur, "%sender%")) {
569 } else if (theme_adium_match (&cur, "%senderColor%")) {
570 /* A color derived from the user's name.
571 * FIXME: If a colon separated list of HTML colors is at
572 * Incoming/SenderColors.txt it will be used instead of
573 * the default colors.
576 /* Ensure we always use the same color when sending messages
580 } else if (contact_id != NULL) {
581 guint hash = g_str_hash (contact_id);
582 replace = colors[hash % G_N_ELEMENTS (colors)];
584 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
585 /* FIXME: The path to the status icon of the sender
586 * (available, away, etc...)
588 } else if (theme_adium_match (&cur, "%messageDirection%")) {
589 /* FIXME: The text direction of the message
590 * (either rtl or ltr)
592 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
593 /* FIXME: The serverside (remotely set) name of the
594 * sender, such as an MSN display name.
596 * We don't have access to that yet so we use
597 * local alias instead.
600 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
601 /* FIXME: This keyword is used to represent the
602 * highlight background color. "X" is the opacity of the
603 * background, ranges from 0 to 1 and can be any decimal
606 } else if (theme_adium_match (&cur, "%message%")) {
608 } else if (theme_adium_match (&cur, "%time%") ||
609 theme_adium_match_with_format (&cur, "%time{", &format)) {
610 const gchar *strftime_format;
612 strftime_format = nsdate_to_strftime (priv->data, format);
614 dup_replace = empathy_time_to_string_local (timestamp,
615 strftime_format ? strftime_format :
616 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
618 dup_replace = empathy_time_to_string_local (timestamp,
619 strftime_format ? strftime_format :
620 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
622 replace = dup_replace;
623 } else if (theme_adium_match (&cur, "%shortTime%")) {
624 dup_replace = empathy_time_to_string_local (timestamp,
625 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
626 replace = dup_replace;
627 } else if (theme_adium_match (&cur, "%service%")) {
628 replace = service_name;
629 } else if (theme_adium_match (&cur, "%variant%")) {
630 /* FIXME: The name of the active message style variant,
631 * with all spaces replaced with an underscore.
632 * A variant named "Alternating Messages - Blue Red"
633 * will become "Alternating_Messages_-_Blue_Red".
635 } else if (theme_adium_match (&cur, "%userIcons%")) {
636 /* FIXME: mus t be "hideIcons" if use preference is set
638 replace = "showIcons";
639 } else if (theme_adium_match (&cur, "%messageClasses%")) {
640 replace = message_classes;
641 } else if (theme_adium_match (&cur, "%status%")) {
642 /* FIXME: A description of the status event. This is
643 * neither in the user's local language nor expected to
644 * be displayed; it may be useful to use a different div
645 * class to present different types of status messages.
646 * The following is a list of some of the more important
647 * status messages; your message style should be able to
648 * handle being shown a status message not in this list,
649 * as even at present the list is incomplete and is
650 * certain to become out of date in the future:
659 * contact_joined (group chats)
663 * encryption (all OTR messages use this status)
664 * purple (all IRC topic and join/part messages use this status)
665 * fileTransferStarted
666 * fileTransferCompleted
669 escape_and_append_len (string, cur, 1);
673 /* Here we have a replacement to make */
674 escape_and_append_len (string, replace, -1);
676 g_free (dup_replace);
679 g_string_append (string, "\")");
681 script = g_string_free (string, FALSE);
682 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
687 theme_adium_append_event_escaped (EmpathyChatView *view,
688 const gchar *escaped)
690 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
691 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
693 theme_adium_append_html (theme, "appendMessage",
694 priv->data->status_html, escaped, NULL, NULL, NULL,
696 empathy_time_get_current (), FALSE, FALSE);
698 /* There is no last contact */
699 if (priv->last_contact) {
700 g_object_unref (priv->last_contact);
701 priv->last_contact = NULL;
706 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
707 WebKitDOMNodeList *nodes)
711 /* Remove focus and firstFocus class */
712 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
713 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
714 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
716 gchar **classes, **iter;
717 GString *new_class_name;
718 gboolean first = TRUE;
720 if (element == NULL) {
724 class_name = webkit_dom_html_element_get_class_name (element);
725 classes = g_strsplit (class_name, " ", -1);
726 new_class_name = g_string_sized_new (strlen (class_name));
727 for (iter = classes; *iter != NULL; iter++) {
728 if (tp_strdiff (*iter, "focus") &&
729 tp_strdiff (*iter, "firstFocus")) {
731 g_string_append_c (new_class_name, ' ');
733 g_string_append (new_class_name, *iter);
738 webkit_dom_html_element_set_class_name (element, new_class_name->str);
741 g_strfreev (classes);
742 g_string_free (new_class_name, TRUE);
747 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
749 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
750 WebKitDOMDocument *dom;
751 WebKitDOMNodeList *nodes;
752 GError *error = NULL;
754 if (!priv->has_unread_message)
757 priv->has_unread_message = FALSE;
759 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
764 /* Get all nodes with focus class */
765 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
767 DEBUG ("Error getting focus nodes: %s",
768 error ? error->message : "No error");
769 g_clear_error (&error);
773 theme_adium_remove_focus_marks (theme, nodes);
777 theme_adium_append_message (EmpathyChatView *view,
780 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
781 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
782 EmpathyContact *sender;
787 const gchar *contact_id;
788 EmpathyAvatar *avatar;
789 const gchar *avatar_filename = NULL;
791 const gchar *html = NULL;
793 const gchar *service_name;
794 GString *message_classes = NULL;
796 gboolean consecutive;
799 if (priv->pages_loading != 0) {
800 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL);
804 /* Get information */
805 sender = empathy_message_get_sender (msg);
806 account = empathy_contact_get_account (sender);
807 service_name = empathy_protocol_name_to_display_name
808 (tp_account_get_protocol (account));
809 if (service_name == NULL)
810 service_name = tp_account_get_protocol (account);
811 timestamp = empathy_message_get_timestamp (msg);
812 body_escaped = theme_adium_parse_body (theme,
813 empathy_message_get_body (msg),
814 empathy_message_get_token (msg));
815 name = empathy_contact_get_logged_alias (sender);
816 contact_id = empathy_contact_get_id (sender);
817 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
819 /* If this is a /me probably */
823 if (priv->data->version >= 4 || !priv->data->custom_template) {
824 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
825 "<span class='actionMessageBody'>%s</span>",
828 str = g_strdup_printf ("*%s*", body_escaped);
830 g_free (body_escaped);
834 /* Get the avatar filename, or a fallback */
835 avatar = empathy_contact_get_avatar (sender);
837 avatar_filename = avatar->filename;
839 if (!avatar_filename) {
840 if (empathy_contact_is_user (sender)) {
841 avatar_filename = priv->data->default_outgoing_avatar_filename;
843 avatar_filename = priv->data->default_incoming_avatar_filename;
845 if (!avatar_filename) {
846 if (!priv->data->default_avatar_filename) {
847 priv->data->default_avatar_filename =
848 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
849 GTK_ICON_SIZE_DIALOG);
851 avatar_filename = priv->data->default_avatar_filename;
855 /* We want to join this message with the last one if
856 * - senders are the same contact,
857 * - last message was recieved recently,
858 * - last message and this message both are/aren't backlog, and
859 * - DisableCombineConsecutive is not set in theme's settings */
860 is_backlog = empathy_message_is_backlog (msg);
861 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
862 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
863 (is_backlog == priv->last_is_backlog) &&
864 !tp_asv_get_boolean (priv->data->info,
865 "DisableCombineConsecutive", NULL);
867 /* Define message classes */
868 message_classes = g_string_new ("message");
869 if (!priv->has_focus && !is_backlog) {
870 if (!priv->has_unread_message) {
871 g_string_append (message_classes, " firstFocus");
872 priv->has_unread_message = TRUE;
874 g_string_append (message_classes, " focus");
877 g_string_append (message_classes, " history");
880 g_string_append (message_classes, " consecutive");
882 if (empathy_contact_is_user (sender)) {
883 g_string_append (message_classes, " outgoing");
885 g_string_append (message_classes, " incoming");
887 if (empathy_message_should_highlight (msg)) {
888 g_string_append (message_classes, " mention");
890 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
891 g_string_append (message_classes, " autoreply");
894 g_string_append (message_classes, " action");
896 /* FIXME: other classes:
897 * status - the message is a status change
898 * event - the message is a notification of something happening
899 * (for example, encryption being turned on)
900 * %status% - See %status% in theme_adium_append_html ()
903 /* This is slightly a hack, but it's the only way to add
904 * arbitrary data to messages in the HTML. We add another
905 * class called "x-empathy-message-id-*" to the message. This
906 * way, we can remove the unread marker for this specific
908 tp_msg = empathy_message_get_tp_message (msg);
909 if (tp_msg != NULL) {
913 id = tp_message_get_pending_message_id (tp_msg, &valid);
915 g_string_append_printf (message_classes,
916 " x-empathy-message-id-%u", id);
920 /* Define javascript function to use */
922 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
924 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
927 if (empathy_contact_is_user (sender)) {
931 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
934 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
937 /* remove all the unread marks when we are sending a message */
938 theme_adium_remove_all_focus_marks (theme);
943 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
946 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
950 theme_adium_append_html (theme, func, html, body_escaped,
951 avatar_filename, name, contact_id,
952 service_name, message_classes->str,
953 timestamp, is_backlog, empathy_contact_is_user (sender));
955 /* Keep the sender of the last displayed message */
956 if (priv->last_contact) {
957 g_object_unref (priv->last_contact);
959 priv->last_contact = g_object_ref (sender);
960 priv->last_timestamp = timestamp;
961 priv->last_is_backlog = is_backlog;
963 g_free (body_escaped);
964 g_string_free (message_classes, TRUE);
968 theme_adium_append_event (EmpathyChatView *view,
971 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
974 if (priv->pages_loading != 0) {
975 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str);
979 str_escaped = g_markup_escape_text (str, -1);
980 theme_adium_append_event_escaped (view, str_escaped);
981 g_free (str_escaped);
985 theme_adium_append_event_markup (EmpathyChatView *view,
986 const gchar *markup_text,
987 const gchar *fallback_text)
989 theme_adium_append_event_escaped (view, markup_text);
993 theme_adium_edit_message (EmpathyChatView *view,
994 EmpathyMessage *message)
996 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
997 WebKitDOMDocument *doc;
998 WebKitDOMElement *span;
999 gchar *id, *parsed_body;
1000 gchar *tooltip, *timestamp;
1001 GtkIconInfo *icon_info;
1002 GError *error = NULL;
1004 if (priv->pages_loading != 0) {
1005 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL);
1009 id = g_strdup_printf ("message-token-%s",
1010 empathy_message_get_supersedes (message));
1011 /* we don't pass a token here, because doing so will return another
1012 * <span> element, and we don't want nested <span> elements */
1013 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1014 empathy_message_get_body (message), NULL);
1016 /* find the element */
1017 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1018 span = webkit_dom_document_get_element_by_id (doc, id);
1021 DEBUG ("Failed to find id '%s'", id);
1025 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1026 DEBUG ("Not a HTML element");
1030 /* update the HTML */
1031 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1032 parsed_body, &error);
1034 if (error != NULL) {
1035 DEBUG ("Error setting new inner-HTML: %s", error->message);
1036 g_error_free (error);
1041 timestamp = empathy_time_to_string_local (
1042 empathy_message_get_timestamp (message),
1044 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1046 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1052 /* mark this message as edited */
1053 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1054 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1056 if (icon_info != NULL) {
1057 /* set the icon as a background image using CSS
1058 * FIXME: the icon won't update in response to theme changes */
1059 gchar *style = g_strdup_printf (
1060 "background-image:url('%s');"
1061 "background-repeat:no-repeat;"
1062 "background-position:left center;"
1063 "padding-left:19px;", /* 16px icon + 3px padding */
1064 gtk_icon_info_get_filename (icon_info));
1066 webkit_dom_element_set_attribute (span, "style", style, &error);
1068 if (error != NULL) {
1069 DEBUG ("Error setting element style: %s",
1071 g_clear_error (&error);
1076 gtk_icon_info_free (icon_info);
1082 DEBUG ("Could not find message to edit with: %s",
1083 empathy_message_get_body (message));
1087 g_free (parsed_body);
1091 theme_adium_scroll (EmpathyChatView *view,
1092 gboolean allow_scrolling)
1094 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1096 priv->allow_scrolling = allow_scrolling;
1097 if (allow_scrolling) {
1098 empathy_chat_view_scroll_down (view);
1103 theme_adium_scroll_down (EmpathyChatView *view)
1105 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1109 theme_adium_get_has_selection (EmpathyChatView *view)
1111 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1115 theme_adium_clear (EmpathyChatView *view)
1117 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1119 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1121 /* Clear last contact to avoid trying to add a 'joined'
1122 * message when we don't have an insertion point. */
1123 if (priv->last_contact) {
1124 g_object_unref (priv->last_contact);
1125 priv->last_contact = NULL;
1130 theme_adium_find_previous (EmpathyChatView *view,
1131 const gchar *search_criteria,
1132 gboolean new_search,
1133 gboolean match_case)
1135 /* FIXME: Doesn't respect new_search */
1136 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1137 search_criteria, match_case,
1142 theme_adium_find_next (EmpathyChatView *view,
1143 const gchar *search_criteria,
1144 gboolean new_search,
1145 gboolean match_case)
1147 /* FIXME: Doesn't respect new_search */
1148 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1149 search_criteria, match_case,
1154 theme_adium_find_abilities (EmpathyChatView *view,
1155 const gchar *search_criteria,
1156 gboolean match_case,
1157 gboolean *can_do_previous,
1158 gboolean *can_do_next)
1160 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1161 * find_next and find_previous to work around this problem. */
1162 if (can_do_previous)
1163 *can_do_previous = TRUE;
1165 *can_do_next = TRUE;
1169 theme_adium_highlight (EmpathyChatView *view,
1171 gboolean match_case)
1173 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1174 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1175 text, match_case, 0);
1176 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1181 theme_adium_copy_clipboard (EmpathyChatView *view)
1183 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1187 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1190 WebKitDOMDocument *dom;
1191 WebKitDOMNodeList *nodes;
1193 GError *error = NULL;
1195 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1200 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1202 /* Get all nodes with focus class */
1203 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1206 if (nodes == NULL) {
1207 DEBUG ("Error getting focus nodes: %s",
1208 error ? error->message : "No error");
1209 g_clear_error (&error);
1213 theme_adium_remove_focus_marks (self, nodes);
1217 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1220 EmpathyThemeAdium *self = user_data;
1221 guint32 id = GPOINTER_TO_UINT (data);
1223 theme_adium_remove_mark_from_message (self, id);
1227 theme_adium_focus_toggled (EmpathyChatView *view,
1230 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1232 priv->has_focus = has_focus;
1233 if (!priv->has_focus) {
1234 /* We've lost focus, so let's make sure all the acked
1235 * messages have lost their unread marker. */
1236 g_queue_foreach (&priv->acked_messages,
1237 theme_adium_remove_acked_message_unread_mark_foreach,
1239 g_queue_clear (&priv->acked_messages);
1241 priv->has_unread_message = FALSE;
1246 theme_adium_message_acknowledged (EmpathyChatView *view,
1247 EmpathyMessage *message)
1249 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1250 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1255 tp_msg = empathy_message_get_tp_message (message);
1257 if (tp_msg == NULL) {
1261 id = tp_message_get_pending_message_id (tp_msg, &valid);
1263 g_warning ("Acknoledged message doesn't have a pending ID");
1267 /* We only want to actually remove the unread marker if the
1268 * view doesn't have focus. If we did it all the time we would
1269 * never see the unread markers, ever! So, we'll queue these
1270 * up, and when we lose focus, we'll remove the markers. */
1271 if (priv->has_focus) {
1272 g_queue_push_tail (&priv->acked_messages,
1273 GUINT_TO_POINTER (id));
1277 theme_adium_remove_mark_from_message (self, id);
1281 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1283 if (event->button == 3) {
1284 gboolean developer_tools_enabled;
1286 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1287 "enable-developer-extras", &developer_tools_enabled, NULL);
1289 /* We currently have no way to add an inspector menu
1290 * item ourselves, so we disable our customized menu
1291 * if the developer extras are enabled. */
1292 if (!developer_tools_enabled) {
1293 empathy_webkit_context_menu_for_event (
1294 WEBKIT_WEB_VIEW (widget), event,
1295 EMPATHY_WEBKIT_MENU_CLEAR);
1300 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1304 theme_adium_iface_init (EmpathyChatViewIface *iface)
1306 iface->append_message = theme_adium_append_message;
1307 iface->append_event = theme_adium_append_event;
1308 iface->append_event_markup = theme_adium_append_event_markup;
1309 iface->edit_message = theme_adium_edit_message;
1310 iface->scroll = theme_adium_scroll;
1311 iface->scroll_down = theme_adium_scroll_down;
1312 iface->get_has_selection = theme_adium_get_has_selection;
1313 iface->clear = theme_adium_clear;
1314 iface->find_previous = theme_adium_find_previous;
1315 iface->find_next = theme_adium_find_next;
1316 iface->find_abilities = theme_adium_find_abilities;
1317 iface->highlight = theme_adium_highlight;
1318 iface->copy_clipboard = theme_adium_copy_clipboard;
1319 iface->focus_toggled = theme_adium_focus_toggled;
1320 iface->message_acknowledged = theme_adium_message_acknowledged;
1324 theme_adium_load_finished_cb (WebKitWebView *view,
1325 WebKitWebFrame *frame,
1328 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1329 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1332 DEBUG ("Page loaded");
1333 priv->pages_loading--;
1335 if (priv->pages_loading != 0)
1338 /* Display queued messages */
1339 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1340 QueuedItem *item = l->data;
1344 case QUEUED_MESSAGE:
1345 theme_adium_append_message (chat_view, item->msg);
1349 theme_adium_edit_message (chat_view, item->msg);
1353 theme_adium_append_event (chat_view, item->str);
1357 free_queued_item (item);
1360 g_queue_clear (&priv->message_queue);
1364 theme_adium_finalize (GObject *object)
1366 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1368 empathy_adium_data_unref (priv->data);
1370 g_object_unref (priv->gsettings_chat);
1371 g_object_unref (priv->gsettings_desktop);
1373 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1377 theme_adium_dispose (GObject *object)
1379 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1381 if (priv->smiley_manager) {
1382 g_object_unref (priv->smiley_manager);
1383 priv->smiley_manager = NULL;
1386 if (priv->last_contact) {
1387 g_object_unref (priv->last_contact);
1388 priv->last_contact = NULL;
1391 if (priv->inspector_window) {
1392 gtk_widget_destroy (priv->inspector_window);
1393 priv->inspector_window = NULL;
1396 if (priv->acked_messages.length > 0) {
1397 g_queue_clear (&priv->acked_messages);
1400 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1404 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1405 EmpathyThemeAdium *theme)
1407 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1409 if (priv->inspector_window) {
1410 gtk_widget_show_all (priv->inspector_window);
1417 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1418 EmpathyThemeAdium *theme)
1420 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1422 if (priv->inspector_window) {
1423 gtk_widget_hide (priv->inspector_window);
1429 static WebKitWebView *
1430 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1431 WebKitWebView *web_view,
1432 EmpathyThemeAdium *theme)
1434 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1435 GtkWidget *scrolled_window;
1436 GtkWidget *inspector_web_view;
1438 if (!priv->inspector_window) {
1439 /* Create main window */
1440 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1441 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1443 g_signal_connect (priv->inspector_window, "delete-event",
1444 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1446 /* Pack a scrolled window */
1447 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1448 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1449 GTK_POLICY_AUTOMATIC,
1450 GTK_POLICY_AUTOMATIC);
1451 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1453 gtk_widget_show (scrolled_window);
1455 /* Pack a webview in the scrolled window. That webview will be
1456 * used to render the inspector tool. */
1457 inspector_web_view = webkit_web_view_new ();
1458 gtk_container_add (GTK_CONTAINER (scrolled_window),
1459 inspector_web_view);
1460 gtk_widget_show (scrolled_window);
1462 return WEBKIT_WEB_VIEW (inspector_web_view);
1469 theme_adium_constructed (GObject *object)
1471 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1472 const gchar *font_family = NULL;
1474 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1475 WebKitWebInspector *webkit_inspector;
1477 /* Set default settings */
1478 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1479 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1481 if (font_family && font_size) {
1482 g_object_set (webkit_web_view_get_settings (webkit_view),
1483 "default-font-family", font_family,
1484 "default-font-size", font_size,
1487 empathy_webkit_bind_font_setting (webkit_view,
1488 priv->gsettings_desktop,
1489 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1492 /* Setup webkit inspector */
1493 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1494 g_signal_connect (webkit_inspector, "inspect-web-view",
1495 G_CALLBACK (theme_adium_inspect_web_view_cb),
1497 g_signal_connect (webkit_inspector, "show-window",
1498 G_CALLBACK (theme_adium_inspector_show_window_cb),
1500 g_signal_connect (webkit_inspector, "close-window",
1501 G_CALLBACK (theme_adium_inspector_close_window_cb),
1505 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1507 priv->in_construction = FALSE;
1511 theme_adium_get_property (GObject *object,
1516 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1519 case PROP_ADIUM_DATA:
1520 g_value_set_boxed (value, priv->data);
1523 g_value_set_string (value, priv->variant);
1526 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1532 theme_adium_set_property (GObject *object,
1534 const GValue *value,
1537 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1538 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1541 case PROP_ADIUM_DATA:
1542 g_assert (priv->data == NULL);
1543 priv->data = g_value_dup_boxed (value);
1546 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1549 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1555 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1557 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1558 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1560 object_class->finalize = theme_adium_finalize;
1561 object_class->dispose = theme_adium_dispose;
1562 object_class->constructed = theme_adium_constructed;
1563 object_class->get_property = theme_adium_get_property;
1564 object_class->set_property = theme_adium_set_property;
1566 widget_class->button_press_event = theme_adium_button_press_event;
1568 g_object_class_install_property (object_class,
1570 g_param_spec_boxed ("adium-data",
1572 "Data for the adium theme",
1573 EMPATHY_TYPE_ADIUM_DATA,
1574 G_PARAM_CONSTRUCT_ONLY |
1576 G_PARAM_STATIC_STRINGS));
1577 g_object_class_install_property (object_class,
1579 g_param_spec_string ("variant",
1580 "The theme variant",
1581 "Variant name for the theme",
1585 G_PARAM_STATIC_STRINGS));
1587 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1591 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1593 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1594 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1598 priv->in_construction = TRUE;
1599 g_queue_init (&priv->message_queue);
1600 priv->allow_scrolling = TRUE;
1601 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1603 g_signal_connect (theme, "load-finished",
1604 G_CALLBACK (theme_adium_load_finished_cb),
1606 g_signal_connect (theme, "navigation-policy-decision-requested",
1607 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1610 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1611 priv->gsettings_desktop = g_settings_new (
1612 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1614 g_signal_connect (priv->gsettings_chat,
1615 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1616 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1619 theme_adium_update_enable_webkit_developer_tools (theme);
1623 empathy_theme_adium_new (EmpathyAdiumData *data,
1624 const gchar *variant)
1626 g_return_val_if_fail (data != NULL, NULL);
1628 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1635 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1636 const gchar *variant)
1638 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1639 gchar *variant_path;
1642 if (!tp_strdiff (priv->variant, variant)) {
1646 g_free (priv->variant);
1647 priv->variant = g_strdup (variant);
1649 if (priv->in_construction) {
1653 DEBUG ("Update view with variant: '%s'", variant);
1654 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1656 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1658 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1660 g_free (variant_path);
1663 g_object_notify (G_OBJECT (theme), "variant");
1667 empathy_theme_adium_show_inspector (EmpathyThemeAdium *theme)
1669 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
1670 WebKitWebInspector *inspector;
1672 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1673 "enable-developer-extras", TRUE,
1676 inspector = webkit_web_view_get_inspector (web_view);
1677 webkit_web_inspector_show (inspector);
1681 empathy_adium_path_is_valid (const gchar *path)
1686 /* The theme is not valid if there is no Info.plist */
1687 file = g_build_filename (path, "Contents", "Info.plist",
1689 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1695 /* We ship a default Template.html as fallback if there is any problem
1696 * with the one inside the theme. The only other required file is
1697 * Content.html OR Incoming/Content.html*/
1698 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1700 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1704 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1705 "Content.html", NULL);
1706 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1714 empathy_adium_info_new (const gchar *path)
1718 GHashTable *info = NULL;
1720 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1722 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1723 value = empathy_plist_parse_from_file (file);
1729 info = g_value_dup_boxed (value);
1730 tp_g_value_slice_free (value);
1732 /* Insert the theme's path into the hash table,
1733 * keys have to be dupped */
1734 tp_asv_set_string (info, g_strdup ("path"), path);
1740 adium_info_get_version (GHashTable *info)
1742 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1745 static const gchar *
1746 adium_info_get_no_variant_name (GHashTable *info)
1748 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1749 return name ? name : _("Normal");
1753 adium_info_dup_path_for_variant (GHashTable *info,
1754 const gchar *variant)
1756 guint version = adium_info_get_version (info);
1757 const gchar *no_variant = adium_info_get_no_variant_name (info);
1758 GPtrArray *variants;
1761 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1762 return g_strdup ("main.css");
1765 variants = empathy_adium_info_get_available_variants (info);
1766 if (variants->len == 0)
1767 return g_strdup ("main.css");
1769 /* Verify the variant exists, fallback to the first one */
1770 for (i = 0; i < variants->len; i++) {
1771 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1775 if (i == variants->len) {
1776 DEBUG ("Variant %s does not exist", variant);
1777 variant = g_ptr_array_index (variants, 0);
1780 return g_strdup_printf ("Variants/%s.css", variant);
1785 empathy_adium_info_get_default_variant (GHashTable *info)
1787 if (adium_info_get_version (info) <= 2) {
1788 return adium_info_get_no_variant_name (info);
1791 return tp_asv_get_string (info, "DefaultVariant");
1795 empathy_adium_info_get_available_variants (GHashTable *info)
1797 GPtrArray *variants;
1802 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1803 if (variants != NULL) {
1807 variants = g_ptr_array_new_with_free_func (g_free);
1808 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1809 G_TYPE_PTR_ARRAY, variants);
1811 path = tp_asv_get_string (info, "path");
1812 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1813 dir = g_dir_open (dirpath, 0, NULL);
1817 for (name = g_dir_read_name (dir);
1819 name = g_dir_read_name (dir)) {
1820 gchar *display_name;
1822 if (!g_str_has_suffix (name, ".css")) {
1826 display_name = g_strdup (name);
1827 strstr (display_name, ".css")[0] = '\0';
1828 g_ptr_array_add (variants, display_name);
1834 if (adium_info_get_version (info) <= 2) {
1835 g_ptr_array_add (variants,
1836 g_strdup (adium_info_get_no_variant_name (info)));
1843 empathy_adium_data_get_type (void)
1845 static GType type_id = 0;
1849 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1850 (GBoxedCopyFunc) empathy_adium_data_ref,
1851 (GBoxedFreeFunc) empathy_adium_data_unref);
1858 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1860 EmpathyAdiumData *data;
1861 gchar *template_html = NULL;
1862 gchar *footer_html = NULL;
1865 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1867 data = g_slice_new0 (EmpathyAdiumData);
1868 data->ref_count = 1;
1869 data->path = g_strdup (path);
1870 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1871 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1872 data->info = g_hash_table_ref (info);
1873 data->version = adium_info_get_version (info);
1874 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1875 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1876 g_str_equal, g_free, g_free);
1878 DEBUG ("Loading theme at %s", path);
1880 #define LOAD(path, var) \
1881 tmp = g_build_filename (data->basedir, path, NULL); \
1882 g_file_get_contents (tmp, &var, NULL, NULL); \
1885 #define LOAD_CONST(path, var) \
1888 LOAD (path, content); \
1889 if (content != NULL) { \
1890 g_ptr_array_add (data->strings_to_free, content); \
1895 /* Load html files */
1896 LOAD_CONST ("Content.html", data->content_html);
1897 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1898 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1899 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1900 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1901 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1902 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1903 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1904 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1905 LOAD_CONST ("Status.html", data->status_html);
1906 LOAD ("Template.html", template_html);
1907 LOAD ("Footer.html", footer_html);
1912 /* HTML fallbacks: If we have at least content OR in_content, then
1913 * everything else gets a fallback */
1915 #define FALLBACK(html, fallback) \
1916 if (html == NULL) { \
1920 /* in_nextcontent -> in_content -> content */
1921 FALLBACK (data->in_content_html, data->content_html);
1922 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1924 /* context -> content */
1925 FALLBACK (data->in_context_html, data->in_content_html);
1926 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1927 FALLBACK (data->out_context_html, data->out_content_html);
1928 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1931 FALLBACK (data->out_content_html, data->in_content_html);
1932 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1933 FALLBACK (data->out_context_html, data->in_context_html);
1934 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1936 /* status -> in_content */
1937 FALLBACK (data->status_html, data->in_content_html);
1941 /* template -> empathy's template */
1942 data->custom_template = (template_html != NULL);
1943 if (template_html == NULL) {
1944 tmp = empathy_file_lookup ("Template.html", "data");
1945 g_file_get_contents (tmp, &template_html, NULL, NULL);
1949 /* Default avatar */
1950 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1951 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1952 data->default_incoming_avatar_filename = tmp;
1956 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1957 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1958 data->default_outgoing_avatar_filename = tmp;
1963 /* Old custom templates had only 4 parameters.
1964 * New templates have 5 parameters */
1965 if (data->version <= 2 && data->custom_template) {
1966 tmp = string_with_format (template_html,
1968 "%@", /* Leave variant unset */
1969 "", /* The header */
1970 footer_html ? footer_html : "",
1973 tmp = string_with_format (template_html,
1975 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1976 "%@", /* Leave variant unset */
1977 "", /* The header */
1978 footer_html ? footer_html : "",
1981 g_ptr_array_add (data->strings_to_free, tmp);
1982 data->template_html = tmp;
1984 g_free (template_html);
1985 g_free (footer_html);
1991 empathy_adium_data_new (const gchar *path)
1993 EmpathyAdiumData *data;
1996 info = empathy_adium_info_new (path);
1997 data = empathy_adium_data_new_with_info (path, info);
1998 g_hash_table_unref (info);
2004 empathy_adium_data_ref (EmpathyAdiumData *data)
2006 g_return_val_if_fail (data != NULL, NULL);
2008 g_atomic_int_inc (&data->ref_count);
2014 empathy_adium_data_unref (EmpathyAdiumData *data)
2016 g_return_if_fail (data != NULL);
2018 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2019 g_free (data->path);
2020 g_free (data->basedir);
2021 g_free (data->default_avatar_filename);
2022 g_free (data->default_incoming_avatar_filename);
2023 g_free (data->default_outgoing_avatar_filename);
2024 g_hash_table_unref (data->info);
2025 g_ptr_array_unref (data->strings_to_free);
2026 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2028 g_slice_free (EmpathyAdiumData, data);
2033 empathy_adium_data_get_info (EmpathyAdiumData *data)
2035 g_return_val_if_fail (data != NULL, NULL);
2041 empathy_adium_data_get_path (EmpathyAdiumData *data)
2043 g_return_val_if_fail (data != NULL, NULL);