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-images.h>
27 #include <tp-account-widgets/tpaw-time.h>
28 #include <tp-account-widgets/tpaw-pixbuf-utils.h>
29 #include <tp-account-widgets/tpaw-utils.h>
31 #include "empathy-gsettings.h"
32 #include "empathy-images.h"
33 #include "empathy-plist.h"
34 #include "empathy-smiley-manager.h"
35 #include "empathy-ui-utils.h"
36 #include "empathy-utils.h"
37 #include "empathy-webkit-utils.h"
39 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
40 #include "empathy-debug.h"
42 #define BORING_DPI_DEFAULT 96
44 /* "Join" consecutive messages with timestamps within five minutes */
45 #define MESSAGE_JOIN_PERIOD 5*60
47 struct _EmpathyThemeAdiumPriv
49 EmpathyAdiumData *data;
50 EmpathySmileyManager *smiley_manager;
51 EmpathyContact *first_contact;
52 EmpathyContact *last_contact;
53 gint64 first_timestamp;
54 gint64 last_timestamp;
55 gboolean first_is_backlog;
56 gboolean last_is_backlog;
58 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
60 /* Queue of guint32 of pending message id to remove unread
61 * marker for when we lose focus. */
62 GQueue acked_messages;
63 GtkWidget *inspector_window;
65 GSettings *gsettings_chat;
66 GSettings *gsettings_desktop;
69 gboolean has_unread_message;
70 gboolean allow_scrolling;
72 gboolean in_construction;
73 gboolean show_avatars;
76 struct _EmpathyAdiumData
81 gchar *default_avatar_filename;
82 gchar *default_incoming_avatar_filename;
83 gchar *default_outgoing_avatar_filename;
86 gboolean custom_template;
87 /* gchar* -> gchar* both owned */
88 GHashTable *date_format_cache;
91 const gchar *template_html;
92 const gchar *content_html;
93 const gchar *in_content_html;
94 const gchar *in_context_html;
95 const gchar *in_nextcontent_html;
96 const gchar *in_nextcontext_html;
97 const gchar *out_content_html;
98 const gchar *out_context_html;
99 const gchar *out_nextcontent_html;
100 const gchar *out_nextcontext_html;
101 const gchar *status_html;
103 /* Above html strings are pointers to strings stored in this array.
104 * We do this because of fallbacks, some htmls could be pointing the
106 GPtrArray *strings_to_free;
109 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
110 const gchar *variant);
119 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
120 WEBKIT_TYPE_WEB_VIEW)
134 gboolean should_highlight;
138 queue_item (GQueue *queue,
142 gboolean should_highlight,
145 QueuedItem *item = g_slice_new0 (QueuedItem);
149 item->msg = g_object_ref (msg);
150 item->str = g_strdup (str);
151 item->should_highlight = should_highlight;
154 g_queue_push_head (queue, item);
156 g_queue_push_tail (queue, item);
162 free_queued_item (QueuedItem *item)
164 tp_clear_object (&item->msg);
167 g_slice_free (QueuedItem, item);
171 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
172 WebKitWebFrame *web_frame,
173 WebKitNetworkRequest *request,
174 WebKitWebNavigationAction *action,
175 WebKitWebPolicyDecision *decision,
180 /* Only call url_show on clicks */
181 if (webkit_web_navigation_action_get_reason (action) !=
182 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
184 webkit_web_policy_decision_use (decision);
188 uri = webkit_network_request_get_uri (request);
189 empathy_url_show (GTK_WIDGET (view), uri);
191 webkit_web_policy_decision_ignore (decision);
195 /* Replace each %@ in format with string passed in args */
197 string_with_format (const gchar *format,
198 const gchar *first_string,
205 va_start (args, first_string);
206 result = g_string_sized_new (strlen (format));
207 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
211 next = strstr (format, "%@");
215 g_string_append_len (result, format, next - format);
216 g_string_append (result, str);
219 g_string_append (result, format);
222 return g_string_free (result, FALSE);
226 theme_adium_load_template (EmpathyThemeAdium *self)
232 self->priv->pages_loading++;
233 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
235 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
236 self->priv->variant);
238 template = string_with_format (self->priv->data->template_html,
241 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
242 template, basedir_uri);
244 g_free (basedir_uri);
245 g_free (variant_path);
250 theme_adium_parse_body (EmpathyThemeAdium *self,
254 TpawStringParser *parsers;
257 /* Check if we have to parse smileys */
258 parsers = empathy_webkit_get_string_parser (
259 g_settings_get_boolean (self->priv->gsettings_chat,
260 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
262 /* Parse text and construct string with links and smileys replaced
263 * by html tags. Also escape text to make sure html code is
264 * displayed verbatim. */
265 string = g_string_sized_new (strlen (text));
267 /* wrap this in HTML that allows us to find the message for later
269 if (!tp_str_empty (token))
270 g_string_append_printf (string,
271 "<span id=\"message-token-%s\">",
274 tpaw_string_parser_substr (text, -1, parsers, string);
276 if (!tp_str_empty (token))
277 g_string_append (string, "</span>");
279 /* Wrap body in order to make tabs and multiple spaces displayed
280 * properly. See bug #625745. */
281 g_string_prepend (string, "<div style=\"display: inline; "
282 "white-space: pre-wrap\"'>");
283 g_string_append (string, "</div>");
285 return g_string_free (string, FALSE);
289 escape_and_append_len (GString *string, const gchar *str, gint len)
291 while (str != NULL && *str != '\0' && len != 0)
297 g_string_append (string, "\\\\");
301 g_string_append (string, "\\\"");
304 /* Remove end of lines */
307 g_string_append_c (string, *str);
315 /* If *str starts with match, returns TRUE and move pointer to the end */
317 theme_adium_match (const gchar **str,
322 len = strlen (match);
323 if (strncmp (*str, match, len) == 0)
332 /* Like theme_adium_match() but also return the X part if match is
335 theme_adium_match_with_format (const gchar **str,
339 const gchar *cur = *str;
342 if (!theme_adium_match (&cur, match))
347 end = strstr (cur, "}%");
351 *format = g_strndup (cur , end - cur);
356 /* List of colors used by %senderColor%. Copied from
357 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
359 static gchar *colors[] = {
360 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
361 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
362 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
363 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
364 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
365 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
366 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
367 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
368 "lightblue", "lightcoral",
369 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
370 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
371 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
372 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
373 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
374 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
375 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
376 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
377 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
378 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
383 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
385 /* Convert from NSDateFormatter
386 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
387 * to strftime supported by g_date_time_format.
388 * FIXME: table is incomplete, doc of g_date_time_format has a table of
390 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
391 * in 2.29.x we have to explictely request padding with %0x */
392 static const gchar *convert_table[] = {
394 "A", NULL, // 0~86399999 (Millisecond of Day)
396 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
397 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
398 "cc", "%u", // 1~7 (Day of Week)
399 "c", "%u", // 1~7 (Day of Week)
401 "dd", "%d", // 1~31 (0 padded Day of Month)
402 "d", "%d", // 1~31 (0 padded Day of Month)
403 "D", "%j", // 1~366 (0 padded Day of Year)
405 "e", "%u", // 1~7 (0 padded Day of Week)
406 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
407 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
408 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
409 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
411 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
413 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
414 "GGGG", NULL, // Before Christ/Anno Domini
415 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
416 "GG", NULL, // BC/AD (Era Designator Abbreviated)
417 "G", NULL, // BC/AD (Era Designator Abbreviated)
419 "h", "%I", // 1~12 (0 padded Hour (12hr))
420 "H", "%H", // 0~23 (0 padded Hour (24hr))
422 "k", NULL, // 1~24 (0 padded Hour (24hr)
423 "K", NULL, // 0~11 (0 padded Hour (12hr))
425 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
426 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
427 "LL", "%m", // 1~12 (0 padded Month)
428 "L", "%m", // 1~12 (0 padded Month)
430 "m", "%M", // 0~59 (0 padded Minute)
431 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
432 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
433 "MM", "%m", // 1~12 (0 padded Month)
434 "M", "%m", // 1~12 (0 padded Month)
436 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
437 "qqq", NULL, // Q1/Q2/Q3/Q4
438 "qq", NULL, // 1~4 (0 padded Quarter)
439 "q", NULL, // 1~4 (0 padded Quarter)
440 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
441 "QQQ", NULL, // Q1/Q2/Q3/Q4
442 "QQ", NULL, // 1~4 (0 padded Quarter)
443 "Q", NULL, // 1~4 (0 padded Quarter)
445 "s", "%S", // 0~59 (0 padded Second)
446 "S", NULL, // (rounded Sub-Second)
448 "u", "%Y", // (0 padded Year)
450 "vvvv", "%Z", // (General GMT Timezone Name)
451 "vvv", "%Z", // (General GMT Timezone Abbreviation)
452 "vv", "%Z", // (General GMT Timezone Abbreviation)
453 "v", "%Z", // (General GMT Timezone Abbreviation)
455 "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)
456 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
458 "yyyy", "%Y", // (Full Year)
459 "yyy", "%y", // (2 Digits Year)
460 "yy", "%y", // (2 Digits Year)
461 "y", "%Y", // (Full Year)
462 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
463 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
464 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
465 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
467 "zzzz", NULL, // (Specific GMT Timezone Name)
468 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
469 "zz", NULL, // (Specific GMT Timezone Abbreviation)
470 "z", NULL, // (Specific GMT Timezone Abbreviation)
471 "Z", "%z", // +0000 (RFC 822 Timezone)
480 str = g_hash_table_lookup (data->date_format_cache, nsdate);
485 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
486 * by corresponding strftime tag. */
487 string = g_string_sized_new (strlen (nsdate));
488 for (i = 0; nsdate[i] != '\0'; i++)
490 gboolean found = FALSE;
492 /* even indexes are NSDateFormatter tag, odd indexes are the
493 * corresponding strftime tag */
494 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
496 if (g_str_has_prefix (nsdate + i, convert_table[j]))
505 /* If we don't have a replacement, just ignore that tag */
506 if (convert_table[j + 1] != NULL)
507 g_string_append (string, convert_table[j + 1]);
509 i += strlen (convert_table[j]) - 1;
513 g_string_append_c (string, nsdate[i]);
517 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
519 /* The cache takes ownership of string->str */
520 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
521 return g_string_free (string, FALSE);
525 theme_adium_add_html (EmpathyThemeAdium *self,
528 const gchar *message,
529 const gchar *avatar_filename,
531 const gchar *contact_id,
532 const gchar *service_name,
533 const gchar *message_classes,
537 PangoDirection direction)
541 const gchar *cur = NULL;
545 /* Make some search-and-replace in the html code */
546 string = g_string_sized_new (strlen (html) + strlen (message));
547 g_string_append_printf (string, "%s(\"", func);
549 for (cur = html; *cur != '\0'; cur++)
551 const gchar *replace = NULL;
552 gchar *dup_replace = NULL;
553 gchar *format = NULL;
555 /* Those are all well known keywords that needs replacement in
556 * html files. Please keep them in the same order than the adium
557 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
558 if (theme_adium_match (&cur, "%userIconPath%"))
560 replace = avatar_filename;
562 else if (theme_adium_match (&cur, "%senderScreenName%"))
564 replace = contact_id;
566 else if (theme_adium_match (&cur, "%sender%"))
570 else if (theme_adium_match (&cur, "%senderColor%"))
572 /* A color derived from the user's name.
573 * FIXME: If a colon separated list of HTML colors is at
574 * Incoming/SenderColors.txt it will be used instead of
575 * the default colors.
578 /* Ensure we always use the same color when sending messages
584 else if (contact_id != NULL)
586 guint hash = g_str_hash (contact_id);
587 replace = colors[hash % G_N_ELEMENTS (colors)];
590 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
592 /* FIXME: The path to the status icon of the sender
593 * (available, away, etc...)
596 else if (theme_adium_match (&cur, "%messageDirection%"))
600 case PANGO_DIRECTION_LTR:
601 case PANGO_DIRECTION_TTB_LTR:
602 case PANGO_DIRECTION_WEAK_LTR:
605 case PANGO_DIRECTION_RTL:
606 case PANGO_DIRECTION_TTB_RTL:
607 case PANGO_DIRECTION_WEAK_RTL:
610 case PANGO_DIRECTION_NEUTRAL:
615 else if (theme_adium_match (&cur, "%senderDisplayName%"))
617 /* FIXME: The serverside (remotely set) name of the
618 * sender, such as an MSN display name.
620 * We don't have access to that yet so we use
621 * local alias instead.
625 else if (theme_adium_match (&cur, "%senderPrefix%"))
627 /* FIXME: If we supported IRC user mode flags, this
628 * would be replaced with @ if the user is an op, + if
629 * the user has voice, etc. as per
630 * http://hg.adium.im/adium/rev/b586b027de42. But we
631 * don't, so for now we just strip it. */
633 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
636 /* FIXME: This keyword is used to represent the
637 * highlight background color. "X" is the opacity of the
638 * background, ranges from 0 to 1 and can be any decimal
642 else if (theme_adium_match (&cur, "%message%"))
646 else if (theme_adium_match (&cur, "%time%") ||
647 theme_adium_match_with_format (&cur, "%time{", &format))
649 const gchar *strftime_format;
651 strftime_format = nsdate_to_strftime (self->priv->data, format);
653 dup_replace = tpaw_time_to_string_local (timestamp,
654 strftime_format ? strftime_format :
655 TPAW_TIME_DATE_FORMAT_DISPLAY_SHORT);
657 dup_replace = tpaw_time_to_string_local (timestamp,
658 strftime_format ? strftime_format :
659 TPAW_TIME_FORMAT_DISPLAY_SHORT);
661 replace = dup_replace;
663 else if (theme_adium_match (&cur, "%shortTime%"))
665 dup_replace = tpaw_time_to_string_local (timestamp,
666 TPAW_TIME_FORMAT_DISPLAY_SHORT);
667 replace = dup_replace;
669 else if (theme_adium_match (&cur, "%service%"))
671 replace = service_name;
673 else if (theme_adium_match (&cur, "%variant%"))
675 /* FIXME: The name of the active message style variant,
676 * with all spaces replaced with an underscore.
677 * A variant named "Alternating Messages - Blue Red"
678 * will become "Alternating_Messages_-_Blue_Red".
681 else if (theme_adium_match (&cur, "%userIcons%"))
683 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
685 else if (theme_adium_match (&cur, "%messageClasses%"))
687 replace = message_classes;
689 else if (theme_adium_match (&cur, "%status%"))
691 /* FIXME: A description of the status event. This is
692 * neither in the user's local language nor expected to
693 * be displayed; it may be useful to use a different div
694 * class to present different types of status messages.
695 * The following is a list of some of the more important
696 * status messages; your message style should be able to
697 * handle being shown a status message not in this list,
698 * as even at present the list is incomplete and is
699 * certain to become out of date in the future:
708 * contact_joined (group chats)
712 * encryption (all OTR messages use this status)
713 * purple (all IRC topic and join/part messages use this status)
714 * fileTransferStarted
715 * fileTransferCompleted
720 escape_and_append_len (string, cur, 1);
724 /* Here we have a replacement to make */
725 escape_and_append_len (string, replace, -1);
727 g_free (dup_replace);
730 g_string_append (string, "\")");
732 bytes = g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
733 G_RESOURCE_LOOKUP_FLAGS_NONE,
735 js = (const gchar *) g_bytes_get_data (bytes, NULL);
736 g_string_prepend (string, js);
737 g_bytes_unref (bytes);
739 script = g_string_free (string, FALSE);
740 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
745 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
746 const gchar *escaped,
747 PangoDirection direction)
749 theme_adium_add_html (self, "appendMessage",
750 self->priv->data->status_html, escaped, NULL, NULL, NULL,
751 NULL, "event", tpaw_time_get_current (), FALSE, FALSE, direction);
753 /* There is no last contact */
754 if (self->priv->last_contact)
756 g_object_unref (self->priv->last_contact);
757 self->priv->last_contact = NULL;
762 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
763 WebKitDOMNodeList *nodes)
767 /* Remove focus and firstFocus class */
768 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
770 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
771 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
773 gchar **classes, **iter;
774 GString *new_class_name;
775 gboolean first = TRUE;
780 class_name = webkit_dom_html_element_get_class_name (element);
781 classes = g_strsplit (class_name, " ", -1);
782 new_class_name = g_string_sized_new (strlen (class_name));
784 for (iter = classes; *iter != NULL; iter++)
786 if (tp_strdiff (*iter, "focus") &&
787 tp_strdiff (*iter, "firstFocus"))
790 g_string_append_c (new_class_name, ' ');
792 g_string_append (new_class_name, *iter);
797 webkit_dom_html_element_set_class_name (element, new_class_name->str);
800 g_strfreev (classes);
801 g_string_free (new_class_name, TRUE);
806 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
808 WebKitDOMDocument *dom;
809 WebKitDOMNodeList *nodes;
810 GError *error = NULL;
812 if (!self->priv->has_unread_message)
815 self->priv->has_unread_message = FALSE;
817 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
821 /* Get all nodes with focus class */
822 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
826 DEBUG ("Error getting focus nodes: %s",
827 error ? error->message : "No error");
828 g_clear_error (&error);
832 theme_adium_remove_focus_marks (self, nodes);
837 ADD_CONSECUTIVE_MSG_SCROLL = 0,
838 ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
840 ADD_MSG_NO_SCROLL = 3
844 * theme_adium_add_message:
845 * @self: The #EmpathyThemeAdium used by the view.
846 * @msg: An #EmpathyMessage that is to be added to the view.
847 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
848 * @prev_timestamp: (out): Timestamp of the previous message.
849 * @prev_is_backlog: (out): Whether the previous message was fetched
851 * @should_highlight: Whether the message should be highlighted. eg.,
852 * if it matches the user's username in multi-user chat.
853 * @js_funcs: An array of JavaScript function names
855 * Shows @msg in the chat view by adding to @self. Addition is defined
856 * by the JavaScript functions listed in @js_funcs. Common examples
857 * are appending new incoming messages or prepending old messages from
860 * @js_funcs should be an array with exactly 4 entries. The entries
861 * should be the names of JavaScript functions that take the raw HTML
862 * that is to be added to the view as an argument and take the following
863 * actions, in this order:
864 * - add a new consecutive message and scroll to it if needed,
865 * - add a new consecutive message and do not scroll,
866 * - add a new non-consecutive message and scroll to it if needed, and
867 * - add a new non-consecutive message and do not scroll
869 * A message is considered to be consecutive with the previous one if
870 * all the following conditions are met:
871 * - senders are the same contact,
872 * - last message was recieved recently,
873 * - last message and this message both are/aren't backlog, and
874 * - DisableCombineConsecutive is not set in theme's settings
877 theme_adium_add_message (EmpathyThemeAdium *self,
879 EmpathyContact **prev_contact,
880 gint64 *prev_timestamp,
881 gboolean *prev_is_backlog,
882 gboolean should_highlight,
883 const gchar *js_funcs[])
885 EmpathyContact *sender;
888 gchar *body_escaped, *name_escaped;
890 const gchar *contact_id;
891 EmpathyAvatar *avatar;
892 const gchar *avatar_filename = NULL;
894 const gchar *html = NULL;
896 const gchar *service_name;
897 GString *message_classes = NULL;
899 gboolean consecutive;
901 PangoDirection direction;
904 /* Get information */
905 sender = empathy_message_get_sender (msg);
906 account = empathy_contact_get_account (sender);
907 service_name = tpaw_protocol_name_to_display_name
908 (tp_account_get_protocol_name (account));
909 if (service_name == NULL)
910 service_name = tp_account_get_protocol_name (account);
911 timestamp = empathy_message_get_timestamp (msg);
912 body_escaped = theme_adium_parse_body (self,
913 empathy_message_get_body (msg),
914 empathy_message_get_token (msg));
915 name = empathy_contact_get_logged_alias (sender);
916 contact_id = empathy_contact_get_id (sender);
917 action = (empathy_message_get_tptype (msg) ==
918 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
920 name_escaped = g_markup_escape_text (name, -1);
922 /* If this is a /me probably */
927 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
929 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
930 "<span class='actionMessageBody'>%s</span>",
931 name_escaped, body_escaped);
935 str = g_strdup_printf ("*%s*", body_escaped);
938 g_free (body_escaped);
942 /* Get the avatar filename, or a fallback */
943 avatar = empathy_contact_get_avatar (sender);
945 avatar_filename = avatar->filename;
947 if (!avatar_filename)
949 if (empathy_contact_is_user (sender))
950 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
952 avatar_filename = self->priv->data->default_incoming_avatar_filename;
954 if (!avatar_filename)
956 if (!self->priv->data->default_avatar_filename)
957 self->priv->data->default_avatar_filename =
958 tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT,
959 GTK_ICON_SIZE_DIALOG);
961 avatar_filename = self->priv->data->default_avatar_filename;
965 is_backlog = empathy_message_is_backlog (msg);
966 consecutive = empathy_contact_equal (*prev_contact, sender) &&
967 (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
968 (is_backlog == *prev_is_backlog) &&
969 !tp_asv_get_boolean (self->priv->data->info,
970 "DisableCombineConsecutive", NULL);
972 /* Define message classes */
973 message_classes = g_string_new ("message");
974 if (!self->priv->has_focus && !is_backlog)
976 if (!self->priv->has_unread_message)
978 g_string_append (message_classes, " firstFocus");
979 self->priv->has_unread_message = TRUE;
981 g_string_append (message_classes, " focus");
985 g_string_append (message_classes, " history");
988 g_string_append (message_classes, " consecutive");
990 if (empathy_contact_is_user (sender))
991 g_string_append (message_classes, " outgoing");
993 g_string_append (message_classes, " incoming");
995 if (should_highlight)
996 g_string_append (message_classes, " mention");
998 if (empathy_message_get_tptype (msg) ==
999 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
1000 g_string_append (message_classes, " autoreply");
1003 g_string_append (message_classes, " action");
1005 /* FIXME: other classes:
1006 * status - the message is a status change
1007 * event - the message is a notification of something happening
1008 * (for example, encryption being turned on)
1009 * %status% - See %status% in theme_adium_add_html ()
1012 /* This is slightly a hack, but it's the only way to add
1013 * arbitrary data to messages in the HTML. We add another
1014 * class called "x-empathy-message-id-*" to the message. This
1015 * way, we can remove the unread marker for this specific
1017 tp_msg = empathy_message_get_tp_message (msg);
1023 id = tp_message_get_pending_message_id (tp_msg, &valid);
1025 g_string_append_printf (message_classes,
1026 " x-empathy-message-id-%u", id);
1029 /* Define javascript function to use */
1031 func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1032 js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1034 func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1035 js_funcs[ADD_MSG_NO_SCROLL];
1037 if (empathy_contact_is_user (sender))
1042 html = consecutive ? self->priv->data->out_nextcontext_html :
1043 self->priv->data->out_context_html;
1046 html = consecutive ? self->priv->data->out_nextcontent_html :
1047 self->priv->data->out_content_html;
1049 /* remove all the unread marks when we are sending a message */
1050 theme_adium_remove_all_focus_marks (self);
1057 html = consecutive ? self->priv->data->in_nextcontext_html :
1058 self->priv->data->in_context_html;
1061 html = consecutive ? self->priv->data->in_nextcontent_html :
1062 self->priv->data->in_content_html;
1065 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1067 theme_adium_add_html (self, func, html, body_escaped,
1068 avatar_filename, name_escaped, contact_id,
1069 service_name, message_classes->str,
1070 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1072 /* Keep the sender of the last displayed message */
1074 g_object_unref (*prev_contact);
1076 *prev_contact = g_object_ref (sender);
1077 *prev_timestamp = timestamp;
1078 *prev_is_backlog = is_backlog;
1080 g_free (body_escaped);
1081 g_free (name_escaped);
1082 g_string_free (message_classes, TRUE);
1086 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1087 EmpathyMessage *msg,
1088 gboolean should_highlight)
1090 const gchar *js_funcs[] = { "appendNextMessage",
1091 "appendNextMessageNoScroll",
1093 "appendMessageNoScroll" };
1095 if (self->priv->pages_loading != 0)
1097 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1098 should_highlight, FALSE);
1102 theme_adium_add_message (self, msg, &self->priv->last_contact,
1103 &self->priv->last_timestamp, &self->priv->last_is_backlog,
1104 should_highlight, js_funcs);
1108 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1112 PangoDirection direction;
1114 if (self->priv->pages_loading != 0)
1116 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1120 direction = pango_find_base_dir (str, -1);
1121 str_escaped = g_markup_escape_text (str, -1);
1122 theme_adium_append_event_escaped (self, str_escaped, direction);
1123 g_free (str_escaped);
1127 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1128 const gchar *markup_text,
1129 const gchar *fallback_text)
1131 PangoDirection direction;
1133 direction = pango_find_base_dir (fallback_text, -1);
1134 theme_adium_append_event_escaped (self, markup_text, direction);
1138 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1139 EmpathyMessage *msg,
1140 gboolean should_highlight)
1142 const gchar *js_funcs[] = { "prependPrev",
1147 if (self->priv->pages_loading != 0)
1149 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1150 should_highlight, TRUE);
1154 theme_adium_add_message (self, msg, &self->priv->first_contact,
1155 &self->priv->first_timestamp, &self->priv->first_is_backlog,
1156 should_highlight, js_funcs);
1160 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1161 EmpathyMessage *message)
1163 WebKitDOMDocument *doc;
1164 WebKitDOMElement *span;
1165 gchar *id, *parsed_body;
1166 gchar *tooltip, *timestamp;
1167 GtkIconInfo *icon_info;
1168 GError *error = NULL;
1170 if (self->priv->pages_loading != 0)
1172 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1176 id = g_strdup_printf ("message-token-%s",
1177 empathy_message_get_supersedes (message));
1178 /* we don't pass a token here, because doing so will return another
1179 * <span> element, and we don't want nested <span> elements */
1180 parsed_body = theme_adium_parse_body (self,
1181 empathy_message_get_body (message), NULL);
1183 /* find the element */
1184 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1185 span = webkit_dom_document_get_element_by_id (doc, id);
1189 DEBUG ("Failed to find id '%s'", id);
1193 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1195 DEBUG ("Not a HTML element");
1199 /* update the HTML */
1200 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1201 parsed_body, &error);
1205 DEBUG ("Error setting new inner-HTML: %s", error->message);
1206 g_error_free (error);
1211 timestamp = tpaw_time_to_string_local (
1212 empathy_message_get_timestamp (message),
1214 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1216 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1222 /* mark this message as edited */
1223 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1224 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1226 if (icon_info != NULL)
1228 /* set the icon as a background image using CSS
1229 * FIXME: the icon won't update in response to theme changes */
1230 gchar *style = g_strdup_printf (
1231 "background-image:url('%s');"
1232 "background-repeat:no-repeat;"
1233 "background-position:left center;"
1234 "padding-left:19px;", /* 16px icon + 3px padding */
1235 gtk_icon_info_get_filename (icon_info));
1237 webkit_dom_element_set_attribute (span, "style", style, &error);
1241 DEBUG ("Error setting element style: %s",
1243 g_clear_error (&error);
1248 gtk_icon_info_free (icon_info);
1254 DEBUG ("Could not find message to edit with: %s",
1255 empathy_message_get_body (message));
1259 g_free (parsed_body);
1263 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1264 gboolean allow_scrolling)
1266 self->priv->allow_scrolling = allow_scrolling;
1268 if (allow_scrolling)
1269 empathy_theme_adium_scroll_down (self);
1273 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1275 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1279 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1281 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1285 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1287 theme_adium_load_template (self);
1289 /* Clear last contact to avoid trying to add a 'joined'
1290 * message when we don't have an insertion point. */
1291 if (self->priv->last_contact)
1293 g_object_unref (self->priv->last_contact);
1294 self->priv->last_contact = NULL;
1299 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1300 const gchar *search_criteria,
1301 gboolean new_search,
1302 gboolean match_case)
1304 /* FIXME: Doesn't respect new_search */
1305 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1306 search_criteria, match_case, FALSE, TRUE);
1310 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1311 const gchar *search_criteria,
1312 gboolean new_search,
1313 gboolean match_case)
1315 /* FIXME: Doesn't respect new_search */
1316 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1317 search_criteria, match_case, TRUE, TRUE);
1321 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1322 const gchar *search_criteria,
1323 gboolean match_case,
1324 gboolean *can_do_previous,
1325 gboolean *can_do_next)
1327 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1328 * find_next and find_previous to work around this problem. */
1329 if (can_do_previous)
1330 *can_do_previous = TRUE;
1332 *can_do_next = TRUE;
1336 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1338 gboolean match_case)
1340 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1341 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1342 text, match_case, 0);
1343 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1348 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1350 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1354 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1357 WebKitDOMDocument *dom;
1358 WebKitDOMNodeList *nodes;
1360 GError *error = NULL;
1362 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1366 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1368 /* Get all nodes with focus class */
1369 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1374 DEBUG ("Error getting focus nodes: %s",
1375 error ? error->message : "No error");
1376 g_clear_error (&error);
1380 theme_adium_remove_focus_marks (self, nodes);
1384 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1387 EmpathyThemeAdium *self = user_data;
1388 guint32 id = GPOINTER_TO_UINT (data);
1390 theme_adium_remove_mark_from_message (self, id);
1394 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1397 self->priv->has_focus = has_focus;
1398 if (!self->priv->has_focus)
1400 /* We've lost focus, so let's make sure all the acked
1401 * messages have lost their unread marker. */
1402 g_queue_foreach (&self->priv->acked_messages,
1403 theme_adium_remove_acked_message_unread_mark_foreach, self);
1404 g_queue_clear (&self->priv->acked_messages);
1406 self->priv->has_unread_message = FALSE;
1411 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1412 EmpathyMessage *message)
1418 tp_msg = empathy_message_get_tp_message (message);
1423 id = tp_message_get_pending_message_id (tp_msg, &valid);
1426 g_warning ("Acknoledged message doesn't have a pending ID");
1430 /* We only want to actually remove the unread marker if the
1431 * view doesn't have focus. If we did it all the time we would
1432 * never see the unread markers, ever! So, we'll queue these
1433 * up, and when we lose focus, we'll remove the markers. */
1434 if (self->priv->has_focus)
1436 g_queue_push_tail (&self->priv->acked_messages,
1437 GUINT_TO_POINTER (id));
1441 theme_adium_remove_mark_from_message (self, id);
1445 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1446 GtkWidget *default_menu,
1447 WebKitHitTestResult *hit_test_result,
1448 gboolean triggered_with_keyboard,
1452 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1454 if (g_settings_get_boolean (self->priv->gsettings_chat,
1455 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1456 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1458 menu = empathy_webkit_create_context_menu (
1459 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1461 gtk_widget_show_all (menu);
1463 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1464 gtk_get_current_event_time ());
1470 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1471 gboolean show_avatars)
1473 self->priv->show_avatars = show_avatars;
1477 theme_adium_load_finished_cb (WebKitWebView *view,
1478 WebKitWebFrame *frame,
1481 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1484 DEBUG ("Page loaded");
1485 self->priv->pages_loading--;
1487 if (self->priv->pages_loading != 0)
1490 /* Display queued messages */
1491 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1493 QueuedItem *item = l->data;
1497 case QUEUED_MESSAGE:
1498 empathy_theme_adium_append_message (self, item->msg,
1499 item->should_highlight);
1503 empathy_theme_adium_edit_message (self, item->msg);
1507 empathy_theme_adium_append_event (self, item->str);
1511 free_queued_item (item);
1514 g_queue_clear (&self->priv->message_queue);
1518 theme_adium_finalize (GObject *object)
1520 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1522 empathy_adium_data_unref (self->priv->data);
1524 g_object_unref (self->priv->gsettings_chat);
1525 g_object_unref (self->priv->gsettings_desktop);
1527 g_free (self->priv->variant);
1529 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1533 theme_adium_dispose (GObject *object)
1535 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1537 if (self->priv->smiley_manager)
1539 g_object_unref (self->priv->smiley_manager);
1540 self->priv->smiley_manager = NULL;
1543 g_clear_object (&self->priv->first_contact);
1545 if (self->priv->last_contact)
1547 g_object_unref (self->priv->last_contact);
1548 self->priv->last_contact = NULL;
1551 if (self->priv->inspector_window)
1553 gtk_widget_destroy (self->priv->inspector_window);
1554 self->priv->inspector_window = NULL;
1557 if (self->priv->acked_messages.length > 0)
1559 g_queue_clear (&self->priv->acked_messages);
1562 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1566 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1567 EmpathyThemeAdium *self)
1569 if (self->priv->inspector_window)
1571 gtk_widget_show_all (self->priv->inspector_window);
1578 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1579 EmpathyThemeAdium *self)
1581 if (self->priv->inspector_window)
1583 gtk_widget_hide (self->priv->inspector_window);
1589 static WebKitWebView *
1590 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1591 WebKitWebView *web_view,
1592 EmpathyThemeAdium *self)
1594 GtkWidget *scrolled_window;
1595 GtkWidget *inspector_web_view;
1597 if (!self->priv->inspector_window)
1599 /* Create main window */
1600 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1602 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1605 g_signal_connect (self->priv->inspector_window, "delete-event",
1606 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1608 /* Pack a scrolled window */
1609 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1611 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1612 GTK_POLICY_AUTOMATIC,
1613 GTK_POLICY_AUTOMATIC);
1614 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1616 gtk_widget_show (scrolled_window);
1618 /* Pack a webview in the scrolled window. That webview will be
1619 * used to render the inspector tool. */
1620 inspector_web_view = webkit_web_view_new ();
1621 gtk_container_add (GTK_CONTAINER (scrolled_window),
1622 inspector_web_view);
1623 gtk_widget_show (scrolled_window);
1625 return WEBKIT_WEB_VIEW (inspector_web_view);
1632 theme_adium_constructed (GObject *object)
1634 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1635 const gchar *font_family = NULL;
1637 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1638 WebKitWebInspector *webkit_inspector;
1640 /* Set default settings */
1641 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1642 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1644 if (font_family && font_size)
1646 g_object_set (webkit_web_view_get_settings (webkit_view),
1647 "default-font-family", font_family,
1648 "default-font-size", font_size,
1653 empathy_webkit_bind_font_setting (webkit_view,
1654 self->priv->gsettings_desktop,
1655 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1658 /* Setup webkit inspector */
1659 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1660 g_signal_connect (webkit_inspector, "inspect-web-view",
1661 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1662 g_signal_connect (webkit_inspector, "show-window",
1663 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1664 g_signal_connect (webkit_inspector, "close-window",
1665 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1668 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1670 self->priv->in_construction = FALSE;
1674 theme_adium_get_property (GObject *object,
1679 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1683 case PROP_ADIUM_DATA:
1684 g_value_set_boxed (value, self->priv->data);
1687 g_value_set_string (value, self->priv->variant);
1690 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1696 theme_adium_set_property (GObject *object,
1698 const GValue *value,
1701 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1705 case PROP_ADIUM_DATA:
1706 g_assert (self->priv->data == NULL);
1707 self->priv->data = g_value_dup_boxed (value);
1710 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1713 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1719 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1721 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1723 object_class->finalize = theme_adium_finalize;
1724 object_class->dispose = theme_adium_dispose;
1725 object_class->constructed = theme_adium_constructed;
1726 object_class->get_property = theme_adium_get_property;
1727 object_class->set_property = theme_adium_set_property;
1729 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1730 g_param_spec_boxed ("adium-data",
1732 "Data for the adium theme",
1733 EMPATHY_TYPE_ADIUM_DATA,
1734 G_PARAM_CONSTRUCT_ONLY |
1736 G_PARAM_STATIC_STRINGS));
1738 g_object_class_install_property (object_class, PROP_VARIANT,
1739 g_param_spec_string ("variant",
1740 "The theme variant",
1741 "Variant name for the theme",
1745 G_PARAM_STATIC_STRINGS));
1747 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1751 empathy_theme_adium_init (EmpathyThemeAdium *self)
1753 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1754 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1756 self->priv->in_construction = TRUE;
1757 g_queue_init (&self->priv->message_queue);
1758 self->priv->allow_scrolling = TRUE;
1759 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1761 /* Show avatars by default. */
1762 self->priv->show_avatars = TRUE;
1764 g_signal_connect (self, "load-finished",
1765 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1766 g_signal_connect (self, "navigation-policy-decision-requested",
1767 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1768 g_signal_connect (self, "context-menu",
1769 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1771 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1772 self->priv->gsettings_desktop = g_settings_new (
1773 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1777 empathy_theme_adium_new (EmpathyAdiumData *data,
1778 const gchar *variant)
1780 g_return_val_if_fail (data != NULL, NULL);
1782 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1789 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1790 const gchar *variant)
1792 gchar *variant_path;
1795 if (!tp_strdiff (self->priv->variant, variant))
1798 g_free (self->priv->variant);
1799 self->priv->variant = g_strdup (variant);
1801 if (self->priv->in_construction)
1804 DEBUG ("Update view with variant: '%s'", variant);
1805 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1806 self->priv->variant);
1807 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1810 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1812 g_free (variant_path);
1815 g_object_notify (G_OBJECT (self), "variant");
1819 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1821 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1823 empathy_webkit_show_inspector (web_view);
1827 empathy_adium_path_is_valid (const gchar *path)
1837 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1838 tmp = g_strsplit (path, "/", 0);
1842 dir = tmp[g_strv_length (tmp) - 1];
1844 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1852 /* The theme is not valid if there is no Info.plist */
1853 file = g_build_filename (path, "Contents", "Info.plist",
1855 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1861 /* We ship a default Template.html as fallback if there is any problem
1862 * with the one inside the theme. The only other required file is
1863 * Content.html OR Incoming/Content.html*/
1864 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1866 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1871 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1872 "Content.html", NULL);
1873 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1881 empathy_adium_info_new (const gchar *path)
1885 GHashTable *info = NULL;
1887 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1889 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1890 value = empathy_plist_parse_from_file (file);
1896 info = g_value_dup_boxed (value);
1897 tp_g_value_slice_free (value);
1899 /* Insert the theme's path into the hash table,
1900 * keys have to be dupped */
1901 tp_asv_set_string (info, g_strdup ("path"), path);
1907 adium_info_get_version (GHashTable *info)
1909 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1912 static const gchar *
1913 adium_info_get_no_variant_name (GHashTable *info)
1915 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1916 return name ? name : _("Normal");
1920 adium_info_dup_path_for_variant (GHashTable *info,
1921 const gchar *variant)
1923 guint version = adium_info_get_version (info);
1924 const gchar *no_variant = adium_info_get_no_variant_name (info);
1925 GPtrArray *variants;
1928 if (version <= 2 && !tp_strdiff (variant, no_variant))
1929 return g_strdup ("main.css");
1931 variants = empathy_adium_info_get_available_variants (info);
1932 if (variants->len == 0)
1933 return g_strdup ("main.css");
1935 /* Verify the variant exists, fallback to the first one */
1936 for (i = 0; i < variants->len; i++)
1938 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1942 if (i == variants->len)
1944 DEBUG ("Variant %s does not exist", variant);
1945 variant = g_ptr_array_index (variants, 0);
1948 return g_strdup_printf ("Variants/%s.css", variant);
1953 empathy_adium_info_get_default_variant (GHashTable *info)
1955 if (adium_info_get_version (info) <= 2)
1956 return adium_info_get_no_variant_name (info);
1958 return tp_asv_get_string (info, "DefaultVariant");
1962 empathy_adium_info_get_available_variants (GHashTable *info)
1964 GPtrArray *variants;
1969 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1970 if (variants != NULL)
1973 variants = g_ptr_array_new_with_free_func (g_free);
1974 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1975 G_TYPE_PTR_ARRAY, variants);
1977 path = tp_asv_get_string (info, "path");
1978 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1979 dir = g_dir_open (dirpath, 0, NULL);
1984 for (name = g_dir_read_name (dir);
1986 name = g_dir_read_name (dir))
1988 gchar *display_name;
1990 if (!g_str_has_suffix (name, ".css"))
1993 display_name = g_strdup (name);
1994 strstr (display_name, ".css")[0] = '\0';
1995 g_ptr_array_add (variants, display_name);
2002 if (adium_info_get_version (info) <= 2)
2003 g_ptr_array_add (variants,
2004 g_strdup (adium_info_get_no_variant_name (info)));
2010 empathy_adium_data_get_type (void)
2012 static GType type_id = 0;
2016 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2017 (GBoxedCopyFunc) empathy_adium_data_ref,
2018 (GBoxedFreeFunc) empathy_adium_data_unref);
2025 empathy_adium_data_new_with_info (const gchar *path,
2028 EmpathyAdiumData *data;
2029 gchar *template_html = NULL;
2030 gchar *footer_html = NULL;
2033 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2035 data = g_slice_new0 (EmpathyAdiumData);
2036 data->ref_count = 1;
2037 data->path = g_strdup (path);
2038 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2039 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2040 data->info = g_hash_table_ref (info);
2041 data->version = adium_info_get_version (info);
2042 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2043 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2044 g_str_equal, g_free, g_free);
2046 DEBUG ("Loading theme at %s", path);
2048 #define LOAD(path, var) \
2049 tmp = g_build_filename (data->basedir, path, NULL); \
2050 g_file_get_contents (tmp, &var, NULL, NULL); \
2053 #define LOAD_CONST(path, var) \
2056 LOAD (path, content); \
2057 if (content != NULL) { \
2058 g_ptr_array_add (data->strings_to_free, content); \
2063 /* Load html files */
2064 LOAD_CONST ("Content.html", data->content_html);
2065 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2066 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2067 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2068 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2069 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2070 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2071 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2072 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2073 LOAD_CONST ("Status.html", data->status_html);
2074 LOAD ("Template.html", template_html);
2075 LOAD ("Footer.html", footer_html);
2080 /* HTML fallbacks: If we have at least content OR in_content, then
2081 * everything else gets a fallback */
2083 #define FALLBACK(html, fallback) \
2084 if (html == NULL) { \
2088 /* in_nextcontent -> in_content -> content */
2089 FALLBACK (data->in_content_html, data->content_html);
2090 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2092 /* context -> content */
2093 FALLBACK (data->in_context_html, data->in_content_html);
2094 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2095 FALLBACK (data->out_context_html, data->out_content_html);
2096 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2099 FALLBACK (data->out_content_html, data->in_content_html);
2100 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2101 FALLBACK (data->out_context_html, data->in_context_html);
2102 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2104 /* status -> in_content */
2105 FALLBACK (data->status_html, data->in_content_html);
2109 /* template -> empathy's template */
2110 data->custom_template = (template_html != NULL);
2111 if (template_html == NULL)
2113 GError *error = NULL;
2115 tmp = empathy_file_lookup ("Template.html", "data");
2117 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2118 g_warning ("couldn't load Empathy's default theme "
2119 "template: %s", error->message);
2120 g_return_val_if_reached (data);
2126 /* Default avatar */
2127 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2128 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2130 data->default_incoming_avatar_filename = tmp;
2137 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2138 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2140 data->default_outgoing_avatar_filename = tmp;
2147 /* Old custom templates had only 4 parameters.
2148 * New templates have 5 parameters */
2149 if (data->version <= 2 && data->custom_template)
2151 tmp = string_with_format (template_html,
2153 "%@", /* Leave variant unset */
2154 "", /* The header */
2155 footer_html ? footer_html : "",
2160 tmp = string_with_format (template_html,
2162 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2163 "%@", /* Leave variant unset */
2164 "", /* The header */
2165 footer_html ? footer_html : "",
2168 g_ptr_array_add (data->strings_to_free, tmp);
2169 data->template_html = tmp;
2171 g_free (template_html);
2172 g_free (footer_html);
2178 empathy_adium_data_new (const gchar *path)
2180 EmpathyAdiumData *data;
2183 info = empathy_adium_info_new (path);
2184 data = empathy_adium_data_new_with_info (path, info);
2185 g_hash_table_unref (info);
2191 empathy_adium_data_ref (EmpathyAdiumData *data)
2193 g_return_val_if_fail (data != NULL, NULL);
2195 g_atomic_int_inc (&data->ref_count);
2201 empathy_adium_data_unref (EmpathyAdiumData *data)
2203 g_return_if_fail (data != NULL);
2205 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2206 g_free (data->path);
2207 g_free (data->basedir);
2208 g_free (data->default_avatar_filename);
2209 g_free (data->default_incoming_avatar_filename);
2210 g_free (data->default_outgoing_avatar_filename);
2211 g_hash_table_unref (data->info);
2212 g_ptr_array_unref (data->strings_to_free);
2213 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2215 g_slice_free (EmpathyAdiumData, data);
2220 empathy_adium_data_get_info (EmpathyAdiumData *data)
2222 g_return_val_if_fail (data != NULL, NULL);
2228 empathy_adium_data_get_path (EmpathyAdiumData *data)
2230 g_return_val_if_fail (data != NULL, NULL);