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,
537 const gchar *cur = NULL;
540 /* Make some search-and-replace in the html code */
541 string = g_string_sized_new (strlen (html) + strlen (message));
542 g_string_append_printf (string, "%s(\"", func);
544 for (cur = html; *cur != '\0'; cur++)
546 const gchar *replace = NULL;
547 gchar *dup_replace = NULL;
548 gchar *format = NULL;
550 /* Those are all well known keywords that needs replacement in
551 * html files. Please keep them in the same order than the adium
552 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
553 if (theme_adium_match (&cur, "%userIconPath%"))
555 replace = avatar_filename;
557 else if (theme_adium_match (&cur, "%senderScreenName%"))
559 replace = contact_id;
561 else if (theme_adium_match (&cur, "%sender%"))
565 else if (theme_adium_match (&cur, "%senderColor%"))
567 /* A color derived from the user's name.
568 * FIXME: If a colon separated list of HTML colors is at
569 * Incoming/SenderColors.txt it will be used instead of
570 * the default colors.
573 /* Ensure we always use the same color when sending messages
579 else if (contact_id != NULL)
581 guint hash = g_str_hash (contact_id);
582 replace = colors[hash % G_N_ELEMENTS (colors)];
585 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
587 /* FIXME: The path to the status icon of the sender
588 * (available, away, etc...)
591 else if (theme_adium_match (&cur, "%messageDirection%"))
593 /* FIXME: The text direction of the message
594 * (either rtl or ltr)
597 else if (theme_adium_match (&cur, "%senderDisplayName%"))
599 /* FIXME: The serverside (remotely set) name of the
600 * sender, such as an MSN display name.
602 * We don't have access to that yet so we use
603 * local alias instead.
607 else if (theme_adium_match (&cur, "%senderPrefix%"))
609 /* FIXME: If we supported IRC user mode flags, this
610 * would be replaced with @ if the user is an op, + if
611 * the user has voice, etc. as per
612 * http://hg.adium.im/adium/rev/b586b027de42. But we
613 * don't, so for now we just strip it. */
615 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
618 /* FIXME: This keyword is used to represent the
619 * highlight background color. "X" is the opacity of the
620 * background, ranges from 0 to 1 and can be any decimal
624 else if (theme_adium_match (&cur, "%message%"))
628 else if (theme_adium_match (&cur, "%time%") ||
629 theme_adium_match_with_format (&cur, "%time{", &format))
631 const gchar *strftime_format;
633 strftime_format = nsdate_to_strftime (self->priv->data, format);
635 dup_replace = empathy_time_to_string_local (timestamp,
636 strftime_format ? strftime_format :
637 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
639 dup_replace = empathy_time_to_string_local (timestamp,
640 strftime_format ? strftime_format :
641 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
643 replace = dup_replace;
645 else if (theme_adium_match (&cur, "%shortTime%"))
647 dup_replace = empathy_time_to_string_local (timestamp,
648 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
649 replace = dup_replace;
651 else if (theme_adium_match (&cur, "%service%"))
653 replace = service_name;
655 else if (theme_adium_match (&cur, "%variant%"))
657 /* FIXME: The name of the active message style variant,
658 * with all spaces replaced with an underscore.
659 * A variant named "Alternating Messages - Blue Red"
660 * will become "Alternating_Messages_-_Blue_Red".
663 else if (theme_adium_match (&cur, "%userIcons%"))
665 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
667 else if (theme_adium_match (&cur, "%messageClasses%"))
669 replace = message_classes;
671 else if (theme_adium_match (&cur, "%status%"))
673 /* FIXME: A description of the status event. This is
674 * neither in the user's local language nor expected to
675 * be displayed; it may be useful to use a different div
676 * class to present different types of status messages.
677 * The following is a list of some of the more important
678 * status messages; your message style should be able to
679 * handle being shown a status message not in this list,
680 * as even at present the list is incomplete and is
681 * certain to become out of date in the future:
690 * contact_joined (group chats)
694 * encryption (all OTR messages use this status)
695 * purple (all IRC topic and join/part messages use this status)
696 * fileTransferStarted
697 * fileTransferCompleted
702 escape_and_append_len (string, cur, 1);
706 /* Here we have a replacement to make */
707 escape_and_append_len (string, replace, -1);
709 g_free (dup_replace);
712 g_string_append (string, "\")");
714 script = g_string_free (string, FALSE);
715 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
720 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
721 const gchar *escaped)
723 theme_adium_append_html (self, "appendMessage",
724 self->priv->data->status_html, escaped, NULL, NULL, NULL,
725 NULL, "event", empathy_time_get_current (), FALSE, FALSE);
727 /* There is no last contact */
728 if (self->priv->last_contact)
730 g_object_unref (self->priv->last_contact);
731 self->priv->last_contact = NULL;
736 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
737 WebKitDOMNodeList *nodes)
741 /* Remove focus and firstFocus class */
742 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
744 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
745 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
747 gchar **classes, **iter;
748 GString *new_class_name;
749 gboolean first = TRUE;
754 class_name = webkit_dom_html_element_get_class_name (element);
755 classes = g_strsplit (class_name, " ", -1);
756 new_class_name = g_string_sized_new (strlen (class_name));
758 for (iter = classes; *iter != NULL; iter++)
760 if (tp_strdiff (*iter, "focus") &&
761 tp_strdiff (*iter, "firstFocus"))
764 g_string_append_c (new_class_name, ' ');
766 g_string_append (new_class_name, *iter);
771 webkit_dom_html_element_set_class_name (element, new_class_name->str);
774 g_strfreev (classes);
775 g_string_free (new_class_name, TRUE);
780 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
782 WebKitDOMDocument *dom;
783 WebKitDOMNodeList *nodes;
784 GError *error = NULL;
786 if (!self->priv->has_unread_message)
789 self->priv->has_unread_message = FALSE;
791 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
795 /* Get all nodes with focus class */
796 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
800 DEBUG ("Error getting focus nodes: %s",
801 error ? error->message : "No error");
802 g_clear_error (&error);
806 theme_adium_remove_focus_marks (self, nodes);
810 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
812 gboolean should_highlight)
814 EmpathyContact *sender;
817 gchar *body_escaped, *name_escaped;
819 const gchar *contact_id;
820 EmpathyAvatar *avatar;
821 const gchar *avatar_filename = NULL;
823 const gchar *html = NULL;
825 const gchar *service_name;
826 GString *message_classes = NULL;
828 gboolean consecutive;
831 if (self->priv->pages_loading != 0)
833 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
838 /* Get information */
839 sender = empathy_message_get_sender (msg);
840 account = empathy_contact_get_account (sender);
841 service_name = empathy_protocol_name_to_display_name
842 (tp_account_get_protocol_name (account));
843 if (service_name == NULL)
844 service_name = tp_account_get_protocol_name (account);
845 timestamp = empathy_message_get_timestamp (msg);
846 body_escaped = theme_adium_parse_body (self,
847 empathy_message_get_body (msg),
848 empathy_message_get_token (msg));
849 name = empathy_contact_get_logged_alias (sender);
850 contact_id = empathy_contact_get_id (sender);
851 action = (empathy_message_get_tptype (msg) ==
852 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
854 name_escaped = g_markup_escape_text (name, -1);
856 /* If this is a /me probably */
861 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
863 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
864 "<span class='actionMessageBody'>%s</span>",
865 name_escaped, body_escaped);
869 str = g_strdup_printf ("*%s*", body_escaped);
872 g_free (body_escaped);
876 /* Get the avatar filename, or a fallback */
877 avatar = empathy_contact_get_avatar (sender);
879 avatar_filename = avatar->filename;
881 if (!avatar_filename)
883 if (empathy_contact_is_user (sender))
884 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
886 avatar_filename = self->priv->data->default_incoming_avatar_filename;
888 if (!avatar_filename)
890 if (!self->priv->data->default_avatar_filename)
891 self->priv->data->default_avatar_filename =
892 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
893 GTK_ICON_SIZE_DIALOG);
895 avatar_filename = self->priv->data->default_avatar_filename;
899 /* We want to join this message with the last one if
900 * - senders are the same contact,
901 * - last message was recieved recently,
902 * - last message and this message both are/aren't backlog, and
903 * - DisableCombineConsecutive is not set in theme's settings */
904 is_backlog = empathy_message_is_backlog (msg);
905 consecutive = empathy_contact_equal (self->priv->last_contact, sender) &&
906 (timestamp - self->priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
907 (is_backlog == self->priv->last_is_backlog) &&
908 !tp_asv_get_boolean (self->priv->data->info,
909 "DisableCombineConsecutive", NULL);
911 /* Define message classes */
912 message_classes = g_string_new ("message");
913 if (!self->priv->has_focus && !is_backlog)
915 if (!self->priv->has_unread_message)
917 g_string_append (message_classes, " firstFocus");
918 self->priv->has_unread_message = TRUE;
920 g_string_append (message_classes, " focus");
924 g_string_append (message_classes, " history");
927 g_string_append (message_classes, " consecutive");
929 if (empathy_contact_is_user (sender))
930 g_string_append (message_classes, " outgoing");
932 g_string_append (message_classes, " incoming");
934 if (should_highlight)
935 g_string_append (message_classes, " mention");
937 if (empathy_message_get_tptype (msg) ==
938 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
939 g_string_append (message_classes, " autoreply");
942 g_string_append (message_classes, " action");
944 /* FIXME: other classes:
945 * status - the message is a status change
946 * event - the message is a notification of something happening
947 * (for example, encryption being turned on)
948 * %status% - See %status% in theme_adium_append_html ()
951 /* This is slightly a hack, but it's the only way to add
952 * arbitrary data to messages in the HTML. We add another
953 * class called "x-empathy-message-id-*" to the message. This
954 * way, we can remove the unread marker for this specific
956 tp_msg = empathy_message_get_tp_message (msg);
962 id = tp_message_get_pending_message_id (tp_msg, &valid);
964 g_string_append_printf (message_classes,
965 " x-empathy-message-id-%u", id);
968 /* Define javascript function to use */
970 func = self->priv->allow_scrolling ? "appendNextMessage" :
971 "appendNextMessageNoScroll";
973 func = self->priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
975 if (empathy_contact_is_user (sender))
980 html = consecutive ? self->priv->data->out_nextcontext_html :
981 self->priv->data->out_context_html;
984 html = consecutive ? self->priv->data->out_nextcontent_html :
985 self->priv->data->out_content_html;
987 /* remove all the unread marks when we are sending a message */
988 theme_adium_remove_all_focus_marks (self);
995 html = consecutive ? self->priv->data->in_nextcontext_html :
996 self->priv->data->in_context_html;
999 html = consecutive ? self->priv->data->in_nextcontent_html :
1000 self->priv->data->in_content_html;
1003 theme_adium_append_html (self, func, html, body_escaped,
1004 avatar_filename, name_escaped, contact_id,
1005 service_name, message_classes->str,
1006 timestamp, is_backlog, empathy_contact_is_user (sender));
1008 /* Keep the sender of the last displayed message */
1009 if (self->priv->last_contact)
1010 g_object_unref (self->priv->last_contact);
1012 self->priv->last_contact = g_object_ref (sender);
1013 self->priv->last_timestamp = timestamp;
1014 self->priv->last_is_backlog = is_backlog;
1016 g_free (body_escaped);
1017 g_free (name_escaped);
1018 g_string_free (message_classes, TRUE);
1022 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1027 if (self->priv->pages_loading != 0)
1029 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE);
1033 str_escaped = g_markup_escape_text (str, -1);
1034 theme_adium_append_event_escaped (self, str_escaped);
1035 g_free (str_escaped);
1039 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1040 const gchar *markup_text,
1041 const gchar *fallback_text)
1043 theme_adium_append_event_escaped (self, markup_text);
1047 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1048 EmpathyMessage *message)
1050 WebKitDOMDocument *doc;
1051 WebKitDOMElement *span;
1052 gchar *id, *parsed_body;
1053 gchar *tooltip, *timestamp;
1054 GtkIconInfo *icon_info;
1055 GError *error = NULL;
1057 if (self->priv->pages_loading != 0)
1059 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE);
1063 id = g_strdup_printf ("message-token-%s",
1064 empathy_message_get_supersedes (message));
1065 /* we don't pass a token here, because doing so will return another
1066 * <span> element, and we don't want nested <span> elements */
1067 parsed_body = theme_adium_parse_body (self,
1068 empathy_message_get_body (message), NULL);
1070 /* find the element */
1071 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1072 span = webkit_dom_document_get_element_by_id (doc, id);
1076 DEBUG ("Failed to find id '%s'", id);
1080 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1082 DEBUG ("Not a HTML element");
1086 /* update the HTML */
1087 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1088 parsed_body, &error);
1092 DEBUG ("Error setting new inner-HTML: %s", error->message);
1093 g_error_free (error);
1098 timestamp = empathy_time_to_string_local (
1099 empathy_message_get_timestamp (message),
1101 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1103 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1109 /* mark this message as edited */
1110 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1111 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1113 if (icon_info != NULL)
1115 /* set the icon as a background image using CSS
1116 * FIXME: the icon won't update in response to theme changes */
1117 gchar *style = g_strdup_printf (
1118 "background-image:url('%s');"
1119 "background-repeat:no-repeat;"
1120 "background-position:left center;"
1121 "padding-left:19px;", /* 16px icon + 3px padding */
1122 gtk_icon_info_get_filename (icon_info));
1124 webkit_dom_element_set_attribute (span, "style", style, &error);
1128 DEBUG ("Error setting element style: %s",
1130 g_clear_error (&error);
1135 gtk_icon_info_free (icon_info);
1141 DEBUG ("Could not find message to edit with: %s",
1142 empathy_message_get_body (message));
1146 g_free (parsed_body);
1150 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1151 gboolean allow_scrolling)
1153 self->priv->allow_scrolling = allow_scrolling;
1155 if (allow_scrolling)
1156 empathy_theme_adium_scroll_down (self);
1160 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1162 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), "alignChat(true);");
1166 empathy_theme_adium_get_has_selection (EmpathyThemeAdium *self)
1168 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self));
1172 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1174 theme_adium_load_template (self);
1176 /* Clear last contact to avoid trying to add a 'joined'
1177 * message when we don't have an insertion point. */
1178 if (self->priv->last_contact)
1180 g_object_unref (self->priv->last_contact);
1181 self->priv->last_contact = NULL;
1186 empathy_theme_adium_find_previous (EmpathyThemeAdium *self,
1187 const gchar *search_criteria,
1188 gboolean new_search,
1189 gboolean match_case)
1191 /* FIXME: Doesn't respect new_search */
1192 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1193 search_criteria, match_case, FALSE, TRUE);
1197 empathy_theme_adium_find_next (EmpathyThemeAdium *self,
1198 const gchar *search_criteria,
1199 gboolean new_search,
1200 gboolean match_case)
1202 /* FIXME: Doesn't respect new_search */
1203 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self),
1204 search_criteria, match_case, TRUE, TRUE);
1208 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1209 const gchar *search_criteria,
1210 gboolean match_case,
1211 gboolean *can_do_previous,
1212 gboolean *can_do_next)
1214 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1215 * find_next and find_previous to work around this problem. */
1216 if (can_do_previous)
1217 *can_do_previous = TRUE;
1219 *can_do_next = TRUE;
1223 empathy_theme_adium_highlight (EmpathyThemeAdium *self,
1225 gboolean match_case)
1227 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self));
1228 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self),
1229 text, match_case, 0);
1230 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self),
1235 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1237 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self));
1241 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1244 WebKitDOMDocument *dom;
1245 WebKitDOMNodeList *nodes;
1247 GError *error = NULL;
1249 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1253 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1255 /* Get all nodes with focus class */
1256 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1261 DEBUG ("Error getting focus nodes: %s",
1262 error ? error->message : "No error");
1263 g_clear_error (&error);
1267 theme_adium_remove_focus_marks (self, nodes);
1271 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1274 EmpathyThemeAdium *self = user_data;
1275 guint32 id = GPOINTER_TO_UINT (data);
1277 theme_adium_remove_mark_from_message (self, id);
1281 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1284 self->priv->has_focus = has_focus;
1285 if (!self->priv->has_focus)
1287 /* We've lost focus, so let's make sure all the acked
1288 * messages have lost their unread marker. */
1289 g_queue_foreach (&self->priv->acked_messages,
1290 theme_adium_remove_acked_message_unread_mark_foreach, self);
1291 g_queue_clear (&self->priv->acked_messages);
1293 self->priv->has_unread_message = FALSE;
1298 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1299 EmpathyMessage *message)
1305 tp_msg = empathy_message_get_tp_message (message);
1310 id = tp_message_get_pending_message_id (tp_msg, &valid);
1313 g_warning ("Acknoledged message doesn't have a pending ID");
1317 /* We only want to actually remove the unread marker if the
1318 * view doesn't have focus. If we did it all the time we would
1319 * never see the unread markers, ever! So, we'll queue these
1320 * up, and when we lose focus, we'll remove the markers. */
1321 if (self->priv->has_focus)
1323 g_queue_push_tail (&self->priv->acked_messages,
1324 GUINT_TO_POINTER (id));
1328 theme_adium_remove_mark_from_message (self, id);
1332 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1333 GtkWidget *default_menu,
1334 WebKitHitTestResult *hit_test_result,
1335 gboolean triggered_with_keyboard,
1339 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1341 if (g_settings_get_boolean (self->priv->gsettings_chat,
1342 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1343 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1345 menu = empathy_webkit_create_context_menu (
1346 WEBKIT_WEB_VIEW (self), hit_test_result, flags);
1348 gtk_widget_show_all (menu);
1350 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, 3,
1351 gtk_get_current_event_time ());
1357 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1358 gboolean show_avatars)
1360 self->priv->show_avatars = show_avatars;
1364 theme_adium_load_finished_cb (WebKitWebView *view,
1365 WebKitWebFrame *frame,
1368 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1371 DEBUG ("Page loaded");
1372 self->priv->pages_loading--;
1374 if (self->priv->pages_loading != 0)
1377 /* Display queued messages */
1378 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1380 QueuedItem *item = l->data;
1384 case QUEUED_MESSAGE:
1385 empathy_theme_adium_append_message (self, item->msg,
1386 item->should_highlight);
1390 empathy_theme_adium_edit_message (self, item->msg);
1394 empathy_theme_adium_append_event (self, item->str);
1398 free_queued_item (item);
1401 g_queue_clear (&self->priv->message_queue);
1405 theme_adium_finalize (GObject *object)
1407 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1409 empathy_adium_data_unref (self->priv->data);
1411 g_object_unref (self->priv->gsettings_chat);
1412 g_object_unref (self->priv->gsettings_desktop);
1414 g_free (self->priv->variant);
1416 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1420 theme_adium_dispose (GObject *object)
1422 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1424 if (self->priv->smiley_manager)
1426 g_object_unref (self->priv->smiley_manager);
1427 self->priv->smiley_manager = NULL;
1430 if (self->priv->last_contact)
1432 g_object_unref (self->priv->last_contact);
1433 self->priv->last_contact = NULL;
1436 if (self->priv->inspector_window)
1438 gtk_widget_destroy (self->priv->inspector_window);
1439 self->priv->inspector_window = NULL;
1442 if (self->priv->acked_messages.length > 0)
1444 g_queue_clear (&self->priv->acked_messages);
1447 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1451 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1452 EmpathyThemeAdium *self)
1454 if (self->priv->inspector_window)
1456 gtk_widget_show_all (self->priv->inspector_window);
1463 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1464 EmpathyThemeAdium *self)
1466 if (self->priv->inspector_window)
1468 gtk_widget_hide (self->priv->inspector_window);
1474 static WebKitWebView *
1475 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1476 WebKitWebView *web_view,
1477 EmpathyThemeAdium *self)
1479 GtkWidget *scrolled_window;
1480 GtkWidget *inspector_web_view;
1482 if (!self->priv->inspector_window)
1484 /* Create main window */
1485 self->priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1487 gtk_window_set_default_size (GTK_WINDOW (self->priv->inspector_window),
1490 g_signal_connect (self->priv->inspector_window, "delete-event",
1491 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1493 /* Pack a scrolled window */
1494 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1496 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1497 GTK_POLICY_AUTOMATIC,
1498 GTK_POLICY_AUTOMATIC);
1499 gtk_container_add (GTK_CONTAINER (self->priv->inspector_window),
1501 gtk_widget_show (scrolled_window);
1503 /* Pack a webview in the scrolled window. That webview will be
1504 * used to render the inspector tool. */
1505 inspector_web_view = webkit_web_view_new ();
1506 gtk_container_add (GTK_CONTAINER (scrolled_window),
1507 inspector_web_view);
1508 gtk_widget_show (scrolled_window);
1510 return WEBKIT_WEB_VIEW (inspector_web_view);
1517 theme_adium_constructed (GObject *object)
1519 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1520 const gchar *font_family = NULL;
1522 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1523 WebKitWebInspector *webkit_inspector;
1525 /* Set default settings */
1526 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1527 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1529 if (font_family && font_size)
1531 g_object_set (webkit_web_view_get_settings (webkit_view),
1532 "default-font-family", font_family,
1533 "default-font-size", font_size,
1538 empathy_webkit_bind_font_setting (webkit_view,
1539 self->priv->gsettings_desktop,
1540 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1543 /* Setup webkit inspector */
1544 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1545 g_signal_connect (webkit_inspector, "inspect-web-view",
1546 G_CALLBACK (theme_adium_inspect_web_view_cb), object);
1547 g_signal_connect (webkit_inspector, "show-window",
1548 G_CALLBACK (theme_adium_inspector_show_window_cb), object);
1549 g_signal_connect (webkit_inspector, "close-window",
1550 G_CALLBACK (theme_adium_inspector_close_window_cb), object);
1553 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1555 self->priv->in_construction = FALSE;
1559 theme_adium_get_property (GObject *object,
1564 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1568 case PROP_ADIUM_DATA:
1569 g_value_set_boxed (value, self->priv->data);
1572 g_value_set_string (value, self->priv->variant);
1575 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1581 theme_adium_set_property (GObject *object,
1583 const GValue *value,
1586 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1590 case PROP_ADIUM_DATA:
1591 g_assert (self->priv->data == NULL);
1592 self->priv->data = g_value_dup_boxed (value);
1595 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1598 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1604 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1606 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1608 object_class->finalize = theme_adium_finalize;
1609 object_class->dispose = theme_adium_dispose;
1610 object_class->constructed = theme_adium_constructed;
1611 object_class->get_property = theme_adium_get_property;
1612 object_class->set_property = theme_adium_set_property;
1614 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1615 g_param_spec_boxed ("adium-data",
1617 "Data for the adium theme",
1618 EMPATHY_TYPE_ADIUM_DATA,
1619 G_PARAM_CONSTRUCT_ONLY |
1621 G_PARAM_STATIC_STRINGS));
1623 g_object_class_install_property (object_class, PROP_VARIANT,
1624 g_param_spec_string ("variant",
1625 "The theme variant",
1626 "Variant name for the theme",
1630 G_PARAM_STATIC_STRINGS));
1632 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1636 empathy_theme_adium_init (EmpathyThemeAdium *self)
1638 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1639 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1641 self->priv->in_construction = TRUE;
1642 g_queue_init (&self->priv->message_queue);
1643 self->priv->allow_scrolling = TRUE;
1644 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1646 /* Show avatars by default. */
1647 self->priv->show_avatars = TRUE;
1649 g_signal_connect (self, "load-finished",
1650 G_CALLBACK (theme_adium_load_finished_cb), NULL);
1651 g_signal_connect (self, "navigation-policy-decision-requested",
1652 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL);
1653 g_signal_connect (self, "context-menu",
1654 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1656 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1657 self->priv->gsettings_desktop = g_settings_new (
1658 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1662 empathy_theme_adium_new (EmpathyAdiumData *data,
1663 const gchar *variant)
1665 g_return_val_if_fail (data != NULL, NULL);
1667 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1674 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1675 const gchar *variant)
1677 gchar *variant_path;
1680 if (!tp_strdiff (self->priv->variant, variant))
1683 g_free (self->priv->variant);
1684 self->priv->variant = g_strdup (variant);
1686 if (self->priv->in_construction)
1689 DEBUG ("Update view with variant: '%s'", variant);
1690 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1691 self->priv->variant);
1692 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1695 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self), script);
1697 g_free (variant_path);
1700 g_object_notify (G_OBJECT (self), "variant");
1704 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1706 WebKitWebView *web_view = WEBKIT_WEB_VIEW (self);
1708 empathy_webkit_show_inspector (web_view);
1712 empathy_adium_path_is_valid (const gchar *path)
1722 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1723 tmp = g_strsplit (path, "/", 0);
1727 dir = tmp[g_strv_length (tmp) - 1];
1729 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1737 /* The theme is not valid if there is no Info.plist */
1738 file = g_build_filename (path, "Contents", "Info.plist",
1740 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1746 /* We ship a default Template.html as fallback if there is any problem
1747 * with the one inside the theme. The only other required file is
1748 * Content.html OR Incoming/Content.html*/
1749 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1751 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1756 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1757 "Content.html", NULL);
1758 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1766 empathy_adium_info_new (const gchar *path)
1770 GHashTable *info = NULL;
1772 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1774 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1775 value = empathy_plist_parse_from_file (file);
1781 info = g_value_dup_boxed (value);
1782 tp_g_value_slice_free (value);
1784 /* Insert the theme's path into the hash table,
1785 * keys have to be dupped */
1786 tp_asv_set_string (info, g_strdup ("path"), path);
1792 adium_info_get_version (GHashTable *info)
1794 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1797 static const gchar *
1798 adium_info_get_no_variant_name (GHashTable *info)
1800 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1801 return name ? name : _("Normal");
1805 adium_info_dup_path_for_variant (GHashTable *info,
1806 const gchar *variant)
1808 guint version = adium_info_get_version (info);
1809 const gchar *no_variant = adium_info_get_no_variant_name (info);
1810 GPtrArray *variants;
1813 if (version <= 2 && !tp_strdiff (variant, no_variant))
1814 return g_strdup ("main.css");
1816 variants = empathy_adium_info_get_available_variants (info);
1817 if (variants->len == 0)
1818 return g_strdup ("main.css");
1820 /* Verify the variant exists, fallback to the first one */
1821 for (i = 0; i < variants->len; i++)
1823 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1827 if (i == variants->len)
1829 DEBUG ("Variant %s does not exist", variant);
1830 variant = g_ptr_array_index (variants, 0);
1833 return g_strdup_printf ("Variants/%s.css", variant);
1838 empathy_adium_info_get_default_variant (GHashTable *info)
1840 if (adium_info_get_version (info) <= 2)
1841 return adium_info_get_no_variant_name (info);
1843 return tp_asv_get_string (info, "DefaultVariant");
1847 empathy_adium_info_get_available_variants (GHashTable *info)
1849 GPtrArray *variants;
1854 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1855 if (variants != NULL)
1858 variants = g_ptr_array_new_with_free_func (g_free);
1859 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1860 G_TYPE_PTR_ARRAY, variants);
1862 path = tp_asv_get_string (info, "path");
1863 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1864 dir = g_dir_open (dirpath, 0, NULL);
1869 for (name = g_dir_read_name (dir);
1871 name = g_dir_read_name (dir))
1873 gchar *display_name;
1875 if (!g_str_has_suffix (name, ".css"))
1878 display_name = g_strdup (name);
1879 strstr (display_name, ".css")[0] = '\0';
1880 g_ptr_array_add (variants, display_name);
1887 if (adium_info_get_version (info) <= 2)
1888 g_ptr_array_add (variants,
1889 g_strdup (adium_info_get_no_variant_name (info)));
1895 empathy_adium_data_get_type (void)
1897 static GType type_id = 0;
1901 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1902 (GBoxedCopyFunc) empathy_adium_data_ref,
1903 (GBoxedFreeFunc) empathy_adium_data_unref);
1910 empathy_adium_data_new_with_info (const gchar *path,
1913 EmpathyAdiumData *data;
1914 gchar *template_html = NULL;
1915 gchar *footer_html = NULL;
1918 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1920 data = g_slice_new0 (EmpathyAdiumData);
1921 data->ref_count = 1;
1922 data->path = g_strdup (path);
1923 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1924 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1925 data->info = g_hash_table_ref (info);
1926 data->version = adium_info_get_version (info);
1927 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1928 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1929 g_str_equal, g_free, g_free);
1931 DEBUG ("Loading theme at %s", path);
1933 #define LOAD(path, var) \
1934 tmp = g_build_filename (data->basedir, path, NULL); \
1935 g_file_get_contents (tmp, &var, NULL, NULL); \
1938 #define LOAD_CONST(path, var) \
1941 LOAD (path, content); \
1942 if (content != NULL) { \
1943 g_ptr_array_add (data->strings_to_free, content); \
1948 /* Load html files */
1949 LOAD_CONST ("Content.html", data->content_html);
1950 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1951 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1952 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1953 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1954 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1955 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1956 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1957 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1958 LOAD_CONST ("Status.html", data->status_html);
1959 LOAD ("Template.html", template_html);
1960 LOAD ("Footer.html", footer_html);
1965 /* HTML fallbacks: If we have at least content OR in_content, then
1966 * everything else gets a fallback */
1968 #define FALLBACK(html, fallback) \
1969 if (html == NULL) { \
1973 /* in_nextcontent -> in_content -> content */
1974 FALLBACK (data->in_content_html, data->content_html);
1975 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1977 /* context -> content */
1978 FALLBACK (data->in_context_html, data->in_content_html);
1979 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1980 FALLBACK (data->out_context_html, data->out_content_html);
1981 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1984 FALLBACK (data->out_content_html, data->in_content_html);
1985 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1986 FALLBACK (data->out_context_html, data->in_context_html);
1987 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1989 /* status -> in_content */
1990 FALLBACK (data->status_html, data->in_content_html);
1994 /* template -> empathy's template */
1995 data->custom_template = (template_html != NULL);
1996 if (template_html == NULL)
1998 GError *error = NULL;
2000 tmp = empathy_file_lookup ("Template.html", "data");
2002 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2003 g_warning ("couldn't load Empathy's default theme "
2004 "template: %s", error->message);
2005 g_return_val_if_reached (data);
2011 /* Default avatar */
2012 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2013 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2015 data->default_incoming_avatar_filename = tmp;
2022 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2023 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2025 data->default_outgoing_avatar_filename = tmp;
2032 /* Old custom templates had only 4 parameters.
2033 * New templates have 5 parameters */
2034 if (data->version <= 2 && data->custom_template)
2036 tmp = string_with_format (template_html,
2038 "%@", /* Leave variant unset */
2039 "", /* The header */
2040 footer_html ? footer_html : "",
2045 tmp = string_with_format (template_html,
2047 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2048 "%@", /* Leave variant unset */
2049 "", /* The header */
2050 footer_html ? footer_html : "",
2053 g_ptr_array_add (data->strings_to_free, tmp);
2054 data->template_html = tmp;
2056 g_free (template_html);
2057 g_free (footer_html);
2063 empathy_adium_data_new (const gchar *path)
2065 EmpathyAdiumData *data;
2068 info = empathy_adium_info_new (path);
2069 data = empathy_adium_data_new_with_info (path, info);
2070 g_hash_table_unref (info);
2076 empathy_adium_data_ref (EmpathyAdiumData *data)
2078 g_return_val_if_fail (data != NULL, NULL);
2080 g_atomic_int_inc (&data->ref_count);
2086 empathy_adium_data_unref (EmpathyAdiumData *data)
2088 g_return_if_fail (data != NULL);
2090 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2091 g_free (data->path);
2092 g_free (data->basedir);
2093 g_free (data->default_avatar_filename);
2094 g_free (data->default_incoming_avatar_filename);
2095 g_free (data->default_outgoing_avatar_filename);
2096 g_hash_table_unref (data->info);
2097 g_ptr_array_unref (data->strings_to_free);
2098 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2100 g_slice_free (EmpathyAdiumData, data);
2105 empathy_adium_data_get_info (EmpathyAdiumData *data)
2107 g_return_val_if_fail (data != NULL, NULL);
2113 empathy_adium_data_get_path (EmpathyAdiumData *data)
2115 g_return_val_if_fail (data != NULL, NULL);