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,
738 js = (const gchar *) g_bytes_get_data (bytes, NULL);
739 g_string_prepend (string, js);
740 g_bytes_unref (bytes);
742 script = g_string_free (string, FALSE);
743 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
748 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
749 const gchar *escaped,
750 PangoDirection direction)
752 theme_adium_add_html (self, "appendMessage",
753 self->priv->data->status_html, escaped, NULL, NULL, NULL,
754 NULL, "event", tpaw_time_get_current (), FALSE, FALSE, direction);
756 /* There is no last contact */
757 if (self->priv->last_contact)
759 g_object_unref (self->priv->last_contact);
760 self->priv->last_contact = NULL;
765 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
766 WebKitDOMNodeList *nodes)
770 /* Remove focus and firstFocus class */
771 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
773 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
774 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
776 gchar **classes, **iter;
777 GString *new_class_name;
778 gboolean first = TRUE;
783 class_name = webkit_dom_html_element_get_class_name (element);
784 classes = g_strsplit (class_name, " ", -1);
785 new_class_name = g_string_sized_new (strlen (class_name));
787 for (iter = classes; *iter != NULL; iter++)
789 if (tp_strdiff (*iter, "focus") &&
790 tp_strdiff (*iter, "firstFocus"))
793 g_string_append_c (new_class_name, ' ');
795 g_string_append (new_class_name, *iter);
800 webkit_dom_html_element_set_class_name (element, new_class_name->str);
803 g_strfreev (classes);
804 g_string_free (new_class_name, TRUE);
809 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
811 WebKitDOMDocument *dom;
812 WebKitDOMNodeList *nodes;
813 GError *error = NULL;
815 if (!self->priv->has_unread_message)
818 self->priv->has_unread_message = FALSE;
820 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
824 /* Get all nodes with focus class */
825 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
829 DEBUG ("Error getting focus nodes: %s",
830 error ? error->message : "No error");
831 g_clear_error (&error);
835 theme_adium_remove_focus_marks (self, nodes);
840 ADD_CONSECUTIVE_MSG_SCROLL = 0,
841 ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
843 ADD_MSG_NO_SCROLL = 3
847 * theme_adium_add_message:
848 * @self: The #EmpathyThemeAdium used by the view.
849 * @msg: An #EmpathyMessage that is to be added to the view.
850 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
851 * @prev_timestamp: (out): Timestamp of the previous message.
852 * @prev_is_backlog: (out): Whether the previous message was fetched
854 * @should_highlight: Whether the message should be highlighted. eg.,
855 * if it matches the user's username in multi-user chat.
856 * @js_funcs: An array of JavaScript function names
858 * Shows @msg in the chat view by adding to @self. Addition is defined
859 * by the JavaScript functions listed in @js_funcs. Common examples
860 * are appending new incoming messages or prepending old messages from
863 * @js_funcs should be an array with exactly 4 entries. The entries
864 * should be the names of JavaScript functions that take the raw HTML
865 * that is to be added to the view as an argument and take the following
866 * actions, in this order:
867 * - add a new consecutive message and scroll to it if needed,
868 * - add a new consecutive message and do not scroll,
869 * - add a new non-consecutive message and scroll to it if needed, and
870 * - add a new non-consecutive message and do not scroll
872 * A message is considered to be consecutive with the previous one if
873 * all the following conditions are met:
874 * - senders are the same contact,
875 * - last message was recieved recently,
876 * - last message and this message both are/aren't backlog, and
877 * - DisableCombineConsecutive is not set in theme's settings
880 theme_adium_add_message (EmpathyThemeAdium *self,
882 EmpathyContact **prev_contact,
883 gint64 *prev_timestamp,
884 gboolean *prev_is_backlog,
885 gboolean should_highlight,
886 const gchar *js_funcs[])
888 EmpathyContact *sender;
891 gchar *body_escaped, *name_escaped;
893 const gchar *contact_id;
894 EmpathyAvatar *avatar;
895 const gchar *avatar_filename = NULL;
897 const gchar *html = NULL;
899 const gchar *service_name;
900 GString *message_classes = NULL;
902 gboolean consecutive;
904 PangoDirection direction;
907 /* Get information */
908 sender = empathy_message_get_sender (msg);
909 account = empathy_contact_get_account (sender);
910 service_name = tpaw_protocol_name_to_display_name
911 (tp_account_get_protocol_name (account));
912 if (service_name == NULL)
913 service_name = tp_account_get_protocol_name (account);
914 timestamp = empathy_message_get_timestamp (msg);
915 body_escaped = theme_adium_parse_body (self,
916 empathy_message_get_body (msg),
917 empathy_message_get_token (msg));
918 name = empathy_contact_get_logged_alias (sender);
919 contact_id = empathy_contact_get_id (sender);
920 action = (empathy_message_get_tptype (msg) ==
921 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
923 name_escaped = g_markup_escape_text (name, -1);
925 /* If this is a /me probably */
930 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
932 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
933 "<span class='actionMessageBody'>%s</span>",
934 name_escaped, body_escaped);
938 str = g_strdup_printf ("*%s*", body_escaped);
941 g_free (body_escaped);
945 /* Get the avatar filename, or a fallback */
946 avatar = empathy_contact_get_avatar (sender);
948 avatar_filename = avatar->filename;
950 if (!avatar_filename)
952 if (empathy_contact_is_user (sender))
953 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
955 avatar_filename = self->priv->data->default_incoming_avatar_filename;
957 if (!avatar_filename)
959 if (!self->priv->data->default_avatar_filename)
960 self->priv->data->default_avatar_filename =
961 tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT,
962 GTK_ICON_SIZE_DIALOG);
964 avatar_filename = self->priv->data->default_avatar_filename;
968 is_backlog = empathy_message_is_backlog (msg);
969 consecutive = empathy_contact_equal (*prev_contact, sender) &&
970 (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
971 (is_backlog == *prev_is_backlog) &&
972 !tp_asv_get_boolean (self->priv->data->info,
973 "DisableCombineConsecutive", NULL);
975 /* Define message classes */
976 message_classes = g_string_new ("message");
977 if (!self->priv->has_focus && !is_backlog)
979 if (!self->priv->has_unread_message)
981 g_string_append (message_classes, " firstFocus");
982 self->priv->has_unread_message = TRUE;
984 g_string_append (message_classes, " focus");
988 g_string_append (message_classes, " history");
991 g_string_append (message_classes, " consecutive");
993 if (empathy_contact_is_user (sender))
994 g_string_append (message_classes, " outgoing");
996 g_string_append (message_classes, " incoming");
998 if (should_highlight)
999 g_string_append (message_classes, " mention");
1001 if (empathy_message_get_tptype (msg) ==
1002 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
1003 g_string_append (message_classes, " autoreply");
1006 g_string_append (message_classes, " action");
1008 /* FIXME: other classes:
1009 * status - the message is a status change
1010 * event - the message is a notification of something happening
1011 * (for example, encryption being turned on)
1012 * %status% - See %status% in theme_adium_add_html ()
1015 /* This is slightly a hack, but it's the only way to add
1016 * arbitrary data to messages in the HTML. We add another
1017 * class called "x-empathy-message-id-*" to the message. This
1018 * way, we can remove the unread marker for this specific
1020 tp_msg = empathy_message_get_tp_message (msg);
1026 id = tp_message_get_pending_message_id (tp_msg, &valid);
1028 g_string_append_printf (message_classes,
1029 " x-empathy-message-id-%u", id);
1032 /* Define javascript function to use */
1034 func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1035 js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1037 func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1038 js_funcs[ADD_MSG_NO_SCROLL];
1040 if (empathy_contact_is_user (sender))
1045 html = consecutive ? self->priv->data->out_nextcontext_html :
1046 self->priv->data->out_context_html;
1049 html = consecutive ? self->priv->data->out_nextcontent_html :
1050 self->priv->data->out_content_html;
1052 /* remove all the unread marks when we are sending a message */
1053 theme_adium_remove_all_focus_marks (self);
1060 html = consecutive ? self->priv->data->in_nextcontext_html :
1061 self->priv->data->in_context_html;
1064 html = consecutive ? self->priv->data->in_nextcontent_html :
1065 self->priv->data->in_content_html;
1068 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1070 theme_adium_add_html (self, func, html, body_escaped,
1071 avatar_filename, name_escaped, contact_id,
1072 service_name, message_classes->str,
1073 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1075 /* Keep the sender of the last displayed message */
1077 g_object_unref (*prev_contact);
1079 *prev_contact = g_object_ref (sender);
1080 *prev_timestamp = timestamp;
1081 *prev_is_backlog = is_backlog;
1083 g_free (body_escaped);
1084 g_free (name_escaped);
1085 g_string_free (message_classes, TRUE);
1089 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1090 EmpathyMessage *msg,
1091 gboolean should_highlight)
1093 const gchar *js_funcs[] = { "appendNextMessage",
1094 "appendNextMessageNoScroll",
1096 "appendMessageNoScroll" };
1098 if (self->priv->pages_loading != 0)
1100 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1101 should_highlight, FALSE);
1105 theme_adium_add_message (self, msg, &self->priv->last_contact,
1106 &self->priv->last_timestamp, &self->priv->last_is_backlog,
1107 should_highlight, js_funcs);
1111 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1115 PangoDirection direction;
1117 if (self->priv->pages_loading != 0)
1119 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1123 direction = pango_find_base_dir (str, -1);
1124 str_escaped = g_markup_escape_text (str, -1);
1125 theme_adium_append_event_escaped (self, str_escaped, direction);
1126 g_free (str_escaped);
1130 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1131 const gchar *markup_text,
1132 const gchar *fallback_text)
1134 PangoDirection direction;
1136 direction = pango_find_base_dir (fallback_text, -1);
1137 theme_adium_append_event_escaped (self, markup_text, direction);
1141 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1142 EmpathyMessage *msg,
1143 gboolean should_highlight)
1145 const gchar *js_funcs[] = { "prependPrev",
1150 if (self->priv->pages_loading != 0)
1152 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1153 should_highlight, TRUE);
1157 theme_adium_add_message (self, msg, &self->priv->first_contact,
1158 &self->priv->first_timestamp, &self->priv->first_is_backlog,
1159 should_highlight, js_funcs);
1163 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1164 EmpathyMessage *message)
1166 WebKitDOMDocument *doc;
1167 WebKitDOMElement *span;
1168 gchar *id, *parsed_body;
1169 gchar *tooltip, *timestamp;
1170 GtkIconInfo *icon_info;
1171 GError *error = NULL;
1173 if (self->priv->pages_loading != 0)
1175 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1179 id = g_strdup_printf ("message-token-%s",
1180 empathy_message_get_supersedes (message));
1181 /* we don't pass a token here, because doing so will return another
1182 * <span> element, and we don't want nested <span> elements */
1183 parsed_body = theme_adium_parse_body (self,
1184 empathy_message_get_body (message), NULL);
1186 /* find the element */
1187 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1188 span = webkit_dom_document_get_element_by_id (doc, id);
1192 DEBUG ("Failed to find id '%s'", id);
1196 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1198 DEBUG ("Not a HTML element");
1202 /* update the HTML */
1203 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1204 parsed_body, &error);
1208 DEBUG ("Error setting new inner-HTML: %s", error->message);
1209 g_error_free (error);
1214 timestamp = tpaw_time_to_string_local (
1215 empathy_message_get_timestamp (message),
1217 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1219 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1225 /* mark this message as edited */
1226 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1227 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1229 if (icon_info != NULL)
1231 /* set the icon as a background image using CSS
1232 * FIXME: the icon won't update in response to theme changes */
1233 gchar *style = g_strdup_printf (
1234 "background-image:url('%s');"
1235 "background-repeat:no-repeat;"
1236 "background-position:left center;"
1237 "padding-left:19px;", /* 16px icon + 3px padding */
1238 gtk_icon_info_get_filename (icon_info));
1240 webkit_dom_element_set_attribute (span, "style", style, &error);
1244 DEBUG ("Error setting element style: %s",
1246 g_clear_error (&error);
1251 g_object_unref (icon_info);
1257 DEBUG ("Could not find message to edit with: %s",
1258 empathy_message_get_body (message));
1262 g_free (parsed_body);
1266 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1267 gboolean allow_scrolling)
1269 self->priv->allow_scrolling = allow_scrolling;
1271 if (allow_scrolling)
1272 empathy_theme_adium_scroll_down (self);
1276 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1278 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1282 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1284 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1288 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1290 theme_adium_load_template (self);
1292 /* Clear last contact to avoid trying to add a 'joined'
1293 * message when we don't have an insertion point. */
1294 if (self->priv->last_contact)
1296 g_object_unref (self->priv->last_contact);
1297 self->priv->last_contact = NULL;
1302 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1303 const gchar *search_criteria,
1304 gboolean new_search,
1305 gboolean match_case)
1307 /* FIXME: Doesn't respect new_search */
1308 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1309 search_criteria, match_case, FALSE, TRUE);
1313 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1314 const gchar *search_criteria,
1315 gboolean new_search,
1316 gboolean match_case)
1318 /* FIXME: Doesn't respect new_search */
1319 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1320 search_criteria, match_case, TRUE, TRUE);
1324 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1325 const gchar *search_criteria,
1326 gboolean match_case,
1327 gboolean *can_do_previous,
1328 gboolean *can_do_next)
1330 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1331 * find_next and find_previous to work around this problem. */
1332 if (can_do_previous)
1333 *can_do_previous = TRUE;
1335 *can_do_next = TRUE;
1339 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1341 gboolean match_case)
1343 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1344 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1345 text, match_case, 0);
1346 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1351 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1353 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1357 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1360 WebKitDOMDocument *dom;
1361 WebKitDOMNodeList *nodes;
1363 GError *error = NULL;
1365 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1369 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1371 /* Get all nodes with focus class */
1372 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1377 DEBUG ("Error getting focus nodes: %s",
1378 error ? error->message : "No error");
1379 g_clear_error (&error);
1383 theme_adium_remove_focus_marks (self, nodes);
1387 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1390 EmpathyThemeAdium *self = user_data;
1391 guint32 id = GPOINTER_TO_UINT (data);
1393 theme_adium_remove_mark_from_message (self, id);
1397 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1400 self->priv->has_focus = has_focus;
1401 if (!self->priv->has_focus)
1403 /* We've lost focus, so let's make sure all the acked
1404 * messages have lost their unread marker. */
1405 g_queue_foreach (&self->priv->acked_messages,
1406 theme_adium_remove_acked_message_unread_mark_foreach, self);
1407 g_queue_clear (&self->priv->acked_messages);
1409 self->priv->has_unread_message = FALSE;
1414 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1415 EmpathyMessage *message)
1421 tp_msg = empathy_message_get_tp_message (message);
1426 id = tp_message_get_pending_message_id (tp_msg, &valid);
1429 g_warning ("Acknoledged message doesn't have a pending ID");
1433 /* We only want to actually remove the unread marker if the
1434 * view doesn't have focus. If we did it all the time we would
1435 * never see the unread markers, ever! So, we'll queue these
1436 * up, and when we lose focus, we'll remove the markers. */
1437 if (self->priv->has_focus)
1439 g_queue_push_tail (&self->priv->acked_messages,
1440 GUINT_TO_POINTER (id));
1444 theme_adium_remove_mark_from_message (self, id);
1448 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1449 GtkWidget *default_menu,
1450 WebKitHitTestResult *hit_test_result,
1451 gboolean triggered_with_keyboard,
1455 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1457 if (g_settings_get_boolean (self->priv->gsettings_chat,
1458 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1459 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1461 menu = empathy_webkit_create_context_menu (
1462 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1464 gtk_widget_show_all (menu);
1466 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1467 gtk_get_current_event_time ());
1473 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1474 gboolean show_avatars)
1476 self->priv->show_avatars = show_avatars;
1480 theme_adium_load_finished_cb (WebKitWebView *view,
1481 WebKitWebFrame *frame,
1484 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1487 DEBUG ("Page loaded");
1488 self->priv->pages_loading--;
1490 if (self->priv->pages_loading != 0)
1493 /* Display queued messages */
1494 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1496 QueuedItem *item = l->data;
1500 case QUEUED_MESSAGE:
1501 empathy_theme_adium_append_message (self, item->msg,
1502 item->should_highlight);
1506 empathy_theme_adium_edit_message (self, item->msg);
1510 empathy_theme_adium_append_event (self, item->str);
1514 free_queued_item (item);
1517 g_queue_clear (&self->priv->message_queue);
1521 theme_adium_finalize (GObject *object)
1523 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1525 empathy_adium_data_unref (self->priv->data);
1527 g_object_unref (self->priv->gsettings_chat);
1528 g_object_unref (self->priv->gsettings_desktop);
1530 g_free (self->priv->variant);
1532 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1536 theme_adium_dispose (GObject *object)
1538 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1540 if (self->priv->smiley_manager)
1542 g_object_unref (self->priv->smiley_manager);
1543 self->priv->smiley_manager = NULL;
1546 g_clear_object (&self->priv->first_contact);
1548 if (self->priv->last_contact)
1550 g_object_unref (self->priv->last_contact);
1551 self->priv->last_contact = NULL;
1554 if (self->priv->inspector_window)
1556 gtk_widget_destroy (self->priv->inspector_window);
1557 self->priv->inspector_window = NULL;
1560 if (self->priv->acked_messages.length > 0)
1562 g_queue_clear (&self->priv->acked_messages);
1565 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1569 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1570 EmpathyThemeAdium *self)
1572 if (self->priv->inspector_window)
1574 gtk_widget_show_all (self->priv->inspector_window);
1581 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1582 EmpathyThemeAdium *self)
1584 if (self->priv->inspector_window)
1586 gtk_widget_hide (self->priv->inspector_window);
1592 static WebKitWebView *
1593 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1594 WebKitWebView *web_view,
1595 EmpathyThemeAdium *self)
1597 GtkWidget *scrolled_window;
1598 GtkWidget *inspector_web_view;
1600 if (!self->priv->inspector_window)
1602 /* Create main window */
1603 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1605 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1608 g_signal_connect (self->priv->inspector_window, "delete-event",
1609 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1611 /* Pack a scrolled window */
1612 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1614 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1615 GTK_POLICY_AUTOMATIC,
1616 GTK_POLICY_AUTOMATIC);
1617 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1619 gtk_widget_show (scrolled_window);
1621 /* Pack a webview in the scrolled window. That webview will be
1622 * used to render the inspector tool. */
1623 inspector_web_view = webkit_web_view_new ();
1624 gtk_container_add (GTK_CONTAINER (scrolled_window),
1625 inspector_web_view);
1626 gtk_widget_show (scrolled_window);
1628 return WEBKIT_WEB_VIEW (inspector_web_view);
1635 theme_adium_constructed (GObject *object)
1637 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1638 const gchar *font_family = NULL;
1640 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1641 WebKitWebInspector *webkit_inspector;
1643 /* Set default settings */
1644 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1645 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1647 if (font_family && font_size)
1649 g_object_set (webkit_web_view_get_settings (webkit_view),
1650 "default-font-family", font_family,
1651 "default-font-size", font_size,
1656 empathy_webkit_bind_font_setting (webkit_view,
1657 self->priv->gsettings_desktop,
1658 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1661 /* Setup webkit inspector */
1662 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1663 g_signal_connect (webkit_inspector, "inspect-web-view",
1664 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1665 g_signal_connect (webkit_inspector, "show-window",
1666 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1667 g_signal_connect (webkit_inspector, "close-window",
1668 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1671 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1673 self->priv->in_construction = FALSE;
1677 theme_adium_get_property (GObject *object,
1682 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1686 case PROP_ADIUM_DATA:
1687 g_value_set_boxed (value, self->priv->data);
1690 g_value_set_string (value, self->priv->variant);
1693 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1699 theme_adium_set_property (GObject *object,
1701 const GValue *value,
1704 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1708 case PROP_ADIUM_DATA:
1709 g_assert (self->priv->data == NULL);
1710 self->priv->data = g_value_dup_boxed (value);
1713 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1716 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1722 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1724 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1726 object_class->finalize = theme_adium_finalize;
1727 object_class->dispose = theme_adium_dispose;
1728 object_class->constructed = theme_adium_constructed;
1729 object_class->get_property = theme_adium_get_property;
1730 object_class->set_property = theme_adium_set_property;
1732 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1733 g_param_spec_boxed ("adium-data",
1735 "Data for the adium theme",
1736 EMPATHY_TYPE_ADIUM_DATA,
1737 G_PARAM_CONSTRUCT_ONLY |
1739 G_PARAM_STATIC_STRINGS));
1741 g_object_class_install_property (object_class, PROP_VARIANT,
1742 g_param_spec_string ("variant",
1743 "The theme variant",
1744 "Variant name for the theme",
1748 G_PARAM_STATIC_STRINGS));
1750 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1754 empathy_theme_adium_init (EmpathyThemeAdium *self)
1756 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1757 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1759 self->priv->in_construction = TRUE;
1760 g_queue_init (&self->priv->message_queue);
1761 self->priv->allow_scrolling = TRUE;
1762 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1764 /* Show avatars by default. */
1765 self->priv->show_avatars = TRUE;
1767 g_signal_connect (self, "load-finished",
1768 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1769 g_signal_connect (self, "navigation-policy-decision-requested",
1770 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1771 g_signal_connect (self, "context-menu",
1772 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1774 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1775 self->priv->gsettings_desktop = g_settings_new (
1776 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1780 empathy_theme_adium_new (EmpathyAdiumData *data,
1781 const gchar *variant)
1783 g_return_val_if_fail (data != NULL, NULL);
1785 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1792 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1793 const gchar *variant)
1795 gchar *variant_path;
1798 if (!tp_strdiff (self->priv->variant, variant))
1801 g_free (self->priv->variant);
1802 self->priv->variant = g_strdup (variant);
1804 if (self->priv->in_construction)
1807 DEBUG ("Update view with variant: '%s'", variant);
1808 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1809 self->priv->variant);
1810 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1813 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1815 g_free (variant_path);
1818 g_object_notify (G_OBJECT (self), "variant");
1822 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1824 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1826 empathy_webkit_show_inspector (web_view);
1830 empathy_adium_path_is_valid (const gchar *path)
1840 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1841 tmp = g_strsplit (path, "/", 0);
1845 dir = tmp[g_strv_length (tmp) - 1];
1847 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1855 /* The theme is not valid if there is no Info.plist */
1856 file = g_build_filename (path, "Contents", "Info.plist",
1858 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1864 /* We ship a default Template.html as fallback if there is any problem
1865 * with the one inside the theme. The only other required file is
1866 * Content.html OR Incoming/Content.html*/
1867 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1869 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1874 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1875 "Content.html", NULL);
1876 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1884 empathy_adium_info_new (const gchar *path)
1888 GHashTable *info = NULL;
1890 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1892 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1893 value = empathy_plist_parse_from_file (file);
1899 info = g_value_dup_boxed (value);
1900 tp_g_value_slice_free (value);
1902 /* Insert the theme's path into the hash table,
1903 * keys have to be dupped */
1904 tp_asv_set_string (info, g_strdup ("path"), path);
1910 adium_info_get_version (GHashTable *info)
1912 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1915 static const gchar *
1916 adium_info_get_no_variant_name (GHashTable *info)
1918 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1919 return name ? name : _("Normal");
1923 adium_info_dup_path_for_variant (GHashTable *info,
1924 const gchar *variant)
1926 guint version = adium_info_get_version (info);
1927 const gchar *no_variant = adium_info_get_no_variant_name (info);
1928 GPtrArray *variants;
1931 if (version <= 2 && !tp_strdiff (variant, no_variant))
1932 return g_strdup ("main.css");
1934 variants = empathy_adium_info_get_available_variants (info);
1935 if (variants->len == 0)
1936 return g_strdup ("main.css");
1938 /* Verify the variant exists, fallback to the first one */
1939 for (i = 0; i < variants->len; i++)
1941 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1945 if (i == variants->len)
1947 DEBUG ("Variant %s does not exist", variant);
1948 variant = g_ptr_array_index (variants, 0);
1951 return g_strdup_printf ("Variants/%s.css", variant);
1956 empathy_adium_info_get_default_variant (GHashTable *info)
1958 if (adium_info_get_version (info) <= 2)
1959 return adium_info_get_no_variant_name (info);
1961 return tp_asv_get_string (info, "DefaultVariant");
1965 empathy_adium_info_get_available_variants (GHashTable *info)
1967 GPtrArray *variants;
1972 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1973 if (variants != NULL)
1976 variants = g_ptr_array_new_with_free_func (g_free);
1977 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1978 G_TYPE_PTR_ARRAY, variants);
1980 path = tp_asv_get_string (info, "path");
1981 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1982 dir = g_dir_open (dirpath, 0, NULL);
1987 for (name = g_dir_read_name (dir);
1989 name = g_dir_read_name (dir))
1991 gchar *display_name;
1993 if (!g_str_has_suffix (name, ".css"))
1996 display_name = g_strdup (name);
1997 strstr (display_name, ".css")[0] = '\0';
1998 g_ptr_array_add (variants, display_name);
2005 if (adium_info_get_version (info) <= 2)
2006 g_ptr_array_add (variants,
2007 g_strdup (adium_info_get_no_variant_name (info)));
2013 empathy_adium_data_get_type (void)
2015 static GType type_id = 0;
2019 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2020 (GBoxedCopyFunc) empathy_adium_data_ref,
2021 (GBoxedFreeFunc) empathy_adium_data_unref);
2028 empathy_adium_data_new_with_info (const gchar *path,
2031 EmpathyAdiumData *data;
2032 gchar *template_html = NULL;
2033 gchar *footer_html = NULL;
2036 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2038 data = g_slice_new0 (EmpathyAdiumData);
2039 data->ref_count = 1;
2040 data->path = g_strdup (path);
2041 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2042 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2043 data->info = g_hash_table_ref (info);
2044 data->version = adium_info_get_version (info);
2045 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2046 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2047 g_str_equal, g_free, g_free);
2049 DEBUG ("Loading theme at %s", path);
2051 #define LOAD(path, var) \
2052 tmp = g_build_filename (data->basedir, path, NULL); \
2053 g_file_get_contents (tmp, &var, NULL, NULL); \
2056 #define LOAD_CONST(path, var) \
2059 LOAD (path, content); \
2060 if (content != NULL) { \
2061 g_ptr_array_add (data->strings_to_free, content); \
2066 /* Load html files */
2067 LOAD_CONST ("Content.html", data->content_html);
2068 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2069 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2070 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2071 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2072 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2073 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2074 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2075 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2076 LOAD_CONST ("Status.html", data->status_html);
2077 LOAD ("Template.html", template_html);
2078 LOAD ("Footer.html", footer_html);
2083 /* HTML fallbacks: If we have at least content OR in_content, then
2084 * everything else gets a fallback */
2086 #define FALLBACK(html, fallback) \
2087 if (html == NULL) { \
2091 /* in_nextcontent -> in_content -> content */
2092 FALLBACK (data->in_content_html, data->content_html);
2093 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2095 /* context -> content */
2096 FALLBACK (data->in_context_html, data->in_content_html);
2097 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2098 FALLBACK (data->out_context_html, data->out_content_html);
2099 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2102 FALLBACK (data->out_content_html, data->in_content_html);
2103 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2104 FALLBACK (data->out_context_html, data->in_context_html);
2105 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2107 /* status -> in_content */
2108 FALLBACK (data->status_html, data->in_content_html);
2112 /* template -> empathy's template */
2113 data->custom_template = (template_html != NULL);
2114 if (template_html == NULL)
2116 GError *error = NULL;
2118 tmp = empathy_file_lookup ("Template.html", "data");
2120 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2121 g_warning ("couldn't load Empathy's default theme "
2122 "template: %s", error->message);
2123 g_return_val_if_reached (data);
2129 /* Default avatar */
2130 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2131 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2133 data->default_incoming_avatar_filename = tmp;
2140 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2141 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2143 data->default_outgoing_avatar_filename = tmp;
2150 /* Old custom templates had only 4 parameters.
2151 * New templates have 5 parameters */
2152 if (data->version <= 2 && data->custom_template)
2154 tmp = string_with_format (template_html,
2156 "%@", /* Leave variant unset */
2157 "", /* The header */
2158 footer_html ? footer_html : "",
2163 tmp = string_with_format (template_html,
2165 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2166 "%@", /* Leave variant unset */
2167 "", /* The header */
2168 footer_html ? footer_html : "",
2171 g_ptr_array_add (data->strings_to_free, tmp);
2172 data->template_html = tmp;
2174 g_free (template_html);
2175 g_free (footer_html);
2181 empathy_adium_data_new (const gchar *path)
2183 EmpathyAdiumData *data;
2186 info = empathy_adium_info_new (path);
2187 data = empathy_adium_data_new_with_info (path, info);
2188 g_hash_table_unref (info);
2194 empathy_adium_data_ref (EmpathyAdiumData *data)
2196 g_return_val_if_fail (data != NULL, NULL);
2198 g_atomic_int_inc (&data->ref_count);
2204 empathy_adium_data_unref (EmpathyAdiumData *data)
2206 g_return_if_fail (data != NULL);
2208 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2209 g_free (data->path);
2210 g_free (data->basedir);
2211 g_free (data->default_avatar_filename);
2212 g_free (data->default_incoming_avatar_filename);
2213 g_free (data->default_outgoing_avatar_filename);
2214 g_hash_table_unref (data->info);
2215 g_ptr_array_unref (data->strings_to_free);
2216 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2218 g_slice_free (EmpathyAdiumData, data);
2223 empathy_adium_data_get_info (EmpathyAdiumData *data)
2225 g_return_val_if_fail (data != NULL, NULL);
2231 empathy_adium_data_get_path (EmpathyAdiumData *data)
2233 g_return_val_if_fail (data != NULL, NULL);