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>
23 #include "empathy-theme-adium.h"
25 #include <glib/gi18n-lib.h>
26 #include <tp-account-widgets/tpaw-time.h>
27 #include <tp-account-widgets/tpaw-utils.h>
29 #include "empathy-gsettings.h"
30 #include "empathy-images.h"
31 #include "empathy-plist.h"
32 #include "empathy-smiley-manager.h"
33 #include "empathy-ui-utils.h"
34 #include "empathy-utils.h"
35 #include "empathy-webkit-utils.h"
37 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
38 #include "empathy-debug.h"
40 #define BORING_DPI_DEFAULT 96
42 /* "Join" consecutive messages with timestamps within five minutes */
43 #define MESSAGE_JOIN_PERIOD 5*60
45 struct _EmpathyThemeAdiumPriv
47 EmpathyAdiumData *data;
48 EmpathySmileyManager *smiley_manager;
49 EmpathyContact *first_contact;
50 EmpathyContact *last_contact;
51 gint64 first_timestamp;
52 gint64 last_timestamp;
53 gboolean first_is_backlog;
54 gboolean last_is_backlog;
56 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
58 /* Queue of guint32 of pending message id to remove unread
59 * marker for when we lose focus. */
60 GQueue acked_messages;
61 GtkWidget *inspector_window;
63 GSettings *gsettings_chat;
64 GSettings *gsettings_desktop;
67 gboolean has_unread_message;
68 gboolean allow_scrolling;
70 gboolean in_construction;
71 gboolean show_avatars;
74 struct _EmpathyAdiumData
79 gchar *default_avatar_filename;
80 gchar *default_incoming_avatar_filename;
81 gchar *default_outgoing_avatar_filename;
84 gboolean custom_template;
85 /* gchar* -> gchar* both owned */
86 GHashTable *date_format_cache;
89 const gchar *template_html;
90 const gchar *content_html;
91 const gchar *in_content_html;
92 const gchar *in_context_html;
93 const gchar *in_nextcontent_html;
94 const gchar *in_nextcontext_html;
95 const gchar *out_content_html;
96 const gchar *out_context_html;
97 const gchar *out_nextcontent_html;
98 const gchar *out_nextcontext_html;
99 const gchar *status_html;
101 /* Above html strings are pointers to strings stored in this array.
102 * We do this because of fallbacks, some htmls could be pointing the
104 GPtrArray *strings_to_free;
107 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
108 const gchar *variant);
117 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
118 WEBKIT_TYPE_WEB_VIEW)
132 gboolean should_highlight;
136 queue_item (GQueue *queue,
140 gboolean should_highlight,
143 QueuedItem *item = g_slice_new0 (QueuedItem);
147 item->msg = g_object_ref (msg);
148 item->str = g_strdup (str);
149 item->should_highlight = should_highlight;
152 g_queue_push_head (queue, item);
154 g_queue_push_tail (queue, item);
160 free_queued_item (QueuedItem *item)
162 tp_clear_object (&item->msg);
165 g_slice_free (QueuedItem, item);
169 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
170 WebKitWebFrame *web_frame,
171 WebKitNetworkRequest *request,
172 WebKitWebNavigationAction *action,
173 WebKitWebPolicyDecision *decision,
178 /* Only call url_show on clicks */
179 if (webkit_web_navigation_action_get_reason (action) !=
180 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
182 webkit_web_policy_decision_use (decision);
186 uri = webkit_network_request_get_uri (request);
187 empathy_url_show (GTK_WIDGET (view), uri);
189 webkit_web_policy_decision_ignore (decision);
193 /* Replace each %@ in format with string passed in args */
195 string_with_format (const gchar *format,
196 const gchar *first_string,
203 va_start (args, first_string);
204 result = g_string_sized_new (strlen (format));
205 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
209 next = strstr (format, "%@");
213 g_string_append_len (result, format, next - format);
214 g_string_append (result, str);
217 g_string_append (result, format);
220 return g_string_free (result, FALSE);
224 theme_adium_load_template (EmpathyThemeAdium *self)
230 self->priv->pages_loading++;
231 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
233 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
234 self->priv->variant);
236 template = string_with_format (self->priv->data->template_html,
239 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
240 template, basedir_uri);
242 g_free (basedir_uri);
243 g_free (variant_path);
248 theme_adium_parse_body (EmpathyThemeAdium *self,
252 TpawStringParser *parsers;
255 /* Check if we have to parse smileys */
256 parsers = empathy_webkit_get_string_parser (
257 g_settings_get_boolean (self->priv->gsettings_chat,
258 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
260 /* Parse text and construct string with links and smileys replaced
261 * by html tags. Also escape text to make sure html code is
262 * displayed verbatim. */
263 string = g_string_sized_new (strlen (text));
265 /* wrap this in HTML that allows us to find the message for later
267 if (!tp_str_empty (token))
268 g_string_append_printf (string,
269 "<span id=\"message-token-%s\">",
272 tpaw_string_parser_substr (text, -1, parsers, string);
274 if (!tp_str_empty (token))
275 g_string_append (string, "</span>");
277 /* Wrap body in order to make tabs and multiple spaces displayed
278 * properly. See bug #625745. */
279 g_string_prepend (string, "<div style=\"display: inline; "
280 "white-space: pre-wrap\"'>");
281 g_string_append (string, "</div>");
283 return g_string_free (string, FALSE);
287 escape_and_append_len (GString *string, const gchar *str, gint len)
289 while (str != NULL && *str != '\0' && len != 0)
295 g_string_append (string, "\\\\");
299 g_string_append (string, "\\\"");
302 /* Remove end of lines */
305 g_string_append_c (string, *str);
313 /* If *str starts with match, returns TRUE and move pointer to the end */
315 theme_adium_match (const gchar **str,
320 len = strlen (match);
321 if (strncmp (*str, match, len) == 0)
330 /* Like theme_adium_match() but also return the X part if match is
333 theme_adium_match_with_format (const gchar **str,
337 const gchar *cur = *str;
340 if (!theme_adium_match (&cur, match))
345 end = strstr (cur, "}%");
349 *format = g_strndup (cur , end - cur);
354 /* List of colors used by %senderColor%. Copied from
355 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
357 static gchar *colors[] = {
358 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
359 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
360 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
361 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
362 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
363 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
364 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
365 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
366 "lightblue", "lightcoral",
367 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
368 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
369 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
370 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
371 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
372 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
373 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
374 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
375 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
376 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
381 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
383 /* Convert from NSDateFormatter
384 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
385 * to strftime supported by g_date_time_format.
386 * FIXME: table is incomplete, doc of g_date_time_format has a table of
388 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
389 * in 2.29.x we have to explictely request padding with %0x */
390 static const gchar *convert_table[] = {
392 "A", NULL, // 0~86399999 (Millisecond of Day)
394 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
395 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
396 "cc", "%u", // 1~7 (Day of Week)
397 "c", "%u", // 1~7 (Day of Week)
399 "dd", "%d", // 1~31 (0 padded Day of Month)
400 "d", "%d", // 1~31 (0 padded Day of Month)
401 "D", "%j", // 1~366 (0 padded Day of Year)
403 "e", "%u", // 1~7 (0 padded Day of Week)
404 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
405 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
406 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
407 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
409 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
411 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
412 "GGGG", NULL, // Before Christ/Anno Domini
413 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
414 "GG", NULL, // BC/AD (Era Designator Abbreviated)
415 "G", NULL, // BC/AD (Era Designator Abbreviated)
417 "h", "%I", // 1~12 (0 padded Hour (12hr))
418 "H", "%H", // 0~23 (0 padded Hour (24hr))
420 "k", NULL, // 1~24 (0 padded Hour (24hr)
421 "K", NULL, // 0~11 (0 padded Hour (12hr))
423 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
424 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
425 "LL", "%m", // 1~12 (0 padded Month)
426 "L", "%m", // 1~12 (0 padded Month)
428 "m", "%M", // 0~59 (0 padded Minute)
429 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
430 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
431 "MM", "%m", // 1~12 (0 padded Month)
432 "M", "%m", // 1~12 (0 padded Month)
434 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
435 "qqq", NULL, // Q1/Q2/Q3/Q4
436 "qq", NULL, // 1~4 (0 padded Quarter)
437 "q", NULL, // 1~4 (0 padded Quarter)
438 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
439 "QQQ", NULL, // Q1/Q2/Q3/Q4
440 "QQ", NULL, // 1~4 (0 padded Quarter)
441 "Q", NULL, // 1~4 (0 padded Quarter)
443 "s", "%S", // 0~59 (0 padded Second)
444 "S", NULL, // (rounded Sub-Second)
446 "u", "%Y", // (0 padded Year)
448 "vvvv", "%Z", // (General GMT Timezone Name)
449 "vvv", "%Z", // (General GMT Timezone Abbreviation)
450 "vv", "%Z", // (General GMT Timezone Abbreviation)
451 "v", "%Z", // (General GMT Timezone Abbreviation)
453 "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)
454 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
456 "yyyy", "%Y", // (Full Year)
457 "yyy", "%y", // (2 Digits Year)
458 "yy", "%y", // (2 Digits Year)
459 "y", "%Y", // (Full Year)
460 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
461 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
462 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
463 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
465 "zzzz", NULL, // (Specific GMT Timezone Name)
466 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
467 "zz", NULL, // (Specific GMT Timezone Abbreviation)
468 "z", NULL, // (Specific GMT Timezone Abbreviation)
469 "Z", "%z", // +0000 (RFC 822 Timezone)
478 str = g_hash_table_lookup (data->date_format_cache, nsdate);
483 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
484 * by corresponding strftime tag. */
485 string = g_string_sized_new (strlen (nsdate));
486 for (i = 0; nsdate[i] != '\0'; i++)
488 gboolean found = FALSE;
490 /* even indexes are NSDateFormatter tag, odd indexes are the
491 * corresponding strftime tag */
492 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
494 if (g_str_has_prefix (nsdate + i, convert_table[j]))
503 /* If we don't have a replacement, just ignore that tag */
504 if (convert_table[j + 1] != NULL)
505 g_string_append (string, convert_table[j + 1]);
507 i += strlen (convert_table[j]) - 1;
511 g_string_append_c (string, nsdate[i]);
515 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
517 /* The cache takes ownership of string->str */
518 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
519 return g_string_free (string, FALSE);
523 theme_adium_add_html (EmpathyThemeAdium *self,
526 const gchar *message,
527 const gchar *avatar_filename,
529 const gchar *contact_id,
530 const gchar *service_name,
531 const gchar *message_classes,
535 PangoDirection direction)
539 const gchar *cur = NULL;
543 /* Make some search-and-replace in the html code */
544 string = g_string_sized_new (strlen (html) + strlen (message));
545 g_string_append_printf (string, "%s(\"", func);
547 for (cur = html; *cur != '\0'; cur++)
549 const gchar *replace = NULL;
550 gchar *dup_replace = NULL;
551 gchar *format = NULL;
553 /* Those are all well known keywords that needs replacement in
554 * html files. Please keep them in the same order than the adium
555 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
556 if (theme_adium_match (&cur, "%userIconPath%"))
558 replace = avatar_filename;
560 else if (theme_adium_match (&cur, "%senderScreenName%"))
562 replace = contact_id;
564 else if (theme_adium_match (&cur, "%sender%"))
568 else if (theme_adium_match (&cur, "%senderColor%"))
570 /* A color derived from the user's name.
571 * FIXME: If a colon separated list of HTML colors is at
572 * Incoming/SenderColors.txt it will be used instead of
573 * the default colors.
576 /* Ensure we always use the same color when sending messages
582 else if (contact_id != NULL)
584 guint hash = g_str_hash (contact_id);
585 replace = colors[hash % G_N_ELEMENTS (colors)];
588 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
590 /* FIXME: The path to the status icon of the sender
591 * (available, away, etc...)
594 else if (theme_adium_match (&cur, "%messageDirection%"))
598 case PANGO_DIRECTION_LTR:
599 case PANGO_DIRECTION_TTB_LTR:
600 case PANGO_DIRECTION_WEAK_LTR:
603 case PANGO_DIRECTION_RTL:
604 case PANGO_DIRECTION_TTB_RTL:
605 case PANGO_DIRECTION_WEAK_RTL:
608 case PANGO_DIRECTION_NEUTRAL:
613 else if (theme_adium_match (&cur, "%senderDisplayName%"))
615 /* FIXME: The serverside (remotely set) name of the
616 * sender, such as an MSN display name.
618 * We don't have access to that yet so we use
619 * local alias instead.
623 else if (theme_adium_match (&cur, "%senderPrefix%"))
625 /* FIXME: If we supported IRC user mode flags, this
626 * would be replaced with @ if the user is an op, + if
627 * the user has voice, etc. as per
628 * http://hg.adium.im/adium/rev/b586b027de42. But we
629 * don't, so for now we just strip it. */
631 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
634 /* FIXME: This keyword is used to represent the
635 * highlight background color. "X" is the opacity of the
636 * background, ranges from 0 to 1 and can be any decimal
640 else if (theme_adium_match (&cur, "%message%"))
644 else if (theme_adium_match (&cur, "%time%") ||
645 theme_adium_match_with_format (&cur, "%time{", &format))
647 const gchar *strftime_format;
649 strftime_format = nsdate_to_strftime (self->priv->data, format);
651 dup_replace = tpaw_time_to_string_local (timestamp,
652 strftime_format ? strftime_format :
653 TPAW_TIME_DATE_FORMAT_DISPLAY_SHORT);
655 dup_replace = tpaw_time_to_string_local (timestamp,
656 strftime_format ? strftime_format :
657 TPAW_TIME_FORMAT_DISPLAY_SHORT);
659 replace = dup_replace;
661 else if (theme_adium_match (&cur, "%shortTime%"))
663 dup_replace = tpaw_time_to_string_local (timestamp,
664 TPAW_TIME_FORMAT_DISPLAY_SHORT);
665 replace = dup_replace;
667 else if (theme_adium_match (&cur, "%service%"))
669 replace = service_name;
671 else if (theme_adium_match (&cur, "%variant%"))
673 /* FIXME: The name of the active message style variant,
674 * with all spaces replaced with an underscore.
675 * A variant named "Alternating Messages - Blue Red"
676 * will become "Alternating_Messages_-_Blue_Red".
679 else if (theme_adium_match (&cur, "%userIcons%"))
681 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
683 else if (theme_adium_match (&cur, "%messageClasses%"))
685 replace = message_classes;
687 else if (theme_adium_match (&cur, "%status%"))
689 /* FIXME: A description of the status event. This is
690 * neither in the user's local language nor expected to
691 * be displayed; it may be useful to use a different div
692 * class to present different types of status messages.
693 * The following is a list of some of the more important
694 * status messages; your message style should be able to
695 * handle being shown a status message not in this list,
696 * as even at present the list is incomplete and is
697 * certain to become out of date in the future:
706 * contact_joined (group chats)
710 * encryption (all OTR messages use this status)
711 * purple (all IRC topic and join/part messages use this status)
712 * fileTransferStarted
713 * fileTransferCompleted
718 escape_and_append_len (string, cur, 1);
722 /* Here we have a replacement to make */
723 escape_and_append_len (string, replace, -1);
725 g_free (dup_replace);
728 g_string_append (string, "\")");
730 bytes = g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
731 G_RESOURCE_LOOKUP_FLAGS_NONE,
733 js = (const gchar *) g_bytes_get_data (bytes, NULL);
734 g_string_prepend (string, js);
735 g_bytes_unref (bytes);
737 script = g_string_free (string, FALSE);
738 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
743 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
744 const gchar *escaped,
745 PangoDirection direction)
747 theme_adium_add_html (self, "appendMessage",
748 self->priv->data->status_html, escaped, NULL, NULL, NULL,
749 NULL, "event", tpaw_time_get_current (), FALSE, FALSE, direction);
751 /* There is no last contact */
752 if (self->priv->last_contact)
754 g_object_unref (self->priv->last_contact);
755 self->priv->last_contact = NULL;
760 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
761 WebKitDOMNodeList *nodes)
765 /* Remove focus and firstFocus class */
766 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
768 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
769 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
771 gchar **classes, **iter;
772 GString *new_class_name;
773 gboolean first = TRUE;
778 class_name = webkit_dom_html_element_get_class_name (element);
779 classes = g_strsplit (class_name, " ", -1);
780 new_class_name = g_string_sized_new (strlen (class_name));
782 for (iter = classes; *iter != NULL; iter++)
784 if (tp_strdiff (*iter, "focus") &&
785 tp_strdiff (*iter, "firstFocus"))
788 g_string_append_c (new_class_name, ' ');
790 g_string_append (new_class_name, *iter);
795 webkit_dom_html_element_set_class_name (element, new_class_name->str);
798 g_strfreev (classes);
799 g_string_free (new_class_name, TRUE);
804 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
806 WebKitDOMDocument *dom;
807 WebKitDOMNodeList *nodes;
808 GError *error = NULL;
810 if (!self->priv->has_unread_message)
813 self->priv->has_unread_message = FALSE;
815 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
819 /* Get all nodes with focus class */
820 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
824 DEBUG ("Error getting focus nodes: %s",
825 error ? error->message : "No error");
826 g_clear_error (&error);
830 theme_adium_remove_focus_marks (self, nodes);
835 ADD_CONSECUTIVE_MSG_SCROLL = 0,
836 ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
838 ADD_MSG_NO_SCROLL = 3
842 * theme_adium_add_message:
843 * @self: The #EmpathyThemeAdium used by the view.
844 * @msg: An #EmpathyMessage that is to be added to the view.
845 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
846 * @prev_timestamp: (out): Timestamp of the previous message.
847 * @prev_is_backlog: (out): Whether the previous message was fetched
849 * @should_highlight: Whether the message should be highlighted. eg.,
850 * if it matches the user's username in multi-user chat.
851 * @js_funcs: An array of JavaScript function names
853 * Shows @msg in the chat view by adding to @self. Addition is defined
854 * by the JavaScript functions listed in @js_funcs. Common examples
855 * are appending new incoming messages or prepending old messages from
858 * @js_funcs should be an array with exactly 4 entries. The entries
859 * should be the names of JavaScript functions that take the raw HTML
860 * that is to be added to the view as an argument and take the following
861 * actions, in this order:
862 * - add a new consecutive message and scroll to it if needed,
863 * - add a new consecutive message and do not scroll,
864 * - add a new non-consecutive message and scroll to it if needed, and
865 * - add a new non-consecutive message and do not scroll
867 * A message is considered to be consecutive with the previous one if
868 * all the following conditions are met:
869 * - senders are the same contact,
870 * - last message was recieved recently,
871 * - last message and this message both are/aren't backlog, and
872 * - DisableCombineConsecutive is not set in theme's settings
875 theme_adium_add_message (EmpathyThemeAdium *self,
877 EmpathyContact **prev_contact,
878 gint64 *prev_timestamp,
879 gboolean *prev_is_backlog,
880 gboolean should_highlight,
881 const gchar *js_funcs[])
883 EmpathyContact *sender;
886 gchar *body_escaped, *name_escaped;
888 const gchar *contact_id;
889 EmpathyAvatar *avatar;
890 const gchar *avatar_filename = NULL;
892 const gchar *html = NULL;
894 const gchar *service_name;
895 GString *message_classes = NULL;
897 gboolean consecutive;
899 PangoDirection direction;
902 /* Get information */
903 sender = empathy_message_get_sender (msg);
904 account = empathy_contact_get_account (sender);
905 service_name = tpaw_protocol_name_to_display_name
906 (tp_account_get_protocol_name (account));
907 if (service_name == NULL)
908 service_name = tp_account_get_protocol_name (account);
909 timestamp = empathy_message_get_timestamp (msg);
910 body_escaped = theme_adium_parse_body (self,
911 empathy_message_get_body (msg),
912 empathy_message_get_token (msg));
913 name = empathy_contact_get_logged_alias (sender);
914 contact_id = empathy_contact_get_id (sender);
915 action = (empathy_message_get_tptype (msg) ==
916 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
918 name_escaped = g_markup_escape_text (name, -1);
920 /* If this is a /me probably */
925 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
927 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
928 "<span class='actionMessageBody'>%s</span>",
929 name_escaped, body_escaped);
933 str = g_strdup_printf ("*%s*", body_escaped);
936 g_free (body_escaped);
940 /* Get the avatar filename, or a fallback */
941 avatar = empathy_contact_get_avatar (sender);
943 avatar_filename = avatar->filename;
945 if (!avatar_filename)
947 if (empathy_contact_is_user (sender))
948 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
950 avatar_filename = self->priv->data->default_incoming_avatar_filename;
952 if (!avatar_filename)
954 if (!self->priv->data->default_avatar_filename)
955 self->priv->data->default_avatar_filename =
956 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
957 GTK_ICON_SIZE_DIALOG);
959 avatar_filename = self->priv->data->default_avatar_filename;
963 is_backlog = empathy_message_is_backlog (msg);
964 consecutive = empathy_contact_equal (*prev_contact, sender) &&
965 (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
966 (is_backlog == *prev_is_backlog) &&
967 !tp_asv_get_boolean (self->priv->data->info,
968 "DisableCombineConsecutive", NULL);
970 /* Define message classes */
971 message_classes = g_string_new ("message");
972 if (!self->priv->has_focus && !is_backlog)
974 if (!self->priv->has_unread_message)
976 g_string_append (message_classes, " firstFocus");
977 self->priv->has_unread_message = TRUE;
979 g_string_append (message_classes, " focus");
983 g_string_append (message_classes, " history");
986 g_string_append (message_classes, " consecutive");
988 if (empathy_contact_is_user (sender))
989 g_string_append (message_classes, " outgoing");
991 g_string_append (message_classes, " incoming");
993 if (should_highlight)
994 g_string_append (message_classes, " mention");
996 if (empathy_message_get_tptype (msg) ==
997 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
998 g_string_append (message_classes, " autoreply");
1001 g_string_append (message_classes, " action");
1003 /* FIXME: other classes:
1004 * status - the message is a status change
1005 * event - the message is a notification of something happening
1006 * (for example, encryption being turned on)
1007 * %status% - See %status% in theme_adium_add_html ()
1010 /* This is slightly a hack, but it's the only way to add
1011 * arbitrary data to messages in the HTML. We add another
1012 * class called "x-empathy-message-id-*" to the message. This
1013 * way, we can remove the unread marker for this specific
1015 tp_msg = empathy_message_get_tp_message (msg);
1021 id = tp_message_get_pending_message_id (tp_msg, &valid);
1023 g_string_append_printf (message_classes,
1024 " x-empathy-message-id-%u", id);
1027 /* Define javascript function to use */
1029 func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1030 js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1032 func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1033 js_funcs[ADD_MSG_NO_SCROLL];
1035 if (empathy_contact_is_user (sender))
1040 html = consecutive ? self->priv->data->out_nextcontext_html :
1041 self->priv->data->out_context_html;
1044 html = consecutive ? self->priv->data->out_nextcontent_html :
1045 self->priv->data->out_content_html;
1047 /* remove all the unread marks when we are sending a message */
1048 theme_adium_remove_all_focus_marks (self);
1055 html = consecutive ? self->priv->data->in_nextcontext_html :
1056 self->priv->data->in_context_html;
1059 html = consecutive ? self->priv->data->in_nextcontent_html :
1060 self->priv->data->in_content_html;
1063 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1065 theme_adium_add_html (self, func, html, body_escaped,
1066 avatar_filename, name_escaped, contact_id,
1067 service_name, message_classes->str,
1068 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1070 /* Keep the sender of the last displayed message */
1072 g_object_unref (*prev_contact);
1074 *prev_contact = g_object_ref (sender);
1075 *prev_timestamp = timestamp;
1076 *prev_is_backlog = is_backlog;
1078 g_free (body_escaped);
1079 g_free (name_escaped);
1080 g_string_free (message_classes, TRUE);
1084 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1085 EmpathyMessage *msg,
1086 gboolean should_highlight)
1088 const gchar *js_funcs[] = { "appendNextMessage",
1089 "appendNextMessageNoScroll",
1091 "appendMessageNoScroll" };
1093 if (self->priv->pages_loading != 0)
1095 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1096 should_highlight, FALSE);
1100 theme_adium_add_message (self, msg, &self->priv->last_contact,
1101 &self->priv->last_timestamp, &self->priv->last_is_backlog,
1102 should_highlight, js_funcs);
1106 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1110 PangoDirection direction;
1112 if (self->priv->pages_loading != 0)
1114 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1118 direction = pango_find_base_dir (str, -1);
1119 str_escaped = g_markup_escape_text (str, -1);
1120 theme_adium_append_event_escaped (self, str_escaped, direction);
1121 g_free (str_escaped);
1125 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1126 const gchar *markup_text,
1127 const gchar *fallback_text)
1129 PangoDirection direction;
1131 direction = pango_find_base_dir (fallback_text, -1);
1132 theme_adium_append_event_escaped (self, markup_text, direction);
1136 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1137 EmpathyMessage *msg,
1138 gboolean should_highlight)
1140 const gchar *js_funcs[] = { "prependPrev",
1145 if (self->priv->pages_loading != 0)
1147 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1148 should_highlight, TRUE);
1152 theme_adium_add_message (self, msg, &self->priv->first_contact,
1153 &self->priv->first_timestamp, &self->priv->first_is_backlog,
1154 should_highlight, js_funcs);
1158 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1159 EmpathyMessage *message)
1161 WebKitDOMDocument *doc;
1162 WebKitDOMElement *span;
1163 gchar *id, *parsed_body;
1164 gchar *tooltip, *timestamp;
1165 GtkIconInfo *icon_info;
1166 GError *error = NULL;
1168 if (self->priv->pages_loading != 0)
1170 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1174 id = g_strdup_printf ("message-token-%s",
1175 empathy_message_get_supersedes (message));
1176 /* we don't pass a token here, because doing so will return another
1177 * <span> element, and we don't want nested <span> elements */
1178 parsed_body = theme_adium_parse_body (self,
1179 empathy_message_get_body (message), NULL);
1181 /* find the element */
1182 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1183 span = webkit_dom_document_get_element_by_id (doc, id);
1187 DEBUG ("Failed to find id '%s'", id);
1191 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1193 DEBUG ("Not a HTML element");
1197 /* update the HTML */
1198 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1199 parsed_body, &error);
1203 DEBUG ("Error setting new inner-HTML: %s", error->message);
1204 g_error_free (error);
1209 timestamp = tpaw_time_to_string_local (
1210 empathy_message_get_timestamp (message),
1212 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1214 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1220 /* mark this message as edited */
1221 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1222 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1224 if (icon_info != NULL)
1226 /* set the icon as a background image using CSS
1227 * FIXME: the icon won't update in response to theme changes */
1228 gchar *style = g_strdup_printf (
1229 "background-image:url('%s');"
1230 "background-repeat:no-repeat;"
1231 "background-position:left center;"
1232 "padding-left:19px;", /* 16px icon + 3px padding */
1233 gtk_icon_info_get_filename (icon_info));
1235 webkit_dom_element_set_attribute (span, "style", style, &error);
1239 DEBUG ("Error setting element style: %s",
1241 g_clear_error (&error);
1246 gtk_icon_info_free (icon_info);
1252 DEBUG ("Could not find message to edit with: %s",
1253 empathy_message_get_body (message));
1257 g_free (parsed_body);
1261 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1262 gboolean allow_scrolling)
1264 self->priv->allow_scrolling = allow_scrolling;
1266 if (allow_scrolling)
1267 empathy_theme_adium_scroll_down (self);
1271 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1273 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1277 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1279 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1283 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1285 theme_adium_load_template (self);
1287 /* Clear last contact to avoid trying to add a 'joined'
1288 * message when we don't have an insertion point. */
1289 if (self->priv->last_contact)
1291 g_object_unref (self->priv->last_contact);
1292 self->priv->last_contact = NULL;
1297 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1298 const gchar *search_criteria,
1299 gboolean new_search,
1300 gboolean match_case)
1302 /* FIXME: Doesn't respect new_search */
1303 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1304 search_criteria, match_case, FALSE, TRUE);
1308 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1309 const gchar *search_criteria,
1310 gboolean new_search,
1311 gboolean match_case)
1313 /* FIXME: Doesn't respect new_search */
1314 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1315 search_criteria, match_case, TRUE, TRUE);
1319 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1320 const gchar *search_criteria,
1321 gboolean match_case,
1322 gboolean *can_do_previous,
1323 gboolean *can_do_next)
1325 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1326 * find_next and find_previous to work around this problem. */
1327 if (can_do_previous)
1328 *can_do_previous = TRUE;
1330 *can_do_next = TRUE;
1334 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1336 gboolean match_case)
1338 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1339 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1340 text, match_case, 0);
1341 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1346 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1348 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1352 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1355 WebKitDOMDocument *dom;
1356 WebKitDOMNodeList *nodes;
1358 GError *error = NULL;
1360 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1364 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1366 /* Get all nodes with focus class */
1367 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1372 DEBUG ("Error getting focus nodes: %s",
1373 error ? error->message : "No error");
1374 g_clear_error (&error);
1378 theme_adium_remove_focus_marks (self, nodes);
1382 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1385 EmpathyThemeAdium *self = user_data;
1386 guint32 id = GPOINTER_TO_UINT (data);
1388 theme_adium_remove_mark_from_message (self, id);
1392 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1395 self->priv->has_focus = has_focus;
1396 if (!self->priv->has_focus)
1398 /* We've lost focus, so let's make sure all the acked
1399 * messages have lost their unread marker. */
1400 g_queue_foreach (&self->priv->acked_messages,
1401 theme_adium_remove_acked_message_unread_mark_foreach, self);
1402 g_queue_clear (&self->priv->acked_messages);
1404 self->priv->has_unread_message = FALSE;
1409 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1410 EmpathyMessage *message)
1416 tp_msg = empathy_message_get_tp_message (message);
1421 id = tp_message_get_pending_message_id (tp_msg, &valid);
1424 g_warning ("Acknoledged message doesn't have a pending ID");
1428 /* We only want to actually remove the unread marker if the
1429 * view doesn't have focus. If we did it all the time we would
1430 * never see the unread markers, ever! So, we'll queue these
1431 * up, and when we lose focus, we'll remove the markers. */
1432 if (self->priv->has_focus)
1434 g_queue_push_tail (&self->priv->acked_messages,
1435 GUINT_TO_POINTER (id));
1439 theme_adium_remove_mark_from_message (self, id);
1443 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1444 GtkWidget *default_menu,
1445 WebKitHitTestResult *hit_test_result,
1446 gboolean triggered_with_keyboard,
1450 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1452 if (g_settings_get_boolean (self->priv->gsettings_chat,
1453 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1454 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1456 menu = empathy_webkit_create_context_menu (
1457 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1459 gtk_widget_show_all (menu);
1461 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1462 gtk_get_current_event_time ());
1468 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1469 gboolean show_avatars)
1471 self->priv->show_avatars = show_avatars;
1475 theme_adium_load_finished_cb (WebKitWebView *view,
1476 WebKitWebFrame *frame,
1479 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1482 DEBUG ("Page loaded");
1483 self->priv->pages_loading--;
1485 if (self->priv->pages_loading != 0)
1488 /* Display queued messages */
1489 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1491 QueuedItem *item = l->data;
1495 case QUEUED_MESSAGE:
1496 empathy_theme_adium_append_message (self, item->msg,
1497 item->should_highlight);
1501 empathy_theme_adium_edit_message (self, item->msg);
1505 empathy_theme_adium_append_event (self, item->str);
1509 free_queued_item (item);
1512 g_queue_clear (&self->priv->message_queue);
1516 theme_adium_finalize (GObject *object)
1518 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1520 empathy_adium_data_unref (self->priv->data);
1522 g_object_unref (self->priv->gsettings_chat);
1523 g_object_unref (self->priv->gsettings_desktop);
1525 g_free (self->priv->variant);
1527 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1531 theme_adium_dispose (GObject *object)
1533 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1535 if (self->priv->smiley_manager)
1537 g_object_unref (self->priv->smiley_manager);
1538 self->priv->smiley_manager = NULL;
1541 g_clear_object (&self->priv->first_contact);
1543 if (self->priv->last_contact)
1545 g_object_unref (self->priv->last_contact);
1546 self->priv->last_contact = NULL;
1549 if (self->priv->inspector_window)
1551 gtk_widget_destroy (self->priv->inspector_window);
1552 self->priv->inspector_window = NULL;
1555 if (self->priv->acked_messages.length > 0)
1557 g_queue_clear (&self->priv->acked_messages);
1560 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1564 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1565 EmpathyThemeAdium *self)
1567 if (self->priv->inspector_window)
1569 gtk_widget_show_all (self->priv->inspector_window);
1576 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1577 EmpathyThemeAdium *self)
1579 if (self->priv->inspector_window)
1581 gtk_widget_hide (self->priv->inspector_window);
1587 static WebKitWebView *
1588 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1589 WebKitWebView *web_view,
1590 EmpathyThemeAdium *self)
1592 GtkWidget *scrolled_window;
1593 GtkWidget *inspector_web_view;
1595 if (!self->priv->inspector_window)
1597 /* Create main window */
1598 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1600 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1603 g_signal_connect (self->priv->inspector_window, "delete-event",
1604 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1606 /* Pack a scrolled window */
1607 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1609 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1610 GTK_POLICY_AUTOMATIC,
1611 GTK_POLICY_AUTOMATIC);
1612 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1614 gtk_widget_show (scrolled_window);
1616 /* Pack a webview in the scrolled window. That webview will be
1617 * used to render the inspector tool. */
1618 inspector_web_view = webkit_web_view_new ();
1619 gtk_container_add (GTK_CONTAINER (scrolled_window),
1620 inspector_web_view);
1621 gtk_widget_show (scrolled_window);
1623 return WEBKIT_WEB_VIEW (inspector_web_view);
1630 theme_adium_constructed (GObject *object)
1632 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1633 const gchar *font_family = NULL;
1635 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1636 WebKitWebInspector *webkit_inspector;
1638 /* Set default settings */
1639 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1640 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1642 if (font_family && font_size)
1644 g_object_set (webkit_web_view_get_settings (webkit_view),
1645 "default-font-family", font_family,
1646 "default-font-size", font_size,
1651 empathy_webkit_bind_font_setting (webkit_view,
1652 self->priv->gsettings_desktop,
1653 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1656 /* Setup webkit inspector */
1657 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1658 g_signal_connect (webkit_inspector, "inspect-web-view",
1659 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1660 g_signal_connect (webkit_inspector, "show-window",
1661 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1662 g_signal_connect (webkit_inspector, "close-window",
1663 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1666 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1668 self->priv->in_construction = FALSE;
1672 theme_adium_get_property (GObject *object,
1677 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1681 case PROP_ADIUM_DATA:
1682 g_value_set_boxed (value, self->priv->data);
1685 g_value_set_string (value, self->priv->variant);
1688 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1694 theme_adium_set_property (GObject *object,
1696 const GValue *value,
1699 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1703 case PROP_ADIUM_DATA:
1704 g_assert (self->priv->data == NULL);
1705 self->priv->data = g_value_dup_boxed (value);
1708 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1711 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1717 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1719 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1721 object_class->finalize = theme_adium_finalize;
1722 object_class->dispose = theme_adium_dispose;
1723 object_class->constructed = theme_adium_constructed;
1724 object_class->get_property = theme_adium_get_property;
1725 object_class->set_property = theme_adium_set_property;
1727 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1728 g_param_spec_boxed ("adium-data",
1730 "Data for the adium theme",
1731 EMPATHY_TYPE_ADIUM_DATA,
1732 G_PARAM_CONSTRUCT_ONLY |
1734 G_PARAM_STATIC_STRINGS));
1736 g_object_class_install_property (object_class, PROP_VARIANT,
1737 g_param_spec_string ("variant",
1738 "The theme variant",
1739 "Variant name for the theme",
1743 G_PARAM_STATIC_STRINGS));
1745 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1749 empathy_theme_adium_init (EmpathyThemeAdium *self)
1751 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1752 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1754 self->priv->in_construction = TRUE;
1755 g_queue_init (&self->priv->message_queue);
1756 self->priv->allow_scrolling = TRUE;
1757 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1759 /* Show avatars by default. */
1760 self->priv->show_avatars = TRUE;
1762 g_signal_connect (self, "load-finished",
1763 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1764 g_signal_connect (self, "navigation-policy-decision-requested",
1765 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1766 g_signal_connect (self, "context-menu",
1767 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1769 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1770 self->priv->gsettings_desktop = g_settings_new (
1771 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1775 empathy_theme_adium_new (EmpathyAdiumData *data,
1776 const gchar *variant)
1778 g_return_val_if_fail (data != NULL, NULL);
1780 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1787 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1788 const gchar *variant)
1790 gchar *variant_path;
1793 if (!tp_strdiff (self->priv->variant, variant))
1796 g_free (self->priv->variant);
1797 self->priv->variant = g_strdup (variant);
1799 if (self->priv->in_construction)
1802 DEBUG ("Update view with variant: '%s'", variant);
1803 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1804 self->priv->variant);
1805 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1808 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1810 g_free (variant_path);
1813 g_object_notify (G_OBJECT (self), "variant");
1817 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1819 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1821 empathy_webkit_show_inspector (web_view);
1825 empathy_adium_path_is_valid (const gchar *path)
1835 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1836 tmp = g_strsplit (path, "/", 0);
1840 dir = tmp[g_strv_length (tmp) - 1];
1842 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1850 /* The theme is not valid if there is no Info.plist */
1851 file = g_build_filename (path, "Contents", "Info.plist",
1853 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1859 /* We ship a default Template.html as fallback if there is any problem
1860 * with the one inside the theme. The only other required file is
1861 * Content.html OR Incoming/Content.html*/
1862 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1864 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1869 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1870 "Content.html", NULL);
1871 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1879 empathy_adium_info_new (const gchar *path)
1883 GHashTable *info = NULL;
1885 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1887 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1888 value = empathy_plist_parse_from_file (file);
1894 info = g_value_dup_boxed (value);
1895 tp_g_value_slice_free (value);
1897 /* Insert the theme's path into the hash table,
1898 * keys have to be dupped */
1899 tp_asv_set_string (info, g_strdup ("path"), path);
1905 adium_info_get_version (GHashTable *info)
1907 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1910 static const gchar *
1911 adium_info_get_no_variant_name (GHashTable *info)
1913 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1914 return name ? name : _("Normal");
1918 adium_info_dup_path_for_variant (GHashTable *info,
1919 const gchar *variant)
1921 guint version = adium_info_get_version (info);
1922 const gchar *no_variant = adium_info_get_no_variant_name (info);
1923 GPtrArray *variants;
1926 if (version <= 2 && !tp_strdiff (variant, no_variant))
1927 return g_strdup ("main.css");
1929 variants = empathy_adium_info_get_available_variants (info);
1930 if (variants->len == 0)
1931 return g_strdup ("main.css");
1933 /* Verify the variant exists, fallback to the first one */
1934 for (i = 0; i < variants->len; i++)
1936 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1940 if (i == variants->len)
1942 DEBUG ("Variant %s does not exist", variant);
1943 variant = g_ptr_array_index (variants, 0);
1946 return g_strdup_printf ("Variants/%s.css", variant);
1951 empathy_adium_info_get_default_variant (GHashTable *info)
1953 if (adium_info_get_version (info) <= 2)
1954 return adium_info_get_no_variant_name (info);
1956 return tp_asv_get_string (info, "DefaultVariant");
1960 empathy_adium_info_get_available_variants (GHashTable *info)
1962 GPtrArray *variants;
1967 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1968 if (variants != NULL)
1971 variants = g_ptr_array_new_with_free_func (g_free);
1972 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1973 G_TYPE_PTR_ARRAY, variants);
1975 path = tp_asv_get_string (info, "path");
1976 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1977 dir = g_dir_open (dirpath, 0, NULL);
1982 for (name = g_dir_read_name (dir);
1984 name = g_dir_read_name (dir))
1986 gchar *display_name;
1988 if (!g_str_has_suffix (name, ".css"))
1991 display_name = g_strdup (name);
1992 strstr (display_name, ".css")[0] = '\0';
1993 g_ptr_array_add (variants, display_name);
2000 if (adium_info_get_version (info) <= 2)
2001 g_ptr_array_add (variants,
2002 g_strdup (adium_info_get_no_variant_name (info)));
2008 empathy_adium_data_get_type (void)
2010 static GType type_id = 0;
2014 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2015 (GBoxedCopyFunc) empathy_adium_data_ref,
2016 (GBoxedFreeFunc) empathy_adium_data_unref);
2023 empathy_adium_data_new_with_info (const gchar *path,
2026 EmpathyAdiumData *data;
2027 gchar *template_html = NULL;
2028 gchar *footer_html = NULL;
2031 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2033 data = g_slice_new0 (EmpathyAdiumData);
2034 data->ref_count = 1;
2035 data->path = g_strdup (path);
2036 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2037 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2038 data->info = g_hash_table_ref (info);
2039 data->version = adium_info_get_version (info);
2040 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2041 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2042 g_str_equal, g_free, g_free);
2044 DEBUG ("Loading theme at %s", path);
2046 #define LOAD(path, var) \
2047 tmp = g_build_filename (data->basedir, path, NULL); \
2048 g_file_get_contents (tmp, &var, NULL, NULL); \
2051 #define LOAD_CONST(path, var) \
2054 LOAD (path, content); \
2055 if (content != NULL) { \
2056 g_ptr_array_add (data->strings_to_free, content); \
2061 /* Load html files */
2062 LOAD_CONST ("Content.html", data->content_html);
2063 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2064 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2065 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2066 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2067 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2068 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2069 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2070 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2071 LOAD_CONST ("Status.html", data->status_html);
2072 LOAD ("Template.html", template_html);
2073 LOAD ("Footer.html", footer_html);
2078 /* HTML fallbacks: If we have at least content OR in_content, then
2079 * everything else gets a fallback */
2081 #define FALLBACK(html, fallback) \
2082 if (html == NULL) { \
2086 /* in_nextcontent -> in_content -> content */
2087 FALLBACK (data->in_content_html, data->content_html);
2088 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2090 /* context -> content */
2091 FALLBACK (data->in_context_html, data->in_content_html);
2092 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2093 FALLBACK (data->out_context_html, data->out_content_html);
2094 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2097 FALLBACK (data->out_content_html, data->in_content_html);
2098 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2099 FALLBACK (data->out_context_html, data->in_context_html);
2100 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2102 /* status -> in_content */
2103 FALLBACK (data->status_html, data->in_content_html);
2107 /* template -> empathy's template */
2108 data->custom_template = (template_html != NULL);
2109 if (template_html == NULL)
2111 GError *error = NULL;
2113 tmp = empathy_file_lookup ("Template.html", "data");
2115 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2116 g_warning ("couldn't load Empathy's default theme "
2117 "template: %s", error->message);
2118 g_return_val_if_reached (data);
2124 /* Default avatar */
2125 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2126 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2128 data->default_incoming_avatar_filename = tmp;
2135 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2136 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2138 data->default_outgoing_avatar_filename = tmp;
2145 /* Old custom templates had only 4 parameters.
2146 * New templates have 5 parameters */
2147 if (data->version <= 2 && data->custom_template)
2149 tmp = string_with_format (template_html,
2151 "%@", /* Leave variant unset */
2152 "", /* The header */
2153 footer_html ? footer_html : "",
2158 tmp = string_with_format (template_html,
2160 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2161 "%@", /* Leave variant unset */
2162 "", /* The header */
2163 footer_html ? footer_html : "",
2166 g_ptr_array_add (data->strings_to_free, tmp);
2167 data->template_html = tmp;
2169 g_free (template_html);
2170 g_free (footer_html);
2176 empathy_adium_data_new (const gchar *path)
2178 EmpathyAdiumData *data;
2181 info = empathy_adium_info_new (path);
2182 data = empathy_adium_data_new_with_info (path, info);
2183 g_hash_table_unref (info);
2189 empathy_adium_data_ref (EmpathyAdiumData *data)
2191 g_return_val_if_fail (data != NULL, NULL);
2193 g_atomic_int_inc (&data->ref_count);
2199 empathy_adium_data_unref (EmpathyAdiumData *data)
2201 g_return_if_fail (data != NULL);
2203 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2204 g_free (data->path);
2205 g_free (data->basedir);
2206 g_free (data->default_avatar_filename);
2207 g_free (data->default_incoming_avatar_filename);
2208 g_free (data->default_outgoing_avatar_filename);
2209 g_hash_table_unref (data->info);
2210 g_ptr_array_unref (data->strings_to_free);
2211 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2213 g_slice_free (EmpathyAdiumData, data);
2218 empathy_adium_data_get_info (EmpathyAdiumData *data)
2220 g_return_val_if_fail (data != NULL, NULL);
2226 empathy_adium_data_get_path (EmpathyAdiumData *data)
2228 g_return_val_if_fail (data != NULL, NULL);