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;
73 gboolean in_construction;
74 } EmpathyThemeAdiumPriv;
76 struct _EmpathyAdiumData {
80 gchar *default_avatar_filename;
81 gchar *default_incoming_avatar_filename;
82 gchar *default_outgoing_avatar_filename;
85 gboolean custom_template;
86 /* gchar* -> gchar* both owned */
87 GHashTable *date_format_cache;
90 const gchar *template_html;
91 const gchar *content_html;
92 const gchar *in_content_html;
93 const gchar *in_context_html;
94 const gchar *in_nextcontent_html;
95 const gchar *in_nextcontext_html;
96 const gchar *out_content_html;
97 const gchar *out_context_html;
98 const gchar *out_nextcontent_html;
99 const gchar *out_nextcontext_html;
100 const gchar *status_html;
102 /* Above html strings are pointers to strings stored in this array.
103 * We do this because of fallbacks, some htmls could be pointing the
105 GPtrArray *strings_to_free;
108 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
109 static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
117 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
118 WEBKIT_TYPE_WEB_VIEW,
119 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
120 theme_adium_iface_init));
123 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
125 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
126 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
127 gboolean enable_webkit_developer_tools;
129 enable_webkit_developer_tools = g_settings_get_boolean (
130 priv->gsettings_chat,
131 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
133 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
134 "enable-developer-extras",
135 enable_webkit_developer_tools,
140 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
144 EmpathyThemeAdium *theme = user_data;
146 theme_adium_update_enable_webkit_developer_tools (theme);
150 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
151 WebKitWebFrame *web_frame,
152 WebKitNetworkRequest *request,
153 WebKitWebNavigationAction *action,
154 WebKitWebPolicyDecision *decision,
159 /* Only call url_show on clicks */
160 if (webkit_web_navigation_action_get_reason (action) !=
161 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
162 webkit_web_policy_decision_use (decision);
166 uri = webkit_network_request_get_uri (request);
167 empathy_url_show (GTK_WIDGET (view), uri);
169 webkit_web_policy_decision_ignore (decision);
174 theme_adium_copy_address_cb (GtkMenuItem *menuitem,
177 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
179 GtkClipboard *clipboard;
181 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
183 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
184 gtk_clipboard_set_text (clipboard, uri, -1);
186 clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
187 gtk_clipboard_set_text (clipboard, uri, -1);
193 theme_adium_open_address_cb (GtkMenuItem *menuitem,
196 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
199 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
201 empathy_url_show (GTK_WIDGET (menuitem), uri);
206 /* Replace each %@ in format with string passed in args */
208 string_with_format (const gchar *format,
209 const gchar *first_string,
216 va_start (args, first_string);
217 result = g_string_sized_new (strlen (format));
218 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
221 next = strstr (format, "%@");
226 g_string_append_len (result, format, next - format);
227 g_string_append (result, str);
230 g_string_append (result, format);
233 return g_string_free (result, FALSE);
237 theme_adium_load_template (EmpathyThemeAdium *theme)
239 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
244 priv->pages_loading++;
245 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
246 variant_path = adium_info_dup_path_for_variant (priv->data->info,
248 template = string_with_format (priv->data->template_html,
250 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
251 template, basedir_uri);
252 g_free (basedir_uri);
253 g_free (variant_path);
258 theme_adium_match_newline (const gchar *text,
260 EmpathyStringReplace replace_func,
261 EmpathyStringParser *sub_parsers,
264 GString *string = user_data;
272 /* Replace \n by <br/> */
273 for (i = 0; i < len && text[i] != '\0'; i++) {
274 if (text[i] == '\n') {
275 empathy_string_parser_substr (text + prev,
276 i - prev, sub_parsers,
278 g_string_append (string, "<br/>");
282 empathy_string_parser_substr (text + prev, i - prev,
283 sub_parsers, user_data);
287 theme_adium_replace_smiley (const gchar *text,
292 EmpathySmileyHit *hit = match_data;
293 GString *string = user_data;
295 /* Replace smiley by a <img/> tag */
296 g_string_append_printf (string,
297 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
298 hit->path, (int)len, text, (int)len, text);
301 static EmpathyStringParser string_parsers[] = {
302 {empathy_string_match_link, empathy_string_replace_link},
303 {theme_adium_match_newline, NULL},
304 {empathy_string_match_all, empathy_string_replace_escaped},
308 static EmpathyStringParser string_parsers_with_smiley[] = {
309 {empathy_string_match_link, empathy_string_replace_link},
310 {empathy_string_match_smiley, theme_adium_replace_smiley},
311 {theme_adium_match_newline, NULL},
312 {empathy_string_match_all, empathy_string_replace_escaped},
317 theme_adium_parse_body (EmpathyThemeAdium *self,
321 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
322 EmpathyStringParser *parsers;
325 /* Check if we have to parse smileys */
326 if (g_settings_get_boolean (priv->gsettings_chat,
327 EMPATHY_PREFS_CHAT_SHOW_SMILEYS))
328 parsers = string_parsers_with_smiley;
330 parsers = string_parsers;
332 /* Parse text and construct string with links and smileys replaced
333 * by html tags. Also escape text to make sure html code is
334 * displayed verbatim. */
335 string = g_string_sized_new (strlen (text));
337 /* wrap this in HTML that allows us to find the message for later
339 if (!tp_str_empty (token))
340 g_string_append_printf (string,
341 "<span id=\"message-token-%s\">",
344 empathy_string_parser_substr (text, -1, parsers, string);
346 if (!tp_str_empty (token))
347 g_string_append (string, "</span>");
349 /* Wrap body in order to make tabs and multiple spaces displayed
350 * properly. See bug #625745. */
351 g_string_prepend (string, "<div style=\"display: inline; "
352 "white-space: pre-wrap\"'>");
353 g_string_append (string, "</div>");
355 return g_string_free (string, FALSE);
359 escape_and_append_len (GString *string, const gchar *str, gint len)
361 while (str != NULL && *str != '\0' && len != 0) {
365 g_string_append (string, "\\\\");
369 g_string_append (string, "\\\"");
372 /* Remove end of lines */
375 g_string_append_c (string, *str);
383 /* If *str starts with match, returns TRUE and move pointer to the end */
385 theme_adium_match (const gchar **str,
390 len = strlen (match);
391 if (strncmp (*str, match, len) == 0) {
399 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
401 theme_adium_match_with_format (const gchar **str,
405 const gchar *cur = *str;
408 if (!theme_adium_match (&cur, match)) {
413 end = strstr (cur, "}%");
418 *format = g_strndup (cur , end - cur);
423 /* List of colors used by %senderColor%. Copied from
424 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
426 static gchar *colors[] = {
427 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
428 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
429 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
430 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
431 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
432 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
433 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
434 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
435 "lightblue", "lightcoral",
436 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
437 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
438 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
439 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
440 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
441 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
442 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
443 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
444 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
445 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
450 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
452 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
453 * to strftime supported by g_date_time_format.
454 * FIXME: table is incomplete, doc of g_date_time_format has a table of
456 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
457 * in 2.29.x we have to explictely request padding with %0x */
458 static const gchar *convert_table[] = {
460 "A", NULL, // 0~86399999 (Millisecond of Day)
462 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
463 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
464 "cc", "%u", // 1~7 (Day of Week)
465 "c", "%u", // 1~7 (Day of Week)
467 "dd", "%d", // 1~31 (0 padded Day of Month)
468 "d", "%d", // 1~31 (0 padded Day of Month)
469 "D", "%j", // 1~366 (0 padded Day of Year)
471 "e", "%u", // 1~7 (0 padded Day of Week)
472 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
473 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
474 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
475 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
477 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
479 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
480 "GGGG", NULL, // Before Christ/Anno Domini
481 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
482 "GG", NULL, // BC/AD (Era Designator Abbreviated)
483 "G", NULL, // BC/AD (Era Designator Abbreviated)
485 "h", "%I", // 1~12 (0 padded Hour (12hr))
486 "H", "%H", // 0~23 (0 padded Hour (24hr))
488 "k", NULL, // 1~24 (0 padded Hour (24hr)
489 "K", NULL, // 0~11 (0 padded Hour (12hr))
491 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
492 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
493 "LL", "%m", // 1~12 (0 padded Month)
494 "L", "%m", // 1~12 (0 padded Month)
496 "m", "%M", // 0~59 (0 padded Minute)
497 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
498 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
499 "MM", "%m", // 1~12 (0 padded Month)
500 "M", "%m", // 1~12 (0 padded Month)
502 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
503 "qqq", NULL, // Q1/Q2/Q3/Q4
504 "qq", NULL, // 1~4 (0 padded Quarter)
505 "q", NULL, // 1~4 (0 padded Quarter)
506 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
507 "QQQ", NULL, // Q1/Q2/Q3/Q4
508 "QQ", NULL, // 1~4 (0 padded Quarter)
509 "Q", NULL, // 1~4 (0 padded Quarter)
511 "s", "%S", // 0~59 (0 padded Second)
512 "S", NULL, // (rounded Sub-Second)
514 "u", "%Y", // (0 padded Year)
516 "vvvv", "%Z", // (General GMT Timezone Name)
517 "vvv", "%Z", // (General GMT Timezone Abbreviation)
518 "vv", "%Z", // (General GMT Timezone Abbreviation)
519 "v", "%Z", // (General GMT Timezone Abbreviation)
521 "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)
522 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
524 "yyyy", "%Y", // (Full Year)
525 "yyy", "%y", // (2 Digits Year)
526 "yy", "%y", // (2 Digits Year)
527 "y", "%Y", // (Full Year)
528 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
529 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
530 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
531 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
533 "zzzz", NULL, // (Specific GMT Timezone Name)
534 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
535 "zz", NULL, // (Specific GMT Timezone Abbreviation)
536 "z", NULL, // (Specific GMT Timezone Abbreviation)
537 "Z", "%z", // +0000 (RFC 822 Timezone)
543 if (nsdate == NULL) {
547 str = g_hash_table_lookup (data->date_format_cache, nsdate);
552 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
553 * by corresponding strftime tag. */
554 string = g_string_sized_new (strlen (nsdate));
555 for (i = 0; nsdate[i] != '\0'; i++) {
556 gboolean found = FALSE;
558 /* even indexes are NSDateFormatter tag, odd indexes are the
559 * corresponding strftime tag */
560 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
561 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
567 /* If we don't have a replacement, just ignore that tag */
568 if (convert_table[j + 1] != NULL) {
569 g_string_append (string, convert_table[j + 1]);
571 i += strlen (convert_table[j]) - 1;
573 g_string_append_c (string, nsdate[i]);
577 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
579 /* The cache takes ownership of string->str */
580 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
581 return g_string_free (string, FALSE);
586 theme_adium_append_html (EmpathyThemeAdium *theme,
589 const gchar *message,
590 const gchar *avatar_filename,
592 const gchar *contact_id,
593 const gchar *service_name,
594 const gchar *message_classes,
598 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
600 const gchar *cur = NULL;
603 /* Make some search-and-replace in the html code */
604 string = g_string_sized_new (strlen (html) + strlen (message));
605 g_string_append_printf (string, "%s(\"", func);
606 for (cur = html; *cur != '\0'; cur++) {
607 const gchar *replace = NULL;
608 gchar *dup_replace = NULL;
609 gchar *format = NULL;
611 /* Those are all well known keywords that needs replacement in
612 * html files. Please keep them in the same order than the adium
613 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
614 if (theme_adium_match (&cur, "%userIconPath%")) {
615 replace = avatar_filename;
616 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
617 replace = contact_id;
618 } else if (theme_adium_match (&cur, "%sender%")) {
620 } else if (theme_adium_match (&cur, "%senderColor%")) {
621 /* A color derived from the user's name.
622 * FIXME: If a colon separated list of HTML colors is at
623 * Incoming/SenderColors.txt it will be used instead of
624 * the default colors.
626 if (contact_id != NULL) {
627 guint hash = g_str_hash (contact_id);
628 replace = colors[hash % G_N_ELEMENTS (colors)];
630 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
631 /* FIXME: The path to the status icon of the sender
632 * (available, away, etc...)
634 } else if (theme_adium_match (&cur, "%messageDirection%")) {
635 /* FIXME: The text direction of the message
636 * (either rtl or ltr)
638 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
639 /* FIXME: The serverside (remotely set) name of the
640 * sender, such as an MSN display name.
642 * We don't have access to that yet so we use
643 * local alias instead.
646 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
647 /* FIXME: This keyword is used to represent the
648 * highlight background color. "X" is the opacity of the
649 * background, ranges from 0 to 1 and can be any decimal
652 } else if (theme_adium_match (&cur, "%message%")) {
654 } else if (theme_adium_match (&cur, "%time%") ||
655 theme_adium_match_with_format (&cur, "%time{", &format)) {
656 const gchar *strftime_format;
658 strftime_format = nsdate_to_strftime (priv->data, format);
660 dup_replace = empathy_time_to_string_local (timestamp,
661 strftime_format ? strftime_format :
662 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
664 dup_replace = empathy_time_to_string_local (timestamp,
665 strftime_format ? strftime_format :
666 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
668 replace = dup_replace;
669 } else if (theme_adium_match (&cur, "%shortTime%")) {
670 dup_replace = empathy_time_to_string_local (timestamp,
671 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
672 replace = dup_replace;
673 } else if (theme_adium_match (&cur, "%service%")) {
674 replace = service_name;
675 } else if (theme_adium_match (&cur, "%variant%")) {
676 /* FIXME: The name of the active message style variant,
677 * with all spaces replaced with an underscore.
678 * A variant named "Alternating Messages - Blue Red"
679 * will become "Alternating_Messages_-_Blue_Red".
681 } else if (theme_adium_match (&cur, "%userIcons%")) {
682 /* FIXME: mus t be "hideIcons" if use preference is set
684 replace = "showIcons";
685 } else if (theme_adium_match (&cur, "%messageClasses%")) {
686 replace = message_classes;
687 } else if (theme_adium_match (&cur, "%status%")) {
688 /* FIXME: A description of the status event. This is
689 * neither in the user's local language nor expected to
690 * be displayed; it may be useful to use a different div
691 * class to present different types of status messages.
692 * The following is a list of some of the more important
693 * status messages; your message style should be able to
694 * handle being shown a status message not in this list,
695 * as even at present the list is incomplete and is
696 * certain to become out of date in the future:
705 * contact_joined (group chats)
709 * encryption (all OTR messages use this status)
710 * purple (all IRC topic and join/part messages use this status)
711 * fileTransferStarted
712 * fileTransferCompleted
715 escape_and_append_len (string, cur, 1);
719 /* Here we have a replacement to make */
720 escape_and_append_len (string, replace, -1);
722 g_free (dup_replace);
725 g_string_append (string, "\")");
727 script = g_string_free (string, FALSE);
728 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
733 theme_adium_append_event_escaped (EmpathyChatView *view,
734 const gchar *escaped)
736 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
737 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
739 theme_adium_append_html (theme, "appendMessage",
740 priv->data->status_html, escaped, NULL, NULL, NULL,
742 empathy_time_get_current (), FALSE);
744 /* There is no last contact */
745 if (priv->last_contact) {
746 g_object_unref (priv->last_contact);
747 priv->last_contact = NULL;
752 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
753 WebKitDOMNodeList *nodes)
757 /* Remove focus and firstFocus class */
758 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
759 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
760 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
762 gchar **classes, **iter;
763 GString *new_class_name;
764 gboolean first = TRUE;
766 if (element == NULL) {
770 class_name = webkit_dom_html_element_get_class_name (element);
771 classes = g_strsplit (class_name, " ", -1);
772 new_class_name = g_string_sized_new (strlen (class_name));
773 for (iter = classes; *iter != NULL; iter++) {
774 if (tp_strdiff (*iter, "focus") &&
775 tp_strdiff (*iter, "firstFocus")) {
777 g_string_append_c (new_class_name, ' ');
779 g_string_append (new_class_name, *iter);
784 webkit_dom_html_element_set_class_name (element, new_class_name->str);
787 g_strfreev (classes);
788 g_string_free (new_class_name, TRUE);
793 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
795 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
796 WebKitDOMDocument *dom;
797 WebKitDOMNodeList *nodes;
798 GError *error = NULL;
800 if (!priv->has_unread_message)
803 priv->has_unread_message = FALSE;
805 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
810 /* Get all nodes with focus class */
811 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
813 DEBUG ("Error getting focus nodes: %s",
814 error ? error->message : "No error");
815 g_clear_error (&error);
819 theme_adium_remove_focus_marks (theme, nodes);
823 theme_adium_append_message (EmpathyChatView *view,
826 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
827 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
828 EmpathyContact *sender;
833 const gchar *contact_id;
834 EmpathyAvatar *avatar;
835 const gchar *avatar_filename = NULL;
837 const gchar *html = NULL;
839 const gchar *service_name;
840 GString *message_classes = NULL;
842 gboolean consecutive;
845 if (priv->pages_loading != 0) {
846 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
847 g_value_set_object (value, msg);
848 g_queue_push_tail (&priv->message_queue, value);
852 /* Get information */
853 sender = empathy_message_get_sender (msg);
854 account = empathy_contact_get_account (sender);
855 service_name = empathy_protocol_name_to_display_name
856 (tp_account_get_protocol (account));
857 if (service_name == NULL)
858 service_name = tp_account_get_protocol (account);
859 timestamp = empathy_message_get_timestamp (msg);
860 body_escaped = theme_adium_parse_body (theme,
861 empathy_message_get_body (msg),
862 empathy_message_get_token (msg));
863 name = empathy_contact_get_alias (sender);
864 contact_id = empathy_contact_get_id (sender);
865 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
867 /* If this is a /me probably */
871 if (priv->data->version >= 4 || !priv->data->custom_template) {
872 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
873 "<span class='actionMessageBody'>%s</span>",
876 str = g_strdup_printf ("*%s*", body_escaped);
878 g_free (body_escaped);
882 /* Get the avatar filename, or a fallback */
883 avatar = empathy_contact_get_avatar (sender);
885 avatar_filename = avatar->filename;
887 if (!avatar_filename) {
888 if (empathy_contact_is_user (sender)) {
889 avatar_filename = priv->data->default_outgoing_avatar_filename;
891 avatar_filename = priv->data->default_incoming_avatar_filename;
893 if (!avatar_filename) {
894 if (!priv->data->default_avatar_filename) {
895 priv->data->default_avatar_filename =
896 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
897 GTK_ICON_SIZE_DIALOG);
899 avatar_filename = priv->data->default_avatar_filename;
903 /* We want to join this message with the last one if
904 * - senders are the same contact,
905 * - last message was recieved recently,
906 * - last message and this message both are/aren't backlog, and
907 * - DisableCombineConsecutive is not set in theme's settings */
908 is_backlog = empathy_message_is_backlog (msg);
909 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
910 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
911 (is_backlog == priv->last_is_backlog) &&
912 !tp_asv_get_boolean (priv->data->info,
913 "DisableCombineConsecutive", NULL);
915 /* Define message classes */
916 message_classes = g_string_new ("message");
917 if (!priv->has_focus && !is_backlog) {
918 if (!priv->has_unread_message) {
919 g_string_append (message_classes, " firstFocus");
920 priv->has_unread_message = TRUE;
922 g_string_append (message_classes, " focus");
925 g_string_append (message_classes, " history");
928 g_string_append (message_classes, " consecutive");
930 if (empathy_contact_is_user (sender)) {
931 g_string_append (message_classes, " outgoing");
933 g_string_append (message_classes, " incoming");
935 if (empathy_message_should_highlight (msg)) {
936 g_string_append (message_classes, " mention");
938 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
939 g_string_append (message_classes, " autoreply");
942 g_string_append (message_classes, " action");
944 /* FIXME: other classes:
945 * status - the message is a status change
946 * event - the message is a notification of something happening
947 * (for example, encryption being turned on)
948 * %status% - See %status% in theme_adium_append_html ()
951 /* This is slightly a hack, but it's the only way to add
952 * arbitrary data to messages in the HTML. We add another
953 * class called "x-empathy-message-id-*" to the message. This
954 * way, we can remove the unread marker for this specific
956 tp_msg = empathy_message_get_tp_message (msg);
957 if (tp_msg != NULL) {
958 gchar *tmp = tp_escape_as_identifier (
959 tp_message_get_token (tp_msg));
960 g_string_append_printf (message_classes,
961 " x-empathy-message-id-%s", tmp);
965 /* Define javascript function to use */
967 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
969 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
972 if (empathy_contact_is_user (sender)) {
976 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
979 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
982 /* remove all the unread marks when we are sending a message */
983 theme_adium_remove_all_focus_marks (theme);
988 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
991 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
995 theme_adium_append_html (theme, func, html, body_escaped,
996 avatar_filename, name, contact_id,
997 service_name, message_classes->str,
998 timestamp, is_backlog);
1000 /* Keep the sender of the last displayed message */
1001 if (priv->last_contact) {
1002 g_object_unref (priv->last_contact);
1004 priv->last_contact = g_object_ref (sender);
1005 priv->last_timestamp = timestamp;
1006 priv->last_is_backlog = is_backlog;
1008 g_free (body_escaped);
1009 g_string_free (message_classes, TRUE);
1013 theme_adium_append_event (EmpathyChatView *view,
1016 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1019 if (priv->pages_loading != 0) {
1020 g_queue_push_tail (&priv->message_queue,
1021 tp_g_value_slice_new_string (str));
1025 str_escaped = g_markup_escape_text (str, -1);
1026 theme_adium_append_event_escaped (view, str_escaped);
1027 g_free (str_escaped);
1031 theme_adium_edit_message (EmpathyChatView *view,
1032 EmpathyMessage *message)
1034 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1035 WebKitDOMDocument *doc;
1036 WebKitDOMElement *span;
1037 gchar *id, *parsed_body;
1038 GError *error = NULL;
1040 if (priv->pages_loading != 0) {
1041 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
1042 g_value_set_object (value, message);
1043 g_queue_push_tail (&priv->message_queue, value);
1047 id = g_strdup_printf ("message-token-%s",
1048 empathy_message_get_supersedes (message));
1049 /* we don't pass a token here, because doing so will return another
1050 * <span> element, and we don't want nested <span> elements */
1051 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1052 empathy_message_get_body (message), NULL);
1054 /* find the element */
1055 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1056 span = webkit_dom_document_get_element_by_id (doc, id);
1059 DEBUG ("Failed to find id '%s'", id);
1063 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1064 DEBUG ("Not a HTML element");
1068 /* update the HTML */
1069 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1070 parsed_body, &error);
1072 if (error != NULL) {
1073 DEBUG ("Error setting new inner-HTML: %s", error->message);
1074 g_error_free (error);
1080 DEBUG ("Could not find message to edit with: %s",
1081 empathy_message_get_body (message));
1085 g_free (parsed_body);
1089 theme_adium_scroll (EmpathyChatView *view,
1090 gboolean allow_scrolling)
1092 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1094 priv->allow_scrolling = allow_scrolling;
1095 if (allow_scrolling) {
1096 empathy_chat_view_scroll_down (view);
1101 theme_adium_scroll_down (EmpathyChatView *view)
1103 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1107 theme_adium_get_has_selection (EmpathyChatView *view)
1109 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1113 theme_adium_clear (EmpathyChatView *view)
1115 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1117 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1119 /* Clear last contact to avoid trying to add a 'joined'
1120 * message when we don't have an insertion point. */
1121 if (priv->last_contact) {
1122 g_object_unref (priv->last_contact);
1123 priv->last_contact = NULL;
1128 theme_adium_find_previous (EmpathyChatView *view,
1129 const gchar *search_criteria,
1130 gboolean new_search,
1131 gboolean match_case)
1133 /* FIXME: Doesn't respect new_search */
1134 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1135 search_criteria, match_case,
1140 theme_adium_find_next (EmpathyChatView *view,
1141 const gchar *search_criteria,
1142 gboolean new_search,
1143 gboolean match_case)
1145 /* FIXME: Doesn't respect new_search */
1146 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1147 search_criteria, match_case,
1152 theme_adium_find_abilities (EmpathyChatView *view,
1153 const gchar *search_criteria,
1154 gboolean match_case,
1155 gboolean *can_do_previous,
1156 gboolean *can_do_next)
1158 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1159 * find_next and find_previous to work around this problem. */
1160 if (can_do_previous)
1161 *can_do_previous = TRUE;
1163 *can_do_next = TRUE;
1167 theme_adium_highlight (EmpathyChatView *view,
1169 gboolean match_case)
1171 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1172 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1173 text, match_case, 0);
1174 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1179 theme_adium_copy_clipboard (EmpathyChatView *view)
1181 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1185 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1188 WebKitDOMDocument *dom;
1189 WebKitDOMNodeList *nodes;
1191 GError *error = NULL;
1193 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1198 tmp = tp_escape_as_identifier (token);
1199 class = g_strdup_printf (".x-empathy-message-id-%s", tmp);
1202 /* Get all nodes with focus class */
1203 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1206 if (nodes == NULL) {
1207 DEBUG ("Error getting focus nodes: %s",
1208 error ? error->message : "No error");
1209 g_clear_error (&error);
1213 theme_adium_remove_focus_marks (self, nodes);
1217 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1220 EmpathyThemeAdium *self = user_data;
1221 gchar *token = data;
1223 theme_adium_remove_mark_from_message (self, token);
1228 theme_adium_focus_toggled (EmpathyChatView *view,
1231 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1233 priv->has_focus = has_focus;
1234 if (!priv->has_focus) {
1235 /* We've lost focus, so let's make sure all the acked
1236 * messages have lost their unread marker. */
1237 g_queue_foreach (&priv->acked_messages,
1238 theme_adium_remove_acked_message_unread_mark_foreach,
1240 g_queue_clear (&priv->acked_messages);
1242 priv->has_unread_message = FALSE;
1247 theme_adium_message_acknowledged (EmpathyChatView *view,
1248 EmpathyMessage *message)
1250 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1251 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1254 tp_msg = empathy_message_get_tp_message (message);
1256 if (tp_msg == NULL) {
1260 /* We only want to actually remove the unread marker if the
1261 * view doesn't have focus. If we did it all the time we would
1262 * never see the unread markers, ever! So, we'll queue these
1263 * up, and when we lose focus, we'll remove the markers. */
1264 if (priv->has_focus) {
1265 g_queue_push_tail (&priv->acked_messages,
1266 g_strdup (tp_message_get_token (tp_msg)));
1270 theme_adium_remove_mark_from_message (self,
1271 tp_message_get_token (tp_msg));
1275 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1277 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1279 g_object_unref (hit_test_result);
1283 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1285 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1286 WebKitHitTestResult *hit_test_result;
1287 WebKitHitTestResultContext context;
1291 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1292 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1295 menu = empathy_context_menu_new (GTK_WIDGET (view));
1297 /* Select all item */
1298 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1299 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1301 g_signal_connect_swapped (item, "activate",
1302 G_CALLBACK (webkit_web_view_select_all),
1305 /* Copy menu item */
1306 if (webkit_web_view_can_copy_clipboard (view)) {
1307 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1308 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1310 g_signal_connect_swapped (item, "activate",
1311 G_CALLBACK (webkit_web_view_copy_clipboard),
1315 /* Clear menu item */
1316 item = gtk_separator_menu_item_new ();
1317 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1319 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1320 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1322 g_signal_connect_swapped (item, "activate",
1323 G_CALLBACK (empathy_chat_view_clear),
1326 /* We will only add the following menu items if we are
1327 * right-clicking a link */
1328 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1330 item = gtk_separator_menu_item_new ();
1331 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1333 /* Copy Link Address menu item */
1334 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1335 g_signal_connect (item, "activate",
1336 G_CALLBACK (theme_adium_copy_address_cb),
1338 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1340 /* Open Link menu item */
1341 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1342 g_signal_connect (item, "activate",
1343 G_CALLBACK (theme_adium_open_address_cb),
1345 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1348 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1349 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1352 /* Display the menu */
1353 gtk_widget_show_all (menu);
1354 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1355 event->button, event->time);
1359 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1361 if (event->button == 3) {
1362 gboolean developer_tools_enabled;
1364 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1365 "enable-developer-extras", &developer_tools_enabled, NULL);
1367 /* We currently have no way to add an inspector menu
1368 * item ourselves, so we disable our customized menu
1369 * if the developer extras are enabled. */
1370 if (!developer_tools_enabled) {
1371 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1376 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1380 theme_adium_iface_init (EmpathyChatViewIface *iface)
1382 iface->append_message = theme_adium_append_message;
1383 iface->append_event = theme_adium_append_event;
1384 iface->edit_message = theme_adium_edit_message;
1385 iface->scroll = theme_adium_scroll;
1386 iface->scroll_down = theme_adium_scroll_down;
1387 iface->get_has_selection = theme_adium_get_has_selection;
1388 iface->clear = theme_adium_clear;
1389 iface->find_previous = theme_adium_find_previous;
1390 iface->find_next = theme_adium_find_next;
1391 iface->find_abilities = theme_adium_find_abilities;
1392 iface->highlight = theme_adium_highlight;
1393 iface->copy_clipboard = theme_adium_copy_clipboard;
1394 iface->focus_toggled = theme_adium_focus_toggled;
1395 iface->message_acknowledged = theme_adium_message_acknowledged;
1399 theme_adium_load_finished_cb (WebKitWebView *view,
1400 WebKitWebFrame *frame,
1403 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1404 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1407 DEBUG ("Page loaded");
1408 priv->pages_loading--;
1410 if (priv->pages_loading != 0)
1413 /* Display queued messages */
1414 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1415 GValue *value = l->data;
1417 if (G_VALUE_HOLDS_OBJECT (value)) {
1418 EmpathyMessage *message = g_value_get_object (value);
1420 if (empathy_message_is_edit (message))
1421 theme_adium_edit_message (chat_view, message);
1423 theme_adium_append_message (chat_view, message);
1425 theme_adium_append_event (chat_view,
1426 g_value_get_string (value));
1429 tp_g_value_slice_free (value);
1432 g_queue_clear (&priv->message_queue);
1436 theme_adium_finalize (GObject *object)
1438 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1440 empathy_adium_data_unref (priv->data);
1441 g_object_unref (priv->gsettings_chat);
1443 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1447 theme_adium_dispose (GObject *object)
1449 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1451 if (priv->smiley_manager) {
1452 g_object_unref (priv->smiley_manager);
1453 priv->smiley_manager = NULL;
1456 if (priv->last_contact) {
1457 g_object_unref (priv->last_contact);
1458 priv->last_contact = NULL;
1461 if (priv->inspector_window) {
1462 gtk_widget_destroy (priv->inspector_window);
1463 priv->inspector_window = NULL;
1466 if (priv->acked_messages.length > 0) {
1467 g_queue_foreach (&priv->acked_messages, (GFunc) g_free, NULL);
1468 g_queue_clear (&priv->acked_messages);
1471 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1475 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1476 EmpathyThemeAdium *theme)
1478 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1480 if (priv->inspector_window) {
1481 gtk_widget_show_all (priv->inspector_window);
1488 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1489 EmpathyThemeAdium *theme)
1491 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1493 if (priv->inspector_window) {
1494 gtk_widget_hide (priv->inspector_window);
1500 static WebKitWebView *
1501 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1502 WebKitWebView *web_view,
1503 EmpathyThemeAdium *theme)
1505 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1506 GtkWidget *scrolled_window;
1507 GtkWidget *inspector_web_view;
1509 if (!priv->inspector_window) {
1510 /* Create main window */
1511 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1512 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1514 g_signal_connect (priv->inspector_window, "delete-event",
1515 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1517 /* Pack a scrolled window */
1518 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1519 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1520 GTK_POLICY_AUTOMATIC,
1521 GTK_POLICY_AUTOMATIC);
1522 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1524 gtk_widget_show (scrolled_window);
1526 /* Pack a webview in the scrolled window. That webview will be
1527 * used to render the inspector tool. */
1528 inspector_web_view = webkit_web_view_new ();
1529 gtk_container_add (GTK_CONTAINER (scrolled_window),
1530 inspector_web_view);
1531 gtk_widget_show (scrolled_window);
1533 return WEBKIT_WEB_VIEW (inspector_web_view);
1539 static PangoFontDescription *
1540 theme_adium_get_default_font (void)
1542 GSettings *gsettings;
1543 PangoFontDescription *pango_fd;
1546 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1548 font_family = g_settings_get_string (gsettings,
1549 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1551 if (font_family == NULL)
1554 pango_fd = pango_font_description_from_string (font_family);
1555 g_free (font_family);
1556 g_object_unref (gsettings);
1561 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1565 g_object_set (w_settings, "default-font-family", name, NULL);
1566 g_object_set (w_settings, "default-font-size", size, NULL);
1570 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1572 PangoFontDescription *default_font_desc;
1573 GdkScreen *current_screen;
1575 gint pango_font_size = 0;
1577 default_font_desc = theme_adium_get_default_font ();
1578 if (default_font_desc == NULL)
1580 pango_font_size = pango_font_description_get_size (default_font_desc)
1582 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1583 current_screen = gdk_screen_get_default ();
1584 if (current_screen != NULL) {
1585 dpi = gdk_screen_get_resolution (current_screen);
1587 dpi = BORING_DPI_DEFAULT;
1589 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1591 theme_adium_set_webkit_font (w_settings,
1592 pango_font_description_get_family (default_font_desc),
1594 pango_font_description_free (default_font_desc);
1598 theme_adium_constructed (GObject *object)
1600 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1601 const gchar *font_family = NULL;
1603 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1604 WebKitWebSettings *webkit_settings;
1605 WebKitWebInspector *webkit_inspector;
1607 /* Set default settings */
1608 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1609 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1610 webkit_settings = webkit_web_view_get_settings (webkit_view);
1612 if (font_family && font_size) {
1613 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1615 theme_adium_set_default_font (webkit_settings);
1618 /* Setup webkit inspector */
1619 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1620 g_signal_connect (webkit_inspector, "inspect-web-view",
1621 G_CALLBACK (theme_adium_inspect_web_view_cb),
1623 g_signal_connect (webkit_inspector, "show-window",
1624 G_CALLBACK (theme_adium_inspector_show_window_cb),
1626 g_signal_connect (webkit_inspector, "close-window",
1627 G_CALLBACK (theme_adium_inspector_close_window_cb),
1631 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1633 priv->in_construction = FALSE;
1637 theme_adium_get_property (GObject *object,
1642 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1645 case PROP_ADIUM_DATA:
1646 g_value_set_boxed (value, priv->data);
1649 g_value_set_string (value, priv->variant);
1652 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1658 theme_adium_set_property (GObject *object,
1660 const GValue *value,
1663 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1664 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1667 case PROP_ADIUM_DATA:
1668 g_assert (priv->data == NULL);
1669 priv->data = g_value_dup_boxed (value);
1672 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1675 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1681 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1683 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1684 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1686 object_class->finalize = theme_adium_finalize;
1687 object_class->dispose = theme_adium_dispose;
1688 object_class->constructed = theme_adium_constructed;
1689 object_class->get_property = theme_adium_get_property;
1690 object_class->set_property = theme_adium_set_property;
1692 widget_class->button_press_event = theme_adium_button_press_event;
1694 g_object_class_install_property (object_class,
1696 g_param_spec_boxed ("adium-data",
1698 "Data for the adium theme",
1699 EMPATHY_TYPE_ADIUM_DATA,
1700 G_PARAM_CONSTRUCT_ONLY |
1702 G_PARAM_STATIC_STRINGS));
1703 g_object_class_install_property (object_class,
1705 g_param_spec_string ("variant",
1706 "The theme variant",
1707 "Variant name for the theme",
1711 G_PARAM_STATIC_STRINGS));
1713 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1717 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1719 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1720 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1724 priv->in_construction = TRUE;
1725 g_queue_init (&priv->message_queue);
1726 priv->allow_scrolling = TRUE;
1727 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1729 g_signal_connect (theme, "load-finished",
1730 G_CALLBACK (theme_adium_load_finished_cb),
1732 g_signal_connect (theme, "navigation-policy-decision-requested",
1733 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1736 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1737 g_signal_connect (priv->gsettings_chat,
1738 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1739 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1742 theme_adium_update_enable_webkit_developer_tools (theme);
1746 empathy_theme_adium_new (EmpathyAdiumData *data,
1747 const gchar *variant)
1749 g_return_val_if_fail (data != NULL, NULL);
1751 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1758 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1759 const gchar *variant)
1761 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1762 gchar *variant_path;
1765 if (!tp_strdiff (priv->variant, variant)) {
1769 g_free (priv->variant);
1770 priv->variant = g_strdup (variant);
1772 if (priv->in_construction) {
1776 DEBUG ("Update view with variant: '%s'", variant);
1777 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1779 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1781 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1783 g_free (variant_path);
1786 g_object_notify (G_OBJECT (theme), "variant");
1790 empathy_adium_path_is_valid (const gchar *path)
1795 /* The theme is not valid if there is no Info.plist */
1796 file = g_build_filename (path, "Contents", "Info.plist",
1798 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1804 /* We ship a default Template.html as fallback if there is any problem
1805 * with the one inside the theme. The only other required file is
1806 * Content.html OR Incoming/Content.html*/
1807 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1809 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1813 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1814 "Content.html", NULL);
1815 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1823 empathy_adium_info_new (const gchar *path)
1827 GHashTable *info = NULL;
1829 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1831 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1832 value = empathy_plist_parse_from_file (file);
1838 info = g_value_dup_boxed (value);
1839 tp_g_value_slice_free (value);
1841 /* Insert the theme's path into the hash table,
1842 * keys have to be dupped */
1843 tp_asv_set_string (info, g_strdup ("path"), path);
1849 adium_info_get_version (GHashTable *info)
1851 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1854 static const gchar *
1855 adium_info_get_no_variant_name (GHashTable *info)
1857 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1858 return name ? name : _("Normal");
1862 adium_info_dup_path_for_variant (GHashTable *info,
1863 const gchar *variant)
1865 guint version = adium_info_get_version (info);
1866 const gchar *no_variant = adium_info_get_no_variant_name (info);
1867 GPtrArray *variants;
1870 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1871 return g_strdup ("main.css");
1874 /* Verify the variant exists, fallback to the first one */
1875 variants = empathy_adium_info_get_available_variants (info);
1876 for (i = 0; i < variants->len; i++) {
1877 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1881 if (i == variants->len) {
1882 DEBUG ("Variant %s does not exist", variant);
1883 variant = g_ptr_array_index (variants, 0);
1886 return g_strdup_printf ("Variants/%s.css", variant);
1891 empathy_adium_info_get_default_variant (GHashTable *info)
1893 if (adium_info_get_version (info) <= 2) {
1894 return adium_info_get_no_variant_name (info);
1897 return tp_asv_get_string (info, "DefaultVariant");
1901 empathy_adium_info_get_available_variants (GHashTable *info)
1903 GPtrArray *variants;
1908 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1909 if (variants != NULL) {
1913 variants = g_ptr_array_new_with_free_func (g_free);
1914 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1915 G_TYPE_PTR_ARRAY, variants);
1917 path = tp_asv_get_string (info, "path");
1918 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1919 dir = g_dir_open (dirpath, 0, NULL);
1923 for (name = g_dir_read_name (dir);
1925 name = g_dir_read_name (dir)) {
1926 gchar *display_name;
1928 if (!g_str_has_suffix (name, ".css")) {
1932 display_name = g_strdup (name);
1933 strstr (display_name, ".css")[0] = '\0';
1934 g_ptr_array_add (variants, display_name);
1940 if (adium_info_get_version (info) <= 2) {
1941 g_ptr_array_add (variants,
1942 g_strdup (adium_info_get_no_variant_name (info)));
1949 empathy_adium_data_get_type (void)
1951 static GType type_id = 0;
1955 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1956 (GBoxedCopyFunc) empathy_adium_data_ref,
1957 (GBoxedFreeFunc) empathy_adium_data_unref);
1964 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1966 EmpathyAdiumData *data;
1967 gchar *template_html = NULL;
1968 gchar *footer_html = NULL;
1971 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1973 data = g_slice_new0 (EmpathyAdiumData);
1974 data->ref_count = 1;
1975 data->path = g_strdup (path);
1976 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1977 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1978 data->info = g_hash_table_ref (info);
1979 data->version = adium_info_get_version (info);
1980 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1981 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1982 g_str_equal, g_free, g_free);
1984 DEBUG ("Loading theme at %s", path);
1986 #define LOAD(path, var) \
1987 tmp = g_build_filename (data->basedir, path, NULL); \
1988 g_file_get_contents (tmp, &var, NULL, NULL); \
1991 #define LOAD_CONST(path, var) \
1994 LOAD (path, content); \
1995 if (content != NULL) { \
1996 g_ptr_array_add (data->strings_to_free, content); \
2001 /* Load html files */
2002 LOAD_CONST ("Content.html", data->content_html);
2003 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2004 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2005 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2006 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2007 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2008 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2009 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2010 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2011 LOAD_CONST ("Status.html", data->status_html);
2012 LOAD ("Template.html", template_html);
2013 LOAD ("Footer.html", footer_html);
2018 /* HTML fallbacks: If we have at least content OR in_content, then
2019 * everything else gets a fallback */
2021 #define FALLBACK(html, fallback) \
2022 if (html == NULL) { \
2026 /* in_nextcontent -> in_content -> content */
2027 FALLBACK (data->in_content_html, data->content_html);
2028 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2030 /* context -> content */
2031 FALLBACK (data->in_context_html, data->in_content_html);
2032 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2033 FALLBACK (data->out_context_html, data->out_content_html);
2034 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2037 FALLBACK (data->out_content_html, data->in_content_html);
2038 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2039 FALLBACK (data->out_context_html, data->in_context_html);
2040 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2042 /* status -> in_content */
2043 FALLBACK (data->status_html, data->in_content_html);
2047 /* template -> empathy's template */
2048 data->custom_template = (template_html != NULL);
2049 if (template_html == NULL) {
2050 tmp = empathy_file_lookup ("Template.html", "data");
2051 g_file_get_contents (tmp, &template_html, NULL, NULL);
2055 /* Default avatar */
2056 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2057 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
2058 data->default_incoming_avatar_filename = tmp;
2062 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2063 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
2064 data->default_outgoing_avatar_filename = tmp;
2069 /* Old custom templates had only 4 parameters.
2070 * New templates have 5 parameters */
2071 if (data->version <= 2 && data->custom_template) {
2072 tmp = string_with_format (template_html,
2074 "%@", /* Leave variant unset */
2075 "", /* The header */
2076 footer_html ? footer_html : "",
2079 tmp = string_with_format (template_html,
2081 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2082 "%@", /* Leave variant unset */
2083 "", /* The header */
2084 footer_html ? footer_html : "",
2087 g_ptr_array_add (data->strings_to_free, tmp);
2088 data->template_html = tmp;
2090 g_free (template_html);
2091 g_free (footer_html);
2097 empathy_adium_data_new (const gchar *path)
2099 EmpathyAdiumData *data;
2102 info = empathy_adium_info_new (path);
2103 data = empathy_adium_data_new_with_info (path, info);
2104 g_hash_table_unref (info);
2110 empathy_adium_data_ref (EmpathyAdiumData *data)
2112 g_return_val_if_fail (data != NULL, NULL);
2114 g_atomic_int_inc (&data->ref_count);
2120 empathy_adium_data_unref (EmpathyAdiumData *data)
2122 g_return_if_fail (data != NULL);
2124 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2125 g_free (data->path);
2126 g_free (data->basedir);
2127 g_free (data->default_avatar_filename);
2128 g_free (data->default_incoming_avatar_filename);
2129 g_free (data->default_outgoing_avatar_filename);
2130 g_hash_table_unref (data->info);
2131 g_ptr_array_unref (data->strings_to_free);
2132 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2134 g_slice_free (EmpathyAdiumData, data);
2139 empathy_adium_data_get_info (EmpathyAdiumData *data)
2141 g_return_val_if_fail (data != NULL, NULL);
2147 empathy_adium_data_get_path (EmpathyAdiumData *data)
2149 g_return_val_if_fail (data != NULL, NULL);