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>
27 #include "empathy-gsettings.h"
28 #include "empathy-images.h"
29 #include "empathy-plist.h"
30 #include "empathy-smiley-manager.h"
31 #include "empathy-time.h"
32 #include "empathy-ui-utils.h"
33 #include "empathy-utils.h"
34 #include "empathy-webkit-utils.h"
36 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
37 #include "empathy-debug.h"
39 #define BORING_DPI_DEFAULT 96
41 /* "Join" consecutive messages with timestamps within five minutes */
42 #define MESSAGE_JOIN_PERIOD 5*60
44 struct _EmpathyThemeAdiumPriv
46 EmpathyAdiumData *data;
47 EmpathySmileyManager *smiley_manager;
48 EmpathyContact *first_contact;
49 EmpathyContact *last_contact;
50 gint64 first_timestamp;
51 gint64 last_timestamp;
52 gboolean first_is_backlog;
53 gboolean last_is_backlog;
55 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
57 /* Queue of guint32 of pending message id to remove unread
58 * marker for when we lose focus. */
59 GQueue acked_messages;
60 GtkWidget *inspector_window;
62 GSettings *gsettings_chat;
63 GSettings *gsettings_desktop;
66 gboolean has_unread_message;
67 gboolean allow_scrolling;
69 gboolean in_construction;
70 gboolean show_avatars;
73 struct _EmpathyAdiumData
78 gchar *default_avatar_filename;
79 gchar *default_incoming_avatar_filename;
80 gchar *default_outgoing_avatar_filename;
83 gboolean custom_template;
84 /* gchar* -> gchar* both owned */
85 GHashTable *date_format_cache;
88 const gchar *template_html;
89 const gchar *content_html;
90 const gchar *in_content_html;
91 const gchar *in_context_html;
92 const gchar *in_nextcontent_html;
93 const gchar *in_nextcontext_html;
94 const gchar *out_content_html;
95 const gchar *out_context_html;
96 const gchar *out_nextcontent_html;
97 const gchar *out_nextcontext_html;
98 const gchar *status_html;
100 /* Above html strings are pointers to strings stored in this array.
101 * We do this because of fallbacks, some htmls could be pointing the
103 GPtrArray *strings_to_free;
106 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
107 const gchar *variant);
116 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
117 WEBKIT_TYPE_WEB_VIEW)
131 gboolean should_highlight;
135 queue_item (GQueue *queue,
139 gboolean should_highlight,
142 QueuedItem *item = g_slice_new0 (QueuedItem);
146 item->msg = g_object_ref (msg);
147 item->str = g_strdup (str);
148 item->should_highlight = should_highlight;
151 g_queue_push_head (queue, item);
153 g_queue_push_tail (queue, item);
159 free_queued_item (QueuedItem *item)
161 tp_clear_object (&item->msg);
164 g_slice_free (QueuedItem, item);
168 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
169 WebKitWebFrame *web_frame,
170 WebKitNetworkRequest *request,
171 WebKitWebNavigationAction *action,
172 WebKitWebPolicyDecision *decision,
177 /* Only call url_show on clicks */
178 if (webkit_web_navigation_action_get_reason (action) !=
179 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED)
181 webkit_web_policy_decision_use (decision);
185 uri = webkit_network_request_get_uri (request);
186 empathy_url_show (GTK_WIDGET (view), uri);
188 webkit_web_policy_decision_ignore (decision);
192 /* Replace each %@ in format with string passed in args */
194 string_with_format (const gchar *format,
195 const gchar *first_string,
202 va_start (args, first_string);
203 result = g_string_sized_new (strlen (format));
204 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
208 next = strstr (format, "%@");
212 g_string_append_len (result, format, next - format);
213 g_string_append (result, str);
216 g_string_append (result, format);
219 return g_string_free (result, FALSE);
223 theme_adium_load_template (EmpathyThemeAdium *self)
229 self->priv->pages_loading++;
230 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
232 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
233 self->priv->variant);
235 template = string_with_format (self->priv->data->template_html,
238 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self),
239 template, basedir_uri);
241 g_free (basedir_uri);
242 g_free (variant_path);
247 theme_adium_parse_body (EmpathyThemeAdium *self,
251 EmpathyStringParser *parsers;
254 /* Check if we have to parse smileys */
255 parsers = empathy_webkit_get_string_parser (
256 g_settings_get_boolean (self->priv->gsettings_chat,
257 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
259 /* Parse text and construct string with links and smileys replaced
260 * by html tags. Also escape text to make sure html code is
261 * displayed verbatim. */
262 string = g_string_sized_new (strlen (text));
264 /* wrap this in HTML that allows us to find the message for later
266 if (!tp_str_empty (token))
267 g_string_append_printf (string,
268 "<span id=\"message-token-%s\">",
271 empathy_string_parser_substr (text, -1, parsers, string);
273 if (!tp_str_empty (token))
274 g_string_append (string, "</span>");
276 /* Wrap body in order to make tabs and multiple spaces displayed
277 * properly. See bug #625745. */
278 g_string_prepend (string, "<div style=\"display: inline; "
279 "white-space: pre-wrap\"'>");
280 g_string_append (string, "</div>");
282 return g_string_free (string, FALSE);
286 escape_and_append_len (GString *string, const gchar *str, gint len)
288 while (str != NULL && *str != '\0' && len != 0)
294 g_string_append (string, "\\\\");
298 g_string_append (string, "\\\"");
301 /* Remove end of lines */
304 g_string_append_c (string, *str);
312 /* If *str starts with match, returns TRUE and move pointer to the end */
314 theme_adium_match (const gchar **str,
319 len = strlen (match);
320 if (strncmp (*str, match, len) == 0)
329 /* Like theme_adium_match() but also return the X part if match is
332 theme_adium_match_with_format (const gchar **str,
336 const gchar *cur = *str;
339 if (!theme_adium_match (&cur, match))
344 end = strstr (cur, "}%");
348 *format = g_strndup (cur , end - cur);
353 /* List of colors used by %senderColor%. Copied from
354 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
356 static gchar *colors[] = {
357 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
358 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
359 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
360 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
361 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
362 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
363 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
364 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
365 "lightblue", "lightcoral",
366 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
367 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
368 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
369 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
370 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
371 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
372 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
373 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
374 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
375 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
380 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
382 /* Convert from NSDateFormatter
383 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
384 * to strftime supported by g_date_time_format.
385 * FIXME: table is incomplete, doc of g_date_time_format has a table of
387 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
388 * in 2.29.x we have to explictely request padding with %0x */
389 static const gchar *convert_table[] = {
391 "A", NULL, // 0~86399999 (Millisecond of Day)
393 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
394 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
395 "cc", "%u", // 1~7 (Day of Week)
396 "c", "%u", // 1~7 (Day of Week)
398 "dd", "%d", // 1~31 (0 padded Day of Month)
399 "d", "%d", // 1~31 (0 padded Day of Month)
400 "D", "%j", // 1~366 (0 padded Day of Year)
402 "e", "%u", // 1~7 (0 padded Day of Week)
403 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
404 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
405 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
406 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
408 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
410 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
411 "GGGG", NULL, // Before Christ/Anno Domini
412 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
413 "GG", NULL, // BC/AD (Era Designator Abbreviated)
414 "G", NULL, // BC/AD (Era Designator Abbreviated)
416 "h", "%I", // 1~12 (0 padded Hour (12hr))
417 "H", "%H", // 0~23 (0 padded Hour (24hr))
419 "k", NULL, // 1~24 (0 padded Hour (24hr)
420 "K", NULL, // 0~11 (0 padded Hour (12hr))
422 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
423 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
424 "LL", "%m", // 1~12 (0 padded Month)
425 "L", "%m", // 1~12 (0 padded Month)
427 "m", "%M", // 0~59 (0 padded Minute)
428 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
429 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
430 "MM", "%m", // 1~12 (0 padded Month)
431 "M", "%m", // 1~12 (0 padded Month)
433 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
434 "qqq", NULL, // Q1/Q2/Q3/Q4
435 "qq", NULL, // 1~4 (0 padded Quarter)
436 "q", NULL, // 1~4 (0 padded Quarter)
437 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
438 "QQQ", NULL, // Q1/Q2/Q3/Q4
439 "QQ", NULL, // 1~4 (0 padded Quarter)
440 "Q", NULL, // 1~4 (0 padded Quarter)
442 "s", "%S", // 0~59 (0 padded Second)
443 "S", NULL, // (rounded Sub-Second)
445 "u", "%Y", // (0 padded Year)
447 "vvvv", "%Z", // (General GMT Timezone Name)
448 "vvv", "%Z", // (General GMT Timezone Abbreviation)
449 "vv", "%Z", // (General GMT Timezone Abbreviation)
450 "v", "%Z", // (General GMT Timezone Abbreviation)
452 "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)
453 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
455 "yyyy", "%Y", // (Full Year)
456 "yyy", "%y", // (2 Digits Year)
457 "yy", "%y", // (2 Digits Year)
458 "y", "%Y", // (Full Year)
459 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
460 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
461 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
462 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
464 "zzzz", NULL, // (Specific GMT Timezone Name)
465 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
466 "zz", NULL, // (Specific GMT Timezone Abbreviation)
467 "z", NULL, // (Specific GMT Timezone Abbreviation)
468 "Z", "%z", // +0000 (RFC 822 Timezone)
477 str = g_hash_table_lookup (data->date_format_cache, nsdate);
482 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
483 * by corresponding strftime tag. */
484 string = g_string_sized_new (strlen (nsdate));
485 for (i = 0; nsdate[i] != '\0'; i++)
487 gboolean found = FALSE;
489 /* even indexes are NSDateFormatter tag, odd indexes are the
490 * corresponding strftime tag */
491 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
493 if (g_str_has_prefix (nsdate + i, convert_table[j]))
502 /* If we don't have a replacement, just ignore that tag */
503 if (convert_table[j + 1] != NULL)
504 g_string_append (string, convert_table[j + 1]);
506 i += strlen (convert_table[j]) - 1;
510 g_string_append_c (string, nsdate[i]);
514 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
516 /* The cache takes ownership of string->str */
517 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
518 return g_string_free (string, FALSE);
522 theme_adium_add_html (EmpathyThemeAdium *self,
525 const gchar *message,
526 const gchar *avatar_filename,
528 const gchar *contact_id,
529 const gchar *service_name,
530 const gchar *message_classes,
534 PangoDirection direction)
538 const gchar *cur = NULL;
542 /* Make some search-and-replace in the html code */
543 string = g_string_sized_new (strlen (html) + strlen (message));
544 g_string_append_printf (string, "%s(\"", func);
546 for (cur = html; *cur != '\0'; cur++)
548 const gchar *replace = NULL;
549 gchar *dup_replace = NULL;
550 gchar *format = NULL;
552 /* Those are all well known keywords that needs replacement in
553 * html files. Please keep them in the same order than the adium
554 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
555 if (theme_adium_match (&cur, "%userIconPath%"))
557 replace = avatar_filename;
559 else if (theme_adium_match (&cur, "%senderScreenName%"))
561 replace = contact_id;
563 else if (theme_adium_match (&cur, "%sender%"))
567 else if (theme_adium_match (&cur, "%senderColor%"))
569 /* A color derived from the user's name.
570 * FIXME: If a colon separated list of HTML colors is at
571 * Incoming/SenderColors.txt it will be used instead of
572 * the default colors.
575 /* Ensure we always use the same color when sending messages
581 else if (contact_id != NULL)
583 guint hash = g_str_hash (contact_id);
584 replace = colors[hash % G_N_ELEMENTS (colors)];
587 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
589 /* FIXME: The path to the status icon of the sender
590 * (available, away, etc...)
593 else if (theme_adium_match (&cur, "%messageDirection%"))
597 case PANGO_DIRECTION_LTR:
598 case PANGO_DIRECTION_TTB_LTR:
599 case PANGO_DIRECTION_WEAK_LTR:
602 case PANGO_DIRECTION_RTL:
603 case PANGO_DIRECTION_TTB_RTL:
604 case PANGO_DIRECTION_WEAK_RTL:
607 case PANGO_DIRECTION_NEUTRAL:
612 else if (theme_adium_match (&cur, "%senderDisplayName%"))
614 /* FIXME: The serverside (remotely set) name of the
615 * sender, such as an MSN display name.
617 * We don't have access to that yet so we use
618 * local alias instead.
622 else if (theme_adium_match (&cur, "%senderPrefix%"))
624 /* FIXME: If we supported IRC user mode flags, this
625 * would be replaced with @ if the user is an op, + if
626 * the user has voice, etc. as per
627 * http://hg.adium.im/adium/rev/b586b027de42. But we
628 * don't, so for now we just strip it. */
630 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
633 /* FIXME: This keyword is used to represent the
634 * highlight background color. "X" is the opacity of the
635 * background, ranges from 0 to 1 and can be any decimal
639 else if (theme_adium_match (&cur, "%message%"))
643 else if (theme_adium_match (&cur, "%time%") ||
644 theme_adium_match_with_format (&cur, "%time{", &format))
646 const gchar *strftime_format;
648 strftime_format = nsdate_to_strftime (self->priv->data, format);
650 dup_replace = empathy_time_to_string_local (timestamp,
651 strftime_format ? strftime_format :
652 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
654 dup_replace = empathy_time_to_string_local (timestamp,
655 strftime_format ? strftime_format :
656 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
658 replace = dup_replace;
660 else if (theme_adium_match (&cur, "%shortTime%"))
662 dup_replace = empathy_time_to_string_local (timestamp,
663 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
664 replace = dup_replace;
666 else if (theme_adium_match (&cur, "%service%"))
668 replace = service_name;
670 else if (theme_adium_match (&cur, "%variant%"))
672 /* FIXME: The name of the active message style variant,
673 * with all spaces replaced with an underscore.
674 * A variant named "Alternating Messages - Blue Red"
675 * will become "Alternating_Messages_-_Blue_Red".
678 else if (theme_adium_match (&cur, "%userIcons%"))
680 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
682 else if (theme_adium_match (&cur, "%messageClasses%"))
684 replace = message_classes;
686 else if (theme_adium_match (&cur, "%status%"))
688 /* FIXME: A description of the status event. This is
689 * neither in the user's local language nor expected to
690 * be displayed; it may be useful to use a different div
691 * class to present different types of status messages.
692 * The following is a list of some of the more important
693 * status messages; your message style should be able to
694 * handle being shown a status message not in this list,
695 * as even at present the list is incomplete and is
696 * certain to become out of date in the future:
705 * contact_joined (group chats)
709 * encryption (all OTR messages use this status)
710 * purple (all IRC topic and join/part messages use this status)
711 * fileTransferStarted
712 * fileTransferCompleted
717 escape_and_append_len (string, cur, 1);
721 /* Here we have a replacement to make */
722 escape_and_append_len (string, replace, -1);
724 g_free (dup_replace);
727 g_string_append (string, "\")");
729 bytes = g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
730 G_RESOURCE_LOOKUP_FLAGS_NONE,
732 js = (const gchar *) g_bytes_get_data (bytes, NULL);
733 g_string_prepend (string, js);
734 g_bytes_unref (bytes);
736 script = g_string_free (string, FALSE);
737 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
742 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
743 const gchar *escaped,
744 PangoDirection direction)
746 theme_adium_add_html (self, "appendMessage",
747 self->priv->data->status_html, escaped, NULL, NULL, NULL,
748 NULL, "event", empathy_time_get_current (), FALSE, FALSE, direction);
750 /* There is no last contact */
751 if (self->priv->last_contact)
753 g_object_unref (self->priv->last_contact);
754 self->priv->last_contact = NULL;
759 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
760 WebKitDOMNodeList *nodes)
764 /* Remove focus and firstFocus class */
765 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
767 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
768 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
770 gchar **classes, **iter;
771 GString *new_class_name;
772 gboolean first = TRUE;
777 class_name = webkit_dom_html_element_get_class_name (element);
778 classes = g_strsplit (class_name, " ", -1);
779 new_class_name = g_string_sized_new (strlen (class_name));
781 for (iter = classes; *iter != NULL; iter++)
783 if (tp_strdiff (*iter, "focus") &&
784 tp_strdiff (*iter, "firstFocus"))
787 g_string_append_c (new_class_name, ' ');
789 g_string_append (new_class_name, *iter);
794 webkit_dom_html_element_set_class_name (element, new_class_name->str);
797 g_strfreev (classes);
798 g_string_free (new_class_name, TRUE);
803 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
805 WebKitDOMDocument *dom;
806 WebKitDOMNodeList *nodes;
807 GError *error = NULL;
809 if (!self->priv->has_unread_message)
812 self->priv->has_unread_message = FALSE;
814 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
818 /* Get all nodes with focus class */
819 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
823 DEBUG ("Error getting focus nodes: %s",
824 error ? error->message : "No error");
825 g_clear_error (&error);
829 theme_adium_remove_focus_marks (self, nodes);
834 ADD_CONSECUTIVE_MSG_SCROLL = 0,
835 ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
837 ADD_MSG_NO_SCROLL = 3
841 * theme_adium_add_message:
842 * @self: The #EmpathyThemeAdium used by the view.
843 * @msg: An #EmpathyMessage that is to be added to the view.
844 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
845 * @prev_timestamp: (out): Timestamp of the previous message.
846 * @prev_is_backlog: (out): Whether the previous message was fetched
848 * @should_highlight: Whether the message should be highlighted. eg.,
849 * if it matches the user's username in multi-user chat.
850 * @js_funcs: An array of JavaScript function names
852 * Shows @msg in the chat view by adding to @self. Addition is defined
853 * by the JavaScript functions listed in @js_funcs. Common examples
854 * are appending new incoming messages or prepending old messages from
857 * @js_funcs should be an array with exactly 4 entries. The entries
858 * should be the names of JavaScript functions that take the raw HTML
859 * that is to be added to the view as an argument and take the following
860 * actions, in this order:
861 * - add a new consecutive message and scroll to it if needed,
862 * - add a new consecutive message and do not scroll,
863 * - add a new non-consecutive message and scroll to it if needed, and
864 * - add a new non-consecutive message and do not scroll
866 * A message is considered to be consecutive with the previous one if
867 * all the following conditions are met:
868 * - senders are the same contact,
869 * - last message was recieved recently,
870 * - last message and this message both are/aren't backlog, and
871 * - DisableCombineConsecutive is not set in theme's settings
874 theme_adium_add_message (EmpathyThemeAdium *self,
876 EmpathyContact **prev_contact,
877 gint64 *prev_timestamp,
878 gboolean *prev_is_backlog,
879 gboolean should_highlight,
880 const gchar *js_funcs[])
882 EmpathyContact *sender;
885 gchar *body_escaped, *name_escaped;
887 const gchar *contact_id;
888 EmpathyAvatar *avatar;
889 const gchar *avatar_filename = NULL;
891 const gchar *html = NULL;
893 const gchar *service_name;
894 GString *message_classes = NULL;
896 gboolean consecutive;
898 PangoDirection direction;
901 /* Get information */
902 sender = empathy_message_get_sender (msg);
903 account = empathy_contact_get_account (sender);
904 service_name = empathy_protocol_name_to_display_name
905 (tp_account_get_protocol_name (account));
906 if (service_name == NULL)
907 service_name = tp_account_get_protocol_name (account);
908 timestamp = empathy_message_get_timestamp (msg);
909 body_escaped = theme_adium_parse_body (self,
910 empathy_message_get_body (msg),
911 empathy_message_get_token (msg));
912 name = empathy_contact_get_logged_alias (sender);
913 contact_id = empathy_contact_get_id (sender);
914 action = (empathy_message_get_tptype (msg) ==
915 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
917 name_escaped = g_markup_escape_text (name, -1);
919 /* If this is a /me probably */
924 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
926 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
927 "<span class='actionMessageBody'>%s</span>",
928 name_escaped, body_escaped);
932 str = g_strdup_printf ("*%s*", body_escaped);
935 g_free (body_escaped);
939 /* Get the avatar filename, or a fallback */
940 avatar = empathy_contact_get_avatar (sender);
942 avatar_filename = avatar->filename;
944 if (!avatar_filename)
946 if (empathy_contact_is_user (sender))
947 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
949 avatar_filename = self->priv->data->default_incoming_avatar_filename;
951 if (!avatar_filename)
953 if (!self->priv->data->default_avatar_filename)
954 self->priv->data->default_avatar_filename =
955 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
956 GTK_ICON_SIZE_DIALOG);
958 avatar_filename = self->priv->data->default_avatar_filename;
962 is_backlog = empathy_message_is_backlog (msg);
963 consecutive = empathy_contact_equal (*prev_contact, sender) &&
964 (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
965 (is_backlog == *prev_is_backlog) &&
966 !tp_asv_get_boolean (self->priv->data->info,
967 "DisableCombineConsecutive", NULL);
969 /* Define message classes */
970 message_classes = g_string_new ("message");
971 if (!self->priv->has_focus && !is_backlog)
973 if (!self->priv->has_unread_message)
975 g_string_append (message_classes, " firstFocus");
976 self->priv->has_unread_message = TRUE;
978 g_string_append (message_classes, " focus");
982 g_string_append (message_classes, " history");
985 g_string_append (message_classes, " consecutive");
987 if (empathy_contact_is_user (sender))
988 g_string_append (message_classes, " outgoing");
990 g_string_append (message_classes, " incoming");
992 if (should_highlight)
993 g_string_append (message_classes, " mention");
995 if (empathy_message_get_tptype (msg) ==
996 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
997 g_string_append (message_classes, " autoreply");
1000 g_string_append (message_classes, " action");
1002 /* FIXME: other classes:
1003 * status - the message is a status change
1004 * event - the message is a notification of something happening
1005 * (for example, encryption being turned on)
1006 * %status% - See %status% in theme_adium_add_html ()
1009 /* This is slightly a hack, but it's the only way to add
1010 * arbitrary data to messages in the HTML. We add another
1011 * class called "x-empathy-message-id-*" to the message. This
1012 * way, we can remove the unread marker for this specific
1014 tp_msg = empathy_message_get_tp_message (msg);
1020 id = tp_message_get_pending_message_id (tp_msg, &valid);
1022 g_string_append_printf (message_classes,
1023 " x-empathy-message-id-%u", id);
1026 /* Define javascript function to use */
1028 func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1029 js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1031 func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1032 js_funcs[ADD_MSG_NO_SCROLL];
1034 if (empathy_contact_is_user (sender))
1039 html = consecutive ? self->priv->data->out_nextcontext_html :
1040 self->priv->data->out_context_html;
1043 html = consecutive ? self->priv->data->out_nextcontent_html :
1044 self->priv->data->out_content_html;
1046 /* remove all the unread marks when we are sending a message */
1047 theme_adium_remove_all_focus_marks (self);
1054 html = consecutive ? self->priv->data->in_nextcontext_html :
1055 self->priv->data->in_context_html;
1058 html = consecutive ? self->priv->data->in_nextcontent_html :
1059 self->priv->data->in_content_html;
1062 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1064 theme_adium_add_html (self, func, html, body_escaped,
1065 avatar_filename, name_escaped, contact_id,
1066 service_name, message_classes->str,
1067 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1069 /* Keep the sender of the last displayed message */
1071 g_object_unref (*prev_contact);
1073 *prev_contact = g_object_ref (sender);
1074 *prev_timestamp = timestamp;
1075 *prev_is_backlog = is_backlog;
1077 g_free (body_escaped);
1078 g_free (name_escaped);
1079 g_string_free (message_classes, TRUE);
1083 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1084 EmpathyMessage *msg,
1085 gboolean should_highlight)
1087 const gchar *js_funcs[] = { "appendNextMessage",
1088 "appendNextMessageNoScroll",
1090 "appendMessageNoScroll" };
1092 if (self->priv->pages_loading != 0)
1094 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1095 should_highlight, FALSE);
1099 theme_adium_add_message (self, msg, &self->priv->last_contact,
1100 &self->priv->last_timestamp, &self->priv->last_is_backlog,
1101 should_highlight, js_funcs);
1105 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1109 PangoDirection direction;
1111 if (self->priv->pages_loading != 0)
1113 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1117 direction = pango_find_base_dir (str, -1);
1118 str_escaped = g_markup_escape_text (str, -1);
1119 theme_adium_append_event_escaped (self, str_escaped, direction);
1120 g_free (str_escaped);
1124 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1125 const gchar *markup_text,
1126 const gchar *fallback_text)
1128 PangoDirection direction;
1130 direction = pango_find_base_dir (fallback_text, -1);
1131 theme_adium_append_event_escaped (self, markup_text, direction);
1135 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1136 EmpathyMessage *msg,
1137 gboolean should_highlight)
1139 const gchar *js_funcs[] = { "prependPrev",
1144 if (self->priv->pages_loading != 0)
1146 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1147 should_highlight, TRUE);
1151 theme_adium_add_message (self, msg, &self->priv->first_contact,
1152 &self->priv->first_timestamp, &self->priv->first_is_backlog,
1153 should_highlight, js_funcs);
1157 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1158 EmpathyMessage *message)
1160 WebKitDOMDocument *doc;
1161 WebKitDOMElement *span;
1162 gchar *id, *parsed_body;
1163 gchar *tooltip, *timestamp;
1164 GtkIconInfo *icon_info;
1165 GError *error = NULL;
1167 if (self->priv->pages_loading != 0)
1169 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1173 id = g_strdup_printf ("message-token-%s",
1174 empathy_message_get_supersedes (message));
1175 /* we don't pass a token here, because doing so will return another
1176 * <span> element, and we don't want nested <span> elements */
1177 parsed_body = theme_adium_parse_body (self,
1178 empathy_message_get_body (message), NULL);
1180 /* find the element */
1181 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1182 span = webkit_dom_document_get_element_by_id (doc, id);
1186 DEBUG ("Failed to find id '%s'", id);
1190 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1192 DEBUG ("Not a HTML element");
1196 /* update the HTML */
1197 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1198 parsed_body, &error);
1202 DEBUG ("Error setting new inner-HTML: %s", error->message);
1203 g_error_free (error);
1208 timestamp = empathy_time_to_string_local (
1209 empathy_message_get_timestamp (message),
1211 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1213 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1219 /* mark this message as edited */
1220 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1221 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1223 if (icon_info != NULL)
1225 /* set the icon as a background image using CSS
1226 * FIXME: the icon won't update in response to theme changes */
1227 gchar *style = g_strdup_printf (
1228 "background-image:url('%s');"
1229 "background-repeat:no-repeat;"
1230 "background-position:left center;"
1231 "padding-left:19px;", /* 16px icon + 3px padding */
1232 gtk_icon_info_get_filename (icon_info));
1234 webkit_dom_element_set_attribute (span, "style", style, &error);
1238 DEBUG ("Error setting element style: %s",
1240 g_clear_error (&error);
1245 gtk_icon_info_free (icon_info);
1251 DEBUG ("Could not find message to edit with: %s",
1252 empathy_message_get_body (message));
1256 g_free (parsed_body);
1260 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1261 gboolean allow_scrolling)
1263 self->priv->allow_scrolling = allow_scrolling;
1265 if (allow_scrolling)
1266 empathy_theme_adium_scroll_down (self);
1270 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1272 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1276 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1278 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1282 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1284 theme_adium_load_template (self);
1286 /* Clear last contact to avoid trying to add a 'joined'
1287 * message when we don't have an insertion point. */
1288 if (self->priv->last_contact)
1290 g_object_unref (self->priv->last_contact);
1291 self->priv->last_contact = NULL;
1296 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1297 const gchar *search_criteria,
1298 gboolean new_search,
1299 gboolean match_case)
1301 /* FIXME: Doesn't respect new_search */
1302 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1303 search_criteria, match_case, FALSE, TRUE);
1307 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1308 const gchar *search_criteria,
1309 gboolean new_search,
1310 gboolean match_case)
1312 /* FIXME: Doesn't respect new_search */
1313 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1314 search_criteria, match_case, TRUE, TRUE);
1318 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1319 const gchar *search_criteria,
1320 gboolean match_case,
1321 gboolean *can_do_previous,
1322 gboolean *can_do_next)
1324 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1325 * find_next and find_previous to work around this problem. */
1326 if (can_do_previous)
1327 *can_do_previous = TRUE;
1329 *can_do_next = TRUE;
1333 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1335 gboolean match_case)
1337 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1338 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1339 text, match_case, 0);
1340 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1345 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1347 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1351 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1354 WebKitDOMDocument *dom;
1355 WebKitDOMNodeList *nodes;
1357 GError *error = NULL;
1359 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1363 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1365 /* Get all nodes with focus class */
1366 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1371 DEBUG ("Error getting focus nodes: %s",
1372 error ? error->message : "No error");
1373 g_clear_error (&error);
1377 theme_adium_remove_focus_marks (self, nodes);
1381 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1384 EmpathyThemeAdium *self = user_data;
1385 guint32 id = GPOINTER_TO_UINT (data);
1387 theme_adium_remove_mark_from_message (self, id);
1391 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1394 self->priv->has_focus = has_focus;
1395 if (!self->priv->has_focus)
1397 /* We've lost focus, so let's make sure all the acked
1398 * messages have lost their unread marker. */
1399 g_queue_foreach (&self->priv->acked_messages,
1400 theme_adium_remove_acked_message_unread_mark_foreach, self);
1401 g_queue_clear (&self->priv->acked_messages);
1403 self->priv->has_unread_message = FALSE;
1408 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1409 EmpathyMessage *message)
1415 tp_msg = empathy_message_get_tp_message (message);
1420 id = tp_message_get_pending_message_id (tp_msg, &valid);
1423 g_warning ("Acknoledged message doesn't have a pending ID");
1427 /* We only want to actually remove the unread marker if the
1428 * view doesn't have focus. If we did it all the time we would
1429 * never see the unread markers, ever! So, we'll queue these
1430 * up, and when we lose focus, we'll remove the markers. */
1431 if (self->priv->has_focus)
1433 g_queue_push_tail (&self->priv->acked_messages,
1434 GUINT_TO_POINTER (id));
1438 theme_adium_remove_mark_from_message (self, id);
1442 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1443 GtkWidget *default_menu,
1444 WebKitHitTestResult *hit_test_result,
1445 gboolean triggered_with_keyboard,
1449 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1451 if (g_settings_get_boolean (self->priv->gsettings_chat,
1452 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1453 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1455 menu = empathy_webkit_create_context_menu (
1456 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1458 gtk_widget_show_all (menu);
1460 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1461 gtk_get_current_event_time ());
1467 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1468 gboolean show_avatars)
1470 self->priv->show_avatars = show_avatars;
1474 theme_adium_load_finished_cb (WebKitWebView *view,
1475 WebKitWebFrame *frame,
1478 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1481 DEBUG ("Page loaded");
1482 self->priv->pages_loading--;
1484 if (self->priv->pages_loading != 0)
1487 /* Display queued messages */
1488 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1490 QueuedItem *item = l->data;
1494 case QUEUED_MESSAGE:
1495 empathy_theme_adium_append_message (self, item->msg,
1496 item->should_highlight);
1500 empathy_theme_adium_edit_message (self, item->msg);
1504 empathy_theme_adium_append_event (self, item->str);
1508 free_queued_item (item);
1511 g_queue_clear (&self->priv->message_queue);
1515 theme_adium_finalize (GObject *object)
1517 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1519 empathy_adium_data_unref (self->priv->data);
1521 g_object_unref (self->priv->gsettings_chat);
1522 g_object_unref (self->priv->gsettings_desktop);
1524 g_free (self->priv->variant);
1526 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1530 theme_adium_dispose (GObject *object)
1532 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1534 if (self->priv->smiley_manager)
1536 g_object_unref (self->priv->smiley_manager);
1537 self->priv->smiley_manager = NULL;
1540 g_clear_object (&self->priv->first_contact);
1542 if (self->priv->last_contact)
1544 g_object_unref (self->priv->last_contact);
1545 self->priv->last_contact = NULL;
1548 if (self->priv->inspector_window)
1550 gtk_widget_destroy (self->priv->inspector_window);
1551 self->priv->inspector_window = NULL;
1554 if (self->priv->acked_messages.length > 0)
1556 g_queue_clear (&self->priv->acked_messages);
1559 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1563 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1564 EmpathyThemeAdium *self)
1566 if (self->priv->inspector_window)
1568 gtk_widget_show_all (self->priv->inspector_window);
1575 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1576 EmpathyThemeAdium *self)
1578 if (self->priv->inspector_window)
1580 gtk_widget_hide (self->priv->inspector_window);
1586 static WebKitWebView *
1587 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1588 WebKitWebView *web_view,
1589 EmpathyThemeAdium *self)
1591 GtkWidget *scrolled_window;
1592 GtkWidget *inspector_web_view;
1594 if (!self->priv->inspector_window)
1596 /* Create main window */
1597 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1599 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1602 g_signal_connect (self->priv->inspector_window, "delete-event",
1603 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1605 /* Pack a scrolled window */
1606 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1608 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1609 GTK_POLICY_AUTOMATIC,
1610 GTK_POLICY_AUTOMATIC);
1611 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1613 gtk_widget_show (scrolled_window);
1615 /* Pack a webview in the scrolled window. That webview will be
1616 * used to render the inspector tool. */
1617 inspector_web_view = webkit_web_view_new ();
1618 gtk_container_add (GTK_CONTAINER (scrolled_window),
1619 inspector_web_view);
1620 gtk_widget_show (scrolled_window);
1622 return WEBKIT_WEB_VIEW (inspector_web_view);
1629 theme_adium_constructed (GObject *object)
1631 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1632 const gchar *font_family = NULL;
1634 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1635 WebKitWebInspector *webkit_inspector;
1637 /* Set default settings */
1638 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1639 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1641 if (font_family && font_size)
1643 g_object_set (webkit_web_view_get_settings (webkit_view),
1644 "default-font-family", font_family,
1645 "default-font-size", font_size,
1650 empathy_webkit_bind_font_setting (webkit_view,
1651 self->priv->gsettings_desktop,
1652 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1655 /* Setup webkit inspector */
1656 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1657 g_signal_connect (webkit_inspector, "inspect-web-view",
1658 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1659 g_signal_connect (webkit_inspector, "show-window",
1660 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1661 g_signal_connect (webkit_inspector, "close-window",
1662 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1665 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1667 self->priv->in_construction = FALSE;
1671 theme_adium_get_property (GObject *object,
1676 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1680 case PROP_ADIUM_DATA:
1681 g_value_set_boxed (value, self->priv->data);
1684 g_value_set_string (value, self->priv->variant);
1687 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1693 theme_adium_set_property (GObject *object,
1695 const GValue *value,
1698 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1702 case PROP_ADIUM_DATA:
1703 g_assert (self->priv->data == NULL);
1704 self->priv->data = g_value_dup_boxed (value);
1707 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1710 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1716 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1718 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1720 object_class->finalize = theme_adium_finalize;
1721 object_class->dispose = theme_adium_dispose;
1722 object_class->constructed = theme_adium_constructed;
1723 object_class->get_property = theme_adium_get_property;
1724 object_class->set_property = theme_adium_set_property;
1726 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1727 g_param_spec_boxed ("adium-data",
1729 "Data for the adium theme",
1730 EMPATHY_TYPE_ADIUM_DATA,
1731 G_PARAM_CONSTRUCT_ONLY |
1733 G_PARAM_STATIC_STRINGS));
1735 g_object_class_install_property (object_class, PROP_VARIANT,
1736 g_param_spec_string ("variant",
1737 "The theme variant",
1738 "Variant name for the theme",
1742 G_PARAM_STATIC_STRINGS));
1744 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1748 empathy_theme_adium_init (EmpathyThemeAdium *self)
1750 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1751 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1753 self->priv->in_construction = TRUE;
1754 g_queue_init (&self->priv->message_queue);
1755 self->priv->allow_scrolling = TRUE;
1756 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1758 /* Show avatars by default. */
1759 self->priv->show_avatars = TRUE;
1761 g_signal_connect (self, "load-finished",
1762 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1763 g_signal_connect (self, "navigation-policy-decision-requested",
1764 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1765 g_signal_connect (self, "context-menu",
1766 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1768 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1769 self->priv->gsettings_desktop = g_settings_new (
1770 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1774 empathy_theme_adium_new (EmpathyAdiumData *data,
1775 const gchar *variant)
1777 g_return_val_if_fail (data != NULL, NULL);
1779 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1786 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1787 const gchar *variant)
1789 gchar *variant_path;
1792 if (!tp_strdiff (self->priv->variant, variant))
1795 g_free (self->priv->variant);
1796 self->priv->variant = g_strdup (variant);
1798 if (self->priv->in_construction)
1801 DEBUG ("Update view with variant: '%s'", variant);
1802 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1803 self->priv->variant);
1804 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1807 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1809 g_free (variant_path);
1812 g_object_notify (G_OBJECT (self), "variant");
1816 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1818 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1820 empathy_webkit_show_inspector (web_view);
1824 empathy_adium_path_is_valid (const gchar *path)
1834 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1835 tmp = g_strsplit (path, "/", 0);
1839 dir = tmp[g_strv_length (tmp) - 1];
1841 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1849 /* The theme is not valid if there is no Info.plist */
1850 file = g_build_filename (path, "Contents", "Info.plist",
1852 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1858 /* We ship a default Template.html as fallback if there is any problem
1859 * with the one inside the theme. The only other required file is
1860 * Content.html OR Incoming/Content.html*/
1861 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1863 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1868 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1869 "Content.html", NULL);
1870 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1878 empathy_adium_info_new (const gchar *path)
1882 GHashTable *info = NULL;
1884 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1886 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1887 value = empathy_plist_parse_from_file (file);
1893 info = g_value_dup_boxed (value);
1894 tp_g_value_slice_free (value);
1896 /* Insert the theme's path into the hash table,
1897 * keys have to be dupped */
1898 tp_asv_set_string (info, g_strdup ("path"), path);
1904 adium_info_get_version (GHashTable *info)
1906 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1909 static const gchar *
1910 adium_info_get_no_variant_name (GHashTable *info)
1912 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1913 return name ? name : _("Normal");
1917 adium_info_dup_path_for_variant (GHashTable *info,
1918 const gchar *variant)
1920 guint version = adium_info_get_version (info);
1921 const gchar *no_variant = adium_info_get_no_variant_name (info);
1922 GPtrArray *variants;
1925 if (version <= 2 && !tp_strdiff (variant, no_variant))
1926 return g_strdup ("main.css");
1928 variants = empathy_adium_info_get_available_variants (info);
1929 if (variants->len == 0)
1930 return g_strdup ("main.css");
1932 /* Verify the variant exists, fallback to the first one */
1933 for (i = 0; i < variants->len; i++)
1935 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1939 if (i == variants->len)
1941 DEBUG ("Variant %s does not exist", variant);
1942 variant = g_ptr_array_index (variants, 0);
1945 return g_strdup_printf ("Variants/%s.css", variant);
1950 empathy_adium_info_get_default_variant (GHashTable *info)
1952 if (adium_info_get_version (info) <= 2)
1953 return adium_info_get_no_variant_name (info);
1955 return tp_asv_get_string (info, "DefaultVariant");
1959 empathy_adium_info_get_available_variants (GHashTable *info)
1961 GPtrArray *variants;
1966 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1967 if (variants != NULL)
1970 variants = g_ptr_array_new_with_free_func (g_free);
1971 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1972 G_TYPE_PTR_ARRAY, variants);
1974 path = tp_asv_get_string (info, "path");
1975 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1976 dir = g_dir_open (dirpath, 0, NULL);
1981 for (name = g_dir_read_name (dir);
1983 name = g_dir_read_name (dir))
1985 gchar *display_name;
1987 if (!g_str_has_suffix (name, ".css"))
1990 display_name = g_strdup (name);
1991 strstr (display_name, ".css")[0] = '\0';
1992 g_ptr_array_add (variants, display_name);
1999 if (adium_info_get_version (info) <= 2)
2000 g_ptr_array_add (variants,
2001 g_strdup (adium_info_get_no_variant_name (info)));
2007 empathy_adium_data_get_type (void)
2009 static GType type_id = 0;
2013 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
2014 (GBoxedCopyFunc) empathy_adium_data_ref,
2015 (GBoxedFreeFunc) empathy_adium_data_unref);
2022 empathy_adium_data_new_with_info (const gchar *path,
2025 EmpathyAdiumData *data;
2026 gchar *template_html = NULL;
2027 gchar *footer_html = NULL;
2030 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
2032 data = g_slice_new0 (EmpathyAdiumData);
2033 data->ref_count = 1;
2034 data->path = g_strdup (path);
2035 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
2036 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
2037 data->info = g_hash_table_ref (info);
2038 data->version = adium_info_get_version (info);
2039 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
2040 data->date_format_cache = g_hash_table_new_full (g_str_hash,
2041 g_str_equal, g_free, g_free);
2043 DEBUG ("Loading theme at %s", path);
2045 #define LOAD(path, var) \
2046 tmp = g_build_filename (data->basedir, path, NULL); \
2047 g_file_get_contents (tmp, &var, NULL, NULL); \
2050 #define LOAD_CONST(path, var) \
2053 LOAD (path, content); \
2054 if (content != NULL) { \
2055 g_ptr_array_add (data->strings_to_free, content); \
2060 /* Load html files */
2061 LOAD_CONST ("Content.html", data->content_html);
2062 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2063 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2064 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2065 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2066 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2067 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2068 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2069 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2070 LOAD_CONST ("Status.html", data->status_html);
2071 LOAD ("Template.html", template_html);
2072 LOAD ("Footer.html", footer_html);
2077 /* HTML fallbacks: If we have at least content OR in_content, then
2078 * everything else gets a fallback */
2080 #define FALLBACK(html, fallback) \
2081 if (html == NULL) { \
2085 /* in_nextcontent -> in_content -> content */
2086 FALLBACK (data->in_content_html, data->content_html);
2087 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2089 /* context -> content */
2090 FALLBACK (data->in_context_html, data->in_content_html);
2091 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2092 FALLBACK (data->out_context_html, data->out_content_html);
2093 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2096 FALLBACK (data->out_content_html, data->in_content_html);
2097 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2098 FALLBACK (data->out_context_html, data->in_context_html);
2099 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2101 /* status -> in_content */
2102 FALLBACK (data->status_html, data->in_content_html);
2106 /* template -> empathy's template */
2107 data->custom_template = (template_html != NULL);
2108 if (template_html == NULL)
2110 GError *error = NULL;
2112 tmp = empathy_file_lookup ("Template.html", "data");
2114 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2115 g_warning ("couldn't load Empathy's default theme "
2116 "template: %s", error->message);
2117 g_return_val_if_reached (data);
2123 /* Default avatar */
2124 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2125 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2127 data->default_incoming_avatar_filename = tmp;
2134 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2135 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2137 data->default_outgoing_avatar_filename = tmp;
2144 /* Old custom templates had only 4 parameters.
2145 * New templates have 5 parameters */
2146 if (data->version <= 2 && data->custom_template)
2148 tmp = string_with_format (template_html,
2150 "%@", /* Leave variant unset */
2151 "", /* The header */
2152 footer_html ? footer_html : "",
2157 tmp = string_with_format (template_html,
2159 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2160 "%@", /* Leave variant unset */
2161 "", /* The header */
2162 footer_html ? footer_html : "",
2165 g_ptr_array_add (data->strings_to_free, tmp);
2166 data->template_html = tmp;
2168 g_free (template_html);
2169 g_free (footer_html);
2175 empathy_adium_data_new (const gchar *path)
2177 EmpathyAdiumData *data;
2180 info = empathy_adium_info_new (path);
2181 data = empathy_adium_data_new_with_info (path, info);
2182 g_hash_table_unref (info);
2188 empathy_adium_data_ref (EmpathyAdiumData *data)
2190 g_return_val_if_fail (data != NULL, NULL);
2192 g_atomic_int_inc (&data->ref_count);
2198 empathy_adium_data_unref (EmpathyAdiumData *data)
2200 g_return_if_fail (data != NULL);
2202 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2203 g_free (data->path);
2204 g_free (data->basedir);
2205 g_free (data->default_avatar_filename);
2206 g_free (data->default_incoming_avatar_filename);
2207 g_free (data->default_outgoing_avatar_filename);
2208 g_hash_table_unref (data->info);
2209 g_ptr_array_unref (data->strings_to_free);
2210 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2212 g_slice_free (EmpathyAdiumData, data);
2217 empathy_adium_data_get_info (EmpathyAdiumData *data)
2219 g_return_val_if_fail (data != NULL, NULL);
2225 empathy_adium_data_get_path (EmpathyAdiumData *data)
2227 g_return_val_if_fail (data != NULL, NULL);