2 * Copyright (C) 2008-2012 Collabora Ltd.
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 * Authors: Xavier Claessens <xclaesse@gmail.com>
24 #include <glib/gi18n-lib.h>
26 #include <webkit/webkit.h>
27 #include <telepathy-glib/telepathy-glib.h>
29 #include <pango/pango.h>
32 #include <libempathy/empathy-gsettings.h>
33 #include <libempathy/empathy-time.h>
34 #include <libempathy/empathy-utils.h>
36 #include "empathy-theme-adium.h"
37 #include "empathy-smiley-manager.h"
38 #include "empathy-ui-utils.h"
39 #include "empathy-plist.h"
40 #include "empathy-images.h"
41 #include "empathy-webkit-utils.h"
43 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
44 #include <libempathy/empathy-debug.h>
46 #define BORING_DPI_DEFAULT 96
48 /* "Join" consecutive messages with timestamps within five minutes */
49 #define MESSAGE_JOIN_PERIOD 5*60
51 struct _EmpathyThemeAdiumPriv
53 EmpathyAdiumData *data;
54 EmpathySmileyManager *smiley_manager;
55 EmpathyContact *last_contact;
56 gint64 last_timestamp;
57 gboolean last_is_backlog;
59 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
61 /* Queue of guint32 of pending message id to remove unread
62 * marker for when we lose focus. */
63 GQueue acked_messages;
64 GtkWidget *inspector_window;
66 GSettings *gsettings_chat;
67 GSettings *gsettings_desktop;
70 gboolean has_unread_message;
71 gboolean allow_scrolling;
73 gboolean in_construction;
74 gboolean show_avatars;
77 struct _EmpathyAdiumData
82 gchar *default_avatar_filename;
83 gchar *default_incoming_avatar_filename;
84 gchar *default_outgoing_avatar_filename;
87 gboolean custom_template;
88 /* gchar* -> gchar* both owned */
89 GHashTable *date_format_cache;
92 const gchar *template_html;
93 const gchar *content_html;
94 const gchar *in_content_html;
95 const gchar *in_context_html;
96 const gchar *in_nextcontent_html;
97 const gchar *in_nextcontext_html;
98 const gchar *out_content_html;
99 const gchar *out_context_html;
100 const gchar *out_nextcontent_html;
101 const gchar *out_nextcontext_html;
102 const gchar *status_html;
104 /* Above html strings are pointers to strings stored in this array.
105 * We do this because of fallbacks, some htmls could be pointing the
107 GPtrArray *strings_to_free;
110 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
111 const gchar *variant);
120 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
121 WEBKIT_TYPE_WEB_VIEW)
135 gboolean should_highlight;
139 queue_item (GQueue *queue,
143 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;
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);
523 theme_adium_append_html (EmpathyThemeAdium *self,
526 const gchar *message,
527 const gchar *avatar_filename,
529 const gchar *contact_id,
530 const gchar *service_name,
531 const gchar *message_classes,
535 PangoDirection direction)
538 const gchar *cur = NULL;
541 /* Make some search-and-replace in the html code */
542 string = g_string_sized_new (strlen (html) + strlen (message));
543 g_string_append_printf (string, "%s(\"", func);
545 for (cur = html; *cur != '\0'; cur++)
547 const gchar *replace = NULL;
548 gchar *dup_replace = NULL;
549 gchar *format = NULL;
551 /* Those are all well known keywords that needs replacement in
552 * html files. Please keep them in the same order than the adium
553 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
554 if (theme_adium_match (&cur, "%userIconPath%"))
556 replace = avatar_filename;
558 else if (theme_adium_match (&cur, "%senderScreenName%"))
560 replace = contact_id;
562 else if (theme_adium_match (&cur, "%sender%"))
566 else if (theme_adium_match (&cur, "%senderColor%"))
568 /* A color derived from the user's name.
569 * FIXME: If a colon separated list of HTML colors is at
570 * Incoming/SenderColors.txt it will be used instead of
571 * the default colors.
574 /* Ensure we always use the same color when sending messages
580 else if (contact_id != NULL)
582 guint hash = g_str_hash (contact_id);
583 replace = colors[hash % G_N_ELEMENTS (colors)];
586 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
588 /* FIXME: The path to the status icon of the sender
589 * (available, away, etc...)
592 else if (theme_adium_match (&cur, "%messageDirection%"))
596 case PANGO_DIRECTION_LTR:
597 case PANGO_DIRECTION_TTB_LTR:
598 case PANGO_DIRECTION_WEAK_LTR:
601 case PANGO_DIRECTION_RTL:
602 case PANGO_DIRECTION_TTB_RTL:
603 case PANGO_DIRECTION_WEAK_RTL:
606 case PANGO_DIRECTION_NEUTRAL:
611 else if (theme_adium_match (&cur, "%senderDisplayName%"))
613 /* FIXME: The serverside (remotely set) name of the
614 * sender, such as an MSN display name.
616 * We don't have access to that yet so we use
617 * local alias instead.
621 else if (theme_adium_match (&cur, "%senderPrefix%"))
623 /* FIXME: If we supported IRC user mode flags, this
624 * would be replaced with @ if the user is an op, + if
625 * the user has voice, etc. as per
626 * http://hg.adium.im/adium/rev/b586b027de42. But we
627 * don't, so for now we just strip it. */
629 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
632 /* FIXME: This keyword is used to represent the
633 * highlight background color. "X" is the opacity of the
634 * background, ranges from 0 to 1 and can be any decimal
638 else if (theme_adium_match (&cur, "%message%"))
642 else if (theme_adium_match (&cur, "%time%") ||
643 theme_adium_match_with_format (&cur, "%time{", &format))
645 const gchar *strftime_format;
647 strftime_format = nsdate_to_strftime (self->priv->data, format);
649 dup_replace = empathy_time_to_string_local (timestamp,
650 strftime_format ? strftime_format :
651 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
653 dup_replace = empathy_time_to_string_local (timestamp,
654 strftime_format ? strftime_format :
655 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
657 replace = dup_replace;
659 else if (theme_adium_match (&cur, "%shortTime%"))
661 dup_replace = empathy_time_to_string_local (timestamp,
662 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
663 replace = dup_replace;
665 else if (theme_adium_match (&cur, "%service%"))
667 replace = service_name;
669 else if (theme_adium_match (&cur, "%variant%"))
671 /* FIXME: The name of the active message style variant,
672 * with all spaces replaced with an underscore.
673 * A variant named "Alternating Messages - Blue Red"
674 * will become "Alternating_Messages_-_Blue_Red".
677 else if (theme_adium_match (&cur, "%userIcons%"))
679 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
681 else if (theme_adium_match (&cur, "%messageClasses%"))
683 replace = message_classes;
685 else if (theme_adium_match (&cur, "%status%"))
687 /* FIXME: A description of the status event. This is
688 * neither in the user's local language nor expected to
689 * be displayed; it may be useful to use a different div
690 * class to present different types of status messages.
691 * The following is a list of some of the more important
692 * status messages; your message style should be able to
693 * handle being shown a status message not in this list,
694 * as even at present the list is incomplete and is
695 * certain to become out of date in the future:
704 * contact_joined (group chats)
708 * encryption (all OTR messages use this status)
709 * purple (all IRC topic and join/part messages use this status)
710 * fileTransferStarted
711 * fileTransferCompleted
716 escape_and_append_len (string, cur, 1);
720 /* Here we have a replacement to make */
721 escape_and_append_len (string, replace, -1);
723 g_free (dup_replace);
726 g_string_append (string, "\")");
728 script = g_string_free (string, FALSE);
729 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
734 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
735 const gchar *escaped,
736 PangoDirection direction)
738 theme_adium_append_html (self, "appendMessage",
739 self->priv->data->status_html, escaped, NULL, NULL, NULL,
740 NULL, "event", empathy_time_get_current (), FALSE, FALSE, direction);
742 /* There is no last contact */
743 if (self->priv->last_contact)
745 g_object_unref (self->priv->last_contact);
746 self->priv->last_contact = NULL;
751 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
752 WebKitDOMNodeList *nodes)
756 /* Remove focus and firstFocus class */
757 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
759 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
760 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
762 gchar **classes, **iter;
763 GString *new_class_name;
764 gboolean first = TRUE;
769 class_name = webkit_dom_html_element_get_class_name (element);
770 classes = g_strsplit (class_name, " ", -1);
771 new_class_name = g_string_sized_new (strlen (class_name));
773 for (iter = classes; *iter != NULL; iter++)
775 if (tp_strdiff (*iter, "focus") &&
776 tp_strdiff (*iter, "firstFocus"))
779 g_string_append_c (new_class_name, ' ');
781 g_string_append (new_class_name, *iter);
786 webkit_dom_html_element_set_class_name (element, new_class_name->str);
789 g_strfreev (classes);
790 g_string_free (new_class_name, TRUE);
795 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
797 WebKitDOMDocument *dom;
798 WebKitDOMNodeList *nodes;
799 GError *error = NULL;
801 if (!self->priv->has_unread_message)
804 self->priv->has_unread_message = FALSE;
806 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
810 /* Get all nodes with focus class */
811 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
815 DEBUG ("Error getting focus nodes: %s",
816 error ? error->message : "No error");
817 g_clear_error (&error);
821 theme_adium_remove_focus_marks (self, nodes);
825 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
827 gboolean should_highlight)
829 EmpathyContact *sender;
832 gchar *body_escaped, *name_escaped;
834 const gchar *contact_id;
835 EmpathyAvatar *avatar;
836 const gchar *avatar_filename = NULL;
838 const gchar *html = NULL;
840 const gchar *service_name;
841 GString *message_classes = NULL;
843 gboolean consecutive;
845 PangoDirection direction;
847 if (self->priv->pages_loading != 0)
849 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
854 /* Get information */
855 sender = empathy_message_get_sender (msg);
856 account = empathy_contact_get_account (sender);
857 service_name = empathy_protocol_name_to_display_name
858 (tp_account_get_protocol_name (account));
859 if (service_name == NULL)
860 service_name = tp_account_get_protocol_name (account);
861 timestamp = empathy_message_get_timestamp (msg);
862 body_escaped = theme_adium_parse_body (self,
863 empathy_message_get_body (msg),
864 empathy_message_get_token (msg));
865 name = empathy_contact_get_logged_alias (sender);
866 contact_id = empathy_contact_get_id (sender);
867 action = (empathy_message_get_tptype (msg) ==
868 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
870 name_escaped = g_markup_escape_text (name, -1);
872 /* If this is a /me probably */
877 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
879 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
880 "<span class='actionMessageBody'>%s</span>",
881 name_escaped, body_escaped);
885 str = g_strdup_printf ("*%s*", body_escaped);
888 g_free (body_escaped);
892 /* Get the avatar filename, or a fallback */
893 avatar = empathy_contact_get_avatar (sender);
895 avatar_filename = avatar->filename;
897 if (!avatar_filename)
899 if (empathy_contact_is_user (sender))
900 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
902 avatar_filename = self->priv->data->default_incoming_avatar_filename;
904 if (!avatar_filename)
906 if (!self->priv->data->default_avatar_filename)
907 self->priv->data->default_avatar_filename =
908 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
909 GTK_ICON_SIZE_DIALOG);
911 avatar_filename = self->priv->data->default_avatar_filename;
915 /* We want to join this message with the last one if
916 * - senders are the same contact,
917 * - last message was recieved recently,
918 * - last message and this message both are/aren't backlog, and
919 * - DisableCombineConsecutive is not set in theme's settings */
920 is_backlog = empathy_message_is_backlog (msg);
921 consecutive = empathy_contact_equal (self->priv->last_contact, sender) &&
922 (timestamp - self->priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
923 (is_backlog == self->priv->last_is_backlog) &&
924 !tp_asv_get_boolean (self->priv->data->info,
925 "DisableCombineConsecutive", NULL);
927 /* Define message classes */
928 message_classes = g_string_new ("message");
929 if (!self->priv->has_focus && !is_backlog)
931 if (!self->priv->has_unread_message)
933 g_string_append (message_classes, " firstFocus");
934 self->priv->has_unread_message = TRUE;
936 g_string_append (message_classes, " focus");
940 g_string_append (message_classes, " history");
943 g_string_append (message_classes, " consecutive");
945 if (empathy_contact_is_user (sender))
946 g_string_append (message_classes, " outgoing");
948 g_string_append (message_classes, " incoming");
950 if (should_highlight)
951 g_string_append (message_classes, " mention");
953 if (empathy_message_get_tptype (msg) ==
954 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
955 g_string_append (message_classes, " autoreply");
958 g_string_append (message_classes, " action");
960 /* FIXME: other classes:
961 * status - the message is a status change
962 * event - the message is a notification of something happening
963 * (for example, encryption being turned on)
964 * %status% - See %status% in theme_adium_append_html ()
967 /* This is slightly a hack, but it's the only way to add
968 * arbitrary data to messages in the HTML. We add another
969 * class called "x-empathy-message-id-*" to the message. This
970 * way, we can remove the unread marker for this specific
972 tp_msg = empathy_message_get_tp_message (msg);
978 id = tp_message_get_pending_message_id (tp_msg, &valid);
980 g_string_append_printf (message_classes,
981 " x-empathy-message-id-%u", id);
984 /* Define javascript function to use */
986 func = self->priv->allow_scrolling ? "appendNextMessage" :
987 "appendNextMessageNoScroll";
989 func = self->priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
991 if (empathy_contact_is_user (sender))
996 html = consecutive ? self->priv->data->out_nextcontext_html :
997 self->priv->data->out_context_html;
1000 html = consecutive ? self->priv->data->out_nextcontent_html :
1001 self->priv->data->out_content_html;
1003 /* remove all the unread marks when we are sending a message */
1004 theme_adium_remove_all_focus_marks (self);
1011 html = consecutive ? self->priv->data->in_nextcontext_html :
1012 self->priv->data->in_context_html;
1015 html = consecutive ? self->priv->data->in_nextcontent_html :
1016 self->priv->data->in_content_html;
1019 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1021 theme_adium_append_html (self, func, html, body_escaped,
1022 avatar_filename, name_escaped, contact_id,
1023 service_name, message_classes->str,
1024 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1026 /* Keep the sender of the last displayed message */
1027 if (self->priv->last_contact)
1028 g_object_unref (self->priv->last_contact);
1030 self->priv->last_contact = g_object_ref (sender);
1031 self->priv->last_timestamp = timestamp;
1032 self->priv->last_is_backlog = is_backlog;
1034 g_free (body_escaped);
1035 g_free (name_escaped);
1036 g_string_free (message_classes, TRUE);
1040 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1044 PangoDirection direction;
1046 if (self->priv->pages_loading != 0)
1048 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
1052 direction = pango_find_base_dir (str, -1);
1053 str_escaped = g_markup_escape_text (str, -1);
1054 theme_adium_append_event_escaped (self, str_escaped, direction);
1055 g_free (str_escaped);
1059 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1060 const gchar *markup_text,
1061 const gchar *fallback_text)
1063 PangoDirection direction;
1065 direction = pango_find_base_dir (fallback_text, -1);
1066 theme_adium_append_event_escaped (self, markup_text, direction);
1070 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1071 EmpathyMessage *message)
1073 WebKitDOMDocument *doc;
1074 WebKitDOMElement *span;
1075 gchar *id, *parsed_body;
1076 gchar *tooltip, *timestamp;
1077 GtkIconInfo *icon_info;
1078 GError *error = NULL;
1080 if (self->priv->pages_loading != 0)
1082 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1086 id = g_strdup_printf ("message-token-%s",
1087 empathy_message_get_supersedes (message));
1088 /* we don't pass a token here, because doing so will return another
1089 * <span> element, and we don't want nested <span> elements */
1090 parsed_body = theme_adium_parse_body (self,
1091 empathy_message_get_body (message), NULL);
1093 /* find the element */
1094 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1095 span = webkit_dom_document_get_element_by_id (doc, id);
1099 DEBUG ("Failed to find id '%s'", id);
1103 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1105 DEBUG ("Not a HTML element");
1109 /* update the HTML */
1110 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1111 parsed_body, &error);
1115 DEBUG ("Error setting new inner-HTML: %s", error->message);
1116 g_error_free (error);
1121 timestamp = empathy_time_to_string_local (
1122 empathy_message_get_timestamp (message),
1124 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1126 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1132 /* mark this message as edited */
1133 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1134 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1136 if (icon_info != NULL)
1138 /* set the icon as a background image using CSS
1139 * FIXME: the icon won't update in response to theme changes */
1140 gchar *style = g_strdup_printf (
1141 "background-image:url('%s');"
1142 "background-repeat:no-repeat;"
1143 "background-position:left center;"
1144 "padding-left:19px;", /* 16px icon + 3px padding */
1145 gtk_icon_info_get_filename (icon_info));
1147 webkit_dom_element_set_attribute (span, "style", style, &error);
1151 DEBUG ("Error setting element style: %s",
1153 g_clear_error (&error);
1158 gtk_icon_info_free (icon_info);
1164 DEBUG ("Could not find message to edit with: %s",
1165 empathy_message_get_body (message));
1169 g_free (parsed_body);
1173 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1174 gboolean allow_scrolling)
1176 self->priv->allow_scrolling = allow_scrolling;
1178 if (allow_scrolling)
1179 empathy_theme_adium_scroll_down (self);
1183 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1185 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1189 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1191 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1195 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1197 theme_adium_load_template (self);
1199 /* Clear last contact to avoid trying to add a 'joined'
1200 * message when we don't have an insertion point. */
1201 if (self->priv->last_contact)
1203 g_object_unref (self->priv->last_contact);
1204 self->priv->last_contact = NULL;
1209 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1210 const gchar *search_criteria,
1211 gboolean new_search,
1212 gboolean match_case)
1214 /* FIXME: Doesn't respect new_search */
1215 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1216 search_criteria, match_case, FALSE, TRUE);
1220 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1221 const gchar *search_criteria,
1222 gboolean new_search,
1223 gboolean match_case)
1225 /* FIXME: Doesn't respect new_search */
1226 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1227 search_criteria, match_case, TRUE, TRUE);
1231 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1232 const gchar *search_criteria,
1233 gboolean match_case,
1234 gboolean *can_do_previous,
1235 gboolean *can_do_next)
1237 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1238 * find_next and find_previous to work around this problem. */
1239 if (can_do_previous)
1240 *can_do_previous = TRUE;
1242 *can_do_next = TRUE;
1246 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1248 gboolean match_case)
1250 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1251 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1252 text, match_case, 0);
1253 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1258 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1260 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1264 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1267 WebKitDOMDocument *dom;
1268 WebKitDOMNodeList *nodes;
1270 GError *error = NULL;
1272 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1276 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1278 /* Get all nodes with focus class */
1279 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1284 DEBUG ("Error getting focus nodes: %s",
1285 error ? error->message : "No error");
1286 g_clear_error (&error);
1290 theme_adium_remove_focus_marks (self, nodes);
1294 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1297 EmpathyThemeAdium *self = user_data;
1298 guint32 id = GPOINTER_TO_UINT (data);
1300 theme_adium_remove_mark_from_message (self, id);
1304 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1307 self->priv->has_focus = has_focus;
1308 if (!self->priv->has_focus)
1310 /* We've lost focus, so let's make sure all the acked
1311 * messages have lost their unread marker. */
1312 g_queue_foreach (&self->priv->acked_messages,
1313 theme_adium_remove_acked_message_unread_mark_foreach, self);
1314 g_queue_clear (&self->priv->acked_messages);
1316 self->priv->has_unread_message = FALSE;
1321 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1322 EmpathyMessage *message)
1328 tp_msg = empathy_message_get_tp_message (message);
1333 id = tp_message_get_pending_message_id (tp_msg, &valid);
1336 g_warning ("Acknoledged message doesn't have a pending ID");
1340 /* We only want to actually remove the unread marker if the
1341 * view doesn't have focus. If we did it all the time we would
1342 * never see the unread markers, ever! So, we'll queue these
1343 * up, and when we lose focus, we'll remove the markers. */
1344 if (self->priv->has_focus)
1346 g_queue_push_tail (&self->priv->acked_messages,
1347 GUINT_TO_POINTER (id));
1351 theme_adium_remove_mark_from_message (self, id);
1355 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1356 GtkWidget *default_menu,
1357 WebKitHitTestResult *hit_test_result,
1358 gboolean triggered_with_keyboard,
1362 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1364 if (g_settings_get_boolean (self->priv->gsettings_chat,
1365 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1366 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1368 menu = empathy_webkit_create_context_menu (
1369 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1371 gtk_widget_show_all (menu);
1373 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1374 gtk_get_current_event_time ());
1380 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1381 gboolean show_avatars)
1383 self->priv->show_avatars = show_avatars;
1387 theme_adium_load_finished_cb (WebKitWebView *view,
1388 WebKitWebFrame *frame,
1391 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1394 DEBUG ("Page loaded");
1395 self->priv->pages_loading--;
1397 if (self->priv->pages_loading != 0)
1400 /* Display queued messages */
1401 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1403 QueuedItem *item = l->data;
1407 case QUEUED_MESSAGE:
1408 empathy_theme_adium_append_message (self, item->msg,
1409 item->should_highlight);
1413 empathy_theme_adium_edit_message (self, item->msg);
1417 empathy_theme_adium_append_event (self, item->str);
1421 free_queued_item (item);
1424 g_queue_clear (&self->priv->message_queue);
1428 theme_adium_finalize (GObject *object)
1430 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1432 empathy_adium_data_unref (self->priv->data);
1434 g_object_unref (self->priv->gsettings_chat);
1435 g_object_unref (self->priv->gsettings_desktop);
1437 g_free (self->priv->variant);
1439 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1443 theme_adium_dispose (GObject *object)
1445 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1447 if (self->priv->smiley_manager)
1449 g_object_unref (self->priv->smiley_manager);
1450 self->priv->smiley_manager = NULL;
1453 if (self->priv->last_contact)
1455 g_object_unref (self->priv->last_contact);
1456 self->priv->last_contact = NULL;
1459 if (self->priv->inspector_window)
1461 gtk_widget_destroy (self->priv->inspector_window);
1462 self->priv->inspector_window = NULL;
1465 if (self->priv->acked_messages.length > 0)
1467 g_queue_clear (&self->priv->acked_messages);
1470 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1474 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1475 EmpathyThemeAdium *self)
1477 if (self->priv->inspector_window)
1479 gtk_widget_show_all (self->priv->inspector_window);
1486 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1487 EmpathyThemeAdium *self)
1489 if (self->priv->inspector_window)
1491 gtk_widget_hide (self->priv->inspector_window);
1497 static WebKitWebView *
1498 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1499 WebKitWebView *web_view,
1500 EmpathyThemeAdium *self)
1502 GtkWidget *scrolled_window;
1503 GtkWidget *inspector_web_view;
1505 if (!self->priv->inspector_window)
1507 /* Create main window */
1508 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1510 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1513 g_signal_connect (self->priv->inspector_window, "delete-event",
1514 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1516 /* Pack a scrolled window */
1517 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1519 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1520 GTK_POLICY_AUTOMATIC,
1521 GTK_POLICY_AUTOMATIC);
1522 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1524 gtk_widget_show (scrolled_window);
1526 /* Pack a webview in the scrolled window. That webview will be
1527 * used to render the inspector tool. */
1528 inspector_web_view = webkit_web_view_new ();
1529 gtk_container_add (GTK_CONTAINER (scrolled_window),
1530 inspector_web_view);
1531 gtk_widget_show (scrolled_window);
1533 return WEBKIT_WEB_VIEW (inspector_web_view);
1540 theme_adium_constructed (GObject *object)
1542 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1543 const gchar *font_family = NULL;
1545 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1546 WebKitWebInspector *webkit_inspector;
1548 /* Set default settings */
1549 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1550 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1552 if (font_family && font_size)
1554 g_object_set (webkit_web_view_get_settings (webkit_view),
1555 "default-font-family", font_family,
1556 "default-font-size", font_size,
1561 empathy_webkit_bind_font_setting (webkit_view,
1562 self->priv->gsettings_desktop,
1563 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1566 /* Setup webkit inspector */
1567 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1568 g_signal_connect (webkit_inspector, "inspect-web-view",
1569 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1570 g_signal_connect (webkit_inspector, "show-window",
1571 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1572 g_signal_connect (webkit_inspector, "close-window",
1573 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1576 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1578 self->priv->in_construction = FALSE;
1582 theme_adium_get_property (GObject *object,
1587 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1591 case PROP_ADIUM_DATA:
1592 g_value_set_boxed (value, self->priv->data);
1595 g_value_set_string (value, self->priv->variant);
1598 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1604 theme_adium_set_property (GObject *object,
1606 const GValue *value,
1609 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1613 case PROP_ADIUM_DATA:
1614 g_assert (self->priv->data == NULL);
1615 self->priv->data = g_value_dup_boxed (value);
1618 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1621 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1627 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1629 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1631 object_class->finalize = theme_adium_finalize;
1632 object_class->dispose = theme_adium_dispose;
1633 object_class->constructed = theme_adium_constructed;
1634 object_class->get_property = theme_adium_get_property;
1635 object_class->set_property = theme_adium_set_property;
1637 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1638 g_param_spec_boxed ("adium-data",
1640 "Data for the adium theme",
1641 EMPATHY_TYPE_ADIUM_DATA,
1642 G_PARAM_CONSTRUCT_ONLY |
1644 G_PARAM_STATIC_STRINGS));
1646 g_object_class_install_property (object_class, PROP_VARIANT,
1647 g_param_spec_string ("variant",
1648 "The theme variant",
1649 "Variant name for the theme",
1653 G_PARAM_STATIC_STRINGS));
1655 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1659 empathy_theme_adium_init (EmpathyThemeAdium *self)
1661 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1662 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1664 self->priv->in_construction = TRUE;
1665 g_queue_init (&self->priv->message_queue);
1666 self->priv->allow_scrolling = TRUE;
1667 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1669 /* Show avatars by default. */
1670 self->priv->show_avatars = TRUE;
1672 g_signal_connect (self, "load-finished",
1673 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1674 g_signal_connect (self, "navigation-policy-decision-requested",
1675 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1676 g_signal_connect (self, "context-menu",
1677 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1679 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1680 self->priv->gsettings_desktop = g_settings_new (
1681 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1685 empathy_theme_adium_new (EmpathyAdiumData *data,
1686 const gchar *variant)
1688 g_return_val_if_fail (data != NULL, NULL);
1690 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1697 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1698 const gchar *variant)
1700 gchar *variant_path;
1703 if (!tp_strdiff (self->priv->variant, variant))
1706 g_free (self->priv->variant);
1707 self->priv->variant = g_strdup (variant);
1709 if (self->priv->in_construction)
1712 DEBUG ("Update view with variant: '%s'", variant);
1713 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1714 self->priv->variant);
1715 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1718 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1720 g_free (variant_path);
1723 g_object_notify (G_OBJECT (self), "variant");
1727 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1729 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1731 empathy_webkit_show_inspector (web_view);
1735 empathy_adium_path_is_valid (const gchar *path)
1745 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1746 tmp = g_strsplit (path, "/", 0);
1750 dir = tmp[g_strv_length (tmp) - 1];
1752 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1760 /* The theme is not valid if there is no Info.plist */
1761 file = g_build_filename (path, "Contents", "Info.plist",
1763 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1769 /* We ship a default Template.html as fallback if there is any problem
1770 * with the one inside the theme. The only other required file is
1771 * Content.html OR Incoming/Content.html*/
1772 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1774 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1779 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1780 "Content.html", NULL);
1781 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1789 empathy_adium_info_new (const gchar *path)
1793 GHashTable *info = NULL;
1795 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1797 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1798 value = empathy_plist_parse_from_file (file);
1804 info = g_value_dup_boxed (value);
1805 tp_g_value_slice_free (value);
1807 /* Insert the theme's path into the hash table,
1808 * keys have to be dupped */
1809 tp_asv_set_string (info, g_strdup ("path"), path);
1815 adium_info_get_version (GHashTable *info)
1817 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1820 static const gchar *
1821 adium_info_get_no_variant_name (GHashTable *info)
1823 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1824 return name ? name : _("Normal");
1828 adium_info_dup_path_for_variant (GHashTable *info,
1829 const gchar *variant)
1831 guint version = adium_info_get_version (info);
1832 const gchar *no_variant = adium_info_get_no_variant_name (info);
1833 GPtrArray *variants;
1836 if (version <= 2 && !tp_strdiff (variant, no_variant))
1837 return g_strdup ("main.css");
1839 variants = empathy_adium_info_get_available_variants (info);
1840 if (variants->len == 0)
1841 return g_strdup ("main.css");
1843 /* Verify the variant exists, fallback to the first one */
1844 for (i = 0; i < variants->len; i++)
1846 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1850 if (i == variants->len)
1852 DEBUG ("Variant %s does not exist", variant);
1853 variant = g_ptr_array_index (variants, 0);
1856 return g_strdup_printf ("Variants/%s.css", variant);
1861 empathy_adium_info_get_default_variant (GHashTable *info)
1863 if (adium_info_get_version (info) <= 2)
1864 return adium_info_get_no_variant_name (info);
1866 return tp_asv_get_string (info, "DefaultVariant");
1870 empathy_adium_info_get_available_variants (GHashTable *info)
1872 GPtrArray *variants;
1877 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1878 if (variants != NULL)
1881 variants = g_ptr_array_new_with_free_func (g_free);
1882 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1883 G_TYPE_PTR_ARRAY, variants);
1885 path = tp_asv_get_string (info, "path");
1886 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1887 dir = g_dir_open (dirpath, 0, NULL);
1892 for (name = g_dir_read_name (dir);
1894 name = g_dir_read_name (dir))
1896 gchar *display_name;
1898 if (!g_str_has_suffix (name, ".css"))
1901 display_name = g_strdup (name);
1902 strstr (display_name, ".css")[0] = '\0';
1903 g_ptr_array_add (variants, display_name);
1910 if (adium_info_get_version (info) <= 2)
1911 g_ptr_array_add (variants,
1912 g_strdup (adium_info_get_no_variant_name (info)));
1918 empathy_adium_data_get_type (void)
1920 static GType type_id = 0;
1924 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1925 (GBoxedCopyFunc) empathy_adium_data_ref,
1926 (GBoxedFreeFunc) empathy_adium_data_unref);
1933 empathy_adium_data_new_with_info (const gchar *path,
1936 EmpathyAdiumData *data;
1937 gchar *template_html = NULL;
1938 gchar *footer_html = NULL;
1941 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1943 data = g_slice_new0 (EmpathyAdiumData);
1944 data->ref_count = 1;
1945 data->path = g_strdup (path);
1946 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1947 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1948 data->info = g_hash_table_ref (info);
1949 data->version = adium_info_get_version (info);
1950 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1951 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1952 g_str_equal, g_free, g_free);
1954 DEBUG ("Loading theme at %s", path);
1956 #define LOAD(path, var) \
1957 tmp = g_build_filename (data->basedir, path, NULL); \
1958 g_file_get_contents (tmp, &var, NULL, NULL); \
1961 #define LOAD_CONST(path, var) \
1964 LOAD (path, content); \
1965 if (content != NULL) { \
1966 g_ptr_array_add (data->strings_to_free, content); \
1971 /* Load html files */
1972 LOAD_CONST ("Content.html", data->content_html);
1973 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1974 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1975 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1976 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1977 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1978 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1979 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1980 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1981 LOAD_CONST ("Status.html", data->status_html);
1982 LOAD ("Template.html", template_html);
1983 LOAD ("Footer.html", footer_html);
1988 /* HTML fallbacks: If we have at least content OR in_content, then
1989 * everything else gets a fallback */
1991 #define FALLBACK(html, fallback) \
1992 if (html == NULL) { \
1996 /* in_nextcontent -> in_content -> content */
1997 FALLBACK (data->in_content_html, data->content_html);
1998 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2000 /* context -> content */
2001 FALLBACK (data->in_context_html, data->in_content_html);
2002 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2003 FALLBACK (data->out_context_html, data->out_content_html);
2004 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2007 FALLBACK (data->out_content_html, data->in_content_html);
2008 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2009 FALLBACK (data->out_context_html, data->in_context_html);
2010 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2012 /* status -> in_content */
2013 FALLBACK (data->status_html, data->in_content_html);
2017 /* template -> empathy's template */
2018 data->custom_template = (template_html != NULL);
2019 if (template_html == NULL)
2021 GError *error = NULL;
2023 tmp = empathy_file_lookup ("Template.html", "data");
2025 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2026 g_warning ("couldn't load Empathy's default theme "
2027 "template: %s", error->message);
2028 g_return_val_if_reached (data);
2034 /* Default avatar */
2035 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2036 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2038 data->default_incoming_avatar_filename = tmp;
2045 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2046 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2048 data->default_outgoing_avatar_filename = tmp;
2055 /* Old custom templates had only 4 parameters.
2056 * New templates have 5 parameters */
2057 if (data->version <= 2 && data->custom_template)
2059 tmp = string_with_format (template_html,
2061 "%@", /* Leave variant unset */
2062 "", /* The header */
2063 footer_html ? footer_html : "",
2068 tmp = string_with_format (template_html,
2070 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2071 "%@", /* Leave variant unset */
2072 "", /* The header */
2073 footer_html ? footer_html : "",
2076 g_ptr_array_add (data->strings_to_free, tmp);
2077 data->template_html = tmp;
2079 g_free (template_html);
2080 g_free (footer_html);
2086 empathy_adium_data_new (const gchar *path)
2088 EmpathyAdiumData *data;
2091 info = empathy_adium_info_new (path);
2092 data = empathy_adium_data_new_with_info (path, info);
2093 g_hash_table_unref (info);
2099 empathy_adium_data_ref (EmpathyAdiumData *data)
2101 g_return_val_if_fail (data != NULL, NULL);
2103 g_atomic_int_inc (&data->ref_count);
2109 empathy_adium_data_unref (EmpathyAdiumData *data)
2111 g_return_if_fail (data != NULL);
2113 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2114 g_free (data->path);
2115 g_free (data->basedir);
2116 g_free (data->default_avatar_filename);
2117 g_free (data->default_incoming_avatar_filename);
2118 g_free (data->default_outgoing_avatar_filename);
2119 g_hash_table_unref (data->info);
2120 g_ptr_array_unref (data->strings_to_free);
2121 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2123 g_slice_free (EmpathyAdiumData, data);
2128 empathy_adium_data_get_info (EmpathyAdiumData *data)
2130 g_return_val_if_fail (data != NULL, NULL);
2136 empathy_adium_data_get_path (EmpathyAdiumData *data)
2138 g_return_val_if_fail (data != NULL, NULL);