2 * Copyright (C) 2008-2012 Collabora Ltd.
3 * Copyright (C) 2012 Red Hat, Inc.
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/telepathy-glib.h>
30 #include <pango/pango.h>
33 #include <libempathy/empathy-gsettings.h>
34 #include <libempathy/empathy-time.h>
35 #include <libempathy/empathy-utils.h>
37 #include "empathy-theme-adium.h"
38 #include "empathy-smiley-manager.h"
39 #include "empathy-ui-utils.h"
40 #include "empathy-plist.h"
41 #include "empathy-images.h"
42 #include "empathy-webkit-utils.h"
44 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
45 #include <libempathy/empathy-debug.h>
47 #define BORING_DPI_DEFAULT 96
49 /* "Join" consecutive messages with timestamps within five minutes */
50 #define MESSAGE_JOIN_PERIOD 5*60
52 struct _EmpathyThemeAdiumPriv
54 EmpathyAdiumData *data;
55 EmpathySmileyManager *smiley_manager;
56 EmpathyContact *first_contact;
57 EmpathyContact *last_contact;
58 gint64 first_timestamp;
59 gint64 last_timestamp;
60 gboolean first_is_backlog;
61 gboolean last_is_backlog;
63 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
65 /* Queue of guint32 of pending message id to remove unread
66 * marker for when we lose focus. */
67 GQueue acked_messages;
68 GtkWidget *inspector_window;
70 GSettings *gsettings_chat;
71 GSettings *gsettings_desktop;
74 gboolean has_unread_message;
75 gboolean allow_scrolling;
77 gboolean in_construction;
78 gboolean show_avatars;
81 struct _EmpathyAdiumData
86 gchar *default_avatar_filename;
87 gchar *default_incoming_avatar_filename;
88 gchar *default_outgoing_avatar_filename;
91 gboolean custom_template;
92 /* gchar* -> gchar* both owned */
93 GHashTable *date_format_cache;
96 const gchar *template_html;
97 const gchar *content_html;
98 const gchar *in_content_html;
99 const gchar *in_context_html;
100 const gchar *in_nextcontent_html;
101 const gchar *in_nextcontext_html;
102 const gchar *out_content_html;
103 const gchar *out_context_html;
104 const gchar *out_nextcontent_html;
105 const gchar *out_nextcontext_html;
106 const gchar *status_html;
108 /* Above html strings are pointers to strings stored in this array.
109 * We do this because of fallbacks, some htmls could be pointing the
111 GPtrArray *strings_to_free;
114 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
115 const gchar *variant);
124 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
125 WEBKIT_TYPE_WEB_VIEW)
139 gboolean should_highlight;
143 queue_item (GQueue *queue,
147 gboolean should_highlight,
150 QueuedItem *item = g_slice_new0 (QueuedItem);
154 item->msg = g_object_ref (msg);
155 item->str = g_strdup (str);
156 item->should_highlight = should_highlight;
159 g_queue_push_head (queue, item);
161 g_queue_push_tail (queue, item);
167 free_queued_item (QueuedItem *item)
169 tp_clear_object (&item->msg);
172 g_slice_free (QueuedItem, item);
176 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
177 WebKitWebFrame *web_frame,
178 WebKitNetworkRequest *request,
179 WebKitWebNavigationAction *action,
180 WebKitWebPolicyDecision *decision,
185 /* Only call url_show on clicks */
186 if (webkit_web_navigation_action_get_reason (action) !=
187 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
189 webkit_web_policy_decision_use (decision);
193 uri = webkit_network_request_get_uri (request);
194 empathy_url_show (GTK_WIDGET (view), uri);
196 webkit_web_policy_decision_ignore (decision);
200 /* Replace each %@ in format with string passed in args */
202 string_with_format (const gchar *format,
203 const gchar *first_string,
210 va_start (args, first_string);
211 result = g_string_sized_new (strlen (format));
212 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
216 next = strstr (format, "%@");
220 g_string_append_len (result, format, next - format);
221 g_string_append (result, str);
224 g_string_append (result, format);
227 return g_string_free (result, FALSE);
231 theme_adium_load_template (EmpathyThemeAdium *self)
237 self->priv->pages_loading++;
238 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
240 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
241 self->priv->variant);
243 template = string_with_format (self->priv->data->template_html,
246 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
247 template, basedir_uri);
249 g_free (basedir_uri);
250 g_free (variant_path);
255 theme_adium_parse_body (EmpathyThemeAdium *self,
259 EmpathyStringParser *parsers;
262 /* Check if we have to parse smileys */
263 parsers = empathy_webkit_get_string_parser (
264 g_settings_get_boolean (self->priv->gsettings_chat,
265 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
267 /* Parse text and construct string with links and smileys replaced
268 * by html tags. Also escape text to make sure html code is
269 * displayed verbatim. */
270 string = g_string_sized_new (strlen (text));
272 /* wrap this in HTML that allows us to find the message for later
274 if (!tp_str_empty (token))
275 g_string_append_printf (string,
276 "<span id=\"message-token-%s\">",
279 empathy_string_parser_substr (text, -1, parsers, string);
281 if (!tp_str_empty (token))
282 g_string_append (string, "</span>");
284 /* Wrap body in order to make tabs and multiple spaces displayed
285 * properly. See bug #625745. */
286 g_string_prepend (string, "<div style=\"display: inline; "
287 "white-space: pre-wrap\"'>");
288 g_string_append (string, "</div>");
290 return g_string_free (string, FALSE);
294 escape_and_append_len (GString *string, const gchar *str, gint len)
296 while (str != NULL && *str != '\0' && len != 0)
302 g_string_append (string, "\\\\");
306 g_string_append (string, "\\\"");
309 /* Remove end of lines */
312 g_string_append_c (string, *str);
320 /* If *str starts with match, returns TRUE and move pointer to the end */
322 theme_adium_match (const gchar **str,
327 len = strlen (match);
328 if (strncmp (*str, match, len) == 0)
337 /* Like theme_adium_match() but also return the X part if match is
340 theme_adium_match_with_format (const gchar **str,
344 const gchar *cur = *str;
347 if (!theme_adium_match (&cur, match))
352 end = strstr (cur, "}%");
356 *format = g_strndup (cur , end - cur);
361 /* List of colors used by %senderColor%. Copied from
362 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
364 static gchar *colors[] = {
365 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
366 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
367 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
368 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
369 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
370 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
371 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
372 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
373 "lightblue", "lightcoral",
374 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
375 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
376 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
377 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
378 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
379 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
380 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
381 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
382 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
383 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
388 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
390 /* Convert from NSDateFormatter
391 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
392 * to strftime supported by g_date_time_format.
393 * FIXME: table is incomplete, doc of g_date_time_format has a table of
395 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
396 * in 2.29.x we have to explictely request padding with %0x */
397 static const gchar *convert_table[] = {
399 "A", NULL, // 0~86399999 (Millisecond of Day)
401 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
402 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
403 "cc", "%u", // 1~7 (Day of Week)
404 "c", "%u", // 1~7 (Day of Week)
406 "dd", "%d", // 1~31 (0 padded Day of Month)
407 "d", "%d", // 1~31 (0 padded Day of Month)
408 "D", "%j", // 1~366 (0 padded Day of Year)
410 "e", "%u", // 1~7 (0 padded Day of Week)
411 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
412 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
413 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
414 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
416 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
418 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
419 "GGGG", NULL, // Before Christ/Anno Domini
420 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
421 "GG", NULL, // BC/AD (Era Designator Abbreviated)
422 "G", NULL, // BC/AD (Era Designator Abbreviated)
424 "h", "%I", // 1~12 (0 padded Hour (12hr))
425 "H", "%H", // 0~23 (0 padded Hour (24hr))
427 "k", NULL, // 1~24 (0 padded Hour (24hr)
428 "K", NULL, // 0~11 (0 padded Hour (12hr))
430 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
431 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
432 "LL", "%m", // 1~12 (0 padded Month)
433 "L", "%m", // 1~12 (0 padded Month)
435 "m", "%M", // 0~59 (0 padded Minute)
436 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
437 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
438 "MM", "%m", // 1~12 (0 padded Month)
439 "M", "%m", // 1~12 (0 padded Month)
441 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
442 "qqq", NULL, // Q1/Q2/Q3/Q4
443 "qq", NULL, // 1~4 (0 padded Quarter)
444 "q", NULL, // 1~4 (0 padded Quarter)
445 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
446 "QQQ", NULL, // Q1/Q2/Q3/Q4
447 "QQ", NULL, // 1~4 (0 padded Quarter)
448 "Q", NULL, // 1~4 (0 padded Quarter)
450 "s", "%S", // 0~59 (0 padded Second)
451 "S", NULL, // (rounded Sub-Second)
453 "u", "%Y", // (0 padded Year)
455 "vvvv", "%Z", // (General GMT Timezone Name)
456 "vvv", "%Z", // (General GMT Timezone Abbreviation)
457 "vv", "%Z", // (General GMT Timezone Abbreviation)
458 "v", "%Z", // (General GMT Timezone Abbreviation)
460 "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)
461 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
463 "yyyy", "%Y", // (Full Year)
464 "yyy", "%y", // (2 Digits Year)
465 "yy", "%y", // (2 Digits Year)
466 "y", "%Y", // (Full Year)
467 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
468 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
469 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
470 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
472 "zzzz", NULL, // (Specific GMT Timezone Name)
473 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
474 "zz", NULL, // (Specific GMT Timezone Abbreviation)
475 "z", NULL, // (Specific GMT Timezone Abbreviation)
476 "Z", "%z", // +0000 (RFC 822 Timezone)
485 str = g_hash_table_lookup (data->date_format_cache, nsdate);
490 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
491 * by corresponding strftime tag. */
492 string = g_string_sized_new (strlen (nsdate));
493 for (i = 0; nsdate[i] != '\0'; i++)
495 gboolean found = FALSE;
497 /* even indexes are NSDateFormatter tag, odd indexes are the
498 * corresponding strftime tag */
499 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
501 if (g_str_has_prefix (nsdate + i, convert_table[j]))
510 /* If we don't have a replacement, just ignore that tag */
511 if (convert_table[j + 1] != NULL)
512 g_string_append (string, convert_table[j + 1]);
514 i += strlen (convert_table[j]) - 1;
518 g_string_append_c (string, nsdate[i]);
522 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
524 /* The cache takes ownership of string->str */
525 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
526 return g_string_free (string, FALSE);
530 theme_adium_add_html (EmpathyThemeAdium *self,
533 const gchar *message,
534 const gchar *avatar_filename,
536 const gchar *contact_id,
537 const gchar *service_name,
538 const gchar *message_classes,
542 PangoDirection direction)
546 const gchar *cur = NULL;
550 /* Make some search-and-replace in the html code */
551 string = g_string_sized_new (strlen (html) + strlen (message));
552 g_string_append_printf (string, "%s(\"", func);
554 for (cur = html; *cur != '\0'; cur++)
556 const gchar *replace = NULL;
557 gchar *dup_replace = NULL;
558 gchar *format = NULL;
560 /* Those are all well known keywords that needs replacement in
561 * html files. Please keep them in the same order than the adium
562 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
563 if (theme_adium_match (&cur, "%userIconPath%"))
565 replace = avatar_filename;
567 else if (theme_adium_match (&cur, "%senderScreenName%"))
569 replace = contact_id;
571 else if (theme_adium_match (&cur, "%sender%"))
575 else if (theme_adium_match (&cur, "%senderColor%"))
577 /* A color derived from the user's name.
578 * FIXME: If a colon separated list of HTML colors is at
579 * Incoming/SenderColors.txt it will be used instead of
580 * the default colors.
583 /* Ensure we always use the same color when sending messages
589 else if (contact_id != NULL)
591 guint hash = g_str_hash (contact_id);
592 replace = colors[hash % G_N_ELEMENTS (colors)];
595 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
597 /* FIXME: The path to the status icon of the sender
598 * (available, away, etc...)
601 else if (theme_adium_match (&cur, "%messageDirection%"))
605 case PANGO_DIRECTION_LTR:
606 case PANGO_DIRECTION_TTB_LTR:
607 case PANGO_DIRECTION_WEAK_LTR:
610 case PANGO_DIRECTION_RTL:
611 case PANGO_DIRECTION_TTB_RTL:
612 case PANGO_DIRECTION_WEAK_RTL:
615 case PANGO_DIRECTION_NEUTRAL:
620 else if (theme_adium_match (&cur, "%senderDisplayName%"))
622 /* FIXME: The serverside (remotely set) name of the
623 * sender, such as an MSN display name.
625 * We don't have access to that yet so we use
626 * local alias instead.
630 else if (theme_adium_match (&cur, "%senderPrefix%"))
632 /* FIXME: If we supported IRC user mode flags, this
633 * would be replaced with @ if the user is an op, + if
634 * the user has voice, etc. as per
635 * http://hg.adium.im/adium/rev/b586b027de42. But we
636 * don't, so for now we just strip it. */
638 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
641 /* FIXME: This keyword is used to represent the
642 * highlight background color. "X" is the opacity of the
643 * background, ranges from 0 to 1 and can be any decimal
647 else if (theme_adium_match (&cur, "%message%"))
651 else if (theme_adium_match (&cur, "%time%") ||
652 theme_adium_match_with_format (&cur, "%time{", &format))
654 const gchar *strftime_format;
656 strftime_format = nsdate_to_strftime (self->priv->data, format);
658 dup_replace = empathy_time_to_string_local (timestamp,
659 strftime_format ? strftime_format :
660 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
662 dup_replace = empathy_time_to_string_local (timestamp,
663 strftime_format ? strftime_format :
664 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
666 replace = dup_replace;
668 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;
674 else if (theme_adium_match (&cur, "%service%"))
676 replace = service_name;
678 else if (theme_adium_match (&cur, "%variant%"))
680 /* FIXME: The name of the active message style variant,
681 * with all spaces replaced with an underscore.
682 * A variant named "Alternating Messages - Blue Red"
683 * will become "Alternating_Messages_-_Blue_Red".
686 else if (theme_adium_match (&cur, "%userIcons%"))
688 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
690 else if (theme_adium_match (&cur, "%messageClasses%"))
692 replace = message_classes;
694 else if (theme_adium_match (&cur, "%status%"))
696 /* FIXME: A description of the status event. This is
697 * neither in the user's local language nor expected to
698 * be displayed; it may be useful to use a different div
699 * class to present different types of status messages.
700 * The following is a list of some of the more important
701 * status messages; your message style should be able to
702 * handle being shown a status message not in this list,
703 * as even at present the list is incomplete and is
704 * certain to become out of date in the future:
713 * contact_joined (group chats)
717 * encryption (all OTR messages use this status)
718 * purple (all IRC topic and join/part messages use this status)
719 * fileTransferStarted
720 * fileTransferCompleted
725 escape_and_append_len (string, cur, 1);
729 /* Here we have a replacement to make */
730 escape_and_append_len (string, replace, -1);
732 g_free (dup_replace);
735 g_string_append (string, "\")");
737 bytes = g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
738 G_RESOURCE_LOOKUP_FLAGS_NONE,
740 js = (const gchar *) g_bytes_get_data (bytes, NULL);
741 g_string_prepend (string, js);
742 g_bytes_unref (bytes);
744 script = g_string_free (string, FALSE);
745 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
750 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
751 const gchar *escaped,
752 PangoDirection direction)
754 theme_adium_add_html (self, "appendMessage",
755 self->priv->data->status_html, escaped, NULL, NULL, NULL,
756 NULL, "event", empathy_time_get_current (), FALSE, FALSE, direction);
758 /* There is no last contact */
759 if (self->priv->last_contact)
761 g_object_unref (self->priv->last_contact);
762 self->priv->last_contact = NULL;
767 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
768 WebKitDOMNodeList *nodes)
772 /* Remove focus and firstFocus class */
773 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
775 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
776 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
778 gchar **classes, **iter;
779 GString *new_class_name;
780 gboolean first = TRUE;
785 class_name = webkit_dom_html_element_get_class_name (element);
786 classes = g_strsplit (class_name, " ", -1);
787 new_class_name = g_string_sized_new (strlen (class_name));
789 for (iter = classes; *iter != NULL; iter++)
791 if (tp_strdiff (*iter, "focus") &&
792 tp_strdiff (*iter, "firstFocus"))
795 g_string_append_c (new_class_name, ' ');
797 g_string_append (new_class_name, *iter);
802 webkit_dom_html_element_set_class_name (element, new_class_name->str);
805 g_strfreev (classes);
806 g_string_free (new_class_name, TRUE);
811 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
813 WebKitDOMDocument *dom;
814 WebKitDOMNodeList *nodes;
815 GError *error = NULL;
817 if (!self->priv->has_unread_message)
820 self->priv->has_unread_message = FALSE;
822 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
826 /* Get all nodes with focus class */
827 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
831 DEBUG ("Error getting focus nodes: %s",
832 error ? error->message : "No error");
833 g_clear_error (&error);
837 theme_adium_remove_focus_marks (self, nodes);
842 ADD_CONSECUTIVE_MSG_SCROLL = 0,
843 ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
845 ADD_MSG_NO_SCROLL = 3
849 * theme_adium_add_message:
850 * @self: The #EmpathyThemeAdium used by the view.
851 * @msg: An #EmpathyMessage that is to be added to the view.
852 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
853 * @prev_timestamp: (out): Timestamp of the previous message.
854 * @prev_is_backlog: (out): Whether the previous message was fetched
856 * @should_highlight: Whether the message should be highlighted. eg.,
857 * if it matches the user's username in multi-user chat.
858 * @js_funcs: An array of JavaScript function names
860 * Shows @msg in the chat view by adding to @self. Addition is defined
861 * by the JavaScript functions listed in @js_funcs. Common examples
862 * are appending new incoming messages or prepending old messages from
865 * @js_funcs should be an array with exactly 4 entries. The entries
866 * should be the names of JavaScript functions that take the raw HTML
867 * that is to be added to the view as an argument and take the following
868 * actions, in this order:
869 * - add a new consecutive message and scroll to it if needed,
870 * - add a new consecutive message and do not scroll,
871 * - add a new non-consecutive message and scroll to it if needed, and
872 * - add a new non-consecutive message and do not scroll
874 * A message is considered to be consecutive with the previous one if
875 * all the following conditions are met:
876 * - senders are the same contact,
877 * - last message was recieved recently,
878 * - last message and this message both are/aren't backlog, and
879 * - DisableCombineConsecutive is not set in theme's settings
882 theme_adium_add_message (EmpathyThemeAdium *self,
884 EmpathyContact **prev_contact,
885 gint64 *prev_timestamp,
886 gboolean *prev_is_backlog,
887 gboolean should_highlight,
888 const gchar *js_funcs[])
890 EmpathyContact *sender;
893 gchar *body_escaped, *name_escaped;
895 const gchar *contact_id;
896 EmpathyAvatar *avatar;
897 const gchar *avatar_filename = NULL;
899 const gchar *html = NULL;
901 const gchar *service_name;
902 GString *message_classes = NULL;
904 gboolean consecutive;
906 PangoDirection direction;
909 /* Get information */
910 sender = empathy_message_get_sender (msg);
911 account = empathy_contact_get_account (sender);
912 service_name = empathy_protocol_name_to_display_name
913 (tp_account_get_protocol_name (account));
914 if (service_name == NULL)
915 service_name = tp_account_get_protocol_name (account);
916 timestamp = empathy_message_get_timestamp (msg);
917 body_escaped = theme_adium_parse_body (self,
918 empathy_message_get_body (msg),
919 empathy_message_get_token (msg));
920 name = empathy_contact_get_logged_alias (sender);
921 contact_id = empathy_contact_get_id (sender);
922 action = (empathy_message_get_tptype (msg) ==
923 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
925 name_escaped = g_markup_escape_text (name, -1);
927 /* If this is a /me probably */
932 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
934 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
935 "<span class='actionMessageBody'>%s</span>",
936 name_escaped, body_escaped);
940 str = g_strdup_printf ("*%s*", body_escaped);
943 g_free (body_escaped);
947 /* Get the avatar filename, or a fallback */
948 avatar = empathy_contact_get_avatar (sender);
950 avatar_filename = avatar->filename;
952 if (!avatar_filename)
954 if (empathy_contact_is_user (sender))
955 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
957 avatar_filename = self->priv->data->default_incoming_avatar_filename;
959 if (!avatar_filename)
961 if (!self->priv->data->default_avatar_filename)
962 self->priv->data->default_avatar_filename =
963 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
964 GTK_ICON_SIZE_DIALOG);
966 avatar_filename = self->priv->data->default_avatar_filename;
970 is_backlog = empathy_message_is_backlog (msg);
971 consecutive = empathy_contact_equal (*prev_contact, sender) &&
972 (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
973 (is_backlog == *prev_is_backlog) &&
974 !tp_asv_get_boolean (self->priv->data->info,
975 "DisableCombineConsecutive", NULL);
977 /* Define message classes */
978 message_classes = g_string_new ("message");
979 if (!self->priv->has_focus && !is_backlog)
981 if (!self->priv->has_unread_message)
983 g_string_append (message_classes, " firstFocus");
984 self->priv->has_unread_message = TRUE;
986 g_string_append (message_classes, " focus");
990 g_string_append (message_classes, " history");
993 g_string_append (message_classes, " consecutive");
995 if (empathy_contact_is_user (sender))
996 g_string_append (message_classes, " outgoing");
998 g_string_append (message_classes, " incoming");
1000 if (should_highlight)
1001 g_string_append (message_classes, " mention");
1003 if (empathy_message_get_tptype (msg) ==
1004 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
1005 g_string_append (message_classes, " autoreply");
1008 g_string_append (message_classes, " action");
1010 /* FIXME: other classes:
1011 * status - the message is a status change
1012 * event - the message is a notification of something happening
1013 * (for example, encryption being turned on)
1014 * %status% - See %status% in theme_adium_add_html ()
1017 /* This is slightly a hack, but it's the only way to add
1018 * arbitrary data to messages in the HTML. We add another
1019 * class called "x-empathy-message-id-*" to the message. This
1020 * way, we can remove the unread marker for this specific
1022 tp_msg = empathy_message_get_tp_message (msg);
1028 id = tp_message_get_pending_message_id (tp_msg, &valid);
1030 g_string_append_printf (message_classes,
1031 " x-empathy-message-id-%u", id);
1034 /* Define javascript function to use */
1036 func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1037 js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1039 func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1040 js_funcs[ADD_MSG_NO_SCROLL];
1042 if (empathy_contact_is_user (sender))
1047 html = consecutive ? self->priv->data->out_nextcontext_html :
1048 self->priv->data->out_context_html;
1051 html = consecutive ? self->priv->data->out_nextcontent_html :
1052 self->priv->data->out_content_html;
1054 /* remove all the unread marks when we are sending a message */
1055 theme_adium_remove_all_focus_marks (self);
1062 html = consecutive ? self->priv->data->in_nextcontext_html :
1063 self->priv->data->in_context_html;
1066 html = consecutive ? self->priv->data->in_nextcontent_html :
1067 self->priv->data->in_content_html;
1070 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1072 theme_adium_add_html (self, func, html, body_escaped,
1073 avatar_filename, name_escaped, contact_id,
1074 service_name, message_classes->str,
1075 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1077 /* Keep the sender of the last displayed message */
1079 g_object_unref (*prev_contact);
1081 *prev_contact = g_object_ref (sender);
1082 *prev_timestamp = timestamp;
1083 *prev_is_backlog = is_backlog;
1085 g_free (body_escaped);
1086 g_free (name_escaped);
1087 g_string_free (message_classes, TRUE);
1091 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1092 EmpathyMessage *msg,
1093 gboolean should_highlight)
1095 const gchar *js_funcs[] = { "appendNextMessage",
1096 "appendNextMessageNoScroll",
1098 "appendMessageNoScroll" };
1100 if (self->priv->pages_loading != 0)
1102 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1103 should_highlight, FALSE);
1107 theme_adium_add_message (self, msg, &self->priv->last_contact,
1108 &self->priv->last_timestamp, &self->priv->last_is_backlog,
1109 should_highlight, js_funcs);
1113 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1117 PangoDirection direction;
1119 if (self->priv->pages_loading != 0)
1121 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1125 direction = pango_find_base_dir (str, -1);
1126 str_escaped = g_markup_escape_text (str, -1);
1127 theme_adium_append_event_escaped (self, str_escaped, direction);
1128 g_free (str_escaped);
1132 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1133 const gchar *markup_text,
1134 const gchar *fallback_text)
1136 PangoDirection direction;
1138 direction = pango_find_base_dir (fallback_text, -1);
1139 theme_adium_append_event_escaped (self, markup_text, direction);
1143 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1144 EmpathyMessage *msg,
1145 gboolean should_highlight)
1147 const gchar *js_funcs[] = { "prependPrev",
1152 if (self->priv->pages_loading != 0)
1154 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1155 should_highlight, TRUE);
1159 theme_adium_add_message (self, msg, &self->priv->first_contact,
1160 &self->priv->first_timestamp, &self->priv->first_is_backlog,
1161 should_highlight, js_funcs);
1165 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1166 EmpathyMessage *message)
1168 WebKitDOMDocument *doc;
1169 WebKitDOMElement *span;
1170 gchar *id, *parsed_body;
1171 gchar *tooltip, *timestamp;
1172 GtkIconInfo *icon_info;
1173 GError *error = NULL;
1175 if (self->priv->pages_loading != 0)
1177 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1181 id = g_strdup_printf ("message-token-%s",
1182 empathy_message_get_supersedes (message));
1183 /* we don't pass a token here, because doing so will return another
1184 * <span> element, and we don't want nested <span> elements */
1185 parsed_body = theme_adium_parse_body (self,
1186 empathy_message_get_body (message), NULL);
1188 /* find the element */
1189 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1190 span = webkit_dom_document_get_element_by_id (doc, id);
1194 DEBUG ("Failed to find id '%s'", id);
1198 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1200 DEBUG ("Not a HTML element");
1204 /* update the HTML */
1205 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1206 parsed_body, &error);
1210 DEBUG ("Error setting new inner-HTML: %s", error->message);
1211 g_error_free (error);
1216 timestamp = empathy_time_to_string_local (
1217 empathy_message_get_timestamp (message),
1219 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1221 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1227 /* mark this message as edited */
1228 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1229 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1231 if (icon_info != NULL)
1233 /* set the icon as a background image using CSS
1234 * FIXME: the icon won't update in response to theme changes */
1235 gchar *style = g_strdup_printf (
1236 "background-image:url('%s');"
1237 "background-repeat:no-repeat;"
1238 "background-position:left center;"
1239 "padding-left:19px;", /* 16px icon + 3px padding */
1240 gtk_icon_info_get_filename (icon_info));
1242 webkit_dom_element_set_attribute (span, "style", style, &error);
1246 DEBUG ("Error setting element style: %s",
1248 g_clear_error (&error);
1253 gtk_icon_info_free (icon_info);
1259 DEBUG ("Could not find message to edit with: %s",
1260 empathy_message_get_body (message));
1264 g_free (parsed_body);
1268 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1269 gboolean allow_scrolling)
1271 self->priv->allow_scrolling = allow_scrolling;
1273 if (allow_scrolling)
1274 empathy_theme_adium_scroll_down (self);
1278 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1280 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1284 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1286 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1290 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1292 theme_adium_load_template (self);
1294 /* Clear last contact to avoid trying to add a 'joined'
1295 * message when we don't have an insertion point. */
1296 if (self->priv->last_contact)
1298 g_object_unref (self->priv->last_contact);
1299 self->priv->last_contact = NULL;
1304 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1305 const gchar *search_criteria,
1306 gboolean new_search,
1307 gboolean match_case)
1309 /* FIXME: Doesn't respect new_search */
1310 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1311 search_criteria, match_case, FALSE, TRUE);
1315 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1316 const gchar *search_criteria,
1317 gboolean new_search,
1318 gboolean match_case)
1320 /* FIXME: Doesn't respect new_search */
1321 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1322 search_criteria, match_case, TRUE, TRUE);
1326 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1327 const gchar *search_criteria,
1328 gboolean match_case,
1329 gboolean *can_do_previous,
1330 gboolean *can_do_next)
1332 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1333 * find_next and find_previous to work around this problem. */
1334 if (can_do_previous)
1335 *can_do_previous = TRUE;
1337 *can_do_next = TRUE;
1341 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1343 gboolean match_case)
1345 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1346 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1347 text, match_case, 0);
1348 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1353 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1355 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1359 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1362 WebKitDOMDocument *dom;
1363 WebKitDOMNodeList *nodes;
1365 GError *error = NULL;
1367 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1371 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1373 /* Get all nodes with focus class */
1374 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1379 DEBUG ("Error getting focus nodes: %s",
1380 error ? error->message : "No error");
1381 g_clear_error (&error);
1385 theme_adium_remove_focus_marks (self, nodes);
1389 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1392 EmpathyThemeAdium *self = user_data;
1393 guint32 id = GPOINTER_TO_UINT (data);
1395 theme_adium_remove_mark_from_message (self, id);
1399 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1402 self->priv->has_focus = has_focus;
1403 if (!self->priv->has_focus)
1405 /* We've lost focus, so let's make sure all the acked
1406 * messages have lost their unread marker. */
1407 g_queue_foreach (&self->priv->acked_messages,
1408 theme_adium_remove_acked_message_unread_mark_foreach, self);
1409 g_queue_clear (&self->priv->acked_messages);
1411 self->priv->has_unread_message = FALSE;
1416 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1417 EmpathyMessage *message)
1423 tp_msg = empathy_message_get_tp_message (message);
1428 id = tp_message_get_pending_message_id (tp_msg, &valid);
1431 g_warning ("Acknoledged message doesn't have a pending ID");
1435 /* We only want to actually remove the unread marker if the
1436 * view doesn't have focus. If we did it all the time we would
1437 * never see the unread markers, ever! So, we'll queue these
1438 * up, and when we lose focus, we'll remove the markers. */
1439 if (self->priv->has_focus)
1441 g_queue_push_tail (&self->priv->acked_messages,
1442 GUINT_TO_POINTER (id));
1446 theme_adium_remove_mark_from_message (self, id);
1450 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1451 GtkWidget *default_menu,
1452 WebKitHitTestResult *hit_test_result,
1453 gboolean triggered_with_keyboard,
1457 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1459 if (g_settings_get_boolean (self->priv->gsettings_chat,
1460 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1461 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1463 menu = empathy_webkit_create_context_menu (
1464 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1466 gtk_widget_show_all (menu);
1468 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1469 gtk_get_current_event_time ());
1475 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1476 gboolean show_avatars)
1478 self->priv->show_avatars = show_avatars;
1482 theme_adium_load_finished_cb (WebKitWebView *view,
1483 WebKitWebFrame *frame,
1486 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1489 DEBUG ("Page loaded");
1490 self->priv->pages_loading--;
1492 if (self->priv->pages_loading != 0)
1495 /* Display queued messages */
1496 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1498 QueuedItem *item = l->data;
1502 case QUEUED_MESSAGE:
1503 empathy_theme_adium_append_message (self, item->msg,
1504 item->should_highlight);
1508 empathy_theme_adium_edit_message (self, item->msg);
1512 empathy_theme_adium_append_event (self, item->str);
1516 free_queued_item (item);
1519 g_queue_clear (&self->priv->message_queue);
1523 theme_adium_finalize (GObject *object)
1525 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1527 empathy_adium_data_unref (self->priv->data);
1529 g_object_unref (self->priv->gsettings_chat);
1530 g_object_unref (self->priv->gsettings_desktop);
1532 g_free (self->priv->variant);
1534 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1538 theme_adium_dispose (GObject *object)
1540 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1542 if (self->priv->smiley_manager)
1544 g_object_unref (self->priv->smiley_manager);
1545 self->priv->smiley_manager = NULL;
1548 g_clear_object (&self->priv->first_contact);
1550 if (self->priv->last_contact)
1552 g_object_unref (self->priv->last_contact);
1553 self->priv->last_contact = NULL;
1556 if (self->priv->inspector_window)
1558 gtk_widget_destroy (self->priv->inspector_window);
1559 self->priv->inspector_window = NULL;
1562 if (self->priv->acked_messages.length > 0)
1564 g_queue_clear (&self->priv->acked_messages);
1567 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1571 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1572 EmpathyThemeAdium *self)
1574 if (self->priv->inspector_window)
1576 gtk_widget_show_all (self->priv->inspector_window);
1583 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1584 EmpathyThemeAdium *self)
1586 if (self->priv->inspector_window)
1588 gtk_widget_hide (self->priv->inspector_window);
1594 static WebKitWebView *
1595 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1596 WebKitWebView *web_view,
1597 EmpathyThemeAdium *self)
1599 GtkWidget *scrolled_window;
1600 GtkWidget *inspector_web_view;
1602 if (!self->priv->inspector_window)
1604 /* Create main window */
1605 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1607 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1610 g_signal_connect (self->priv->inspector_window, "delete-event",
1611 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1613 /* Pack a scrolled window */
1614 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1616 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1617 GTK_POLICY_AUTOMATIC,
1618 GTK_POLICY_AUTOMATIC);
1619 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1621 gtk_widget_show (scrolled_window);
1623 /* Pack a webview in the scrolled window. That webview will be
1624 * used to render the inspector tool. */
1625 inspector_web_view = webkit_web_view_new ();
1626 gtk_container_add (GTK_CONTAINER (scrolled_window),
1627 inspector_web_view);
1628 gtk_widget_show (scrolled_window);
1630 return WEBKIT_WEB_VIEW (inspector_web_view);
1637 theme_adium_constructed (GObject *object)
1639 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1640 const gchar *font_family = NULL;
1642 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1643 WebKitWebInspector *webkit_inspector;
1645 /* Set default settings */
1646 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1647 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1649 if (font_family && font_size)
1651 g_object_set (webkit_web_view_get_settings (webkit_view),
1652 "default-font-family", font_family,
1653 "default-font-size", font_size,
1658 empathy_webkit_bind_font_setting (webkit_view,
1659 self->priv->gsettings_desktop,
1660 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1663 /* Setup webkit inspector */
1664 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1665 g_signal_connect (webkit_inspector, "inspect-web-view",
1666 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1667 g_signal_connect (webkit_inspector, "show-window",
1668 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1669 g_signal_connect (webkit_inspector, "close-window",
1670 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1673 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1675 self->priv->in_construction = FALSE;
1679 theme_adium_get_property (GObject *object,
1684 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1688 case PROP_ADIUM_DATA:
1689 g_value_set_boxed (value, self->priv->data);
1692 g_value_set_string (value, self->priv->variant);
1695 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1701 theme_adium_set_property (GObject *object,
1703 const GValue *value,
1706 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1710 case PROP_ADIUM_DATA:
1711 g_assert (self->priv->data == NULL);
1712 self->priv->data = g_value_dup_boxed (value);
1715 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1718 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1724 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1726 GObjectClass *object_class = G_OBJECT_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 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1735 g_param_spec_boxed ("adium-data",
1737 "Data for the adium theme",
1738 EMPATHY_TYPE_ADIUM_DATA,
1739 G_PARAM_CONSTRUCT_ONLY |
1741 G_PARAM_STATIC_STRINGS));
1743 g_object_class_install_property (object_class, PROP_VARIANT,
1744 g_param_spec_string ("variant",
1745 "The theme variant",
1746 "Variant name for the theme",
1750 G_PARAM_STATIC_STRINGS));
1752 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1756 empathy_theme_adium_init (EmpathyThemeAdium *self)
1758 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1759 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1761 self->priv->in_construction = TRUE;
1762 g_queue_init (&self->priv->message_queue);
1763 self->priv->allow_scrolling = TRUE;
1764 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1766 /* Show avatars by default. */
1767 self->priv->show_avatars = TRUE;
1769 g_signal_connect (self, "load-finished",
1770 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1771 g_signal_connect (self, "navigation-policy-decision-requested",
1772 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1773 g_signal_connect (self, "context-menu",
1774 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1776 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1777 self->priv->gsettings_desktop = g_settings_new (
1778 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1782 empathy_theme_adium_new (EmpathyAdiumData *data,
1783 const gchar *variant)
1785 g_return_val_if_fail (data != NULL, NULL);
1787 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1794 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1795 const gchar *variant)
1797 gchar *variant_path;
1800 if (!tp_strdiff (self->priv->variant, variant))
1803 g_free (self->priv->variant);
1804 self->priv->variant = g_strdup (variant);
1806 if (self->priv->in_construction)
1809 DEBUG ("Update view with variant: '%s'", variant);
1810 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1811 self->priv->variant);
1812 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1815 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1817 g_free (variant_path);
1820 g_object_notify (G_OBJECT (self), "variant");
1824 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1826 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1828 empathy_webkit_show_inspector (web_view);
1832 empathy_adium_path_is_valid (const gchar *path)
1842 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1843 tmp = g_strsplit (path, "/", 0);
1847 dir = tmp[g_strv_length (tmp) - 1];
1849 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1857 /* The theme is not valid if there is no Info.plist */
1858 file = g_build_filename (path, "Contents", "Info.plist",
1860 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1866 /* We ship a default Template.html as fallback if there is any problem
1867 * with the one inside the theme. The only other required file is
1868 * Content.html OR Incoming/Content.html*/
1869 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1871 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1876 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1877 "Content.html", NULL);
1878 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1886 empathy_adium_info_new (const gchar *path)
1890 GHashTable *info = NULL;
1892 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1894 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1895 value = empathy_plist_parse_from_file (file);
1901 info = g_value_dup_boxed (value);
1902 tp_g_value_slice_free (value);
1904 /* Insert the theme's path into the hash table,
1905 * keys have to be dupped */
1906 tp_asv_set_string (info, g_strdup ("path"), path);
1912 adium_info_get_version (GHashTable *info)
1914 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1917 static const gchar *
1918 adium_info_get_no_variant_name (GHashTable *info)
1920 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1921 return name ? name : _("Normal");
1925 adium_info_dup_path_for_variant (GHashTable *info,
1926 const gchar *variant)
1928 guint version = adium_info_get_version (info);
1929 const gchar *no_variant = adium_info_get_no_variant_name (info);
1930 GPtrArray *variants;
1933 if (version <= 2 && !tp_strdiff (variant, no_variant))
1934 return g_strdup ("main.css");
1936 variants = empathy_adium_info_get_available_variants (info);
1937 if (variants->len == 0)
1938 return g_strdup ("main.css");
1940 /* Verify the variant exists, fallback to the first one */
1941 for (i = 0; i < variants->len; i++)
1943 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1947 if (i == variants->len)
1949 DEBUG ("Variant %s does not exist", variant);
1950 variant = g_ptr_array_index (variants, 0);
1953 return g_strdup_printf ("Variants/%s.css", variant);
1958 empathy_adium_info_get_default_variant (GHashTable *info)
1960 if (adium_info_get_version (info) <= 2)
1961 return adium_info_get_no_variant_name (info);
1963 return tp_asv_get_string (info, "DefaultVariant");
1967 empathy_adium_info_get_available_variants (GHashTable *info)
1969 GPtrArray *variants;
1974 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1975 if (variants != NULL)
1978 variants = g_ptr_array_new_with_free_func (g_free);
1979 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1980 G_TYPE_PTR_ARRAY, variants);
1982 path = tp_asv_get_string (info, "path");
1983 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1984 dir = g_dir_open (dirpath, 0, NULL);
1989 for (name = g_dir_read_name (dir);
1991 name = g_dir_read_name (dir))
1993 gchar *display_name;
1995 if (!g_str_has_suffix (name, ".css"))
1998 display_name = g_strdup (name);
1999 strstr (display_name, ".css")[0] = '\0';
2000 g_ptr_array_add (variants, display_name);
2007 if (adium_info_get_version (info) <= 2)
2008 g_ptr_array_add (variants,
2009 g_strdup (adium_info_get_no_variant_name (info)));
2015 empathy_adium_data_get_type (void)
2017 static GType type_id = 0;
2021 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2022 (GBoxedCopyFunc) empathy_adium_data_ref,
2023 (GBoxedFreeFunc) empathy_adium_data_unref);
2030 empathy_adium_data_new_with_info (const gchar *path,
2033 EmpathyAdiumData *data;
2034 gchar *template_html = NULL;
2035 gchar *footer_html = NULL;
2038 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2040 data = g_slice_new0 (EmpathyAdiumData);
2041 data->ref_count = 1;
2042 data->path = g_strdup (path);
2043 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2044 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2045 data->info = g_hash_table_ref (info);
2046 data->version = adium_info_get_version (info);
2047 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2048 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2049 g_str_equal, g_free, g_free);
2051 DEBUG ("Loading theme at %s", path);
2053 #define LOAD(path, var) \
2054 tmp = g_build_filename (data->basedir, path, NULL); \
2055 g_file_get_contents (tmp, &var, NULL, NULL); \
2058 #define LOAD_CONST(path, var) \
2061 LOAD (path, content); \
2062 if (content != NULL) { \
2063 g_ptr_array_add (data->strings_to_free, content); \
2068 /* Load html files */
2069 LOAD_CONST ("Content.html", data->content_html);
2070 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2071 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2072 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2073 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2074 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2075 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2076 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2077 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2078 LOAD_CONST ("Status.html", data->status_html);
2079 LOAD ("Template.html", template_html);
2080 LOAD ("Footer.html", footer_html);
2085 /* HTML fallbacks: If we have at least content OR in_content, then
2086 * everything else gets a fallback */
2088 #define FALLBACK(html, fallback) \
2089 if (html == NULL) { \
2093 /* in_nextcontent -> in_content -> content */
2094 FALLBACK (data->in_content_html, data->content_html);
2095 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2097 /* context -> content */
2098 FALLBACK (data->in_context_html, data->in_content_html);
2099 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2100 FALLBACK (data->out_context_html, data->out_content_html);
2101 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2104 FALLBACK (data->out_content_html, data->in_content_html);
2105 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2106 FALLBACK (data->out_context_html, data->in_context_html);
2107 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2109 /* status -> in_content */
2110 FALLBACK (data->status_html, data->in_content_html);
2114 /* template -> empathy's template */
2115 data->custom_template = (template_html != NULL);
2116 if (template_html == NULL)
2118 GError *error = NULL;
2120 tmp = empathy_file_lookup ("Template.html", "data");
2122 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2123 g_warning ("couldn't load Empathy's default theme "
2124 "template: %s", error->message);
2125 g_return_val_if_reached (data);
2131 /* Default avatar */
2132 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2133 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2135 data->default_incoming_avatar_filename = tmp;
2142 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2143 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2145 data->default_outgoing_avatar_filename = tmp;
2152 /* Old custom templates had only 4 parameters.
2153 * New templates have 5 parameters */
2154 if (data->version <= 2 && data->custom_template)
2156 tmp = string_with_format (template_html,
2158 "%@", /* Leave variant unset */
2159 "", /* The header */
2160 footer_html ? footer_html : "",
2165 tmp = string_with_format (template_html,
2167 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2168 "%@", /* Leave variant unset */
2169 "", /* The header */
2170 footer_html ? footer_html : "",
2173 g_ptr_array_add (data->strings_to_free, tmp);
2174 data->template_html = tmp;
2176 g_free (template_html);
2177 g_free (footer_html);
2183 empathy_adium_data_new (const gchar *path)
2185 EmpathyAdiumData *data;
2188 info = empathy_adium_info_new (path);
2189 data = empathy_adium_data_new_with_info (path, info);
2190 g_hash_table_unref (info);
2196 empathy_adium_data_ref (EmpathyAdiumData *data)
2198 g_return_val_if_fail (data != NULL, NULL);
2200 g_atomic_int_inc (&data->ref_count);
2206 empathy_adium_data_unref (EmpathyAdiumData *data)
2208 g_return_if_fail (data != NULL);
2210 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2211 g_free (data->path);
2212 g_free (data->basedir);
2213 g_free (data->default_avatar_filename);
2214 g_free (data->default_incoming_avatar_filename);
2215 g_free (data->default_outgoing_avatar_filename);
2216 g_hash_table_unref (data->info);
2217 g_ptr_array_unref (data->strings_to_free);
2218 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2220 g_slice_free (EmpathyAdiumData, data);
2225 empathy_adium_data_get_info (EmpathyAdiumData *data)
2227 g_return_val_if_fail (data != NULL, NULL);
2233 empathy_adium_data_get_path (EmpathyAdiumData *data)
2235 g_return_val_if_fail (data != NULL, NULL);