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-string-parser.h"
43 #include "empathy-images.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 GValue* containing an EmpathyMessage or string */
64 /* Queue of owned gchar* of message token to remove unread
65 * marker for when we lose focus. */
66 GQueue acked_messages;
67 GtkWidget *inspector_window;
68 GSettings *gsettings_chat;
70 gboolean has_unread_message;
71 gboolean allow_scrolling;
72 } EmpathyThemeAdiumPriv;
74 struct _EmpathyAdiumData {
78 gchar *default_avatar_filename;
79 gchar *default_incoming_avatar_filename;
80 gchar *default_outgoing_avatar_filename;
83 gboolean custom_template;
84 /* gchar* -> gchar* both owned */
85 GHashTable *date_format_cache;
88 const gchar *template_html;
89 const gchar *content_html;
90 const gchar *in_content_html;
91 const gchar *in_context_html;
92 const gchar *in_nextcontent_html;
93 const gchar *in_nextcontext_html;
94 const gchar *out_content_html;
95 const gchar *out_context_html;
96 const gchar *out_nextcontent_html;
97 const gchar *out_nextcontext_html;
98 const gchar *status_html;
100 /* Above html strings are pointers to strings stored in this array.
101 * We do this because of fallbacks, some htmls could be pointing the
103 GPtrArray *strings_to_free;
106 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
113 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
114 WEBKIT_TYPE_WEB_VIEW,
115 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
116 theme_adium_iface_init));
119 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
121 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
122 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
123 gboolean enable_webkit_developer_tools;
125 enable_webkit_developer_tools = g_settings_get_boolean (
126 priv->gsettings_chat,
127 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
129 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
130 "enable-developer-extras",
131 enable_webkit_developer_tools,
136 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
140 EmpathyThemeAdium *theme = user_data;
142 theme_adium_update_enable_webkit_developer_tools (theme);
146 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
147 WebKitWebFrame *web_frame,
148 WebKitNetworkRequest *request,
149 WebKitWebNavigationAction *action,
150 WebKitWebPolicyDecision *decision,
155 /* Only call url_show on clicks */
156 if (webkit_web_navigation_action_get_reason (action) !=
157 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
158 webkit_web_policy_decision_use (decision);
162 uri = webkit_network_request_get_uri (request);
163 empathy_url_show (GTK_WIDGET (view), uri);
165 webkit_web_policy_decision_ignore (decision);
170 theme_adium_copy_address_cb (GtkMenuItem *menuitem,
173 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
175 GtkClipboard *clipboard;
177 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
179 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
180 gtk_clipboard_set_text (clipboard, uri, -1);
182 clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
183 gtk_clipboard_set_text (clipboard, uri, -1);
189 theme_adium_open_address_cb (GtkMenuItem *menuitem,
192 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
195 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
197 empathy_url_show (GTK_WIDGET (menuitem), uri);
202 /* Replace each %@ in format with string passed in args */
204 string_with_format (const gchar *format,
205 const gchar *first_string,
212 va_start (args, first_string);
213 result = g_string_sized_new (strlen (format));
214 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
217 next = strstr (format, "%@");
222 g_string_append_len (result, format, next - format);
223 g_string_append (result, str);
226 g_string_append (result, format);
229 return g_string_free (result, FALSE);
233 theme_adium_match_newline (const gchar *text,
235 EmpathyStringReplace replace_func,
236 EmpathyStringParser *sub_parsers,
239 GString *string = user_data;
247 /* Replace \n by <br/> */
248 for (i = 0; i < len && text[i] != '\0'; i++) {
249 if (text[i] == '\n') {
250 empathy_string_parser_substr (text + prev,
251 i - prev, sub_parsers,
253 g_string_append (string, "<br/>");
257 empathy_string_parser_substr (text + prev, i - prev,
258 sub_parsers, user_data);
262 theme_adium_replace_smiley (const gchar *text,
267 EmpathySmileyHit *hit = match_data;
268 GString *string = user_data;
270 /* Replace smiley by a <img/> tag */
271 g_string_append_printf (string,
272 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
273 hit->path, (int)len, text, (int)len, text);
276 static EmpathyStringParser string_parsers[] = {
277 {empathy_string_match_link, empathy_string_replace_link},
278 {theme_adium_match_newline, NULL},
279 {empathy_string_match_all, empathy_string_replace_escaped},
283 static EmpathyStringParser string_parsers_with_smiley[] = {
284 {empathy_string_match_link, empathy_string_replace_link},
285 {empathy_string_match_smiley, theme_adium_replace_smiley},
286 {theme_adium_match_newline, NULL},
287 {empathy_string_match_all, empathy_string_replace_escaped},
292 theme_adium_parse_body (EmpathyThemeAdium *self,
295 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
296 EmpathyStringParser *parsers;
299 /* Check if we have to parse smileys */
300 if (g_settings_get_boolean (priv->gsettings_chat,
301 EMPATHY_PREFS_CHAT_SHOW_SMILEYS))
302 parsers = string_parsers_with_smiley;
304 parsers = string_parsers;
306 /* Parse text and construct string with links and smileys replaced
307 * by html tags. Also escape text to make sure html code is
308 * displayed verbatim. */
309 string = g_string_sized_new (strlen (text));
310 empathy_string_parser_substr (text, -1, parsers, string);
312 /* Wrap body in order to make tabs and multiple spaces displayed
313 * properly. See bug #625745. */
314 g_string_prepend (string, "<div style=\"display: inline; "
315 "white-space: pre-wrap\"'>");
316 g_string_append (string, "</div>");
318 return g_string_free (string, FALSE);
322 escape_and_append_len (GString *string, const gchar *str, gint len)
324 while (str != NULL && *str != '\0' && len != 0) {
328 g_string_append (string, "\\\\");
332 g_string_append (string, "\\\"");
335 /* Remove end of lines */
338 g_string_append_c (string, *str);
346 /* If *str starts with match, returns TRUE and move pointer to the end */
348 theme_adium_match (const gchar **str,
353 len = strlen (match);
354 if (strncmp (*str, match, len) == 0) {
362 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
364 theme_adium_match_with_format (const gchar **str,
368 const gchar *cur = *str;
371 if (!theme_adium_match (&cur, match)) {
376 end = strstr (cur, "}%");
381 *format = g_strndup (cur , end - cur);
386 /* List of colors used by %senderColor%. Copied from
387 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
389 static gchar *colors[] = {
390 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
391 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
392 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
393 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
394 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
395 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
396 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
397 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
398 "lightblue", "lightcoral",
399 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
400 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
401 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
402 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
403 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
404 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
405 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
406 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
407 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
408 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
413 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
415 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
416 * to strftime supported by g_date_time_format.
417 * FIXME: table is incomplete, doc of g_date_time_format has a table of
419 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
420 * in 2.29.x we have to explictely request padding with %0x */
421 static const gchar *convert_table[] = {
423 "A", NULL, // 0~86399999 (Millisecond of Day)
425 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
426 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
427 "cc", "%u", // 1~7 (Day of Week)
428 "c", "%u", // 1~7 (Day of Week)
430 "dd", "%d", // 1~31 (0 padded Day of Month)
431 "d", "%d", // 1~31 (0 padded Day of Month)
432 "D", "%j", // 1~366 (0 padded Day of Year)
434 "e", "%u", // 1~7 (0 padded Day of Week)
435 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
436 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
437 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
438 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
440 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
442 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
443 "GGGG", NULL, // Before Christ/Anno Domini
444 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
445 "GG", NULL, // BC/AD (Era Designator Abbreviated)
446 "G", NULL, // BC/AD (Era Designator Abbreviated)
448 "h", "%I", // 1~12 (0 padded Hour (12hr))
449 "H", "%H", // 0~23 (0 padded Hour (24hr))
451 "k", NULL, // 1~24 (0 padded Hour (24hr)
452 "K", NULL, // 0~11 (0 padded Hour (12hr))
454 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
455 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
456 "LL", "%m", // 1~12 (0 padded Month)
457 "L", "%m", // 1~12 (0 padded Month)
459 "m", "%M", // 0~59 (0 padded Minute)
460 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
461 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
462 "MM", "%m", // 1~12 (0 padded Month)
463 "M", "%m", // 1~12 (0 padded Month)
465 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
466 "qqq", NULL, // Q1/Q2/Q3/Q4
467 "qq", NULL, // 1~4 (0 padded Quarter)
468 "q", NULL, // 1~4 (0 padded Quarter)
469 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
470 "QQQ", NULL, // Q1/Q2/Q3/Q4
471 "QQ", NULL, // 1~4 (0 padded Quarter)
472 "Q", NULL, // 1~4 (0 padded Quarter)
474 "s", "%S", // 0~59 (0 padded Second)
475 "S", NULL, // (rounded Sub-Second)
477 "u", "%Y", // (0 padded Year)
479 "vvvv", "%Z", // (General GMT Timezone Name)
480 "vvv", "%Z", // (General GMT Timezone Abbreviation)
481 "vv", "%Z", // (General GMT Timezone Abbreviation)
482 "v", "%Z", // (General GMT Timezone Abbreviation)
484 "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)
485 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
487 "yyyy", "%Y", // (Full Year)
488 "yyy", "%y", // (2 Digits Year)
489 "yy", "%y", // (2 Digits Year)
490 "y", "%Y", // (Full Year)
491 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
492 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
493 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
494 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
496 "zzzz", NULL, // (Specific GMT Timezone Name)
497 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
498 "zz", NULL, // (Specific GMT Timezone Abbreviation)
499 "z", NULL, // (Specific GMT Timezone Abbreviation)
500 "Z", "%z", // +0000 (RFC 822 Timezone)
506 if (nsdate == NULL) {
510 str = g_hash_table_lookup (data->date_format_cache, nsdate);
515 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
516 * by corresponding strftime tag. */
517 string = g_string_sized_new (strlen (nsdate));
518 for (i = 0; nsdate[i] != '\0'; i++) {
519 gboolean found = FALSE;
521 /* even indexes are NSDateFormatter tag, odd indexes are the
522 * corresponding strftime tag */
523 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
524 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
530 /* If we don't have a replacement, just ignore that tag */
531 if (convert_table[j + 1] != NULL) {
532 g_string_append (string, convert_table[j + 1]);
534 i += strlen (convert_table[j]) - 1;
536 g_string_append_c (string, nsdate[i]);
540 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
542 /* The cache takes ownership of string->str */
543 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
544 return g_string_free (string, FALSE);
549 theme_adium_append_html (EmpathyThemeAdium *theme,
552 const gchar *message,
553 const gchar *avatar_filename,
555 const gchar *contact_id,
556 const gchar *service_name,
557 const gchar *message_classes,
561 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
563 const gchar *cur = NULL;
566 /* Make some search-and-replace in the html code */
567 string = g_string_sized_new (strlen (html) + strlen (message));
568 g_string_append_printf (string, "%s(\"", func);
569 for (cur = html; *cur != '\0'; cur++) {
570 const gchar *replace = NULL;
571 gchar *dup_replace = NULL;
572 gchar *format = NULL;
574 /* Those are all well known keywords that needs replacement in
575 * html files. Please keep them in the same order than the adium
576 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
577 if (theme_adium_match (&cur, "%userIconPath%")) {
578 replace = avatar_filename;
579 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
580 replace = contact_id;
581 } else if (theme_adium_match (&cur, "%sender%")) {
583 } else if (theme_adium_match (&cur, "%senderColor%")) {
584 /* A color derived from the user's name.
585 * FIXME: If a colon separated list of HTML colors is at
586 * Incoming/SenderColors.txt it will be used instead of
587 * the default colors.
589 if (contact_id != NULL) {
590 guint hash = g_str_hash (contact_id);
591 replace = colors[hash % G_N_ELEMENTS (colors)];
593 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
594 /* FIXME: The path to the status icon of the sender
595 * (available, away, etc...)
597 } else if (theme_adium_match (&cur, "%messageDirection%")) {
598 /* FIXME: The text direction of the message
599 * (either rtl or ltr)
601 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
602 /* FIXME: The serverside (remotely set) name of the
603 * sender, such as an MSN display name.
605 * We don't have access to that yet so we use
606 * local alias instead.
609 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
610 /* FIXME: This keyword is used to represent the
611 * highlight background color. "X" is the opacity of the
612 * background, ranges from 0 to 1 and can be any decimal
615 } else if (theme_adium_match (&cur, "%message%")) {
617 } else if (theme_adium_match (&cur, "%time%") ||
618 theme_adium_match_with_format (&cur, "%time{", &format)) {
619 const gchar *strftime_format;
621 strftime_format = nsdate_to_strftime (priv->data, format);
623 dup_replace = empathy_time_to_string_local (timestamp,
624 strftime_format ? strftime_format :
625 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
627 dup_replace = empathy_time_to_string_local (timestamp,
628 strftime_format ? strftime_format :
629 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
631 replace = dup_replace;
632 } else if (theme_adium_match (&cur, "%shortTime%")) {
633 dup_replace = empathy_time_to_string_local (timestamp,
634 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
635 replace = dup_replace;
636 } else if (theme_adium_match (&cur, "%service%")) {
637 replace = service_name;
638 } else if (theme_adium_match (&cur, "%variant%")) {
639 /* FIXME: The name of the active message style variant,
640 * with all spaces replaced with an underscore.
641 * A variant named "Alternating Messages - Blue Red"
642 * will become "Alternating_Messages_-_Blue_Red".
644 } else if (theme_adium_match (&cur, "%userIcons%")) {
645 /* FIXME: mus t be "hideIcons" if use preference is set
647 replace = "showIcons";
648 } else if (theme_adium_match (&cur, "%messageClasses%")) {
649 replace = message_classes;
650 } else if (theme_adium_match (&cur, "%status%")) {
651 /* FIXME: A description of the status event. This is
652 * neither in the user's local language nor expected to
653 * be displayed; it may be useful to use a different div
654 * class to present different types of status messages.
655 * The following is a list of some of the more important
656 * status messages; your message style should be able to
657 * handle being shown a status message not in this list,
658 * as even at present the list is incomplete and is
659 * certain to become out of date in the future:
668 * contact_joined (group chats)
672 * encryption (all OTR messages use this status)
673 * purple (all IRC topic and join/part messages use this status)
674 * fileTransferStarted
675 * fileTransferCompleted
678 escape_and_append_len (string, cur, 1);
682 /* Here we have a replacement to make */
683 escape_and_append_len (string, replace, -1);
685 g_free (dup_replace);
688 g_string_append (string, "\")");
690 script = g_string_free (string, FALSE);
691 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
696 theme_adium_append_event_escaped (EmpathyChatView *view,
697 const gchar *escaped)
699 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
700 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
702 theme_adium_append_html (theme, "appendMessage",
703 priv->data->status_html, escaped, NULL, NULL, NULL,
705 empathy_time_get_current (), FALSE);
707 /* There is no last contact */
708 if (priv->last_contact) {
709 g_object_unref (priv->last_contact);
710 priv->last_contact = NULL;
715 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
716 WebKitDOMNodeList *nodes)
720 /* Remove focus and firstFocus class */
721 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
722 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
723 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
725 gchar **classes, **iter;
726 GString *new_class_name;
727 gboolean first = TRUE;
729 if (element == NULL) {
733 class_name = webkit_dom_html_element_get_class_name (element);
734 classes = g_strsplit (class_name, " ", -1);
735 new_class_name = g_string_sized_new (strlen (class_name));
736 for (iter = classes; *iter != NULL; iter++) {
737 if (tp_strdiff (*iter, "focus") &&
738 tp_strdiff (*iter, "firstFocus")) {
740 g_string_append_c (new_class_name, ' ');
742 g_string_append (new_class_name, *iter);
747 webkit_dom_html_element_set_class_name (element, new_class_name->str);
750 g_strfreev (classes);
751 g_string_free (new_class_name, TRUE);
756 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
758 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
759 WebKitDOMDocument *dom;
760 WebKitDOMNodeList *nodes;
761 GError *error = NULL;
763 if (!priv->has_unread_message)
766 priv->has_unread_message = FALSE;
768 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
773 /* Get all nodes with focus class */
774 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
776 DEBUG ("Error getting focus nodes: %s",
777 error ? error->message : "No error");
778 g_clear_error (&error);
782 theme_adium_remove_focus_marks (theme, nodes);
786 theme_adium_append_message (EmpathyChatView *view,
789 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
790 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
791 EmpathyContact *sender;
797 const gchar *contact_id;
798 EmpathyAvatar *avatar;
799 const gchar *avatar_filename = NULL;
801 const gchar *html = NULL;
803 const gchar *service_name;
804 GString *message_classes = NULL;
806 gboolean consecutive;
809 if (priv->pages_loading != 0) {
810 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
811 g_value_set_object (value, msg);
812 g_queue_push_tail (&priv->message_queue, value);
816 /* Get information */
817 sender = empathy_message_get_sender (msg);
818 account = empathy_contact_get_account (sender);
819 service_name = empathy_protocol_name_to_display_name
820 (tp_account_get_protocol (account));
821 if (service_name == NULL)
822 service_name = tp_account_get_protocol (account);
823 timestamp = empathy_message_get_timestamp (msg);
824 body = empathy_message_get_body (msg);
825 body_escaped = theme_adium_parse_body (theme, body);
826 name = empathy_contact_get_alias (sender);
827 contact_id = empathy_contact_get_id (sender);
828 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
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>",
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 (empathy_message_should_highlight (msg)) {
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) {
921 gchar *tmp = tp_escape_as_identifier (
922 tp_message_get_token (tp_msg));
923 g_string_append_printf (message_classes,
924 " x-empathy-message-id-%s", tmp);
928 /* Define javascript function to use */
930 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
932 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
935 if (empathy_contact_is_user (sender)) {
939 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
942 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
945 /* remove all the unread marks when we are sending a message */
946 theme_adium_remove_all_focus_marks (theme);
951 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
954 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
958 theme_adium_append_html (theme, func, html, body_escaped,
959 avatar_filename, name, contact_id,
960 service_name, message_classes->str,
961 timestamp, is_backlog);
963 /* Keep the sender of the last displayed message */
964 if (priv->last_contact) {
965 g_object_unref (priv->last_contact);
967 priv->last_contact = g_object_ref (sender);
968 priv->last_timestamp = timestamp;
969 priv->last_is_backlog = is_backlog;
971 g_free (body_escaped);
972 g_string_free (message_classes, TRUE);
976 theme_adium_append_event (EmpathyChatView *view,
979 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
982 if (priv->pages_loading != 0) {
983 g_queue_push_tail (&priv->message_queue,
984 tp_g_value_slice_new_string (str));
988 str_escaped = g_markup_escape_text (str, -1);
989 theme_adium_append_event_escaped (view, str_escaped);
990 g_free (str_escaped);
994 theme_adium_scroll (EmpathyChatView *view,
995 gboolean allow_scrolling)
997 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
999 priv->allow_scrolling = allow_scrolling;
1000 if (allow_scrolling) {
1001 empathy_chat_view_scroll_down (view);
1006 theme_adium_scroll_down (EmpathyChatView *view)
1008 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1012 theme_adium_get_has_selection (EmpathyChatView *view)
1014 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1018 theme_adium_clear (EmpathyChatView *view)
1020 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1023 priv->pages_loading++;
1024 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
1025 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view),
1026 priv->data->template_html,
1028 g_free (basedir_uri);
1030 /* Clear last contact to avoid trying to add a 'joined'
1031 * message when we don't have an insertion point. */
1032 if (priv->last_contact) {
1033 g_object_unref (priv->last_contact);
1034 priv->last_contact = NULL;
1039 theme_adium_find_previous (EmpathyChatView *view,
1040 const gchar *search_criteria,
1041 gboolean new_search,
1042 gboolean match_case)
1044 /* FIXME: Doesn't respect new_search */
1045 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1046 search_criteria, match_case,
1051 theme_adium_find_next (EmpathyChatView *view,
1052 const gchar *search_criteria,
1053 gboolean new_search,
1054 gboolean match_case)
1056 /* FIXME: Doesn't respect new_search */
1057 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1058 search_criteria, match_case,
1063 theme_adium_find_abilities (EmpathyChatView *view,
1064 const gchar *search_criteria,
1065 gboolean match_case,
1066 gboolean *can_do_previous,
1067 gboolean *can_do_next)
1069 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1070 * find_next and find_previous to work around this problem. */
1071 if (can_do_previous)
1072 *can_do_previous = TRUE;
1074 *can_do_next = TRUE;
1078 theme_adium_highlight (EmpathyChatView *view,
1080 gboolean match_case)
1082 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1083 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1084 text, match_case, 0);
1085 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1090 theme_adium_copy_clipboard (EmpathyChatView *view)
1092 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1096 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1099 WebKitDOMDocument *dom;
1100 WebKitDOMNodeList *nodes;
1102 GError *error = NULL;
1104 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1109 tmp = tp_escape_as_identifier (token);
1110 class = g_strdup_printf (".x-empathy-message-id-%s", tmp);
1113 /* Get all nodes with focus class */
1114 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1117 if (nodes == NULL) {
1118 DEBUG ("Error getting focus nodes: %s",
1119 error ? error->message : "No error");
1120 g_clear_error (&error);
1124 theme_adium_remove_focus_marks (self, nodes);
1128 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1131 EmpathyThemeAdium *self = user_data;
1132 gchar *token = data;
1134 theme_adium_remove_mark_from_message (self, token);
1139 theme_adium_focus_toggled (EmpathyChatView *view,
1142 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1144 priv->has_focus = has_focus;
1145 if (!priv->has_focus) {
1146 /* We've lost focus, so let's make sure all the acked
1147 * messages have lost their unread marker. */
1148 g_queue_foreach (&priv->acked_messages,
1149 theme_adium_remove_acked_message_unread_mark_foreach,
1151 g_queue_clear (&priv->acked_messages);
1153 priv->has_unread_message = FALSE;
1158 theme_adium_message_acknowledged (EmpathyChatView *view,
1159 EmpathyMessage *message)
1161 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1162 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1165 tp_msg = empathy_message_get_tp_message (message);
1167 if (tp_msg == NULL) {
1171 /* We only want to actually remove the unread marker if the
1172 * view doesn't have focus. If we did it all the time we would
1173 * never see the unread markers, ever! So, we'll queue these
1174 * up, and when we lose focus, we'll remove the markers. */
1175 if (priv->has_focus) {
1176 g_queue_push_tail (&priv->acked_messages,
1177 g_strdup (tp_message_get_token (tp_msg)));
1181 theme_adium_remove_mark_from_message (self,
1182 tp_message_get_token (tp_msg));
1186 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1188 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1190 g_object_unref (hit_test_result);
1194 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1196 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1197 WebKitHitTestResult *hit_test_result;
1198 WebKitHitTestResultContext context;
1202 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1203 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1206 menu = empathy_context_menu_new (GTK_WIDGET (view));
1208 /* Select all item */
1209 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1210 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1212 g_signal_connect_swapped (item, "activate",
1213 G_CALLBACK (webkit_web_view_select_all),
1216 /* Copy menu item */
1217 if (webkit_web_view_can_copy_clipboard (view)) {
1218 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1219 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1221 g_signal_connect_swapped (item, "activate",
1222 G_CALLBACK (webkit_web_view_copy_clipboard),
1226 /* Clear menu item */
1227 item = gtk_separator_menu_item_new ();
1228 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1230 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1231 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1233 g_signal_connect_swapped (item, "activate",
1234 G_CALLBACK (empathy_chat_view_clear),
1237 /* We will only add the following menu items if we are
1238 * right-clicking a link */
1239 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1241 item = gtk_separator_menu_item_new ();
1242 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1244 /* Copy Link Address menu item */
1245 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1246 g_signal_connect (item, "activate",
1247 G_CALLBACK (theme_adium_copy_address_cb),
1249 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1251 /* Open Link menu item */
1252 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1253 g_signal_connect (item, "activate",
1254 G_CALLBACK (theme_adium_open_address_cb),
1256 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1259 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1260 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1263 /* Display the menu */
1264 gtk_widget_show_all (menu);
1265 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1266 event->button, event->time);
1270 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1272 if (event->button == 3) {
1273 gboolean developer_tools_enabled;
1275 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1276 "enable-developer-extras", &developer_tools_enabled, NULL);
1278 /* We currently have no way to add an inspector menu
1279 * item ourselves, so we disable our customized menu
1280 * if the developer extras are enabled. */
1281 if (!developer_tools_enabled) {
1282 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1287 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1291 theme_adium_iface_init (EmpathyChatViewIface *iface)
1293 iface->append_message = theme_adium_append_message;
1294 iface->append_event = theme_adium_append_event;
1295 iface->scroll = theme_adium_scroll;
1296 iface->scroll_down = theme_adium_scroll_down;
1297 iface->get_has_selection = theme_adium_get_has_selection;
1298 iface->clear = theme_adium_clear;
1299 iface->find_previous = theme_adium_find_previous;
1300 iface->find_next = theme_adium_find_next;
1301 iface->find_abilities = theme_adium_find_abilities;
1302 iface->highlight = theme_adium_highlight;
1303 iface->copy_clipboard = theme_adium_copy_clipboard;
1304 iface->focus_toggled = theme_adium_focus_toggled;
1305 iface->message_acknowledged = theme_adium_message_acknowledged;
1309 theme_adium_load_finished_cb (WebKitWebView *view,
1310 WebKitWebFrame *frame,
1313 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1314 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1317 DEBUG ("Page loaded");
1318 priv->pages_loading--;
1320 if (priv->pages_loading != 0)
1323 /* Display queued messages */
1324 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1325 GValue *value = l->data;
1327 if (G_VALUE_HOLDS_OBJECT (value)) {
1328 theme_adium_append_message (chat_view,
1329 g_value_get_object (value));
1331 theme_adium_append_event (chat_view,
1332 g_value_get_string (value));
1335 tp_g_value_slice_free (value);
1337 g_queue_clear (&priv->message_queue);
1341 theme_adium_finalize (GObject *object)
1343 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1345 empathy_adium_data_unref (priv->data);
1346 g_object_unref (priv->gsettings_chat);
1348 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1352 theme_adium_dispose (GObject *object)
1354 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1356 if (priv->smiley_manager) {
1357 g_object_unref (priv->smiley_manager);
1358 priv->smiley_manager = NULL;
1361 if (priv->last_contact) {
1362 g_object_unref (priv->last_contact);
1363 priv->last_contact = NULL;
1366 if (priv->inspector_window) {
1367 gtk_widget_destroy (priv->inspector_window);
1368 priv->inspector_window = NULL;
1371 if (priv->acked_messages.length > 0) {
1372 g_queue_foreach (&priv->acked_messages, (GFunc) g_free, NULL);
1373 g_queue_clear (&priv->acked_messages);
1376 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1380 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1381 EmpathyThemeAdium *theme)
1383 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1385 if (priv->inspector_window) {
1386 gtk_widget_show_all (priv->inspector_window);
1393 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1394 EmpathyThemeAdium *theme)
1396 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1398 if (priv->inspector_window) {
1399 gtk_widget_hide (priv->inspector_window);
1405 static WebKitWebView *
1406 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1407 WebKitWebView *web_view,
1408 EmpathyThemeAdium *theme)
1410 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1411 GtkWidget *scrolled_window;
1412 GtkWidget *inspector_web_view;
1414 if (!priv->inspector_window) {
1415 /* Create main window */
1416 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1417 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1419 g_signal_connect (priv->inspector_window, "delete-event",
1420 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1422 /* Pack a scrolled window */
1423 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1424 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1425 GTK_POLICY_AUTOMATIC,
1426 GTK_POLICY_AUTOMATIC);
1427 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1429 gtk_widget_show (scrolled_window);
1431 /* Pack a webview in the scrolled window. That webview will be
1432 * used to render the inspector tool. */
1433 inspector_web_view = webkit_web_view_new ();
1434 gtk_container_add (GTK_CONTAINER (scrolled_window),
1435 inspector_web_view);
1436 gtk_widget_show (scrolled_window);
1438 return WEBKIT_WEB_VIEW (inspector_web_view);
1444 static PangoFontDescription *
1445 theme_adium_get_default_font (void)
1447 GSettings *gsettings;
1448 PangoFontDescription *pango_fd;
1451 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1453 font_family = g_settings_get_string (gsettings,
1454 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1456 if (font_family == NULL)
1459 pango_fd = pango_font_description_from_string (font_family);
1460 g_free (font_family);
1461 g_object_unref (gsettings);
1466 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1470 g_object_set (w_settings, "default-font-family", name, NULL);
1471 g_object_set (w_settings, "default-font-size", size, NULL);
1475 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1477 PangoFontDescription *default_font_desc;
1478 GdkScreen *current_screen;
1480 gint pango_font_size = 0;
1482 default_font_desc = theme_adium_get_default_font ();
1483 if (default_font_desc == NULL)
1485 pango_font_size = pango_font_description_get_size (default_font_desc)
1487 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1488 current_screen = gdk_screen_get_default ();
1489 if (current_screen != NULL) {
1490 dpi = gdk_screen_get_resolution (current_screen);
1492 dpi = BORING_DPI_DEFAULT;
1494 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1496 theme_adium_set_webkit_font (w_settings,
1497 pango_font_description_get_family (default_font_desc),
1499 pango_font_description_free (default_font_desc);
1503 theme_adium_constructed (GObject *object)
1505 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1507 const gchar *font_family = NULL;
1509 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1510 WebKitWebSettings *webkit_settings;
1511 WebKitWebInspector *webkit_inspector;
1513 /* Set default settings */
1514 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1515 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1516 webkit_settings = webkit_web_view_get_settings (webkit_view);
1518 if (font_family && font_size) {
1519 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1521 theme_adium_set_default_font (webkit_settings);
1524 /* Setup webkit inspector */
1525 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1526 g_signal_connect (webkit_inspector, "inspect-web-view",
1527 G_CALLBACK (theme_adium_inspect_web_view_cb),
1529 g_signal_connect (webkit_inspector, "show-window",
1530 G_CALLBACK (theme_adium_inspector_show_window_cb),
1532 g_signal_connect (webkit_inspector, "close-window",
1533 G_CALLBACK (theme_adium_inspector_close_window_cb),
1537 priv->pages_loading = 1;
1539 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
1540 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
1541 priv->data->template_html,
1543 g_free (basedir_uri);
1547 theme_adium_get_property (GObject *object,
1552 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1555 case PROP_ADIUM_DATA:
1556 g_value_set_boxed (value, priv->data);
1559 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1565 theme_adium_set_property (GObject *object,
1567 const GValue *value,
1570 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1573 case PROP_ADIUM_DATA:
1574 g_assert (priv->data == NULL);
1575 priv->data = g_value_dup_boxed (value);
1578 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1584 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1586 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1587 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1589 object_class->finalize = theme_adium_finalize;
1590 object_class->dispose = theme_adium_dispose;
1591 object_class->constructed = theme_adium_constructed;
1592 object_class->get_property = theme_adium_get_property;
1593 object_class->set_property = theme_adium_set_property;
1595 widget_class->button_press_event = theme_adium_button_press_event;
1597 g_object_class_install_property (object_class,
1599 g_param_spec_boxed ("adium-data",
1601 "Data for the adium theme",
1602 EMPATHY_TYPE_ADIUM_DATA,
1603 G_PARAM_CONSTRUCT_ONLY |
1605 G_PARAM_STATIC_STRINGS));
1607 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1611 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1613 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1614 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1618 g_queue_init (&priv->message_queue);
1619 priv->allow_scrolling = TRUE;
1620 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1622 g_signal_connect (theme, "load-finished",
1623 G_CALLBACK (theme_adium_load_finished_cb),
1625 g_signal_connect (theme, "navigation-policy-decision-requested",
1626 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1629 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1630 g_signal_connect (priv->gsettings_chat,
1631 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1632 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1635 theme_adium_update_enable_webkit_developer_tools (theme);
1639 empathy_theme_adium_new (EmpathyAdiumData *data)
1641 g_return_val_if_fail (data != NULL, NULL);
1643 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1649 empathy_adium_path_is_valid (const gchar *path)
1654 /* The theme is not valid if there is no Info.plist */
1655 file = g_build_filename (path, "Contents", "Info.plist",
1657 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1663 /* We ship a default Template.html as fallback if there is any problem
1664 * with the one inside the theme. The only other required file is
1665 * Content.html OR Incoming/Content.html*/
1666 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1668 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1672 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1673 "Content.html", NULL);
1674 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1682 empathy_adium_info_new (const gchar *path)
1686 GHashTable *info = NULL;
1688 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1690 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1691 value = empathy_plist_parse_from_file (file);
1697 info = g_value_dup_boxed (value);
1698 tp_g_value_slice_free (value);
1700 /* Insert the theme's path into the hash table,
1701 * keys have to be dupped */
1702 tp_asv_set_string (info, g_strdup ("path"), path);
1708 adium_info_get_version (GHashTable *info)
1710 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1713 static const gchar *
1714 adium_info_get_no_variant_name (GHashTable *info)
1716 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1717 return name ? name : _("Normal");
1720 static const gchar *
1721 adium_info_get_default_or_first_variant (GHashTable *info)
1724 GPtrArray *variants;
1726 name = empathy_adium_info_get_default_variant (info);
1731 variants = empathy_adium_info_get_available_variants (info);
1732 g_assert (variants->len > 0);
1733 return g_ptr_array_index (variants, 0);
1737 adium_info_dup_path_for_variant (GHashTable *info,
1738 const gchar *variant)
1740 guint version = adium_info_get_version (info);
1741 const gchar *no_variant = adium_info_get_no_variant_name (info);
1743 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1744 return g_strdup ("main.css");
1747 return g_strdup_printf ("Variants/%s.css", variant);
1752 empathy_adium_info_get_default_variant (GHashTable *info)
1754 if (adium_info_get_version (info) <= 2) {
1755 return adium_info_get_no_variant_name (info);
1758 return tp_asv_get_string (info, "DefaultVariant");
1762 empathy_adium_info_get_available_variants (GHashTable *info)
1764 GPtrArray *variants;
1769 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1770 if (variants != NULL) {
1774 variants = g_ptr_array_new_with_free_func (g_free);
1775 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1776 G_TYPE_PTR_ARRAY, variants);
1778 path = tp_asv_get_string (info, "path");
1779 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1780 dir = g_dir_open (dirpath, 0, NULL);
1784 for (name = g_dir_read_name (dir);
1786 name = g_dir_read_name (dir)) {
1787 gchar *display_name;
1789 if (!g_str_has_suffix (name, ".css")) {
1793 display_name = g_strdup (name);
1794 strstr (display_name, ".css")[0] = '\0';
1795 g_ptr_array_add (variants, display_name);
1801 if (adium_info_get_version (info) <= 2) {
1802 g_ptr_array_add (variants,
1803 g_strdup (adium_info_get_no_variant_name (info)));
1810 empathy_adium_data_get_type (void)
1812 static GType type_id = 0;
1816 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1817 (GBoxedCopyFunc) empathy_adium_data_ref,
1818 (GBoxedFreeFunc) empathy_adium_data_unref);
1825 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1827 EmpathyAdiumData *data;
1828 gchar *template_html = NULL;
1829 gchar *footer_html = NULL;
1830 gchar *variant_path;
1833 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1835 data = g_slice_new0 (EmpathyAdiumData);
1836 data->ref_count = 1;
1837 data->path = g_strdup (path);
1838 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1839 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1840 data->info = g_hash_table_ref (info);
1841 data->version = adium_info_get_version (info);
1842 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1843 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1844 g_str_equal, g_free, g_free);
1846 DEBUG ("Loading theme at %s", path);
1848 #define LOAD(path, var) \
1849 tmp = g_build_filename (data->basedir, path, NULL); \
1850 g_file_get_contents (tmp, &var, NULL, NULL); \
1853 #define LOAD_CONST(path, var) \
1856 LOAD (path, content); \
1857 if (content != NULL) { \
1858 g_ptr_array_add (data->strings_to_free, content); \
1863 /* Load html files */
1864 LOAD_CONST ("Content.html", data->content_html);
1865 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1866 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1867 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1868 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1869 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1870 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1871 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1872 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1873 LOAD_CONST ("Status.html", data->status_html);
1874 LOAD ("Template.html", template_html);
1875 LOAD ("Footer.html", footer_html);
1880 /* HTML fallbacks: If we have at least content OR in_content, then
1881 * everything else gets a fallback */
1883 #define FALLBACK(html, fallback) \
1884 if (html == NULL) { \
1888 /* in_nextcontent -> in_content -> content */
1889 FALLBACK (data->in_content_html, data->content_html);
1890 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1892 /* context -> content */
1893 FALLBACK (data->in_context_html, data->in_content_html);
1894 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1895 FALLBACK (data->out_context_html, data->out_content_html);
1896 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1899 FALLBACK (data->out_content_html, data->in_content_html);
1900 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1901 FALLBACK (data->out_context_html, data->in_context_html);
1902 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1904 /* status -> in_content */
1905 FALLBACK (data->status_html, data->in_content_html);
1909 /* template -> empathy's template */
1910 data->custom_template = (template_html != NULL);
1911 if (template_html == NULL) {
1912 tmp = empathy_file_lookup ("Template.html", "data");
1913 g_file_get_contents (tmp, &template_html, NULL, NULL);
1917 /* Default avatar */
1918 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1919 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1920 data->default_incoming_avatar_filename = tmp;
1924 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1925 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1926 data->default_outgoing_avatar_filename = tmp;
1931 variant_path = adium_info_dup_path_for_variant (info,
1932 adium_info_get_default_or_first_variant (info));
1934 /* Old custom templates had only 4 parameters.
1935 * New templates have 5 parameters */
1936 if (data->version <= 2 && data->custom_template) {
1937 tmp = string_with_format (template_html,
1940 "", /* The header */
1941 footer_html ? footer_html : "",
1944 tmp = string_with_format (template_html,
1946 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1948 "", /* The header */
1949 footer_html ? footer_html : "",
1952 g_ptr_array_add (data->strings_to_free, tmp);
1953 data->template_html = tmp;
1955 g_free (template_html);
1956 g_free (footer_html);
1957 g_free (variant_path);
1963 empathy_adium_data_new (const gchar *path)
1965 EmpathyAdiumData *data;
1968 info = empathy_adium_info_new (path);
1969 data = empathy_adium_data_new_with_info (path, info);
1970 g_hash_table_unref (info);
1976 empathy_adium_data_ref (EmpathyAdiumData *data)
1978 g_return_val_if_fail (data != NULL, NULL);
1980 g_atomic_int_inc (&data->ref_count);
1986 empathy_adium_data_unref (EmpathyAdiumData *data)
1988 g_return_if_fail (data != NULL);
1990 if (g_atomic_int_dec_and_test (&data->ref_count)) {
1991 g_free (data->path);
1992 g_free (data->basedir);
1993 g_free (data->default_avatar_filename);
1994 g_free (data->default_incoming_avatar_filename);
1995 g_free (data->default_outgoing_avatar_filename);
1996 g_hash_table_unref (data->info);
1997 g_ptr_array_unref (data->strings_to_free);
1998 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2000 g_slice_free (EmpathyAdiumData, data);
2005 empathy_adium_data_get_info (EmpathyAdiumData *data)
2007 g_return_val_if_fail (data != NULL, NULL);
2013 empathy_adium_data_get_path (EmpathyAdiumData *data)
2015 g_return_val_if_fail (data != NULL, NULL);