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 gboolean show_avatars;
78 } EmpathyThemeAdiumPriv;
80 struct _EmpathyAdiumData {
84 gchar *default_avatar_filename;
85 gchar *default_incoming_avatar_filename;
86 gchar *default_outgoing_avatar_filename;
89 gboolean custom_template;
90 /* gchar* -> gchar* both owned */
91 GHashTable *date_format_cache;
94 const gchar *template_html;
95 const gchar *content_html;
96 const gchar *in_content_html;
97 const gchar *in_context_html;
98 const gchar *in_nextcontent_html;
99 const gchar *in_nextcontext_html;
100 const gchar *out_content_html;
101 const gchar *out_context_html;
102 const gchar *out_nextcontent_html;
103 const gchar *out_nextcontext_html;
104 const gchar *status_html;
106 /* Above html strings are pointers to strings stored in this array.
107 * We do this because of fallbacks, some htmls could be pointing the
109 GPtrArray *strings_to_free;
112 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
113 static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
121 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
122 WEBKIT_TYPE_WEB_VIEW,
123 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
124 theme_adium_iface_init));
136 gboolean should_highlight;
140 queue_item (GQueue *queue,
144 gboolean should_highlight)
146 QueuedItem *item = g_slice_new0 (QueuedItem);
150 item->msg = g_object_ref (msg);
151 item->str = g_strdup (str);
152 item->should_highlight = should_highlight;
154 g_queue_push_tail (queue, item);
160 free_queued_item (QueuedItem *item)
162 tp_clear_object (&item->msg);
165 g_slice_free (QueuedItem, item);
169 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
171 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
172 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
173 gboolean enable_webkit_developer_tools;
175 enable_webkit_developer_tools = g_settings_get_boolean (
176 priv->gsettings_chat,
177 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
179 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
180 "enable-developer-extras",
181 enable_webkit_developer_tools,
186 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
190 EmpathyThemeAdium *theme = user_data;
192 theme_adium_update_enable_webkit_developer_tools (theme);
196 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
197 WebKitWebFrame *web_frame,
198 WebKitNetworkRequest *request,
199 WebKitWebNavigationAction *action,
200 WebKitWebPolicyDecision *decision,
205 /* Only call url_show on clicks */
206 if (webkit_web_navigation_action_get_reason (action) !=
207 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
208 webkit_web_policy_decision_use (decision);
212 uri = webkit_network_request_get_uri (request);
213 empathy_url_show (GTK_WIDGET (view), uri);
215 webkit_web_policy_decision_ignore (decision);
219 /* Replace each %@ in format with string passed in args */
221 string_with_format (const gchar *format,
222 const gchar *first_string,
229 va_start (args, first_string);
230 result = g_string_sized_new (strlen (format));
231 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
234 next = strstr (format, "%@");
239 g_string_append_len (result, format, next - format);
240 g_string_append (result, str);
243 g_string_append (result, format);
246 return g_string_free (result, FALSE);
250 theme_adium_load_template (EmpathyThemeAdium *theme)
252 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
257 priv->pages_loading++;
258 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
259 variant_path = adium_info_dup_path_for_variant (priv->data->info,
261 template = string_with_format (priv->data->template_html,
263 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
264 template, basedir_uri);
265 g_free (basedir_uri);
266 g_free (variant_path);
271 theme_adium_parse_body (EmpathyThemeAdium *self,
275 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
276 EmpathyStringParser *parsers;
279 /* Check if we have to parse smileys */
280 parsers = empathy_webkit_get_string_parser (
281 g_settings_get_boolean (priv->gsettings_chat,
282 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
284 /* Parse text and construct string with links and smileys replaced
285 * by html tags. Also escape text to make sure html code is
286 * displayed verbatim. */
287 string = g_string_sized_new (strlen (text));
289 /* wrap this in HTML that allows us to find the message for later
291 if (!tp_str_empty (token))
292 g_string_append_printf (string,
293 "<span id=\"message-token-%s\">",
296 empathy_string_parser_substr (text, -1, parsers, string);
298 if (!tp_str_empty (token))
299 g_string_append (string, "</span>");
301 /* Wrap body in order to make tabs and multiple spaces displayed
302 * properly. See bug #625745. */
303 g_string_prepend (string, "<div style=\"display: inline; "
304 "white-space: pre-wrap\"'>");
305 g_string_append (string, "</div>");
307 return g_string_free (string, FALSE);
311 escape_and_append_len (GString *string, const gchar *str, gint len)
313 while (str != NULL && *str != '\0' && len != 0) {
317 g_string_append (string, "\\\\");
321 g_string_append (string, "\\\"");
324 /* Remove end of lines */
327 g_string_append_c (string, *str);
335 /* If *str starts with match, returns TRUE and move pointer to the end */
337 theme_adium_match (const gchar **str,
342 len = strlen (match);
343 if (strncmp (*str, match, len) == 0) {
351 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
353 theme_adium_match_with_format (const gchar **str,
357 const gchar *cur = *str;
360 if (!theme_adium_match (&cur, match)) {
365 end = strstr (cur, "}%");
370 *format = g_strndup (cur , end - cur);
375 /* List of colors used by %senderColor%. Copied from
376 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
378 static gchar *colors[] = {
379 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
380 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
381 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
382 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
383 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
384 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
385 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
386 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
387 "lightblue", "lightcoral",
388 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
389 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
390 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
391 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
392 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
393 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
394 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
395 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
396 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
397 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
402 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
404 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
405 * to strftime supported by g_date_time_format.
406 * FIXME: table is incomplete, doc of g_date_time_format has a table of
408 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
409 * in 2.29.x we have to explictely request padding with %0x */
410 static const gchar *convert_table[] = {
412 "A", NULL, // 0~86399999 (Millisecond of Day)
414 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
415 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
416 "cc", "%u", // 1~7 (Day of Week)
417 "c", "%u", // 1~7 (Day of Week)
419 "dd", "%d", // 1~31 (0 padded Day of Month)
420 "d", "%d", // 1~31 (0 padded Day of Month)
421 "D", "%j", // 1~366 (0 padded Day of Year)
423 "e", "%u", // 1~7 (0 padded Day of Week)
424 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
425 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
426 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
427 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
429 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
431 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
432 "GGGG", NULL, // Before Christ/Anno Domini
433 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
434 "GG", NULL, // BC/AD (Era Designator Abbreviated)
435 "G", NULL, // BC/AD (Era Designator Abbreviated)
437 "h", "%I", // 1~12 (0 padded Hour (12hr))
438 "H", "%H", // 0~23 (0 padded Hour (24hr))
440 "k", NULL, // 1~24 (0 padded Hour (24hr)
441 "K", NULL, // 0~11 (0 padded Hour (12hr))
443 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
444 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
445 "LL", "%m", // 1~12 (0 padded Month)
446 "L", "%m", // 1~12 (0 padded Month)
448 "m", "%M", // 0~59 (0 padded Minute)
449 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
450 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
451 "MM", "%m", // 1~12 (0 padded Month)
452 "M", "%m", // 1~12 (0 padded Month)
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)
458 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
459 "QQQ", NULL, // Q1/Q2/Q3/Q4
460 "QQ", NULL, // 1~4 (0 padded Quarter)
461 "Q", NULL, // 1~4 (0 padded Quarter)
463 "s", "%S", // 0~59 (0 padded Second)
464 "S", NULL, // (rounded Sub-Second)
466 "u", "%Y", // (0 padded Year)
468 "vvvv", "%Z", // (General GMT Timezone Name)
469 "vvv", "%Z", // (General GMT Timezone Abbreviation)
470 "vv", "%Z", // (General GMT Timezone Abbreviation)
471 "v", "%Z", // (General GMT Timezone Abbreviation)
473 "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)
474 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
476 "yyyy", "%Y", // (Full Year)
477 "yyy", "%y", // (2 Digits Year)
478 "yy", "%y", // (2 Digits Year)
479 "y", "%Y", // (Full Year)
480 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
481 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
482 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
483 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
485 "zzzz", NULL, // (Specific GMT Timezone Name)
486 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
487 "zz", NULL, // (Specific GMT Timezone Abbreviation)
488 "z", NULL, // (Specific GMT Timezone Abbreviation)
489 "Z", "%z", // +0000 (RFC 822 Timezone)
495 if (nsdate == NULL) {
499 str = g_hash_table_lookup (data->date_format_cache, nsdate);
504 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
505 * by corresponding strftime tag. */
506 string = g_string_sized_new (strlen (nsdate));
507 for (i = 0; nsdate[i] != '\0'; i++) {
508 gboolean found = FALSE;
510 /* even indexes are NSDateFormatter tag, odd indexes are the
511 * corresponding strftime tag */
512 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
513 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
519 /* If we don't have a replacement, just ignore that tag */
520 if (convert_table[j + 1] != NULL) {
521 g_string_append (string, convert_table[j + 1]);
523 i += strlen (convert_table[j]) - 1;
525 g_string_append_c (string, nsdate[i]);
529 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
531 /* The cache takes ownership of string->str */
532 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
533 return g_string_free (string, FALSE);
538 theme_adium_append_html (EmpathyThemeAdium *theme,
541 const gchar *message,
542 const gchar *avatar_filename,
544 const gchar *contact_id,
545 const gchar *service_name,
546 const gchar *message_classes,
551 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
553 const gchar *cur = NULL;
556 /* Make some search-and-replace in the html code */
557 string = g_string_sized_new (strlen (html) + strlen (message));
558 g_string_append_printf (string, "%s(\"", func);
559 for (cur = html; *cur != '\0'; cur++) {
560 const gchar *replace = NULL;
561 gchar *dup_replace = NULL;
562 gchar *format = NULL;
564 /* Those are all well known keywords that needs replacement in
565 * html files. Please keep them in the same order than the adium
566 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
567 if (theme_adium_match (&cur, "%userIconPath%")) {
568 replace = avatar_filename;
569 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
570 replace = contact_id;
571 } else if (theme_adium_match (&cur, "%sender%")) {
573 } else if (theme_adium_match (&cur, "%senderColor%")) {
574 /* A color derived from the user's name.
575 * FIXME: If a colon separated list of HTML colors is at
576 * Incoming/SenderColors.txt it will be used instead of
577 * the default colors.
580 /* Ensure we always use the same color when sending messages
584 } else if (contact_id != NULL) {
585 guint hash = g_str_hash (contact_id);
586 replace = colors[hash % G_N_ELEMENTS (colors)];
588 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
589 /* FIXME: The path to the status icon of the sender
590 * (available, away, etc...)
592 } else if (theme_adium_match (&cur, "%messageDirection%")) {
593 /* FIXME: The text direction of the message
594 * (either rtl or ltr)
596 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
597 /* FIXME: The serverside (remotely set) name of the
598 * sender, such as an MSN display name.
600 * We don't have access to that yet so we use
601 * local alias instead.
604 } else if (theme_adium_match (&cur, "%senderPrefix%")) {
605 /* FIXME: If we supported IRC user mode flags, this
606 * would be replaced with @ if the user is an op, + if
607 * the user has voice, etc. as per
608 * http://hg.adium.im/adium/rev/b586b027de42. But we
609 * don't, so for now we just strip it. */
610 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
611 /* FIXME: This keyword is used to represent the
612 * highlight background color. "X" is the opacity of the
613 * background, ranges from 0 to 1 and can be any decimal
616 } else if (theme_adium_match (&cur, "%message%")) {
618 } else if (theme_adium_match (&cur, "%time%") ||
619 theme_adium_match_with_format (&cur, "%time{", &format)) {
620 const gchar *strftime_format;
622 strftime_format = nsdate_to_strftime (priv->data, format);
624 dup_replace = empathy_time_to_string_local (timestamp,
625 strftime_format ? strftime_format :
626 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
628 dup_replace = empathy_time_to_string_local (timestamp,
629 strftime_format ? strftime_format :
630 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
632 replace = dup_replace;
633 } else if (theme_adium_match (&cur, "%shortTime%")) {
634 dup_replace = empathy_time_to_string_local (timestamp,
635 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
636 replace = dup_replace;
637 } else if (theme_adium_match (&cur, "%service%")) {
638 replace = service_name;
639 } else if (theme_adium_match (&cur, "%variant%")) {
640 /* FIXME: The name of the active message style variant,
641 * with all spaces replaced with an underscore.
642 * A variant named "Alternating Messages - Blue Red"
643 * will become "Alternating_Messages_-_Blue_Red".
645 } else if (theme_adium_match (&cur, "%userIcons%")) {
646 replace = priv->show_avatars ? "showIcons" : "hideIcons";
647 } else if (theme_adium_match (&cur, "%messageClasses%")) {
648 replace = message_classes;
649 } else if (theme_adium_match (&cur, "%status%")) {
650 /* FIXME: A description of the status event. This is
651 * neither in the user's local language nor expected to
652 * be displayed; it may be useful to use a different div
653 * class to present different types of status messages.
654 * The following is a list of some of the more important
655 * status messages; your message style should be able to
656 * handle being shown a status message not in this list,
657 * as even at present the list is incomplete and is
658 * certain to become out of date in the future:
667 * contact_joined (group chats)
671 * encryption (all OTR messages use this status)
672 * purple (all IRC topic and join/part messages use this status)
673 * fileTransferStarted
674 * fileTransferCompleted
677 escape_and_append_len (string, cur, 1);
681 /* Here we have a replacement to make */
682 escape_and_append_len (string, replace, -1);
684 g_free (dup_replace);
687 g_string_append (string, "\")");
689 script = g_string_free (string, FALSE);
690 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
695 theme_adium_append_event_escaped (EmpathyChatView *view,
696 const gchar *escaped)
698 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
699 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
701 theme_adium_append_html (theme, "appendMessage",
702 priv->data->status_html, escaped, NULL, NULL, NULL,
704 empathy_time_get_current (), FALSE, FALSE);
706 /* There is no last contact */
707 if (priv->last_contact) {
708 g_object_unref (priv->last_contact);
709 priv->last_contact = NULL;
714 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
715 WebKitDOMNodeList *nodes)
719 /* Remove focus and firstFocus class */
720 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
721 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
722 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
724 gchar **classes, **iter;
725 GString *new_class_name;
726 gboolean first = TRUE;
728 if (element == NULL) {
732 class_name = webkit_dom_html_element_get_class_name (element);
733 classes = g_strsplit (class_name, " ", -1);
734 new_class_name = g_string_sized_new (strlen (class_name));
735 for (iter = classes; *iter != NULL; iter++) {
736 if (tp_strdiff (*iter, "focus") &&
737 tp_strdiff (*iter, "firstFocus")) {
739 g_string_append_c (new_class_name, ' ');
741 g_string_append (new_class_name, *iter);
746 webkit_dom_html_element_set_class_name (element, new_class_name->str);
749 g_strfreev (classes);
750 g_string_free (new_class_name, TRUE);
755 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
757 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
758 WebKitDOMDocument *dom;
759 WebKitDOMNodeList *nodes;
760 GError *error = NULL;
762 if (!priv->has_unread_message)
765 priv->has_unread_message = FALSE;
767 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
772 /* Get all nodes with focus class */
773 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
775 DEBUG ("Error getting focus nodes: %s",
776 error ? error->message : "No error");
777 g_clear_error (&error);
781 theme_adium_remove_focus_marks (theme, nodes);
785 theme_adium_append_message (EmpathyChatView *view,
787 gboolean should_highlight)
789 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
790 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
791 EmpathyContact *sender;
794 gchar *body_escaped, *name_escaped;
796 const gchar *contact_id;
797 EmpathyAvatar *avatar;
798 const gchar *avatar_filename = NULL;
800 const gchar *html = NULL;
802 const gchar *service_name;
803 GString *message_classes = NULL;
805 gboolean consecutive;
808 if (priv->pages_loading != 0) {
809 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL, should_highlight);
813 /* Get information */
814 sender = empathy_message_get_sender (msg);
815 account = empathy_contact_get_account (sender);
816 service_name = empathy_protocol_name_to_display_name
817 (tp_account_get_protocol (account));
818 if (service_name == NULL)
819 service_name = tp_account_get_protocol (account);
820 timestamp = empathy_message_get_timestamp (msg);
821 body_escaped = theme_adium_parse_body (theme,
822 empathy_message_get_body (msg),
823 empathy_message_get_token (msg));
824 name = empathy_contact_get_logged_alias (sender);
825 contact_id = empathy_contact_get_id (sender);
826 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
828 name_escaped = g_markup_escape_text (name, -1);
830 /* If this is a /me probably */
834 if (priv->data->version >= 4 || !priv->data->custom_template) {
835 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
836 "<span class='actionMessageBody'>%s</span>",
837 name_escaped, body_escaped);
839 str = g_strdup_printf ("*%s*", body_escaped);
841 g_free (body_escaped);
845 /* Get the avatar filename, or a fallback */
846 avatar = empathy_contact_get_avatar (sender);
848 avatar_filename = avatar->filename;
850 if (!avatar_filename) {
851 if (empathy_contact_is_user (sender)) {
852 avatar_filename = priv->data->default_outgoing_avatar_filename;
854 avatar_filename = priv->data->default_incoming_avatar_filename;
856 if (!avatar_filename) {
857 if (!priv->data->default_avatar_filename) {
858 priv->data->default_avatar_filename =
859 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
860 GTK_ICON_SIZE_DIALOG);
862 avatar_filename = priv->data->default_avatar_filename;
866 /* We want to join this message with the last one if
867 * - senders are the same contact,
868 * - last message was recieved recently,
869 * - last message and this message both are/aren't backlog, and
870 * - DisableCombineConsecutive is not set in theme's settings */
871 is_backlog = empathy_message_is_backlog (msg);
872 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
873 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
874 (is_backlog == priv->last_is_backlog) &&
875 !tp_asv_get_boolean (priv->data->info,
876 "DisableCombineConsecutive", NULL);
878 /* Define message classes */
879 message_classes = g_string_new ("message");
880 if (!priv->has_focus && !is_backlog) {
881 if (!priv->has_unread_message) {
882 g_string_append (message_classes, " firstFocus");
883 priv->has_unread_message = TRUE;
885 g_string_append (message_classes, " focus");
888 g_string_append (message_classes, " history");
891 g_string_append (message_classes, " consecutive");
893 if (empathy_contact_is_user (sender)) {
894 g_string_append (message_classes, " outgoing");
896 g_string_append (message_classes, " incoming");
898 if (should_highlight) {
899 g_string_append (message_classes, " mention");
901 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
902 g_string_append (message_classes, " autoreply");
905 g_string_append (message_classes, " action");
907 /* FIXME: other classes:
908 * status - the message is a status change
909 * event - the message is a notification of something happening
910 * (for example, encryption being turned on)
911 * %status% - See %status% in theme_adium_append_html ()
914 /* This is slightly a hack, but it's the only way to add
915 * arbitrary data to messages in the HTML. We add another
916 * class called "x-empathy-message-id-*" to the message. This
917 * way, we can remove the unread marker for this specific
919 tp_msg = empathy_message_get_tp_message (msg);
920 if (tp_msg != NULL) {
924 id = tp_message_get_pending_message_id (tp_msg, &valid);
926 g_string_append_printf (message_classes,
927 " x-empathy-message-id-%u", id);
931 /* Define javascript function to use */
933 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
935 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
938 if (empathy_contact_is_user (sender)) {
942 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
945 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
948 /* remove all the unread marks when we are sending a message */
949 theme_adium_remove_all_focus_marks (theme);
954 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
957 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
961 theme_adium_append_html (theme, func, html, body_escaped,
962 avatar_filename, name_escaped, contact_id,
963 service_name, message_classes->str,
964 timestamp, is_backlog, empathy_contact_is_user (sender));
966 /* Keep the sender of the last displayed message */
967 if (priv->last_contact) {
968 g_object_unref (priv->last_contact);
970 priv->last_contact = g_object_ref (sender);
971 priv->last_timestamp = timestamp;
972 priv->last_is_backlog = is_backlog;
974 g_free (body_escaped);
975 g_free (name_escaped);
976 g_string_free (message_classes, TRUE);
980 theme_adium_append_event (EmpathyChatView *view,
983 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
986 if (priv->pages_loading != 0) {
987 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
991 str_escaped = g_markup_escape_text (str, -1);
992 theme_adium_append_event_escaped (view, str_escaped);
993 g_free (str_escaped);
997 theme_adium_append_event_markup (EmpathyChatView *view,
998 const gchar *markup_text,
999 const gchar *fallback_text)
1001 theme_adium_append_event_escaped (view, markup_text);
1005 theme_adium_edit_message (EmpathyChatView *view,
1006 EmpathyMessage *message)
1008 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1009 WebKitDOMDocument *doc;
1010 WebKitDOMElement *span;
1011 gchar *id, *parsed_body;
1012 gchar *tooltip, *timestamp;
1013 GtkIconInfo *icon_info;
1014 GError *error = NULL;
1016 if (priv->pages_loading != 0) {
1017 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1021 id = g_strdup_printf ("message-token-%s",
1022 empathy_message_get_supersedes (message));
1023 /* we don't pass a token here, because doing so will return another
1024 * <span> element, and we don't want nested <span> elements */
1025 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1026 empathy_message_get_body (message), NULL);
1028 /* find the element */
1029 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1030 span = webkit_dom_document_get_element_by_id (doc, id);
1033 DEBUG ("Failed to find id '%s'", id);
1037 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1038 DEBUG ("Not a HTML element");
1042 /* update the HTML */
1043 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1044 parsed_body, &error);
1046 if (error != NULL) {
1047 DEBUG ("Error setting new inner-HTML: %s", error->message);
1048 g_error_free (error);
1053 timestamp = empathy_time_to_string_local (
1054 empathy_message_get_timestamp (message),
1056 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1058 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1064 /* mark this message as edited */
1065 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1066 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1068 if (icon_info != NULL) {
1069 /* set the icon as a background image using CSS
1070 * FIXME: the icon won't update in response to theme changes */
1071 gchar *style = g_strdup_printf (
1072 "background-image:url('%s');"
1073 "background-repeat:no-repeat;"
1074 "background-position:left center;"
1075 "padding-left:19px;", /* 16px icon + 3px padding */
1076 gtk_icon_info_get_filename (icon_info));
1078 webkit_dom_element_set_attribute (span, "style", style, &error);
1080 if (error != NULL) {
1081 DEBUG ("Error setting element style: %s",
1083 g_clear_error (&error);
1088 gtk_icon_info_free (icon_info);
1094 DEBUG ("Could not find message to edit with: %s",
1095 empathy_message_get_body (message));
1099 g_free (parsed_body);
1103 theme_adium_scroll (EmpathyChatView *view,
1104 gboolean allow_scrolling)
1106 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1108 priv->allow_scrolling = allow_scrolling;
1109 if (allow_scrolling) {
1110 empathy_chat_view_scroll_down (view);
1115 theme_adium_scroll_down (EmpathyChatView *view)
1117 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1121 theme_adium_get_has_selection (EmpathyChatView *view)
1123 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1127 theme_adium_clear (EmpathyChatView *view)
1129 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1131 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1133 /* Clear last contact to avoid trying to add a 'joined'
1134 * message when we don't have an insertion point. */
1135 if (priv->last_contact) {
1136 g_object_unref (priv->last_contact);
1137 priv->last_contact = NULL;
1142 theme_adium_find_previous (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_next (EmpathyChatView *view,
1155 const gchar *search_criteria,
1156 gboolean new_search,
1157 gboolean match_case)
1159 /* FIXME: Doesn't respect new_search */
1160 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1161 search_criteria, match_case,
1166 theme_adium_find_abilities (EmpathyChatView *view,
1167 const gchar *search_criteria,
1168 gboolean match_case,
1169 gboolean *can_do_previous,
1170 gboolean *can_do_next)
1172 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1173 * find_next and find_previous to work around this problem. */
1174 if (can_do_previous)
1175 *can_do_previous = TRUE;
1177 *can_do_next = TRUE;
1181 theme_adium_highlight (EmpathyChatView *view,
1183 gboolean match_case)
1185 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1186 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1187 text, match_case, 0);
1188 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1193 theme_adium_copy_clipboard (EmpathyChatView *view)
1195 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1199 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1202 WebKitDOMDocument *dom;
1203 WebKitDOMNodeList *nodes;
1205 GError *error = NULL;
1207 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1212 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1214 /* Get all nodes with focus class */
1215 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1218 if (nodes == NULL) {
1219 DEBUG ("Error getting focus nodes: %s",
1220 error ? error->message : "No error");
1221 g_clear_error (&error);
1225 theme_adium_remove_focus_marks (self, nodes);
1229 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1232 EmpathyThemeAdium *self = user_data;
1233 guint32 id = GPOINTER_TO_UINT (data);
1235 theme_adium_remove_mark_from_message (self, id);
1239 theme_adium_focus_toggled (EmpathyChatView *view,
1242 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1244 priv->has_focus = has_focus;
1245 if (!priv->has_focus) {
1246 /* We've lost focus, so let's make sure all the acked
1247 * messages have lost their unread marker. */
1248 g_queue_foreach (&priv->acked_messages,
1249 theme_adium_remove_acked_message_unread_mark_foreach,
1251 g_queue_clear (&priv->acked_messages);
1253 priv->has_unread_message = FALSE;
1258 theme_adium_message_acknowledged (EmpathyChatView *view,
1259 EmpathyMessage *message)
1261 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1262 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1267 tp_msg = empathy_message_get_tp_message (message);
1269 if (tp_msg == NULL) {
1273 id = tp_message_get_pending_message_id (tp_msg, &valid);
1275 g_warning ("Acknoledged message doesn't have a pending ID");
1279 /* We only want to actually remove the unread marker if the
1280 * view doesn't have focus. If we did it all the time we would
1281 * never see the unread markers, ever! So, we'll queue these
1282 * up, and when we lose focus, we'll remove the markers. */
1283 if (priv->has_focus) {
1284 g_queue_push_tail (&priv->acked_messages,
1285 GUINT_TO_POINTER (id));
1289 theme_adium_remove_mark_from_message (self, id);
1293 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1295 if (event->button == 3) {
1296 gboolean developer_tools_enabled;
1298 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1299 "enable-developer-extras", &developer_tools_enabled, NULL);
1301 /* We currently have no way to add an inspector menu
1302 * item ourselves, so we disable our customized menu
1303 * if the developer extras are enabled. */
1304 if (!developer_tools_enabled) {
1305 empathy_webkit_context_menu_for_event (
1306 WEBKIT_WEB_VIEW (widget), event,
1307 EMPATHY_WEBKIT_MENU_CLEAR);
1312 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1316 theme_adium_set_show_avatars (EmpathyChatView *view,
1317 gboolean show_avatars)
1319 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
1320 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1322 priv->show_avatars = show_avatars;
1326 theme_adium_iface_init (EmpathyChatViewIface *iface)
1328 iface->append_message = theme_adium_append_message;
1329 iface->append_event = theme_adium_append_event;
1330 iface->append_event_markup = theme_adium_append_event_markup;
1331 iface->edit_message = theme_adium_edit_message;
1332 iface->scroll = theme_adium_scroll;
1333 iface->scroll_down = theme_adium_scroll_down;
1334 iface->get_has_selection = theme_adium_get_has_selection;
1335 iface->clear = theme_adium_clear;
1336 iface->find_previous = theme_adium_find_previous;
1337 iface->find_next = theme_adium_find_next;
1338 iface->find_abilities = theme_adium_find_abilities;
1339 iface->highlight = theme_adium_highlight;
1340 iface->copy_clipboard = theme_adium_copy_clipboard;
1341 iface->focus_toggled = theme_adium_focus_toggled;
1342 iface->message_acknowledged = theme_adium_message_acknowledged;
1343 iface->set_show_avatars = theme_adium_set_show_avatars;
1347 theme_adium_load_finished_cb (WebKitWebView *view,
1348 WebKitWebFrame *frame,
1351 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1352 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1355 DEBUG ("Page loaded");
1356 priv->pages_loading--;
1358 if (priv->pages_loading != 0)
1361 /* Display queued messages */
1362 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1363 QueuedItem *item = l->data;
1367 case QUEUED_MESSAGE:
1368 theme_adium_append_message (chat_view, item->msg,
1369 item->should_highlight);
1373 theme_adium_edit_message (chat_view, item->msg);
1377 theme_adium_append_event (chat_view, item->str);
1381 free_queued_item (item);
1384 g_queue_clear (&priv->message_queue);
1388 theme_adium_finalize (GObject *object)
1390 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1392 empathy_adium_data_unref (priv->data);
1394 g_object_unref (priv->gsettings_chat);
1395 g_object_unref (priv->gsettings_desktop);
1397 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1401 theme_adium_dispose (GObject *object)
1403 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1405 if (priv->smiley_manager) {
1406 g_object_unref (priv->smiley_manager);
1407 priv->smiley_manager = NULL;
1410 if (priv->last_contact) {
1411 g_object_unref (priv->last_contact);
1412 priv->last_contact = NULL;
1415 if (priv->inspector_window) {
1416 gtk_widget_destroy (priv->inspector_window);
1417 priv->inspector_window = NULL;
1420 if (priv->acked_messages.length > 0) {
1421 g_queue_clear (&priv->acked_messages);
1424 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1428 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1429 EmpathyThemeAdium *theme)
1431 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1433 if (priv->inspector_window) {
1434 gtk_widget_show_all (priv->inspector_window);
1441 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1442 EmpathyThemeAdium *theme)
1444 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1446 if (priv->inspector_window) {
1447 gtk_widget_hide (priv->inspector_window);
1453 static WebKitWebView *
1454 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1455 WebKitWebView *web_view,
1456 EmpathyThemeAdium *theme)
1458 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1459 GtkWidget *scrolled_window;
1460 GtkWidget *inspector_web_view;
1462 if (!priv->inspector_window) {
1463 /* Create main window */
1464 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1465 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1467 g_signal_connect (priv->inspector_window, "delete-event",
1468 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1470 /* Pack a scrolled window */
1471 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1472 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1473 GTK_POLICY_AUTOMATIC,
1474 GTK_POLICY_AUTOMATIC);
1475 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1477 gtk_widget_show (scrolled_window);
1479 /* Pack a webview in the scrolled window. That webview will be
1480 * used to render the inspector tool. */
1481 inspector_web_view = webkit_web_view_new ();
1482 gtk_container_add (GTK_CONTAINER (scrolled_window),
1483 inspector_web_view);
1484 gtk_widget_show (scrolled_window);
1486 return WEBKIT_WEB_VIEW (inspector_web_view);
1493 theme_adium_constructed (GObject *object)
1495 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1496 const gchar *font_family = NULL;
1498 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1499 WebKitWebInspector *webkit_inspector;
1501 /* Set default settings */
1502 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1503 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1505 if (font_family && font_size) {
1506 g_object_set (webkit_web_view_get_settings (webkit_view),
1507 "default-font-family", font_family,
1508 "default-font-size", font_size,
1511 empathy_webkit_bind_font_setting (webkit_view,
1512 priv->gsettings_desktop,
1513 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1516 /* Setup webkit inspector */
1517 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1518 g_signal_connect (webkit_inspector, "inspect-web-view",
1519 G_CALLBACK (theme_adium_inspect_web_view_cb),
1521 g_signal_connect (webkit_inspector, "show-window",
1522 G_CALLBACK (theme_adium_inspector_show_window_cb),
1524 g_signal_connect (webkit_inspector, "close-window",
1525 G_CALLBACK (theme_adium_inspector_close_window_cb),
1529 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1531 priv->in_construction = FALSE;
1535 theme_adium_get_property (GObject *object,
1540 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1543 case PROP_ADIUM_DATA:
1544 g_value_set_boxed (value, priv->data);
1547 g_value_set_string (value, priv->variant);
1550 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1556 theme_adium_set_property (GObject *object,
1558 const GValue *value,
1561 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1562 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1565 case PROP_ADIUM_DATA:
1566 g_assert (priv->data == NULL);
1567 priv->data = g_value_dup_boxed (value);
1570 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1573 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1579 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1581 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1582 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1584 object_class->finalize = theme_adium_finalize;
1585 object_class->dispose = theme_adium_dispose;
1586 object_class->constructed = theme_adium_constructed;
1587 object_class->get_property = theme_adium_get_property;
1588 object_class->set_property = theme_adium_set_property;
1590 widget_class->button_press_event = theme_adium_button_press_event;
1592 g_object_class_install_property (object_class,
1594 g_param_spec_boxed ("adium-data",
1596 "Data for the adium theme",
1597 EMPATHY_TYPE_ADIUM_DATA,
1598 G_PARAM_CONSTRUCT_ONLY |
1600 G_PARAM_STATIC_STRINGS));
1601 g_object_class_install_property (object_class,
1603 g_param_spec_string ("variant",
1604 "The theme variant",
1605 "Variant name for the theme",
1609 G_PARAM_STATIC_STRINGS));
1611 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1615 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1617 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1618 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1622 priv->in_construction = TRUE;
1623 g_queue_init (&priv->message_queue);
1624 priv->allow_scrolling = TRUE;
1625 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1627 /* Show avatars by default. */
1628 priv->show_avatars = TRUE;
1630 g_signal_connect (theme, "load-finished",
1631 G_CALLBACK (theme_adium_load_finished_cb),
1633 g_signal_connect (theme, "navigation-policy-decision-requested",
1634 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1637 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1638 priv->gsettings_desktop = g_settings_new (
1639 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1641 g_signal_connect (priv->gsettings_chat,
1642 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1643 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1646 theme_adium_update_enable_webkit_developer_tools (theme);
1650 empathy_theme_adium_new (EmpathyAdiumData *data,
1651 const gchar *variant)
1653 g_return_val_if_fail (data != NULL, NULL);
1655 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1662 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1663 const gchar *variant)
1665 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1666 gchar *variant_path;
1669 if (!tp_strdiff (priv->variant, variant)) {
1673 g_free (priv->variant);
1674 priv->variant = g_strdup (variant);
1676 if (priv->in_construction) {
1680 DEBUG ("Update view with variant: '%s'", variant);
1681 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1683 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1685 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1687 g_free (variant_path);
1690 g_object_notify (G_OBJECT (theme), "variant");
1694 empathy_theme_adium_show_inspector (EmpathyThemeAdium *theme)
1696 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
1697 WebKitWebInspector *inspector;
1699 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
1700 "enable-developer-extras", TRUE,
1703 inspector = webkit_web_view_get_inspector (web_view);
1704 webkit_web_inspector_show (inspector);
1708 empathy_adium_path_is_valid (const gchar *path)
1713 /* The theme is not valid if there is no Info.plist */
1714 file = g_build_filename (path, "Contents", "Info.plist",
1716 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1722 /* We ship a default Template.html as fallback if there is any problem
1723 * with the one inside the theme. The only other required file is
1724 * Content.html OR Incoming/Content.html*/
1725 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1727 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1731 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1732 "Content.html", NULL);
1733 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1741 empathy_adium_info_new (const gchar *path)
1745 GHashTable *info = NULL;
1747 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1749 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1750 value = empathy_plist_parse_from_file (file);
1756 info = g_value_dup_boxed (value);
1757 tp_g_value_slice_free (value);
1759 /* Insert the theme's path into the hash table,
1760 * keys have to be dupped */
1761 tp_asv_set_string (info, g_strdup ("path"), path);
1767 adium_info_get_version (GHashTable *info)
1769 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1772 static const gchar *
1773 adium_info_get_no_variant_name (GHashTable *info)
1775 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1776 return name ? name : _("Normal");
1780 adium_info_dup_path_for_variant (GHashTable *info,
1781 const gchar *variant)
1783 guint version = adium_info_get_version (info);
1784 const gchar *no_variant = adium_info_get_no_variant_name (info);
1785 GPtrArray *variants;
1788 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1789 return g_strdup ("main.css");
1792 variants = empathy_adium_info_get_available_variants (info);
1793 if (variants->len == 0)
1794 return g_strdup ("main.css");
1796 /* Verify the variant exists, fallback to the first one */
1797 for (i = 0; i < variants->len; i++) {
1798 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1802 if (i == variants->len) {
1803 DEBUG ("Variant %s does not exist", variant);
1804 variant = g_ptr_array_index (variants, 0);
1807 return g_strdup_printf ("Variants/%s.css", variant);
1812 empathy_adium_info_get_default_variant (GHashTable *info)
1814 if (adium_info_get_version (info) <= 2) {
1815 return adium_info_get_no_variant_name (info);
1818 return tp_asv_get_string (info, "DefaultVariant");
1822 empathy_adium_info_get_available_variants (GHashTable *info)
1824 GPtrArray *variants;
1829 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1830 if (variants != NULL) {
1834 variants = g_ptr_array_new_with_free_func (g_free);
1835 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1836 G_TYPE_PTR_ARRAY, variants);
1838 path = tp_asv_get_string (info, "path");
1839 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1840 dir = g_dir_open (dirpath, 0, NULL);
1844 for (name = g_dir_read_name (dir);
1846 name = g_dir_read_name (dir)) {
1847 gchar *display_name;
1849 if (!g_str_has_suffix (name, ".css")) {
1853 display_name = g_strdup (name);
1854 strstr (display_name, ".css")[0] = '\0';
1855 g_ptr_array_add (variants, display_name);
1861 if (adium_info_get_version (info) <= 2) {
1862 g_ptr_array_add (variants,
1863 g_strdup (adium_info_get_no_variant_name (info)));
1870 empathy_adium_data_get_type (void)
1872 static GType type_id = 0;
1876 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1877 (GBoxedCopyFunc) empathy_adium_data_ref,
1878 (GBoxedFreeFunc) empathy_adium_data_unref);
1885 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1887 EmpathyAdiumData *data;
1888 gchar *template_html = NULL;
1889 gchar *footer_html = NULL;
1892 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1894 data = g_slice_new0 (EmpathyAdiumData);
1895 data->ref_count = 1;
1896 data->path = g_strdup (path);
1897 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1898 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1899 data->info = g_hash_table_ref (info);
1900 data->version = adium_info_get_version (info);
1901 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1902 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1903 g_str_equal, g_free, g_free);
1905 DEBUG ("Loading theme at %s", path);
1907 #define LOAD(path, var) \
1908 tmp = g_build_filename (data->basedir, path, NULL); \
1909 g_file_get_contents (tmp, &var, NULL, NULL); \
1912 #define LOAD_CONST(path, var) \
1915 LOAD (path, content); \
1916 if (content != NULL) { \
1917 g_ptr_array_add (data->strings_to_free, content); \
1922 /* Load html files */
1923 LOAD_CONST ("Content.html", data->content_html);
1924 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1925 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1926 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1927 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1928 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1929 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1930 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1931 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1932 LOAD_CONST ("Status.html", data->status_html);
1933 LOAD ("Template.html", template_html);
1934 LOAD ("Footer.html", footer_html);
1939 /* HTML fallbacks: If we have at least content OR in_content, then
1940 * everything else gets a fallback */
1942 #define FALLBACK(html, fallback) \
1943 if (html == NULL) { \
1947 /* in_nextcontent -> in_content -> content */
1948 FALLBACK (data->in_content_html, data->content_html);
1949 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1951 /* context -> content */
1952 FALLBACK (data->in_context_html, data->in_content_html);
1953 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1954 FALLBACK (data->out_context_html, data->out_content_html);
1955 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1958 FALLBACK (data->out_content_html, data->in_content_html);
1959 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1960 FALLBACK (data->out_context_html, data->in_context_html);
1961 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1963 /* status -> in_content */
1964 FALLBACK (data->status_html, data->in_content_html);
1968 /* template -> empathy's template */
1969 data->custom_template = (template_html != NULL);
1970 if (template_html == NULL) {
1971 GError *error = NULL;
1973 tmp = empathy_file_lookup ("Template.html", "data");
1975 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
1976 g_warning ("couldn't load Empathy's default theme "
1977 "template: %s", error->message);
1978 g_return_val_if_reached (data);
1984 /* Default avatar */
1985 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1986 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1987 data->default_incoming_avatar_filename = tmp;
1991 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1992 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1993 data->default_outgoing_avatar_filename = tmp;
1998 /* Old custom templates had only 4 parameters.
1999 * New templates have 5 parameters */
2000 if (data->version <= 2 && data->custom_template) {
2001 tmp = string_with_format (template_html,
2003 "%@", /* Leave variant unset */
2004 "", /* The header */
2005 footer_html ? footer_html : "",
2008 tmp = string_with_format (template_html,
2010 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2011 "%@", /* Leave variant unset */
2012 "", /* The header */
2013 footer_html ? footer_html : "",
2016 g_ptr_array_add (data->strings_to_free, tmp);
2017 data->template_html = tmp;
2019 g_free (template_html);
2020 g_free (footer_html);
2026 empathy_adium_data_new (const gchar *path)
2028 EmpathyAdiumData *data;
2031 info = empathy_adium_info_new (path);
2032 data = empathy_adium_data_new_with_info (path, info);
2033 g_hash_table_unref (info);
2039 empathy_adium_data_ref (EmpathyAdiumData *data)
2041 g_return_val_if_fail (data != NULL, NULL);
2043 g_atomic_int_inc (&data->ref_count);
2049 empathy_adium_data_unref (EmpathyAdiumData *data)
2051 g_return_if_fail (data != NULL);
2053 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2054 g_free (data->path);
2055 g_free (data->basedir);
2056 g_free (data->default_avatar_filename);
2057 g_free (data->default_incoming_avatar_filename);
2058 g_free (data->default_outgoing_avatar_filename);
2059 g_hash_table_unref (data->info);
2060 g_ptr_array_unref (data->strings_to_free);
2061 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2063 g_slice_free (EmpathyAdiumData, data);
2068 empathy_adium_data_get_info (EmpathyAdiumData *data)
2070 g_return_val_if_fail (data != NULL, NULL);
2076 empathy_adium_data_get_path (EmpathyAdiumData *data)
2078 g_return_val_if_fail (data != NULL, NULL);