1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2008-2009 Collabora Ltd.
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 * Authors: Xavier Claessens <xclaesse@gmail.com>
25 #include <glib/gi18n-lib.h>
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
31 #include <pango/pango.h>
34 #include <libempathy/empathy-gsettings.h>
35 #include <libempathy/empathy-time.h>
36 #include <libempathy/empathy-utils.h>
38 #include "empathy-theme-adium.h"
39 #include "empathy-smiley-manager.h"
40 #include "empathy-ui-utils.h"
41 #include "empathy-plist.h"
42 #include "empathy-string-parser.h"
43 #include "empathy-images.h"
45 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
46 #include <libempathy/empathy-debug.h>
48 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
50 #define BORING_DPI_DEFAULT 96
52 /* "Join" consecutive messages with timestamps within five minutes */
53 #define MESSAGE_JOIN_PERIOD 5*60
56 EmpathyAdiumData *data;
57 EmpathySmileyManager *smiley_manager;
58 EmpathyContact *last_contact;
59 gint64 last_timestamp;
60 gboolean last_is_backlog;
62 /* Queue of GValue* containing an EmpathyMessage or string */
64 /* Queue of owned gchar* of message token to remove unread
65 * marker for when we lose focus. */
66 GQueue acked_messages;
67 GtkWidget *inspector_window;
68 GSettings *gsettings_chat;
70 gboolean has_unread_message;
71 gboolean allow_scrolling;
72 } EmpathyThemeAdiumPriv;
74 struct _EmpathyAdiumData {
78 gchar *default_avatar_filename;
79 gchar *default_incoming_avatar_filename;
80 gchar *default_outgoing_avatar_filename;
83 gboolean custom_template;
86 const gchar *template_html;
87 const gchar *content_html;
88 const gchar *in_content_html;
89 const gchar *in_context_html;
90 const gchar *in_nextcontent_html;
91 const gchar *in_nextcontext_html;
92 const gchar *out_content_html;
93 const gchar *out_context_html;
94 const gchar *out_nextcontent_html;
95 const gchar *out_nextcontext_html;
96 const gchar *status_html;
98 /* Above html strings are pointers to strings stored in this array.
99 * We do this because of fallbacks, some htmls could be pointing the
101 GPtrArray *strings_to_free;
104 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
111 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
112 WEBKIT_TYPE_WEB_VIEW,
113 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
114 theme_adium_iface_init));
117 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
119 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
120 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
121 gboolean enable_webkit_developer_tools;
123 enable_webkit_developer_tools = g_settings_get_boolean (
124 priv->gsettings_chat,
125 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
127 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
128 "enable-developer-extras",
129 enable_webkit_developer_tools,
134 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
138 EmpathyThemeAdium *theme = user_data;
140 theme_adium_update_enable_webkit_developer_tools (theme);
144 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
145 WebKitWebFrame *web_frame,
146 WebKitNetworkRequest *request,
147 WebKitWebNavigationAction *action,
148 WebKitWebPolicyDecision *decision,
153 /* Only call url_show on clicks */
154 if (webkit_web_navigation_action_get_reason (action) !=
155 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
156 webkit_web_policy_decision_use (decision);
160 uri = webkit_network_request_get_uri (request);
161 empathy_url_show (GTK_WIDGET (view), uri);
163 webkit_web_policy_decision_ignore (decision);
168 theme_adium_copy_address_cb (GtkMenuItem *menuitem,
171 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
173 GtkClipboard *clipboard;
175 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
177 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
178 gtk_clipboard_set_text (clipboard, uri, -1);
180 clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
181 gtk_clipboard_set_text (clipboard, uri, -1);
187 theme_adium_open_address_cb (GtkMenuItem *menuitem,
190 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
193 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
195 empathy_url_show (GTK_WIDGET (menuitem), uri);
200 /* Replace each %@ in format with string passed in args */
202 string_with_format (const gchar *format,
203 const gchar *first_string,
210 va_start (args, first_string);
211 result = g_string_sized_new (strlen (format));
212 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
215 next = strstr (format, "%@");
220 g_string_append_len (result, format, next - format);
221 g_string_append (result, str);
224 g_string_append (result, format);
227 return g_string_free (result, FALSE);
231 theme_adium_match_newline (const gchar *text,
233 EmpathyStringReplace replace_func,
234 EmpathyStringParser *sub_parsers,
237 GString *string = user_data;
245 /* Replace \n by <br/> */
246 for (i = 0; i < len && text[i] != '\0'; i++) {
247 if (text[i] == '\n') {
248 empathy_string_parser_substr (text + prev,
249 i - prev, sub_parsers,
251 g_string_append (string, "<br/>");
255 empathy_string_parser_substr (text + prev, i - prev,
256 sub_parsers, user_data);
260 theme_adium_replace_smiley (const gchar *text,
265 EmpathySmileyHit *hit = match_data;
266 GString *string = user_data;
268 /* Replace smiley by a <img/> tag */
269 g_string_append_printf (string,
270 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
271 hit->path, (int)len, text, (int)len, text);
274 static EmpathyStringParser string_parsers[] = {
275 {empathy_string_match_link, empathy_string_replace_link},
276 {theme_adium_match_newline, NULL},
277 {empathy_string_match_all, empathy_string_replace_escaped},
281 static EmpathyStringParser string_parsers_with_smiley[] = {
282 {empathy_string_match_link, empathy_string_replace_link},
283 {empathy_string_match_smiley, theme_adium_replace_smiley},
284 {theme_adium_match_newline, NULL},
285 {empathy_string_match_all, empathy_string_replace_escaped},
290 theme_adium_parse_body (EmpathyThemeAdium *self,
293 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
294 EmpathyStringParser *parsers;
297 /* Check if we have to parse smileys */
298 if (g_settings_get_boolean (priv->gsettings_chat,
299 EMPATHY_PREFS_CHAT_SHOW_SMILEYS))
300 parsers = string_parsers_with_smiley;
302 parsers = string_parsers;
304 /* Parse text and construct string with links and smileys replaced
305 * by html tags. Also escape text to make sure html code is
306 * displayed verbatim. */
307 string = g_string_sized_new (strlen (text));
308 empathy_string_parser_substr (text, -1, parsers, string);
310 /* Wrap body in order to make tabs and multiple spaces displayed
311 * properly. See bug #625745. */
312 g_string_prepend (string, "<div style=\"display: inline; "
313 "white-space: pre-wrap\"'>");
314 g_string_append (string, "</div>");
316 return g_string_free (string, FALSE);
320 escape_and_append_len (GString *string, const gchar *str, gint len)
322 while (str != NULL && *str != '\0' && len != 0) {
326 g_string_append (string, "\\\\");
330 g_string_append (string, "\\\"");
333 /* Remove end of lines */
336 g_string_append_c (string, *str);
344 /* If *str starts with match, returns TRUE and move pointer to the end */
346 theme_adium_match (const gchar **str,
351 len = strlen (match);
352 if (strncmp (*str, match, len) == 0) {
360 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
362 theme_adium_match_with_format (const gchar **str,
366 const gchar *cur = *str;
369 if (!theme_adium_match (&cur, match)) {
374 end = strstr (cur, "}%");
379 *format = g_strndup (cur , end - cur);
384 /* List of colors used by %senderColor%. Copied from
385 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
387 static gchar *colors[] = {
388 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
389 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
390 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
391 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
392 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
393 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
394 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
395 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
396 "lightblue", "lightcoral",
397 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
398 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
399 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
400 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
401 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
402 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
403 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
404 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
405 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
406 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
411 theme_adium_append_html (EmpathyThemeAdium *theme,
414 const gchar *message,
415 const gchar *avatar_filename,
417 const gchar *contact_id,
418 const gchar *service_name,
419 const gchar *message_classes,
424 const gchar *cur = NULL;
427 /* Make some search-and-replace in the html code */
428 string = g_string_sized_new (strlen (html) + strlen (message));
429 g_string_append_printf (string, "%s(\"", func);
430 for (cur = html; *cur != '\0'; cur++) {
431 const gchar *replace = NULL;
432 gchar *dup_replace = NULL;
433 gchar *format = NULL;
435 /* Those are all well known keywords that needs replacement in
436 * html files. Please keep them in the same order than the adium
437 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
438 if (theme_adium_match (&cur, "%userIconPath%")) {
439 replace = avatar_filename;
440 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
441 replace = contact_id;
442 } else if (theme_adium_match (&cur, "%sender%")) {
444 } else if (theme_adium_match (&cur, "%senderColor%")) {
445 /* A color derived from the user's name.
446 * FIXME: If a colon separated list of HTML colors is at
447 * Incoming/SenderColors.txt it will be used instead of
448 * the default colors.
450 if (contact_id != NULL) {
451 guint hash = g_str_hash (contact_id);
452 replace = colors[hash % G_N_ELEMENTS (colors)];
454 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
455 /* FIXME: The path to the status icon of the sender
456 * (available, away, etc...)
458 } else if (theme_adium_match (&cur, "%messageDirection%")) {
459 /* FIXME: The text direction of the message
460 * (either rtl or ltr)
462 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
463 /* FIXME: The serverside (remotely set) name of the
464 * sender, such as an MSN display name.
466 * We don't have access to that yet so we use
467 * local alias instead.
470 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
471 /* FIXME: This keyword is used to represent the
472 * highlight background color. "X" is the opacity of the
473 * background, ranges from 0 to 1 and can be any decimal
476 } else if (theme_adium_match (&cur, "%message%")) {
478 } else if (theme_adium_match (&cur, "%time%") ||
479 theme_adium_match_with_format (&cur, "%time{", &format)) {
480 /* FIXME: format is not exactly strftime.
481 * See NSDateFormatter spec:
482 * http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/
485 dup_replace = empathy_time_to_string_local (timestamp,
486 format ? format : EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
488 dup_replace = empathy_time_to_string_local (timestamp,
489 format ? format : EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
491 replace = dup_replace;
492 } else if (theme_adium_match (&cur, "%shortTime%")) {
493 dup_replace = empathy_time_to_string_local (timestamp,
494 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
495 replace = dup_replace;
496 } else if (theme_adium_match (&cur, "%service%")) {
497 replace = service_name;
498 } else if (theme_adium_match (&cur, "%variant%")) {
499 /* FIXME: The name of the active message style variant,
500 * with all spaces replaced with an underscore.
501 * A variant named "Alternating Messages - Blue Red"
502 * will become "Alternating_Messages_-_Blue_Red".
504 } else if (theme_adium_match (&cur, "%userIcons%")) {
505 /* FIXME: mus t be "hideIcons" if use preference is set
507 replace = "showIcons";
508 } else if (theme_adium_match (&cur, "%messageClasses%")) {
509 replace = message_classes;
510 } else if (theme_adium_match (&cur, "%status%")) {
511 /* FIXME: A description of the status event. This is
512 * neither in the user's local language nor expected to
513 * be displayed; it may be useful to use a different div
514 * class to present different types of status messages.
515 * The following is a list of some of the more important
516 * status messages; your message style should be able to
517 * handle being shown a status message not in this list,
518 * as even at present the list is incomplete and is
519 * certain to become out of date in the future:
528 * contact_joined (group chats)
532 * encryption (all OTR messages use this status)
533 * purple (all IRC topic and join/part messages use this status)
534 * fileTransferStarted
535 * fileTransferCompleted
538 escape_and_append_len (string, cur, 1);
542 /* Here we have a replacement to make */
543 escape_and_append_len (string, replace, -1);
545 g_free (dup_replace);
548 g_string_append (string, "\")");
550 script = g_string_free (string, FALSE);
551 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
556 theme_adium_append_event_escaped (EmpathyChatView *view,
557 const gchar *escaped)
559 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
560 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
562 theme_adium_append_html (theme, "appendMessage",
563 priv->data->status_html, escaped, NULL, NULL, NULL,
565 empathy_time_get_current (), FALSE);
567 /* There is no last contact */
568 if (priv->last_contact) {
569 g_object_unref (priv->last_contact);
570 priv->last_contact = NULL;
575 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
576 WebKitDOMNodeList *nodes)
580 /* Remove focus and firstFocus class */
581 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
582 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
583 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
585 gchar **classes, **iter;
586 GString *new_class_name;
587 gboolean first = TRUE;
589 if (element == NULL) {
593 class_name = webkit_dom_html_element_get_class_name (element);
594 classes = g_strsplit (class_name, " ", -1);
595 new_class_name = g_string_sized_new (strlen (class_name));
596 for (iter = classes; *iter != NULL; iter++) {
597 if (tp_strdiff (*iter, "focus") &&
598 tp_strdiff (*iter, "firstFocus")) {
600 g_string_append_c (new_class_name, ' ');
602 g_string_append (new_class_name, *iter);
607 webkit_dom_html_element_set_class_name (element, new_class_name->str);
610 g_strfreev (classes);
611 g_string_free (new_class_name, TRUE);
616 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
618 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
619 WebKitDOMDocument *dom;
620 WebKitDOMNodeList *nodes;
621 GError *error = NULL;
623 if (!priv->has_unread_message)
626 priv->has_unread_message = FALSE;
628 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
633 /* Get all nodes with focus class */
634 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
636 DEBUG ("Error getting focus nodes: %s",
637 error ? error->message : "No error");
638 g_clear_error (&error);
642 theme_adium_remove_focus_marks (theme, nodes);
646 theme_adium_append_message (EmpathyChatView *view,
649 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
650 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
651 EmpathyContact *sender;
657 const gchar *contact_id;
658 EmpathyAvatar *avatar;
659 const gchar *avatar_filename = NULL;
661 const gchar *html = NULL;
663 const gchar *service_name;
664 GString *message_classes = NULL;
666 gboolean consecutive;
669 if (priv->pages_loading != 0) {
670 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
671 g_value_set_object (value, msg);
672 g_queue_push_tail (&priv->message_queue, value);
676 /* Get information */
677 sender = empathy_message_get_sender (msg);
678 account = empathy_contact_get_account (sender);
679 service_name = empathy_protocol_name_to_display_name
680 (tp_account_get_protocol (account));
681 if (service_name == NULL)
682 service_name = tp_account_get_protocol (account);
683 timestamp = empathy_message_get_timestamp (msg);
684 body = empathy_message_get_body (msg);
685 body_escaped = theme_adium_parse_body (theme, body);
686 name = empathy_contact_get_alias (sender);
687 contact_id = empathy_contact_get_id (sender);
688 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
690 /* If this is a /me probably */
694 if (priv->data->version >= 4 || !priv->data->custom_template) {
695 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
696 "<span class='actionMessageBody'>%s</span>",
699 str = g_strdup_printf ("*%s*", body_escaped);
701 g_free (body_escaped);
705 /* Get the avatar filename, or a fallback */
706 avatar = empathy_contact_get_avatar (sender);
708 avatar_filename = avatar->filename;
710 if (!avatar_filename) {
711 if (empathy_contact_is_user (sender)) {
712 avatar_filename = priv->data->default_outgoing_avatar_filename;
714 avatar_filename = priv->data->default_incoming_avatar_filename;
716 if (!avatar_filename) {
717 if (!priv->data->default_avatar_filename) {
718 priv->data->default_avatar_filename =
719 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
720 GTK_ICON_SIZE_DIALOG);
722 avatar_filename = priv->data->default_avatar_filename;
726 /* We want to join this message with the last one if
727 * - senders are the same contact,
728 * - last message was recieved recently,
729 * - last message and this message both are/aren't backlog, and
730 * - DisableCombineConsecutive is not set in theme's settings */
731 is_backlog = empathy_message_is_backlog (msg);
732 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
733 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
734 (is_backlog == priv->last_is_backlog) &&
735 !tp_asv_get_boolean (priv->data->info,
736 "DisableCombineConsecutive", NULL);
738 /* Define message classes */
739 message_classes = g_string_new ("message");
740 if (!priv->has_focus && !is_backlog) {
741 if (!priv->has_unread_message) {
742 g_string_append (message_classes, " firstFocus");
743 priv->has_unread_message = TRUE;
745 g_string_append (message_classes, " focus");
748 g_string_append (message_classes, " history");
751 g_string_append (message_classes, " consecutive");
753 if (empathy_contact_is_user (sender)) {
754 g_string_append (message_classes, " outgoing");
756 g_string_append (message_classes, " incoming");
758 if (empathy_message_should_highlight (msg)) {
759 g_string_append (message_classes, " mention");
761 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
762 g_string_append (message_classes, " autoreply");
765 g_string_append (message_classes, " action");
767 /* FIXME: other classes:
768 * status - the message is a status change
769 * event - the message is a notification of something happening
770 * (for example, encryption being turned on)
771 * %status% - See %status% in theme_adium_append_html ()
774 /* This is slightly a hack, but it's the only way to add
775 * arbitrary data to messages in the HTML. We add another
776 * class called "x-empathy-message-id-*" to the message. This
777 * way, we can remove the unread marker for this specific
779 tp_msg = empathy_message_get_tp_message (msg);
780 if (tp_msg != NULL) {
781 gchar *tmp = tp_escape_as_identifier (
782 tp_message_get_token (tp_msg));
783 g_string_append_printf (message_classes,
784 " x-empathy-message-id-%s", tmp);
788 /* Define javascript function to use */
790 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
792 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
795 if (empathy_contact_is_user (sender)) {
799 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
802 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
805 /* remove all the unread marks when we are sending a message */
806 theme_adium_remove_all_focus_marks (theme);
811 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
814 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
818 theme_adium_append_html (theme, func, html, body_escaped,
819 avatar_filename, name, contact_id,
820 service_name, message_classes->str,
821 timestamp, is_backlog);
823 /* Keep the sender of the last displayed message */
824 if (priv->last_contact) {
825 g_object_unref (priv->last_contact);
827 priv->last_contact = g_object_ref (sender);
828 priv->last_timestamp = timestamp;
829 priv->last_is_backlog = is_backlog;
831 g_free (body_escaped);
832 g_string_free (message_classes, TRUE);
836 theme_adium_append_event (EmpathyChatView *view,
839 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
842 if (priv->pages_loading != 0) {
843 g_queue_push_tail (&priv->message_queue,
844 tp_g_value_slice_new_string (str));
848 str_escaped = g_markup_escape_text (str, -1);
849 theme_adium_append_event_escaped (view, str_escaped);
850 g_free (str_escaped);
854 theme_adium_scroll (EmpathyChatView *view,
855 gboolean allow_scrolling)
857 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
859 priv->allow_scrolling = allow_scrolling;
860 if (allow_scrolling) {
861 empathy_chat_view_scroll_down (view);
866 theme_adium_scroll_down (EmpathyChatView *view)
868 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
872 theme_adium_get_has_selection (EmpathyChatView *view)
874 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
878 theme_adium_clear (EmpathyChatView *view)
880 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
883 priv->pages_loading++;
884 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
885 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view),
886 priv->data->template_html,
888 g_free (basedir_uri);
890 /* Clear last contact to avoid trying to add a 'joined'
891 * message when we don't have an insertion point. */
892 if (priv->last_contact) {
893 g_object_unref (priv->last_contact);
894 priv->last_contact = NULL;
899 theme_adium_find_previous (EmpathyChatView *view,
900 const gchar *search_criteria,
904 /* FIXME: Doesn't respect new_search */
905 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
906 search_criteria, match_case,
911 theme_adium_find_next (EmpathyChatView *view,
912 const gchar *search_criteria,
916 /* FIXME: Doesn't respect new_search */
917 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
918 search_criteria, match_case,
923 theme_adium_find_abilities (EmpathyChatView *view,
924 const gchar *search_criteria,
926 gboolean *can_do_previous,
927 gboolean *can_do_next)
929 /* FIXME: Does webkit provide an API for that? We have wrap=true in
930 * find_next and find_previous to work around this problem. */
932 *can_do_previous = TRUE;
938 theme_adium_highlight (EmpathyChatView *view,
942 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
943 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
944 text, match_case, 0);
945 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
950 theme_adium_copy_clipboard (EmpathyChatView *view)
952 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
956 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
959 WebKitDOMDocument *dom;
960 WebKitDOMNodeList *nodes;
962 GError *error = NULL;
964 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
969 tmp = tp_escape_as_identifier (token);
970 class = g_strdup_printf (".x-empathy-message-id-%s", tmp);
973 /* Get all nodes with focus class */
974 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
978 DEBUG ("Error getting focus nodes: %s",
979 error ? error->message : "No error");
980 g_clear_error (&error);
984 theme_adium_remove_focus_marks (self, nodes);
988 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
991 EmpathyThemeAdium *self = user_data;
994 theme_adium_remove_mark_from_message (self, token);
999 theme_adium_focus_toggled (EmpathyChatView *view,
1002 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1004 priv->has_focus = has_focus;
1005 if (!priv->has_focus) {
1006 /* We've lost focus, so let's make sure all the acked
1007 * messages have lost their unread marker. */
1008 g_queue_foreach (&priv->acked_messages,
1009 theme_adium_remove_acked_message_unread_mark_foreach,
1011 g_queue_clear (&priv->acked_messages);
1016 theme_adium_message_acknowledged (EmpathyChatView *view,
1017 EmpathyMessage *message)
1019 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1020 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1023 tp_msg = empathy_message_get_tp_message (message);
1025 if (tp_msg == NULL) {
1029 /* We only want to actually remove the unread marker if the
1030 * view doesn't have focus. If we did it all the time we would
1031 * never see the unread markers, ever! So, we'll queue these
1032 * up, and when we lose focus, we'll remove the markers. */
1033 if (priv->has_focus) {
1034 g_queue_push_tail (&priv->acked_messages,
1035 g_strdup (tp_message_get_token (tp_msg)));
1039 theme_adium_remove_mark_from_message (self,
1040 tp_message_get_token (tp_msg));
1044 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1046 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1048 g_object_unref (hit_test_result);
1052 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1054 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1055 WebKitHitTestResult *hit_test_result;
1056 WebKitHitTestResultContext context;
1060 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1061 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1064 menu = empathy_context_menu_new (GTK_WIDGET (view));
1066 /* Select all item */
1067 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1068 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1070 g_signal_connect_swapped (item, "activate",
1071 G_CALLBACK (webkit_web_view_select_all),
1074 /* Copy menu item */
1075 if (webkit_web_view_can_copy_clipboard (view)) {
1076 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1077 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1079 g_signal_connect_swapped (item, "activate",
1080 G_CALLBACK (webkit_web_view_copy_clipboard),
1084 /* Clear menu item */
1085 item = gtk_separator_menu_item_new ();
1086 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1088 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1089 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1091 g_signal_connect_swapped (item, "activate",
1092 G_CALLBACK (empathy_chat_view_clear),
1095 /* We will only add the following menu items if we are
1096 * right-clicking a link */
1097 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1099 item = gtk_separator_menu_item_new ();
1100 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1102 /* Copy Link Address menu item */
1103 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1104 g_signal_connect (item, "activate",
1105 G_CALLBACK (theme_adium_copy_address_cb),
1107 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1109 /* Open Link menu item */
1110 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1111 g_signal_connect (item, "activate",
1112 G_CALLBACK (theme_adium_open_address_cb),
1114 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1117 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1118 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1121 /* Display the menu */
1122 gtk_widget_show_all (menu);
1123 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1124 event->button, event->time);
1128 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1130 if (event->button == 3) {
1131 gboolean developer_tools_enabled;
1133 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1134 "enable-developer-extras", &developer_tools_enabled, NULL);
1136 /* We currently have no way to add an inspector menu
1137 * item ourselves, so we disable our customized menu
1138 * if the developer extras are enabled. */
1139 if (!developer_tools_enabled) {
1140 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1145 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1149 theme_adium_iface_init (EmpathyChatViewIface *iface)
1151 iface->append_message = theme_adium_append_message;
1152 iface->append_event = theme_adium_append_event;
1153 iface->scroll = theme_adium_scroll;
1154 iface->scroll_down = theme_adium_scroll_down;
1155 iface->get_has_selection = theme_adium_get_has_selection;
1156 iface->clear = theme_adium_clear;
1157 iface->find_previous = theme_adium_find_previous;
1158 iface->find_next = theme_adium_find_next;
1159 iface->find_abilities = theme_adium_find_abilities;
1160 iface->highlight = theme_adium_highlight;
1161 iface->copy_clipboard = theme_adium_copy_clipboard;
1162 iface->focus_toggled = theme_adium_focus_toggled;
1163 iface->message_acknowledged = theme_adium_message_acknowledged;
1167 theme_adium_load_finished_cb (WebKitWebView *view,
1168 WebKitWebFrame *frame,
1171 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1172 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1175 DEBUG ("Page loaded");
1176 priv->pages_loading--;
1178 if (priv->pages_loading != 0)
1181 /* Display queued messages */
1182 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1183 GValue *value = l->data;
1185 if (G_VALUE_HOLDS_OBJECT (value)) {
1186 theme_adium_append_message (chat_view,
1187 g_value_get_object (value));
1189 theme_adium_append_event (chat_view,
1190 g_value_get_string (value));
1193 tp_g_value_slice_free (value);
1195 g_queue_clear (&priv->message_queue);
1199 theme_adium_finalize (GObject *object)
1201 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1203 empathy_adium_data_unref (priv->data);
1204 g_object_unref (priv->gsettings_chat);
1206 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1210 theme_adium_dispose (GObject *object)
1212 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1214 if (priv->smiley_manager) {
1215 g_object_unref (priv->smiley_manager);
1216 priv->smiley_manager = NULL;
1219 if (priv->last_contact) {
1220 g_object_unref (priv->last_contact);
1221 priv->last_contact = NULL;
1224 if (priv->inspector_window) {
1225 gtk_widget_destroy (priv->inspector_window);
1226 priv->inspector_window = NULL;
1229 if (priv->acked_messages.length > 0) {
1230 g_queue_foreach (&priv->acked_messages, (GFunc) g_free, NULL);
1231 g_queue_clear (&priv->acked_messages);
1234 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1238 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1239 EmpathyThemeAdium *theme)
1241 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1243 if (priv->inspector_window) {
1244 gtk_widget_show_all (priv->inspector_window);
1251 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1252 EmpathyThemeAdium *theme)
1254 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1256 if (priv->inspector_window) {
1257 gtk_widget_hide (priv->inspector_window);
1263 static WebKitWebView *
1264 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1265 WebKitWebView *web_view,
1266 EmpathyThemeAdium *theme)
1268 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1269 GtkWidget *scrolled_window;
1270 GtkWidget *inspector_web_view;
1272 if (!priv->inspector_window) {
1273 /* Create main window */
1274 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1275 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1277 g_signal_connect (priv->inspector_window, "delete-event",
1278 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1280 /* Pack a scrolled window */
1281 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1282 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1283 GTK_POLICY_AUTOMATIC,
1284 GTK_POLICY_AUTOMATIC);
1285 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1287 gtk_widget_show (scrolled_window);
1289 /* Pack a webview in the scrolled window. That webview will be
1290 * used to render the inspector tool. */
1291 inspector_web_view = webkit_web_view_new ();
1292 gtk_container_add (GTK_CONTAINER (scrolled_window),
1293 inspector_web_view);
1294 gtk_widget_show (scrolled_window);
1296 return WEBKIT_WEB_VIEW (inspector_web_view);
1302 static PangoFontDescription *
1303 theme_adium_get_default_font (void)
1305 GSettings *gsettings;
1306 PangoFontDescription *pango_fd;
1309 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1311 font_family = g_settings_get_string (gsettings,
1312 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1314 if (font_family == NULL)
1317 pango_fd = pango_font_description_from_string (font_family);
1318 g_free (font_family);
1319 g_object_unref (gsettings);
1324 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1328 g_object_set (w_settings, "default-font-family", name, NULL);
1329 g_object_set (w_settings, "default-font-size", size, NULL);
1333 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1335 PangoFontDescription *default_font_desc;
1336 GdkScreen *current_screen;
1338 gint pango_font_size = 0;
1340 default_font_desc = theme_adium_get_default_font ();
1341 if (default_font_desc == NULL)
1343 pango_font_size = pango_font_description_get_size (default_font_desc)
1345 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1346 current_screen = gdk_screen_get_default ();
1347 if (current_screen != NULL) {
1348 dpi = gdk_screen_get_resolution (current_screen);
1350 dpi = BORING_DPI_DEFAULT;
1352 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1354 theme_adium_set_webkit_font (w_settings,
1355 pango_font_description_get_family (default_font_desc),
1357 pango_font_description_free (default_font_desc);
1361 theme_adium_constructed (GObject *object)
1363 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1365 const gchar *font_family = NULL;
1367 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1368 WebKitWebSettings *webkit_settings;
1369 WebKitWebInspector *webkit_inspector;
1371 /* Set default settings */
1372 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1373 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1374 webkit_settings = webkit_web_view_get_settings (webkit_view);
1376 if (font_family && font_size) {
1377 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1379 theme_adium_set_default_font (webkit_settings);
1382 /* Setup webkit inspector */
1383 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1384 g_signal_connect (webkit_inspector, "inspect-web-view",
1385 G_CALLBACK (theme_adium_inspect_web_view_cb),
1387 g_signal_connect (webkit_inspector, "show-window",
1388 G_CALLBACK (theme_adium_inspector_show_window_cb),
1390 g_signal_connect (webkit_inspector, "close-window",
1391 G_CALLBACK (theme_adium_inspector_close_window_cb),
1395 priv->pages_loading = 1;
1397 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
1398 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
1399 priv->data->template_html,
1401 g_free (basedir_uri);
1405 theme_adium_get_property (GObject *object,
1410 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1413 case PROP_ADIUM_DATA:
1414 g_value_set_boxed (value, priv->data);
1417 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1423 theme_adium_set_property (GObject *object,
1425 const GValue *value,
1428 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1431 case PROP_ADIUM_DATA:
1432 g_assert (priv->data == NULL);
1433 priv->data = g_value_dup_boxed (value);
1436 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1442 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1444 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1445 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1447 object_class->finalize = theme_adium_finalize;
1448 object_class->dispose = theme_adium_dispose;
1449 object_class->constructed = theme_adium_constructed;
1450 object_class->get_property = theme_adium_get_property;
1451 object_class->set_property = theme_adium_set_property;
1453 widget_class->button_press_event = theme_adium_button_press_event;
1455 g_object_class_install_property (object_class,
1457 g_param_spec_boxed ("adium-data",
1459 "Data for the adium theme",
1460 EMPATHY_TYPE_ADIUM_DATA,
1461 G_PARAM_CONSTRUCT_ONLY |
1463 G_PARAM_STATIC_STRINGS));
1465 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1469 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1471 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1472 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1476 g_queue_init (&priv->message_queue);
1477 priv->allow_scrolling = TRUE;
1478 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1480 g_signal_connect (theme, "load-finished",
1481 G_CALLBACK (theme_adium_load_finished_cb),
1483 g_signal_connect (theme, "navigation-policy-decision-requested",
1484 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1487 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1488 g_signal_connect (priv->gsettings_chat,
1489 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1490 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1493 theme_adium_update_enable_webkit_developer_tools (theme);
1497 empathy_theme_adium_new (EmpathyAdiumData *data)
1499 g_return_val_if_fail (data != NULL, NULL);
1501 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1507 empathy_adium_path_is_valid (const gchar *path)
1512 /* The theme is not valid if there is no Info.plist */
1513 file = g_build_filename (path, "Contents", "Info.plist",
1515 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1521 /* We ship a default Template.html as fallback if there is any problem
1522 * with the one inside the theme. The only other required file is
1523 * Content.html OR Incoming/Content.html*/
1524 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1526 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1530 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1531 "Content.html", NULL);
1532 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1540 empathy_adium_info_new (const gchar *path)
1544 GHashTable *info = NULL;
1546 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1548 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1549 value = empathy_plist_parse_from_file (file);
1555 info = g_value_dup_boxed (value);
1556 tp_g_value_slice_free (value);
1558 /* Insert the theme's path into the hash table,
1559 * keys have to be dupped */
1560 tp_asv_set_string (info, g_strdup ("path"), path);
1566 adium_info_get_version (GHashTable *info)
1568 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1571 static const gchar *
1572 adium_info_get_no_variant_name (GHashTable *info)
1574 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1575 return name ? name : _("Normal");
1578 static const gchar *
1579 adium_info_get_default_or_first_variant (GHashTable *info)
1582 GPtrArray *variants;
1584 name = empathy_adium_info_get_default_variant (info);
1589 variants = empathy_adium_info_get_available_variants (info);
1590 g_assert (variants->len > 0);
1591 return g_ptr_array_index (variants, 0);
1595 adium_info_dup_path_for_variant (GHashTable *info,
1596 const gchar *variant)
1598 guint version = adium_info_get_version (info);
1599 const gchar *no_variant = adium_info_get_no_variant_name (info);
1601 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1602 return g_strdup ("main.css");
1605 return g_strdup_printf ("Variants/%s.css", variant);
1610 empathy_adium_info_get_default_variant (GHashTable *info)
1612 if (adium_info_get_version (info) <= 2) {
1613 return adium_info_get_no_variant_name (info);
1616 return tp_asv_get_string (info, "DefaultVariant");
1620 empathy_adium_info_get_available_variants (GHashTable *info)
1622 GPtrArray *variants;
1627 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1628 if (variants != NULL) {
1632 variants = g_ptr_array_new_with_free_func (g_free);
1633 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1634 G_TYPE_PTR_ARRAY, variants);
1636 path = tp_asv_get_string (info, "path");
1637 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1638 dir = g_dir_open (dirpath, 0, NULL);
1642 for (name = g_dir_read_name (dir);
1644 name = g_dir_read_name (dir)) {
1645 gchar *display_name;
1647 if (!g_str_has_suffix (name, ".css")) {
1651 display_name = g_strdup (name);
1652 strstr (display_name, ".css")[0] = '\0';
1653 g_ptr_array_add (variants, display_name);
1659 if (adium_info_get_version (info) <= 2) {
1660 g_ptr_array_add (variants,
1661 g_strdup (adium_info_get_no_variant_name (info)));
1668 empathy_adium_data_get_type (void)
1670 static GType type_id = 0;
1674 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1675 (GBoxedCopyFunc) empathy_adium_data_ref,
1676 (GBoxedFreeFunc) empathy_adium_data_unref);
1683 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1685 EmpathyAdiumData *data;
1686 gchar *template_html = NULL;
1687 gchar *footer_html = NULL;
1688 gchar *variant_path;
1691 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1693 data = g_slice_new0 (EmpathyAdiumData);
1694 data->ref_count = 1;
1695 data->path = g_strdup (path);
1696 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1697 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1698 data->info = g_hash_table_ref (info);
1699 data->version = adium_info_get_version (info);
1700 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1702 DEBUG ("Loading theme at %s", path);
1704 #define LOAD(path, var) \
1705 tmp = g_build_filename (data->basedir, path, NULL); \
1706 g_file_get_contents (tmp, &var, NULL, NULL); \
1709 #define LOAD_CONST(path, var) \
1712 LOAD (path, content); \
1713 if (content != NULL) { \
1714 g_ptr_array_add (data->strings_to_free, content); \
1719 /* Load html files */
1720 LOAD_CONST ("Content.html", data->content_html);
1721 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1722 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1723 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1724 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1725 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1726 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1727 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1728 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1729 LOAD_CONST ("Status.html", data->status_html);
1730 LOAD ("Template.html", template_html);
1731 LOAD ("Footer.html", footer_html);
1736 /* HTML fallbacks: If we have at least content OR in_content, then
1737 * everything else gets a fallback */
1739 #define FALLBACK(html, fallback) \
1740 if (html == NULL) { \
1744 /* in_nextcontent -> in_content -> content */
1745 FALLBACK (data->in_content_html, data->content_html);
1746 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1748 /* context -> content */
1749 FALLBACK (data->in_context_html, data->in_content_html);
1750 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1751 FALLBACK (data->out_context_html, data->out_content_html);
1752 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1755 FALLBACK (data->out_content_html, data->in_content_html);
1756 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1757 FALLBACK (data->out_context_html, data->in_context_html);
1758 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1760 /* status -> in_content */
1761 FALLBACK (data->status_html, data->in_content_html);
1765 /* template -> empathy's template */
1766 data->custom_template = (template_html != NULL);
1767 if (template_html == NULL) {
1768 tmp = empathy_file_lookup ("Template.html", "data");
1769 g_file_get_contents (tmp, &template_html, NULL, NULL);
1773 /* Default avatar */
1774 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1775 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1776 data->default_incoming_avatar_filename = tmp;
1780 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1781 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1782 data->default_outgoing_avatar_filename = tmp;
1787 variant_path = adium_info_dup_path_for_variant (info,
1788 adium_info_get_default_or_first_variant (info));
1790 /* Old custom templates had only 4 parameters.
1791 * New templates have 5 parameters */
1792 if (data->version <= 2 && data->custom_template) {
1793 tmp = string_with_format (template_html,
1796 "", /* The header */
1797 footer_html ? footer_html : "",
1800 tmp = string_with_format (template_html,
1802 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1804 "", /* The header */
1805 footer_html ? footer_html : "",
1808 g_ptr_array_add (data->strings_to_free, tmp);
1809 data->template_html = tmp;
1811 g_free (template_html);
1812 g_free (footer_html);
1813 g_free (variant_path);
1819 empathy_adium_data_new (const gchar *path)
1821 EmpathyAdiumData *data;
1824 info = empathy_adium_info_new (path);
1825 data = empathy_adium_data_new_with_info (path, info);
1826 g_hash_table_unref (info);
1832 empathy_adium_data_ref (EmpathyAdiumData *data)
1834 g_return_val_if_fail (data != NULL, NULL);
1836 g_atomic_int_inc (&data->ref_count);
1842 empathy_adium_data_unref (EmpathyAdiumData *data)
1844 g_return_if_fail (data != NULL);
1846 if (g_atomic_int_dec_and_test (&data->ref_count)) {
1847 g_free (data->path);
1848 g_free (data->basedir);
1849 g_free (data->default_avatar_filename);
1850 g_free (data->default_incoming_avatar_filename);
1851 g_free (data->default_outgoing_avatar_filename);
1852 g_hash_table_unref (data->info);
1853 g_ptr_array_unref (data->strings_to_free);
1855 g_slice_free (EmpathyAdiumData, data);
1860 empathy_adium_data_get_info (EmpathyAdiumData *data)
1862 g_return_val_if_fail (data != NULL, NULL);
1868 empathy_adium_data_get_path (EmpathyAdiumData *data)
1870 g_return_val_if_fail (data != NULL, NULL);