1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2008-2009 Collabora Ltd.
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 * Authors: Xavier Claessens <xclaesse@gmail.com>
25 #include <glib/gi18n-lib.h>
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
31 #include <pango/pango.h>
34 #include <libempathy/empathy-gsettings.h>
35 #include <libempathy/empathy-time.h>
36 #include <libempathy/empathy-utils.h>
38 #include "empathy-theme-adium.h"
39 #include "empathy-smiley-manager.h"
40 #include "empathy-ui-utils.h"
41 #include "empathy-plist.h"
42 #include "empathy-images.h"
43 #include "empathy-webkit-utils.h"
45 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
46 #include <libempathy/empathy-debug.h>
48 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
50 #define BORING_DPI_DEFAULT 96
52 /* "Join" consecutive messages with timestamps within five minutes */
53 #define MESSAGE_JOIN_PERIOD 5*60
56 EmpathyAdiumData *data;
57 EmpathySmileyManager *smiley_manager;
58 EmpathyContact *last_contact;
59 gint64 last_timestamp;
60 gboolean last_is_backlog;
62 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
64 /* Queue of guint32 of pending message id to remove unread
65 * marker for when we lose focus. */
66 GQueue acked_messages;
67 GtkWidget *inspector_window;
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));
135 queue_item (GQueue *queue,
140 QueuedItem *item = g_slice_new0 (QueuedItem);
144 item->msg = g_object_ref (msg);
145 item->str = g_strdup (str);
147 g_queue_push_tail (queue, item);
153 free_queued_item (QueuedItem *item)
155 tp_clear_object (&item->msg);
158 g_slice_free (QueuedItem, item);
162 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
164 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
165 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
166 gboolean enable_webkit_developer_tools;
168 enable_webkit_developer_tools = g_settings_get_boolean (
169 priv->gsettings_chat,
170 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
172 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
173 "enable-developer-extras",
174 enable_webkit_developer_tools,
179 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
183 EmpathyThemeAdium *theme = user_data;
185 theme_adium_update_enable_webkit_developer_tools (theme);
189 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
190 WebKitWebFrame *web_frame,
191 WebKitNetworkRequest *request,
192 WebKitWebNavigationAction *action,
193 WebKitWebPolicyDecision *decision,
198 /* Only call url_show on clicks */
199 if (webkit_web_navigation_action_get_reason (action) !=
200 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
201 webkit_web_policy_decision_use (decision);
205 uri = webkit_network_request_get_uri (request);
206 empathy_url_show (GTK_WIDGET (view), uri);
208 webkit_web_policy_decision_ignore (decision);
213 theme_adium_copy_address_cb (GtkMenuItem *menuitem,
216 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
218 GtkClipboard *clipboard;
220 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
222 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
223 gtk_clipboard_set_text (clipboard, uri, -1);
225 clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
226 gtk_clipboard_set_text (clipboard, uri, -1);
232 theme_adium_open_address_cb (GtkMenuItem *menuitem,
235 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
238 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
240 empathy_url_show (GTK_WIDGET (menuitem), uri);
245 /* Replace each %@ in format with string passed in args */
247 string_with_format (const gchar *format,
248 const gchar *first_string,
255 va_start (args, first_string);
256 result = g_string_sized_new (strlen (format));
257 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
260 next = strstr (format, "%@");
265 g_string_append_len (result, format, next - format);
266 g_string_append (result, str);
269 g_string_append (result, format);
272 return g_string_free (result, FALSE);
276 theme_adium_load_template (EmpathyThemeAdium *theme)
278 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
283 priv->pages_loading++;
284 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
285 variant_path = adium_info_dup_path_for_variant (priv->data->info,
287 template = string_with_format (priv->data->template_html,
289 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
290 template, basedir_uri);
291 g_free (basedir_uri);
292 g_free (variant_path);
297 theme_adium_parse_body (EmpathyThemeAdium *self,
301 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
302 EmpathyStringParser *parsers;
305 /* Check if we have to parse smileys */
306 parsers = empathy_webkit_get_string_parser (
307 g_settings_get_boolean (priv->gsettings_chat,
308 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
310 /* Parse text and construct string with links and smileys replaced
311 * by html tags. Also escape text to make sure html code is
312 * displayed verbatim. */
313 string = g_string_sized_new (strlen (text));
315 /* wrap this in HTML that allows us to find the message for later
317 if (!tp_str_empty (token))
318 g_string_append_printf (string,
319 "<span id=\"message-token-%s\">",
322 empathy_string_parser_substr (text, -1, parsers, string);
324 if (!tp_str_empty (token))
325 g_string_append (string, "</span>");
327 /* Wrap body in order to make tabs and multiple spaces displayed
328 * properly. See bug #625745. */
329 g_string_prepend (string, "<div style=\"display: inline; "
330 "white-space: pre-wrap\"'>");
331 g_string_append (string, "</div>");
333 return g_string_free (string, FALSE);
337 escape_and_append_len (GString *string, const gchar *str, gint len)
339 while (str != NULL && *str != '\0' && len != 0) {
343 g_string_append (string, "\\\\");
347 g_string_append (string, "\\\"");
350 /* Remove end of lines */
353 g_string_append_c (string, *str);
361 /* If *str starts with match, returns TRUE and move pointer to the end */
363 theme_adium_match (const gchar **str,
368 len = strlen (match);
369 if (strncmp (*str, match, len) == 0) {
377 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
379 theme_adium_match_with_format (const gchar **str,
383 const gchar *cur = *str;
386 if (!theme_adium_match (&cur, match)) {
391 end = strstr (cur, "}%");
396 *format = g_strndup (cur , end - cur);
401 /* List of colors used by %senderColor%. Copied from
402 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
404 static gchar *colors[] = {
405 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
406 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
407 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
408 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
409 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
410 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
411 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
412 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
413 "lightblue", "lightcoral",
414 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
415 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
416 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
417 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
418 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
419 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
420 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
421 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
422 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
423 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
428 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
430 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
431 * to strftime supported by g_date_time_format.
432 * FIXME: table is incomplete, doc of g_date_time_format has a table of
434 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
435 * in 2.29.x we have to explictely request padding with %0x */
436 static const gchar *convert_table[] = {
438 "A", NULL, // 0~86399999 (Millisecond of Day)
440 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
441 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
442 "cc", "%u", // 1~7 (Day of Week)
443 "c", "%u", // 1~7 (Day of Week)
445 "dd", "%d", // 1~31 (0 padded Day of Month)
446 "d", "%d", // 1~31 (0 padded Day of Month)
447 "D", "%j", // 1~366 (0 padded Day of Year)
449 "e", "%u", // 1~7 (0 padded Day of Week)
450 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
451 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
452 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
453 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
455 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
457 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
458 "GGGG", NULL, // Before Christ/Anno Domini
459 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
460 "GG", NULL, // BC/AD (Era Designator Abbreviated)
461 "G", NULL, // BC/AD (Era Designator Abbreviated)
463 "h", "%I", // 1~12 (0 padded Hour (12hr))
464 "H", "%H", // 0~23 (0 padded Hour (24hr))
466 "k", NULL, // 1~24 (0 padded Hour (24hr)
467 "K", NULL, // 0~11 (0 padded Hour (12hr))
469 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
470 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
471 "LL", "%m", // 1~12 (0 padded Month)
472 "L", "%m", // 1~12 (0 padded Month)
474 "m", "%M", // 0~59 (0 padded Minute)
475 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
476 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
477 "MM", "%m", // 1~12 (0 padded Month)
478 "M", "%m", // 1~12 (0 padded Month)
480 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
481 "qqq", NULL, // Q1/Q2/Q3/Q4
482 "qq", NULL, // 1~4 (0 padded Quarter)
483 "q", NULL, // 1~4 (0 padded Quarter)
484 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
485 "QQQ", NULL, // Q1/Q2/Q3/Q4
486 "QQ", NULL, // 1~4 (0 padded Quarter)
487 "Q", NULL, // 1~4 (0 padded Quarter)
489 "s", "%S", // 0~59 (0 padded Second)
490 "S", NULL, // (rounded Sub-Second)
492 "u", "%Y", // (0 padded Year)
494 "vvvv", "%Z", // (General GMT Timezone Name)
495 "vvv", "%Z", // (General GMT Timezone Abbreviation)
496 "vv", "%Z", // (General GMT Timezone Abbreviation)
497 "v", "%Z", // (General GMT Timezone Abbreviation)
499 "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)
500 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
502 "yyyy", "%Y", // (Full Year)
503 "yyy", "%y", // (2 Digits Year)
504 "yy", "%y", // (2 Digits Year)
505 "y", "%Y", // (Full Year)
506 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
507 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
508 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
509 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
511 "zzzz", NULL, // (Specific GMT Timezone Name)
512 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
513 "zz", NULL, // (Specific GMT Timezone Abbreviation)
514 "z", NULL, // (Specific GMT Timezone Abbreviation)
515 "Z", "%z", // +0000 (RFC 822 Timezone)
521 if (nsdate == NULL) {
525 str = g_hash_table_lookup (data->date_format_cache, nsdate);
530 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
531 * by corresponding strftime tag. */
532 string = g_string_sized_new (strlen (nsdate));
533 for (i = 0; nsdate[i] != '\0'; i++) {
534 gboolean found = FALSE;
536 /* even indexes are NSDateFormatter tag, odd indexes are the
537 * corresponding strftime tag */
538 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
539 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
545 /* If we don't have a replacement, just ignore that tag */
546 if (convert_table[j + 1] != NULL) {
547 g_string_append (string, convert_table[j + 1]);
549 i += strlen (convert_table[j]) - 1;
551 g_string_append_c (string, nsdate[i]);
555 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
557 /* The cache takes ownership of string->str */
558 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
559 return g_string_free (string, FALSE);
564 theme_adium_append_html (EmpathyThemeAdium *theme,
567 const gchar *message,
568 const gchar *avatar_filename,
570 const gchar *contact_id,
571 const gchar *service_name,
572 const gchar *message_classes,
576 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
578 const gchar *cur = NULL;
581 /* Make some search-and-replace in the html code */
582 string = g_string_sized_new (strlen (html) + strlen (message));
583 g_string_append_printf (string, "%s(\"", func);
584 for (cur = html; *cur != '\0'; cur++) {
585 const gchar *replace = NULL;
586 gchar *dup_replace = NULL;
587 gchar *format = NULL;
589 /* Those are all well known keywords that needs replacement in
590 * html files. Please keep them in the same order than the adium
591 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
592 if (theme_adium_match (&cur, "%userIconPath%")) {
593 replace = avatar_filename;
594 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
595 replace = contact_id;
596 } else if (theme_adium_match (&cur, "%sender%")) {
598 } else if (theme_adium_match (&cur, "%senderColor%")) {
599 /* A color derived from the user's name.
600 * FIXME: If a colon separated list of HTML colors is at
601 * Incoming/SenderColors.txt it will be used instead of
602 * the default colors.
604 if (contact_id != NULL) {
605 guint hash = g_str_hash (contact_id);
606 replace = colors[hash % G_N_ELEMENTS (colors)];
608 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
609 /* FIXME: The path to the status icon of the sender
610 * (available, away, etc...)
612 } else if (theme_adium_match (&cur, "%messageDirection%")) {
613 /* FIXME: The text direction of the message
614 * (either rtl or ltr)
616 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
617 /* FIXME: The serverside (remotely set) name of the
618 * sender, such as an MSN display name.
620 * We don't have access to that yet so we use
621 * local alias instead.
624 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
625 /* FIXME: This keyword is used to represent the
626 * highlight background color. "X" is the opacity of the
627 * background, ranges from 0 to 1 and can be any decimal
630 } else if (theme_adium_match (&cur, "%message%")) {
632 } else if (theme_adium_match (&cur, "%time%") ||
633 theme_adium_match_with_format (&cur, "%time{", &format)) {
634 const gchar *strftime_format;
636 strftime_format = nsdate_to_strftime (priv->data, format);
638 dup_replace = empathy_time_to_string_local (timestamp,
639 strftime_format ? strftime_format :
640 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
642 dup_replace = empathy_time_to_string_local (timestamp,
643 strftime_format ? strftime_format :
644 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
646 replace = dup_replace;
647 } else if (theme_adium_match (&cur, "%shortTime%")) {
648 dup_replace = empathy_time_to_string_local (timestamp,
649 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
650 replace = dup_replace;
651 } else if (theme_adium_match (&cur, "%service%")) {
652 replace = service_name;
653 } else if (theme_adium_match (&cur, "%variant%")) {
654 /* FIXME: The name of the active message style variant,
655 * with all spaces replaced with an underscore.
656 * A variant named "Alternating Messages - Blue Red"
657 * will become "Alternating_Messages_-_Blue_Red".
659 } else if (theme_adium_match (&cur, "%userIcons%")) {
660 /* FIXME: mus t be "hideIcons" if use preference is set
662 replace = "showIcons";
663 } else if (theme_adium_match (&cur, "%messageClasses%")) {
664 replace = message_classes;
665 } else if (theme_adium_match (&cur, "%status%")) {
666 /* FIXME: A description of the status event. This is
667 * neither in the user's local language nor expected to
668 * be displayed; it may be useful to use a different div
669 * class to present different types of status messages.
670 * The following is a list of some of the more important
671 * status messages; your message style should be able to
672 * handle being shown a status message not in this list,
673 * as even at present the list is incomplete and is
674 * certain to become out of date in the future:
683 * contact_joined (group chats)
687 * encryption (all OTR messages use this status)
688 * purple (all IRC topic and join/part messages use this status)
689 * fileTransferStarted
690 * fileTransferCompleted
693 escape_and_append_len (string, cur, 1);
697 /* Here we have a replacement to make */
698 escape_and_append_len (string, replace, -1);
700 g_free (dup_replace);
703 g_string_append (string, "\")");
705 script = g_string_free (string, FALSE);
706 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
711 theme_adium_append_event_escaped (EmpathyChatView *view,
712 const gchar *escaped)
714 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
715 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
717 theme_adium_append_html (theme, "appendMessage",
718 priv->data->status_html, escaped, NULL, NULL, NULL,
720 empathy_time_get_current (), FALSE);
722 /* There is no last contact */
723 if (priv->last_contact) {
724 g_object_unref (priv->last_contact);
725 priv->last_contact = NULL;
730 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
731 WebKitDOMNodeList *nodes)
735 /* Remove focus and firstFocus class */
736 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
737 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
738 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
740 gchar **classes, **iter;
741 GString *new_class_name;
742 gboolean first = TRUE;
744 if (element == NULL) {
748 class_name = webkit_dom_html_element_get_class_name (element);
749 classes = g_strsplit (class_name, " ", -1);
750 new_class_name = g_string_sized_new (strlen (class_name));
751 for (iter = classes; *iter != NULL; iter++) {
752 if (tp_strdiff (*iter, "focus") &&
753 tp_strdiff (*iter, "firstFocus")) {
755 g_string_append_c (new_class_name, ' ');
757 g_string_append (new_class_name, *iter);
762 webkit_dom_html_element_set_class_name (element, new_class_name->str);
765 g_strfreev (classes);
766 g_string_free (new_class_name, TRUE);
771 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
773 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
774 WebKitDOMDocument *dom;
775 WebKitDOMNodeList *nodes;
776 GError *error = NULL;
778 if (!priv->has_unread_message)
781 priv->has_unread_message = FALSE;
783 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
788 /* Get all nodes with focus class */
789 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
791 DEBUG ("Error getting focus nodes: %s",
792 error ? error->message : "No error");
793 g_clear_error (&error);
797 theme_adium_remove_focus_marks (theme, nodes);
801 theme_adium_append_message (EmpathyChatView *view,
804 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
805 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
806 EmpathyContact *sender;
811 const gchar *contact_id;
812 EmpathyAvatar *avatar;
813 const gchar *avatar_filename = NULL;
815 const gchar *html = NULL;
817 const gchar *service_name;
818 GString *message_classes = NULL;
820 gboolean consecutive;
823 if (priv->pages_loading != 0) {
824 queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL);
828 /* Get information */
829 sender = empathy_message_get_sender (msg);
830 account = empathy_contact_get_account (sender);
831 service_name = empathy_protocol_name_to_display_name
832 (tp_account_get_protocol (account));
833 if (service_name == NULL)
834 service_name = tp_account_get_protocol (account);
835 timestamp = empathy_message_get_timestamp (msg);
836 body_escaped = theme_adium_parse_body (theme,
837 empathy_message_get_body (msg),
838 empathy_message_get_token (msg));
839 name = empathy_contact_get_logged_alias (sender);
840 contact_id = empathy_contact_get_id (sender);
841 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
843 /* If this is a /me probably */
847 if (priv->data->version >= 4 || !priv->data->custom_template) {
848 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
849 "<span class='actionMessageBody'>%s</span>",
852 str = g_strdup_printf ("*%s*", body_escaped);
854 g_free (body_escaped);
858 /* Get the avatar filename, or a fallback */
859 avatar = empathy_contact_get_avatar (sender);
861 avatar_filename = avatar->filename;
863 if (!avatar_filename) {
864 if (empathy_contact_is_user (sender)) {
865 avatar_filename = priv->data->default_outgoing_avatar_filename;
867 avatar_filename = priv->data->default_incoming_avatar_filename;
869 if (!avatar_filename) {
870 if (!priv->data->default_avatar_filename) {
871 priv->data->default_avatar_filename =
872 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
873 GTK_ICON_SIZE_DIALOG);
875 avatar_filename = priv->data->default_avatar_filename;
879 /* We want to join this message with the last one if
880 * - senders are the same contact,
881 * - last message was recieved recently,
882 * - last message and this message both are/aren't backlog, and
883 * - DisableCombineConsecutive is not set in theme's settings */
884 is_backlog = empathy_message_is_backlog (msg);
885 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
886 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
887 (is_backlog == priv->last_is_backlog) &&
888 !tp_asv_get_boolean (priv->data->info,
889 "DisableCombineConsecutive", NULL);
891 /* Define message classes */
892 message_classes = g_string_new ("message");
893 if (!priv->has_focus && !is_backlog) {
894 if (!priv->has_unread_message) {
895 g_string_append (message_classes, " firstFocus");
896 priv->has_unread_message = TRUE;
898 g_string_append (message_classes, " focus");
901 g_string_append (message_classes, " history");
904 g_string_append (message_classes, " consecutive");
906 if (empathy_contact_is_user (sender)) {
907 g_string_append (message_classes, " outgoing");
909 g_string_append (message_classes, " incoming");
911 if (empathy_message_should_highlight (msg)) {
912 g_string_append (message_classes, " mention");
914 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
915 g_string_append (message_classes, " autoreply");
918 g_string_append (message_classes, " action");
920 /* FIXME: other classes:
921 * status - the message is a status change
922 * event - the message is a notification of something happening
923 * (for example, encryption being turned on)
924 * %status% - See %status% in theme_adium_append_html ()
927 /* This is slightly a hack, but it's the only way to add
928 * arbitrary data to messages in the HTML. We add another
929 * class called "x-empathy-message-id-*" to the message. This
930 * way, we can remove the unread marker for this specific
932 tp_msg = empathy_message_get_tp_message (msg);
933 if (tp_msg != NULL) {
937 id = tp_message_get_pending_message_id (tp_msg, &valid);
939 g_string_append_printf (message_classes,
940 " x-empathy-message-id-%u", id);
944 /* Define javascript function to use */
946 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
948 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
951 if (empathy_contact_is_user (sender)) {
955 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
958 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
961 /* remove all the unread marks when we are sending a message */
962 theme_adium_remove_all_focus_marks (theme);
967 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
970 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
974 theme_adium_append_html (theme, func, html, body_escaped,
975 avatar_filename, name, contact_id,
976 service_name, message_classes->str,
977 timestamp, is_backlog);
979 /* Keep the sender of the last displayed message */
980 if (priv->last_contact) {
981 g_object_unref (priv->last_contact);
983 priv->last_contact = g_object_ref (sender);
984 priv->last_timestamp = timestamp;
985 priv->last_is_backlog = is_backlog;
987 g_free (body_escaped);
988 g_string_free (message_classes, TRUE);
992 theme_adium_append_event (EmpathyChatView *view,
995 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
998 if (priv->pages_loading != 0) {
999 queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str);
1003 str_escaped = g_markup_escape_text (str, -1);
1004 theme_adium_append_event_escaped (view, str_escaped);
1005 g_free (str_escaped);
1009 theme_adium_edit_message (EmpathyChatView *view,
1010 EmpathyMessage *message)
1012 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1013 WebKitDOMDocument *doc;
1014 WebKitDOMElement *span;
1015 gchar *id, *parsed_body;
1016 gchar *tooltip, *timestamp;
1017 GtkIconInfo *icon_info;
1018 GError *error = NULL;
1020 if (priv->pages_loading != 0) {
1021 queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL);
1025 id = g_strdup_printf ("message-token-%s",
1026 empathy_message_get_supersedes (message));
1027 /* we don't pass a token here, because doing so will return another
1028 * <span> element, and we don't want nested <span> elements */
1029 parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view),
1030 empathy_message_get_body (message), NULL);
1032 /* find the element */
1033 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view));
1034 span = webkit_dom_document_get_element_by_id (doc, id);
1037 DEBUG ("Failed to find id '%s'", id);
1041 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) {
1042 DEBUG ("Not a HTML element");
1046 /* update the HTML */
1047 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1048 parsed_body, &error);
1050 if (error != NULL) {
1051 DEBUG ("Error setting new inner-HTML: %s", error->message);
1052 g_error_free (error);
1057 timestamp = empathy_time_to_string_local (
1058 empathy_message_get_timestamp (message),
1060 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1062 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1068 /* mark this message as edited */
1069 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1070 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1072 if (icon_info != NULL) {
1073 /* set the icon as a background image using CSS
1074 * FIXME: the icon won't update in response to theme changes */
1075 gchar *style = g_strdup_printf (
1076 "background-image:url('%s');"
1077 "background-repeat:no-repeat;"
1078 "background-position:left center;"
1079 "padding-left:19px;", /* 16px icon + 3px padding */
1080 gtk_icon_info_get_filename (icon_info));
1082 webkit_dom_element_set_attribute (span, "style", style, &error);
1084 if (error != NULL) {
1085 DEBUG ("Error setting element style: %s",
1087 g_clear_error (&error);
1092 gtk_icon_info_free (icon_info);
1098 DEBUG ("Could not find message to edit with: %s",
1099 empathy_message_get_body (message));
1103 g_free (parsed_body);
1107 theme_adium_scroll (EmpathyChatView *view,
1108 gboolean allow_scrolling)
1110 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1112 priv->allow_scrolling = allow_scrolling;
1113 if (allow_scrolling) {
1114 empathy_chat_view_scroll_down (view);
1119 theme_adium_scroll_down (EmpathyChatView *view)
1121 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1125 theme_adium_get_has_selection (EmpathyChatView *view)
1127 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1131 theme_adium_clear (EmpathyChatView *view)
1133 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1135 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1137 /* Clear last contact to avoid trying to add a 'joined'
1138 * message when we don't have an insertion point. */
1139 if (priv->last_contact) {
1140 g_object_unref (priv->last_contact);
1141 priv->last_contact = NULL;
1146 theme_adium_find_previous (EmpathyChatView *view,
1147 const gchar *search_criteria,
1148 gboolean new_search,
1149 gboolean match_case)
1151 /* FIXME: Doesn't respect new_search */
1152 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1153 search_criteria, match_case,
1158 theme_adium_find_next (EmpathyChatView *view,
1159 const gchar *search_criteria,
1160 gboolean new_search,
1161 gboolean match_case)
1163 /* FIXME: Doesn't respect new_search */
1164 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1165 search_criteria, match_case,
1170 theme_adium_find_abilities (EmpathyChatView *view,
1171 const gchar *search_criteria,
1172 gboolean match_case,
1173 gboolean *can_do_previous,
1174 gboolean *can_do_next)
1176 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1177 * find_next and find_previous to work around this problem. */
1178 if (can_do_previous)
1179 *can_do_previous = TRUE;
1181 *can_do_next = TRUE;
1185 theme_adium_highlight (EmpathyChatView *view,
1187 gboolean match_case)
1189 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1190 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1191 text, match_case, 0);
1192 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1197 theme_adium_copy_clipboard (EmpathyChatView *view)
1199 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1203 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1206 WebKitDOMDocument *dom;
1207 WebKitDOMNodeList *nodes;
1209 GError *error = NULL;
1211 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1216 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1218 /* Get all nodes with focus class */
1219 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1222 if (nodes == NULL) {
1223 DEBUG ("Error getting focus nodes: %s",
1224 error ? error->message : "No error");
1225 g_clear_error (&error);
1229 theme_adium_remove_focus_marks (self, nodes);
1233 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1236 EmpathyThemeAdium *self = user_data;
1237 guint32 id = GPOINTER_TO_UINT (data);
1239 theme_adium_remove_mark_from_message (self, id);
1243 theme_adium_focus_toggled (EmpathyChatView *view,
1246 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1248 priv->has_focus = has_focus;
1249 if (!priv->has_focus) {
1250 /* We've lost focus, so let's make sure all the acked
1251 * messages have lost their unread marker. */
1252 g_queue_foreach (&priv->acked_messages,
1253 theme_adium_remove_acked_message_unread_mark_foreach,
1255 g_queue_clear (&priv->acked_messages);
1257 priv->has_unread_message = FALSE;
1262 theme_adium_message_acknowledged (EmpathyChatView *view,
1263 EmpathyMessage *message)
1265 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1266 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1271 tp_msg = empathy_message_get_tp_message (message);
1273 if (tp_msg == NULL) {
1277 id = tp_message_get_pending_message_id (tp_msg, &valid);
1279 g_warning ("Acknoledged message doesn't have a pending ID");
1283 /* We only want to actually remove the unread marker if the
1284 * view doesn't have focus. If we did it all the time we would
1285 * never see the unread markers, ever! So, we'll queue these
1286 * up, and when we lose focus, we'll remove the markers. */
1287 if (priv->has_focus) {
1288 g_queue_push_tail (&priv->acked_messages,
1289 GUINT_TO_POINTER (id));
1293 theme_adium_remove_mark_from_message (self, id);
1297 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1299 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1301 g_object_unref (hit_test_result);
1305 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1307 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1308 WebKitHitTestResult *hit_test_result;
1309 WebKitHitTestResultContext context;
1313 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1314 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1317 menu = empathy_context_menu_new (GTK_WIDGET (view));
1319 /* Select all item */
1320 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1321 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1323 g_signal_connect_swapped (item, "activate",
1324 G_CALLBACK (webkit_web_view_select_all),
1327 /* Copy menu item */
1328 if (webkit_web_view_can_copy_clipboard (view)) {
1329 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1330 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1332 g_signal_connect_swapped (item, "activate",
1333 G_CALLBACK (webkit_web_view_copy_clipboard),
1337 /* Clear menu item */
1338 item = gtk_separator_menu_item_new ();
1339 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1341 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1342 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1344 g_signal_connect_swapped (item, "activate",
1345 G_CALLBACK (empathy_chat_view_clear),
1348 /* We will only add the following menu items if we are
1349 * right-clicking a link */
1350 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1352 item = gtk_separator_menu_item_new ();
1353 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1355 /* Copy Link Address menu item */
1356 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1357 g_signal_connect (item, "activate",
1358 G_CALLBACK (theme_adium_copy_address_cb),
1360 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1362 /* Open Link menu item */
1363 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1364 g_signal_connect (item, "activate",
1365 G_CALLBACK (theme_adium_open_address_cb),
1367 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1370 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1371 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1374 /* Display the menu */
1375 gtk_widget_show_all (menu);
1376 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1377 event->button, event->time);
1381 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1383 if (event->button == 3) {
1384 gboolean developer_tools_enabled;
1386 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1387 "enable-developer-extras", &developer_tools_enabled, NULL);
1389 /* We currently have no way to add an inspector menu
1390 * item ourselves, so we disable our customized menu
1391 * if the developer extras are enabled. */
1392 if (!developer_tools_enabled) {
1393 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1398 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1402 theme_adium_iface_init (EmpathyChatViewIface *iface)
1404 iface->append_message = theme_adium_append_message;
1405 iface->append_event = theme_adium_append_event;
1406 iface->edit_message = theme_adium_edit_message;
1407 iface->scroll = theme_adium_scroll;
1408 iface->scroll_down = theme_adium_scroll_down;
1409 iface->get_has_selection = theme_adium_get_has_selection;
1410 iface->clear = theme_adium_clear;
1411 iface->find_previous = theme_adium_find_previous;
1412 iface->find_next = theme_adium_find_next;
1413 iface->find_abilities = theme_adium_find_abilities;
1414 iface->highlight = theme_adium_highlight;
1415 iface->copy_clipboard = theme_adium_copy_clipboard;
1416 iface->focus_toggled = theme_adium_focus_toggled;
1417 iface->message_acknowledged = theme_adium_message_acknowledged;
1421 theme_adium_load_finished_cb (WebKitWebView *view,
1422 WebKitWebFrame *frame,
1425 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1426 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1429 DEBUG ("Page loaded");
1430 priv->pages_loading--;
1432 if (priv->pages_loading != 0)
1435 /* Display queued messages */
1436 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1437 QueuedItem *item = l->data;
1441 case QUEUED_MESSAGE:
1442 theme_adium_append_message (chat_view, item->msg);
1446 theme_adium_edit_message (chat_view, item->msg);
1450 theme_adium_append_event (chat_view, item->str);
1454 free_queued_item (item);
1457 g_queue_clear (&priv->message_queue);
1461 theme_adium_finalize (GObject *object)
1463 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1465 empathy_adium_data_unref (priv->data);
1466 g_object_unref (priv->gsettings_chat);
1468 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1472 theme_adium_dispose (GObject *object)
1474 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1476 if (priv->smiley_manager) {
1477 g_object_unref (priv->smiley_manager);
1478 priv->smiley_manager = NULL;
1481 if (priv->last_contact) {
1482 g_object_unref (priv->last_contact);
1483 priv->last_contact = NULL;
1486 if (priv->inspector_window) {
1487 gtk_widget_destroy (priv->inspector_window);
1488 priv->inspector_window = NULL;
1491 if (priv->acked_messages.length > 0) {
1492 g_queue_clear (&priv->acked_messages);
1495 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1499 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1500 EmpathyThemeAdium *theme)
1502 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1504 if (priv->inspector_window) {
1505 gtk_widget_show_all (priv->inspector_window);
1512 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1513 EmpathyThemeAdium *theme)
1515 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1517 if (priv->inspector_window) {
1518 gtk_widget_hide (priv->inspector_window);
1524 static WebKitWebView *
1525 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1526 WebKitWebView *web_view,
1527 EmpathyThemeAdium *theme)
1529 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1530 GtkWidget *scrolled_window;
1531 GtkWidget *inspector_web_view;
1533 if (!priv->inspector_window) {
1534 /* Create main window */
1535 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1536 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1538 g_signal_connect (priv->inspector_window, "delete-event",
1539 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1541 /* Pack a scrolled window */
1542 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1543 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1544 GTK_POLICY_AUTOMATIC,
1545 GTK_POLICY_AUTOMATIC);
1546 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1548 gtk_widget_show (scrolled_window);
1550 /* Pack a webview in the scrolled window. That webview will be
1551 * used to render the inspector tool. */
1552 inspector_web_view = webkit_web_view_new ();
1553 gtk_container_add (GTK_CONTAINER (scrolled_window),
1554 inspector_web_view);
1555 gtk_widget_show (scrolled_window);
1557 return WEBKIT_WEB_VIEW (inspector_web_view);
1563 static PangoFontDescription *
1564 theme_adium_get_default_font (void)
1566 GSettings *gsettings;
1567 PangoFontDescription *pango_fd;
1570 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1572 font_family = g_settings_get_string (gsettings,
1573 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1575 if (font_family == NULL)
1578 pango_fd = pango_font_description_from_string (font_family);
1579 g_free (font_family);
1580 g_object_unref (gsettings);
1585 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1589 g_object_set (w_settings, "default-font-family", name, NULL);
1590 g_object_set (w_settings, "default-font-size", size, NULL);
1594 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1596 PangoFontDescription *default_font_desc;
1597 GdkScreen *current_screen;
1599 gint pango_font_size = 0;
1601 default_font_desc = theme_adium_get_default_font ();
1602 if (default_font_desc == NULL)
1604 pango_font_size = pango_font_description_get_size (default_font_desc)
1606 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1607 current_screen = gdk_screen_get_default ();
1608 if (current_screen != NULL) {
1609 dpi = gdk_screen_get_resolution (current_screen);
1611 dpi = BORING_DPI_DEFAULT;
1613 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1615 theme_adium_set_webkit_font (w_settings,
1616 pango_font_description_get_family (default_font_desc),
1618 pango_font_description_free (default_font_desc);
1622 theme_adium_constructed (GObject *object)
1624 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1625 const gchar *font_family = NULL;
1627 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1628 WebKitWebSettings *webkit_settings;
1629 WebKitWebInspector *webkit_inspector;
1631 /* Set default settings */
1632 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1633 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1634 webkit_settings = webkit_web_view_get_settings (webkit_view);
1636 if (font_family && font_size) {
1637 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1639 theme_adium_set_default_font (webkit_settings);
1642 /* Setup webkit inspector */
1643 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1644 g_signal_connect (webkit_inspector, "inspect-web-view",
1645 G_CALLBACK (theme_adium_inspect_web_view_cb),
1647 g_signal_connect (webkit_inspector, "show-window",
1648 G_CALLBACK (theme_adium_inspector_show_window_cb),
1650 g_signal_connect (webkit_inspector, "close-window",
1651 G_CALLBACK (theme_adium_inspector_close_window_cb),
1655 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1657 priv->in_construction = FALSE;
1661 theme_adium_get_property (GObject *object,
1666 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1669 case PROP_ADIUM_DATA:
1670 g_value_set_boxed (value, priv->data);
1673 g_value_set_string (value, priv->variant);
1676 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1682 theme_adium_set_property (GObject *object,
1684 const GValue *value,
1687 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1688 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1691 case PROP_ADIUM_DATA:
1692 g_assert (priv->data == NULL);
1693 priv->data = g_value_dup_boxed (value);
1696 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1699 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1705 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1707 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1708 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1710 object_class->finalize = theme_adium_finalize;
1711 object_class->dispose = theme_adium_dispose;
1712 object_class->constructed = theme_adium_constructed;
1713 object_class->get_property = theme_adium_get_property;
1714 object_class->set_property = theme_adium_set_property;
1716 widget_class->button_press_event = theme_adium_button_press_event;
1718 g_object_class_install_property (object_class,
1720 g_param_spec_boxed ("adium-data",
1722 "Data for the adium theme",
1723 EMPATHY_TYPE_ADIUM_DATA,
1724 G_PARAM_CONSTRUCT_ONLY |
1726 G_PARAM_STATIC_STRINGS));
1727 g_object_class_install_property (object_class,
1729 g_param_spec_string ("variant",
1730 "The theme variant",
1731 "Variant name for the theme",
1735 G_PARAM_STATIC_STRINGS));
1737 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1741 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1743 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1744 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1748 priv->in_construction = TRUE;
1749 g_queue_init (&priv->message_queue);
1750 priv->allow_scrolling = TRUE;
1751 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1753 g_signal_connect (theme, "load-finished",
1754 G_CALLBACK (theme_adium_load_finished_cb),
1756 g_signal_connect (theme, "navigation-policy-decision-requested",
1757 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1760 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1761 g_signal_connect (priv->gsettings_chat,
1762 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1763 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1766 theme_adium_update_enable_webkit_developer_tools (theme);
1770 empathy_theme_adium_new (EmpathyAdiumData *data,
1771 const gchar *variant)
1773 g_return_val_if_fail (data != NULL, NULL);
1775 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1782 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1783 const gchar *variant)
1785 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1786 gchar *variant_path;
1789 if (!tp_strdiff (priv->variant, variant)) {
1793 g_free (priv->variant);
1794 priv->variant = g_strdup (variant);
1796 if (priv->in_construction) {
1800 DEBUG ("Update view with variant: '%s'", variant);
1801 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1803 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1805 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1807 g_free (variant_path);
1810 g_object_notify (G_OBJECT (theme), "variant");
1814 empathy_adium_path_is_valid (const gchar *path)
1819 /* The theme is not valid if there is no Info.plist */
1820 file = g_build_filename (path, "Contents", "Info.plist",
1822 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1828 /* We ship a default Template.html as fallback if there is any problem
1829 * with the one inside the theme. The only other required file is
1830 * Content.html OR Incoming/Content.html*/
1831 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1833 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1837 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1838 "Content.html", NULL);
1839 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1847 empathy_adium_info_new (const gchar *path)
1851 GHashTable *info = NULL;
1853 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1855 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1856 value = empathy_plist_parse_from_file (file);
1862 info = g_value_dup_boxed (value);
1863 tp_g_value_slice_free (value);
1865 /* Insert the theme's path into the hash table,
1866 * keys have to be dupped */
1867 tp_asv_set_string (info, g_strdup ("path"), path);
1873 adium_info_get_version (GHashTable *info)
1875 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1878 static const gchar *
1879 adium_info_get_no_variant_name (GHashTable *info)
1881 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1882 return name ? name : _("Normal");
1886 adium_info_dup_path_for_variant (GHashTable *info,
1887 const gchar *variant)
1889 guint version = adium_info_get_version (info);
1890 const gchar *no_variant = adium_info_get_no_variant_name (info);
1891 GPtrArray *variants;
1894 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1895 return g_strdup ("main.css");
1898 /* Verify the variant exists, fallback to the first one */
1899 variants = empathy_adium_info_get_available_variants (info);
1900 for (i = 0; i < variants->len; i++) {
1901 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1905 if (i == variants->len) {
1906 DEBUG ("Variant %s does not exist", variant);
1907 variant = g_ptr_array_index (variants, 0);
1910 return g_strdup_printf ("Variants/%s.css", variant);
1915 empathy_adium_info_get_default_variant (GHashTable *info)
1917 if (adium_info_get_version (info) <= 2) {
1918 return adium_info_get_no_variant_name (info);
1921 return tp_asv_get_string (info, "DefaultVariant");
1925 empathy_adium_info_get_available_variants (GHashTable *info)
1927 GPtrArray *variants;
1932 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1933 if (variants != NULL) {
1937 variants = g_ptr_array_new_with_free_func (g_free);
1938 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1939 G_TYPE_PTR_ARRAY, variants);
1941 path = tp_asv_get_string (info, "path");
1942 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1943 dir = g_dir_open (dirpath, 0, NULL);
1947 for (name = g_dir_read_name (dir);
1949 name = g_dir_read_name (dir)) {
1950 gchar *display_name;
1952 if (!g_str_has_suffix (name, ".css")) {
1956 display_name = g_strdup (name);
1957 strstr (display_name, ".css")[0] = '\0';
1958 g_ptr_array_add (variants, display_name);
1964 if (adium_info_get_version (info) <= 2) {
1965 g_ptr_array_add (variants,
1966 g_strdup (adium_info_get_no_variant_name (info)));
1973 empathy_adium_data_get_type (void)
1975 static GType type_id = 0;
1979 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1980 (GBoxedCopyFunc) empathy_adium_data_ref,
1981 (GBoxedFreeFunc) empathy_adium_data_unref);
1988 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1990 EmpathyAdiumData *data;
1991 gchar *template_html = NULL;
1992 gchar *footer_html = NULL;
1995 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1997 data = g_slice_new0 (EmpathyAdiumData);
1998 data->ref_count = 1;
1999 data->path = g_strdup (path);
2000 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2001 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2002 data->info = g_hash_table_ref (info);
2003 data->version = adium_info_get_version (info);
2004 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2005 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2006 g_str_equal, g_free, g_free);
2008 DEBUG ("Loading theme at %s", path);
2010 #define LOAD(path, var) \
2011 tmp = g_build_filename (data->basedir, path, NULL); \
2012 g_file_get_contents (tmp, &var, NULL, NULL); \
2015 #define LOAD_CONST(path, var) \
2018 LOAD (path, content); \
2019 if (content != NULL) { \
2020 g_ptr_array_add (data->strings_to_free, content); \
2025 /* Load html files */
2026 LOAD_CONST ("Content.html", data->content_html);
2027 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2028 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2029 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2030 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2031 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2032 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2033 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2034 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2035 LOAD_CONST ("Status.html", data->status_html);
2036 LOAD ("Template.html", template_html);
2037 LOAD ("Footer.html", footer_html);
2042 /* HTML fallbacks: If we have at least content OR in_content, then
2043 * everything else gets a fallback */
2045 #define FALLBACK(html, fallback) \
2046 if (html == NULL) { \
2050 /* in_nextcontent -> in_content -> content */
2051 FALLBACK (data->in_content_html, data->content_html);
2052 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2054 /* context -> content */
2055 FALLBACK (data->in_context_html, data->in_content_html);
2056 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2057 FALLBACK (data->out_context_html, data->out_content_html);
2058 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2061 FALLBACK (data->out_content_html, data->in_content_html);
2062 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2063 FALLBACK (data->out_context_html, data->in_context_html);
2064 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2066 /* status -> in_content */
2067 FALLBACK (data->status_html, data->in_content_html);
2071 /* template -> empathy's template */
2072 data->custom_template = (template_html != NULL);
2073 if (template_html == NULL) {
2074 tmp = empathy_file_lookup ("Template.html", "data");
2075 g_file_get_contents (tmp, &template_html, NULL, NULL);
2079 /* Default avatar */
2080 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2081 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
2082 data->default_incoming_avatar_filename = tmp;
2086 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2087 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
2088 data->default_outgoing_avatar_filename = tmp;
2093 /* Old custom templates had only 4 parameters.
2094 * New templates have 5 parameters */
2095 if (data->version <= 2 && data->custom_template) {
2096 tmp = string_with_format (template_html,
2098 "%@", /* Leave variant unset */
2099 "", /* The header */
2100 footer_html ? footer_html : "",
2103 tmp = string_with_format (template_html,
2105 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2106 "%@", /* Leave variant unset */
2107 "", /* The header */
2108 footer_html ? footer_html : "",
2111 g_ptr_array_add (data->strings_to_free, tmp);
2112 data->template_html = tmp;
2114 g_free (template_html);
2115 g_free (footer_html);
2121 empathy_adium_data_new (const gchar *path)
2123 EmpathyAdiumData *data;
2126 info = empathy_adium_info_new (path);
2127 data = empathy_adium_data_new_with_info (path, info);
2128 g_hash_table_unref (info);
2134 empathy_adium_data_ref (EmpathyAdiumData *data)
2136 g_return_val_if_fail (data != NULL, NULL);
2138 g_atomic_int_inc (&data->ref_count);
2144 empathy_adium_data_unref (EmpathyAdiumData *data)
2146 g_return_if_fail (data != NULL);
2148 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2149 g_free (data->path);
2150 g_free (data->basedir);
2151 g_free (data->default_avatar_filename);
2152 g_free (data->default_incoming_avatar_filename);
2153 g_free (data->default_outgoing_avatar_filename);
2154 g_hash_table_unref (data->info);
2155 g_ptr_array_unref (data->strings_to_free);
2156 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2158 g_slice_free (EmpathyAdiumData, data);
2163 empathy_adium_data_get_info (EmpathyAdiumData *data)
2165 g_return_val_if_fail (data != NULL, NULL);
2171 empathy_adium_data_get_path (EmpathyAdiumData *data)
2173 g_return_val_if_fail (data != NULL, NULL);