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 gchar *tooltip, *timestamp;
1039 GtkIconInfo *icon_info;
1040 GError *error = NULL;
1042 if (priv->pages_loading != 0) {
1043 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
1044 g_value_set_object (value, message);
1045 g_queue_push_tail (&priv->message_queue, value);
1049 id = g_strdup_printf ("message-token-%s",
1050 empathy_message_get_supersedes (message));
1051 /* we don't pass a token here, because doing so will return another
1052 * <span> element, and we don't want nested <span> elements */
1053 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1054 empathy_message_get_body (message), NULL);
1056 /* find the element */
1057 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1058 span = webkit_dom_document_get_element_by_id (doc, id);
1061 DEBUG ("Failed to find id '%s'", id);
1065 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1066 DEBUG ("Not a HTML element");
1070 /* update the HTML */
1071 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1072 parsed_body, &error);
1074 if (error != NULL) {
1075 DEBUG ("Error setting new inner-HTML: %s", error->message);
1076 g_error_free (error);
1081 timestamp = empathy_time_to_string_local (
1082 empathy_message_get_timestamp (message),
1084 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1086 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1092 /* mark this message as edited */
1093 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1094 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1096 if (icon_info != NULL) {
1097 /* set the icon as a background image using CSS
1098 * FIXME: the icon won't update in response to theme changes */
1099 gchar *style = g_strdup_printf (
1100 "background-image:url('%s');"
1101 "background-repeat:no-repeat;"
1102 "background-position:left center;"
1103 "padding-left:19px;", /* 16px icon + 3px padding */
1104 gtk_icon_info_get_filename (icon_info));
1106 webkit_dom_element_set_attribute (span, "style", style, &error);
1108 if (error != NULL) {
1109 DEBUG ("Error setting element style: %s",
1111 g_clear_error (&error);
1116 gtk_icon_info_free (icon_info);
1122 DEBUG ("Could not find message to edit with: %s",
1123 empathy_message_get_body (message));
1127 g_free (parsed_body);
1131 theme_adium_scroll (EmpathyChatView *view,
1132 gboolean allow_scrolling)
1134 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1136 priv->allow_scrolling = allow_scrolling;
1137 if (allow_scrolling) {
1138 empathy_chat_view_scroll_down (view);
1143 theme_adium_scroll_down (EmpathyChatView *view)
1145 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1149 theme_adium_get_has_selection (EmpathyChatView *view)
1151 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1155 theme_adium_clear (EmpathyChatView *view)
1157 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1159 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1161 /* Clear last contact to avoid trying to add a 'joined'
1162 * message when we don't have an insertion point. */
1163 if (priv->last_contact) {
1164 g_object_unref (priv->last_contact);
1165 priv->last_contact = NULL;
1170 theme_adium_find_previous (EmpathyChatView *view,
1171 const gchar *search_criteria,
1172 gboolean new_search,
1173 gboolean match_case)
1175 /* FIXME: Doesn't respect new_search */
1176 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1177 search_criteria, match_case,
1182 theme_adium_find_next (EmpathyChatView *view,
1183 const gchar *search_criteria,
1184 gboolean new_search,
1185 gboolean match_case)
1187 /* FIXME: Doesn't respect new_search */
1188 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1189 search_criteria, match_case,
1194 theme_adium_find_abilities (EmpathyChatView *view,
1195 const gchar *search_criteria,
1196 gboolean match_case,
1197 gboolean *can_do_previous,
1198 gboolean *can_do_next)
1200 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1201 * find_next and find_previous to work around this problem. */
1202 if (can_do_previous)
1203 *can_do_previous = TRUE;
1205 *can_do_next = TRUE;
1209 theme_adium_highlight (EmpathyChatView *view,
1211 gboolean match_case)
1213 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1214 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1215 text, match_case, 0);
1216 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1221 theme_adium_copy_clipboard (EmpathyChatView *view)
1223 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1227 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1230 WebKitDOMDocument *dom;
1231 WebKitDOMNodeList *nodes;
1233 GError *error = NULL;
1235 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1240 tmp = tp_escape_as_identifier (token);
1241 class = g_strdup_printf (".x-empathy-message-id-%s", tmp);
1244 /* Get all nodes with focus class */
1245 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1248 if (nodes == NULL) {
1249 DEBUG ("Error getting focus nodes: %s",
1250 error ? error->message : "No error");
1251 g_clear_error (&error);
1255 theme_adium_remove_focus_marks (self, nodes);
1259 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1262 EmpathyThemeAdium *self = user_data;
1263 gchar *token = data;
1265 theme_adium_remove_mark_from_message (self, token);
1270 theme_adium_focus_toggled (EmpathyChatView *view,
1273 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1275 priv->has_focus = has_focus;
1276 if (!priv->has_focus) {
1277 /* We've lost focus, so let's make sure all the acked
1278 * messages have lost their unread marker. */
1279 g_queue_foreach (&priv->acked_messages,
1280 theme_adium_remove_acked_message_unread_mark_foreach,
1282 g_queue_clear (&priv->acked_messages);
1284 priv->has_unread_message = FALSE;
1289 theme_adium_message_acknowledged (EmpathyChatView *view,
1290 EmpathyMessage *message)
1292 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1293 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1296 tp_msg = empathy_message_get_tp_message (message);
1298 if (tp_msg == NULL) {
1302 /* We only want to actually remove the unread marker if the
1303 * view doesn't have focus. If we did it all the time we would
1304 * never see the unread markers, ever! So, we'll queue these
1305 * up, and when we lose focus, we'll remove the markers. */
1306 if (priv->has_focus) {
1307 g_queue_push_tail (&priv->acked_messages,
1308 g_strdup (tp_message_get_token (tp_msg)));
1312 theme_adium_remove_mark_from_message (self,
1313 tp_message_get_token (tp_msg));
1317 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1319 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1321 g_object_unref (hit_test_result);
1325 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1327 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1328 WebKitHitTestResult *hit_test_result;
1329 WebKitHitTestResultContext context;
1333 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1334 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1337 menu = empathy_context_menu_new (GTK_WIDGET (view));
1339 /* Select all item */
1340 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1341 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1343 g_signal_connect_swapped (item, "activate",
1344 G_CALLBACK (webkit_web_view_select_all),
1347 /* Copy menu item */
1348 if (webkit_web_view_can_copy_clipboard (view)) {
1349 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1350 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1352 g_signal_connect_swapped (item, "activate",
1353 G_CALLBACK (webkit_web_view_copy_clipboard),
1357 /* Clear menu item */
1358 item = gtk_separator_menu_item_new ();
1359 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1361 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1362 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1364 g_signal_connect_swapped (item, "activate",
1365 G_CALLBACK (empathy_chat_view_clear),
1368 /* We will only add the following menu items if we are
1369 * right-clicking a link */
1370 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1372 item = gtk_separator_menu_item_new ();
1373 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1375 /* Copy Link Address menu item */
1376 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1377 g_signal_connect (item, "activate",
1378 G_CALLBACK (theme_adium_copy_address_cb),
1380 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1382 /* Open Link menu item */
1383 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1384 g_signal_connect (item, "activate",
1385 G_CALLBACK (theme_adium_open_address_cb),
1387 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1390 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1391 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1394 /* Display the menu */
1395 gtk_widget_show_all (menu);
1396 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1397 event->button, event->time);
1401 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1403 if (event->button == 3) {
1404 gboolean developer_tools_enabled;
1406 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1407 "enable-developer-extras", &developer_tools_enabled, NULL);
1409 /* We currently have no way to add an inspector menu
1410 * item ourselves, so we disable our customized menu
1411 * if the developer extras are enabled. */
1412 if (!developer_tools_enabled) {
1413 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1418 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1422 theme_adium_iface_init (EmpathyChatViewIface *iface)
1424 iface->append_message = theme_adium_append_message;
1425 iface->append_event = theme_adium_append_event;
1426 iface->edit_message = theme_adium_edit_message;
1427 iface->scroll = theme_adium_scroll;
1428 iface->scroll_down = theme_adium_scroll_down;
1429 iface->get_has_selection = theme_adium_get_has_selection;
1430 iface->clear = theme_adium_clear;
1431 iface->find_previous = theme_adium_find_previous;
1432 iface->find_next = theme_adium_find_next;
1433 iface->find_abilities = theme_adium_find_abilities;
1434 iface->highlight = theme_adium_highlight;
1435 iface->copy_clipboard = theme_adium_copy_clipboard;
1436 iface->focus_toggled = theme_adium_focus_toggled;
1437 iface->message_acknowledged = theme_adium_message_acknowledged;
1441 theme_adium_load_finished_cb (WebKitWebView *view,
1442 WebKitWebFrame *frame,
1445 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1446 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1449 DEBUG ("Page loaded");
1450 priv->pages_loading--;
1452 if (priv->pages_loading != 0)
1455 /* Display queued messages */
1456 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1457 GValue *value = l->data;
1459 if (G_VALUE_HOLDS_OBJECT (value)) {
1460 EmpathyMessage *message = g_value_get_object (value);
1462 if (empathy_message_is_edit (message))
1463 theme_adium_edit_message (chat_view, message);
1465 theme_adium_append_message (chat_view, message);
1467 theme_adium_append_event (chat_view,
1468 g_value_get_string (value));
1471 tp_g_value_slice_free (value);
1474 g_queue_clear (&priv->message_queue);
1478 theme_adium_finalize (GObject *object)
1480 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1482 empathy_adium_data_unref (priv->data);
1483 g_object_unref (priv->gsettings_chat);
1485 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1489 theme_adium_dispose (GObject *object)
1491 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1493 if (priv->smiley_manager) {
1494 g_object_unref (priv->smiley_manager);
1495 priv->smiley_manager = NULL;
1498 if (priv->last_contact) {
1499 g_object_unref (priv->last_contact);
1500 priv->last_contact = NULL;
1503 if (priv->inspector_window) {
1504 gtk_widget_destroy (priv->inspector_window);
1505 priv->inspector_window = NULL;
1508 if (priv->acked_messages.length > 0) {
1509 g_queue_foreach (&priv->acked_messages, (GFunc) g_free, NULL);
1510 g_queue_clear (&priv->acked_messages);
1513 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1517 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1518 EmpathyThemeAdium *theme)
1520 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1522 if (priv->inspector_window) {
1523 gtk_widget_show_all (priv->inspector_window);
1530 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1531 EmpathyThemeAdium *theme)
1533 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1535 if (priv->inspector_window) {
1536 gtk_widget_hide (priv->inspector_window);
1542 static WebKitWebView *
1543 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1544 WebKitWebView *web_view,
1545 EmpathyThemeAdium *theme)
1547 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1548 GtkWidget *scrolled_window;
1549 GtkWidget *inspector_web_view;
1551 if (!priv->inspector_window) {
1552 /* Create main window */
1553 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1554 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1556 g_signal_connect (priv->inspector_window, "delete-event",
1557 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1559 /* Pack a scrolled window */
1560 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1561 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1562 GTK_POLICY_AUTOMATIC,
1563 GTK_POLICY_AUTOMATIC);
1564 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1566 gtk_widget_show (scrolled_window);
1568 /* Pack a webview in the scrolled window. That webview will be
1569 * used to render the inspector tool. */
1570 inspector_web_view = webkit_web_view_new ();
1571 gtk_container_add (GTK_CONTAINER (scrolled_window),
1572 inspector_web_view);
1573 gtk_widget_show (scrolled_window);
1575 return WEBKIT_WEB_VIEW (inspector_web_view);
1581 static PangoFontDescription *
1582 theme_adium_get_default_font (void)
1584 GSettings *gsettings;
1585 PangoFontDescription *pango_fd;
1588 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1590 font_family = g_settings_get_string (gsettings,
1591 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1593 if (font_family == NULL)
1596 pango_fd = pango_font_description_from_string (font_family);
1597 g_free (font_family);
1598 g_object_unref (gsettings);
1603 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1607 g_object_set (w_settings, "default-font-family", name, NULL);
1608 g_object_set (w_settings, "default-font-size", size, NULL);
1612 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1614 PangoFontDescription *default_font_desc;
1615 GdkScreen *current_screen;
1617 gint pango_font_size = 0;
1619 default_font_desc = theme_adium_get_default_font ();
1620 if (default_font_desc == NULL)
1622 pango_font_size = pango_font_description_get_size (default_font_desc)
1624 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1625 current_screen = gdk_screen_get_default ();
1626 if (current_screen != NULL) {
1627 dpi = gdk_screen_get_resolution (current_screen);
1629 dpi = BORING_DPI_DEFAULT;
1631 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1633 theme_adium_set_webkit_font (w_settings,
1634 pango_font_description_get_family (default_font_desc),
1636 pango_font_description_free (default_font_desc);
1640 theme_adium_constructed (GObject *object)
1642 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1643 const gchar *font_family = NULL;
1645 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1646 WebKitWebSettings *webkit_settings;
1647 WebKitWebInspector *webkit_inspector;
1649 /* Set default settings */
1650 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1651 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1652 webkit_settings = webkit_web_view_get_settings (webkit_view);
1654 if (font_family && font_size) {
1655 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1657 theme_adium_set_default_font (webkit_settings);
1660 /* Setup webkit inspector */
1661 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1662 g_signal_connect (webkit_inspector, "inspect-web-view",
1663 G_CALLBACK (theme_adium_inspect_web_view_cb),
1665 g_signal_connect (webkit_inspector, "show-window",
1666 G_CALLBACK (theme_adium_inspector_show_window_cb),
1668 g_signal_connect (webkit_inspector, "close-window",
1669 G_CALLBACK (theme_adium_inspector_close_window_cb),
1673 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1675 priv->in_construction = FALSE;
1679 theme_adium_get_property (GObject *object,
1684 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1687 case PROP_ADIUM_DATA:
1688 g_value_set_boxed (value, priv->data);
1691 g_value_set_string (value, priv->variant);
1694 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1700 theme_adium_set_property (GObject *object,
1702 const GValue *value,
1705 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1706 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1709 case PROP_ADIUM_DATA:
1710 g_assert (priv->data == NULL);
1711 priv->data = g_value_dup_boxed (value);
1714 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1717 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1723 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1725 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1726 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1728 object_class->finalize = theme_adium_finalize;
1729 object_class->dispose = theme_adium_dispose;
1730 object_class->constructed = theme_adium_constructed;
1731 object_class->get_property = theme_adium_get_property;
1732 object_class->set_property = theme_adium_set_property;
1734 widget_class->button_press_event = theme_adium_button_press_event;
1736 g_object_class_install_property (object_class,
1738 g_param_spec_boxed ("adium-data",
1740 "Data for the adium theme",
1741 EMPATHY_TYPE_ADIUM_DATA,
1742 G_PARAM_CONSTRUCT_ONLY |
1744 G_PARAM_STATIC_STRINGS));
1745 g_object_class_install_property (object_class,
1747 g_param_spec_string ("variant",
1748 "The theme variant",
1749 "Variant name for the theme",
1753 G_PARAM_STATIC_STRINGS));
1755 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1759 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1761 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1762 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1766 priv->in_construction = TRUE;
1767 g_queue_init (&priv->message_queue);
1768 priv->allow_scrolling = TRUE;
1769 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1771 g_signal_connect (theme, "load-finished",
1772 G_CALLBACK (theme_adium_load_finished_cb),
1774 g_signal_connect (theme, "navigation-policy-decision-requested",
1775 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1778 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1779 g_signal_connect (priv->gsettings_chat,
1780 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1781 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1784 theme_adium_update_enable_webkit_developer_tools (theme);
1788 empathy_theme_adium_new (EmpathyAdiumData *data,
1789 const gchar *variant)
1791 g_return_val_if_fail (data != NULL, NULL);
1793 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1800 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1801 const gchar *variant)
1803 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1804 gchar *variant_path;
1807 if (!tp_strdiff (priv->variant, variant)) {
1811 g_free (priv->variant);
1812 priv->variant = g_strdup (variant);
1814 if (priv->in_construction) {
1818 DEBUG ("Update view with variant: '%s'", variant);
1819 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1821 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1823 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1825 g_free (variant_path);
1828 g_object_notify (G_OBJECT (theme), "variant");
1832 empathy_adium_path_is_valid (const gchar *path)
1837 /* The theme is not valid if there is no Info.plist */
1838 file = g_build_filename (path, "Contents", "Info.plist",
1840 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1846 /* We ship a default Template.html as fallback if there is any problem
1847 * with the one inside the theme. The only other required file is
1848 * Content.html OR Incoming/Content.html*/
1849 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1851 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1855 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1856 "Content.html", NULL);
1857 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1865 empathy_adium_info_new (const gchar *path)
1869 GHashTable *info = NULL;
1871 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1873 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1874 value = empathy_plist_parse_from_file (file);
1880 info = g_value_dup_boxed (value);
1881 tp_g_value_slice_free (value);
1883 /* Insert the theme's path into the hash table,
1884 * keys have to be dupped */
1885 tp_asv_set_string (info, g_strdup ("path"), path);
1891 adium_info_get_version (GHashTable *info)
1893 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1896 static const gchar *
1897 adium_info_get_no_variant_name (GHashTable *info)
1899 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1900 return name ? name : _("Normal");
1904 adium_info_dup_path_for_variant (GHashTable *info,
1905 const gchar *variant)
1907 guint version = adium_info_get_version (info);
1908 const gchar *no_variant = adium_info_get_no_variant_name (info);
1909 GPtrArray *variants;
1912 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1913 return g_strdup ("main.css");
1916 /* Verify the variant exists, fallback to the first one */
1917 variants = empathy_adium_info_get_available_variants (info);
1918 for (i = 0; i < variants->len; i++) {
1919 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1923 if (i == variants->len) {
1924 DEBUG ("Variant %s does not exist", variant);
1925 variant = g_ptr_array_index (variants, 0);
1928 return g_strdup_printf ("Variants/%s.css", variant);
1933 empathy_adium_info_get_default_variant (GHashTable *info)
1935 if (adium_info_get_version (info) <= 2) {
1936 return adium_info_get_no_variant_name (info);
1939 return tp_asv_get_string (info, "DefaultVariant");
1943 empathy_adium_info_get_available_variants (GHashTable *info)
1945 GPtrArray *variants;
1950 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1951 if (variants != NULL) {
1955 variants = g_ptr_array_new_with_free_func (g_free);
1956 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1957 G_TYPE_PTR_ARRAY, variants);
1959 path = tp_asv_get_string (info, "path");
1960 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1961 dir = g_dir_open (dirpath, 0, NULL);
1965 for (name = g_dir_read_name (dir);
1967 name = g_dir_read_name (dir)) {
1968 gchar *display_name;
1970 if (!g_str_has_suffix (name, ".css")) {
1974 display_name = g_strdup (name);
1975 strstr (display_name, ".css")[0] = '\0';
1976 g_ptr_array_add (variants, display_name);
1982 if (adium_info_get_version (info) <= 2) {
1983 g_ptr_array_add (variants,
1984 g_strdup (adium_info_get_no_variant_name (info)));
1991 empathy_adium_data_get_type (void)
1993 static GType type_id = 0;
1997 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1998 (GBoxedCopyFunc) empathy_adium_data_ref,
1999 (GBoxedFreeFunc) empathy_adium_data_unref);
2006 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
2008 EmpathyAdiumData *data;
2009 gchar *template_html = NULL;
2010 gchar *footer_html = NULL;
2013 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2015 data = g_slice_new0 (EmpathyAdiumData);
2016 data->ref_count = 1;
2017 data->path = g_strdup (path);
2018 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2019 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2020 data->info = g_hash_table_ref (info);
2021 data->version = adium_info_get_version (info);
2022 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2023 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2024 g_str_equal, g_free, g_free);
2026 DEBUG ("Loading theme at %s", path);
2028 #define LOAD(path, var) \
2029 tmp = g_build_filename (data->basedir, path, NULL); \
2030 g_file_get_contents (tmp, &var, NULL, NULL); \
2033 #define LOAD_CONST(path, var) \
2036 LOAD (path, content); \
2037 if (content != NULL) { \
2038 g_ptr_array_add (data->strings_to_free, content); \
2043 /* Load html files */
2044 LOAD_CONST ("Content.html", data->content_html);
2045 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2046 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2047 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2048 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2049 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2050 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2051 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2052 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2053 LOAD_CONST ("Status.html", data->status_html);
2054 LOAD ("Template.html", template_html);
2055 LOAD ("Footer.html", footer_html);
2060 /* HTML fallbacks: If we have at least content OR in_content, then
2061 * everything else gets a fallback */
2063 #define FALLBACK(html, fallback) \
2064 if (html == NULL) { \
2068 /* in_nextcontent -> in_content -> content */
2069 FALLBACK (data->in_content_html, data->content_html);
2070 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2072 /* context -> content */
2073 FALLBACK (data->in_context_html, data->in_content_html);
2074 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2075 FALLBACK (data->out_context_html, data->out_content_html);
2076 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2079 FALLBACK (data->out_content_html, data->in_content_html);
2080 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2081 FALLBACK (data->out_context_html, data->in_context_html);
2082 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2084 /* status -> in_content */
2085 FALLBACK (data->status_html, data->in_content_html);
2089 /* template -> empathy's template */
2090 data->custom_template = (template_html != NULL);
2091 if (template_html == NULL) {
2092 tmp = empathy_file_lookup ("Template.html", "data");
2093 g_file_get_contents (tmp, &template_html, NULL, NULL);
2097 /* Default avatar */
2098 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2099 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
2100 data->default_incoming_avatar_filename = tmp;
2104 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2105 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
2106 data->default_outgoing_avatar_filename = tmp;
2111 /* Old custom templates had only 4 parameters.
2112 * New templates have 5 parameters */
2113 if (data->version <= 2 && data->custom_template) {
2114 tmp = string_with_format (template_html,
2116 "%@", /* Leave variant unset */
2117 "", /* The header */
2118 footer_html ? footer_html : "",
2121 tmp = string_with_format (template_html,
2123 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2124 "%@", /* Leave variant unset */
2125 "", /* The header */
2126 footer_html ? footer_html : "",
2129 g_ptr_array_add (data->strings_to_free, tmp);
2130 data->template_html = tmp;
2132 g_free (template_html);
2133 g_free (footer_html);
2139 empathy_adium_data_new (const gchar *path)
2141 EmpathyAdiumData *data;
2144 info = empathy_adium_info_new (path);
2145 data = empathy_adium_data_new_with_info (path, info);
2146 g_hash_table_unref (info);
2152 empathy_adium_data_ref (EmpathyAdiumData *data)
2154 g_return_val_if_fail (data != NULL, NULL);
2156 g_atomic_int_inc (&data->ref_count);
2162 empathy_adium_data_unref (EmpathyAdiumData *data)
2164 g_return_if_fail (data != NULL);
2166 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2167 g_free (data->path);
2168 g_free (data->basedir);
2169 g_free (data->default_avatar_filename);
2170 g_free (data->default_incoming_avatar_filename);
2171 g_free (data->default_outgoing_avatar_filename);
2172 g_hash_table_unref (data->info);
2173 g_ptr_array_unref (data->strings_to_free);
2174 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2176 g_slice_free (EmpathyAdiumData, data);
2181 empathy_adium_data_get_info (EmpathyAdiumData *data)
2183 g_return_val_if_fail (data != NULL, NULL);
2189 empathy_adium_data_get_path (EmpathyAdiumData *data)
2191 g_return_val_if_fail (data != NULL, NULL);