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);
1013 priv->has_unread_message = FALSE;
1018 theme_adium_message_acknowledged (EmpathyChatView *view,
1019 EmpathyMessage *message)
1021 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1022 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1025 tp_msg = empathy_message_get_tp_message (message);
1027 if (tp_msg == NULL) {
1031 /* We only want to actually remove the unread marker if the
1032 * view doesn't have focus. If we did it all the time we would
1033 * never see the unread markers, ever! So, we'll queue these
1034 * up, and when we lose focus, we'll remove the markers. */
1035 if (priv->has_focus) {
1036 g_queue_push_tail (&priv->acked_messages,
1037 g_strdup (tp_message_get_token (tp_msg)));
1041 theme_adium_remove_mark_from_message (self,
1042 tp_message_get_token (tp_msg));
1046 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1048 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1050 g_object_unref (hit_test_result);
1054 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1056 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1057 WebKitHitTestResult *hit_test_result;
1058 WebKitHitTestResultContext context;
1062 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1063 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1066 menu = empathy_context_menu_new (GTK_WIDGET (view));
1068 /* Select all item */
1069 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1070 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1072 g_signal_connect_swapped (item, "activate",
1073 G_CALLBACK (webkit_web_view_select_all),
1076 /* Copy menu item */
1077 if (webkit_web_view_can_copy_clipboard (view)) {
1078 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1079 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1081 g_signal_connect_swapped (item, "activate",
1082 G_CALLBACK (webkit_web_view_copy_clipboard),
1086 /* Clear menu item */
1087 item = gtk_separator_menu_item_new ();
1088 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1090 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1091 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1093 g_signal_connect_swapped (item, "activate",
1094 G_CALLBACK (empathy_chat_view_clear),
1097 /* We will only add the following menu items if we are
1098 * right-clicking a link */
1099 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1101 item = gtk_separator_menu_item_new ();
1102 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1104 /* Copy Link Address menu item */
1105 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1106 g_signal_connect (item, "activate",
1107 G_CALLBACK (theme_adium_copy_address_cb),
1109 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1111 /* Open Link menu item */
1112 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1113 g_signal_connect (item, "activate",
1114 G_CALLBACK (theme_adium_open_address_cb),
1116 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1119 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1120 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1123 /* Display the menu */
1124 gtk_widget_show_all (menu);
1125 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1126 event->button, event->time);
1130 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1132 if (event->button == 3) {
1133 gboolean developer_tools_enabled;
1135 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1136 "enable-developer-extras", &developer_tools_enabled, NULL);
1138 /* We currently have no way to add an inspector menu
1139 * item ourselves, so we disable our customized menu
1140 * if the developer extras are enabled. */
1141 if (!developer_tools_enabled) {
1142 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1147 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1151 theme_adium_iface_init (EmpathyChatViewIface *iface)
1153 iface->append_message = theme_adium_append_message;
1154 iface->append_event = theme_adium_append_event;
1155 iface->scroll = theme_adium_scroll;
1156 iface->scroll_down = theme_adium_scroll_down;
1157 iface->get_has_selection = theme_adium_get_has_selection;
1158 iface->clear = theme_adium_clear;
1159 iface->find_previous = theme_adium_find_previous;
1160 iface->find_next = theme_adium_find_next;
1161 iface->find_abilities = theme_adium_find_abilities;
1162 iface->highlight = theme_adium_highlight;
1163 iface->copy_clipboard = theme_adium_copy_clipboard;
1164 iface->focus_toggled = theme_adium_focus_toggled;
1165 iface->message_acknowledged = theme_adium_message_acknowledged;
1169 theme_adium_load_finished_cb (WebKitWebView *view,
1170 WebKitWebFrame *frame,
1173 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1174 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1177 DEBUG ("Page loaded");
1178 priv->pages_loading--;
1180 if (priv->pages_loading != 0)
1183 /* Display queued messages */
1184 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1185 GValue *value = l->data;
1187 if (G_VALUE_HOLDS_OBJECT (value)) {
1188 theme_adium_append_message (chat_view,
1189 g_value_get_object (value));
1191 theme_adium_append_event (chat_view,
1192 g_value_get_string (value));
1195 tp_g_value_slice_free (value);
1197 g_queue_clear (&priv->message_queue);
1201 theme_adium_finalize (GObject *object)
1203 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1205 empathy_adium_data_unref (priv->data);
1206 g_object_unref (priv->gsettings_chat);
1208 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1212 theme_adium_dispose (GObject *object)
1214 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1216 if (priv->smiley_manager) {
1217 g_object_unref (priv->smiley_manager);
1218 priv->smiley_manager = NULL;
1221 if (priv->last_contact) {
1222 g_object_unref (priv->last_contact);
1223 priv->last_contact = NULL;
1226 if (priv->inspector_window) {
1227 gtk_widget_destroy (priv->inspector_window);
1228 priv->inspector_window = NULL;
1231 if (priv->acked_messages.length > 0) {
1232 g_queue_foreach (&priv->acked_messages, (GFunc) g_free, NULL);
1233 g_queue_clear (&priv->acked_messages);
1236 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1240 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1241 EmpathyThemeAdium *theme)
1243 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1245 if (priv->inspector_window) {
1246 gtk_widget_show_all (priv->inspector_window);
1253 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1254 EmpathyThemeAdium *theme)
1256 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1258 if (priv->inspector_window) {
1259 gtk_widget_hide (priv->inspector_window);
1265 static WebKitWebView *
1266 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1267 WebKitWebView *web_view,
1268 EmpathyThemeAdium *theme)
1270 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1271 GtkWidget *scrolled_window;
1272 GtkWidget *inspector_web_view;
1274 if (!priv->inspector_window) {
1275 /* Create main window */
1276 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1277 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1279 g_signal_connect (priv->inspector_window, "delete-event",
1280 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1282 /* Pack a scrolled window */
1283 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1284 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1285 GTK_POLICY_AUTOMATIC,
1286 GTK_POLICY_AUTOMATIC);
1287 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1289 gtk_widget_show (scrolled_window);
1291 /* Pack a webview in the scrolled window. That webview will be
1292 * used to render the inspector tool. */
1293 inspector_web_view = webkit_web_view_new ();
1294 gtk_container_add (GTK_CONTAINER (scrolled_window),
1295 inspector_web_view);
1296 gtk_widget_show (scrolled_window);
1298 return WEBKIT_WEB_VIEW (inspector_web_view);
1304 static PangoFontDescription *
1305 theme_adium_get_default_font (void)
1307 GSettings *gsettings;
1308 PangoFontDescription *pango_fd;
1311 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1313 font_family = g_settings_get_string (gsettings,
1314 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1316 if (font_family == NULL)
1319 pango_fd = pango_font_description_from_string (font_family);
1320 g_free (font_family);
1321 g_object_unref (gsettings);
1326 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1330 g_object_set (w_settings, "default-font-family", name, NULL);
1331 g_object_set (w_settings, "default-font-size", size, NULL);
1335 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1337 PangoFontDescription *default_font_desc;
1338 GdkScreen *current_screen;
1340 gint pango_font_size = 0;
1342 default_font_desc = theme_adium_get_default_font ();
1343 if (default_font_desc == NULL)
1345 pango_font_size = pango_font_description_get_size (default_font_desc)
1347 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1348 current_screen = gdk_screen_get_default ();
1349 if (current_screen != NULL) {
1350 dpi = gdk_screen_get_resolution (current_screen);
1352 dpi = BORING_DPI_DEFAULT;
1354 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1356 theme_adium_set_webkit_font (w_settings,
1357 pango_font_description_get_family (default_font_desc),
1359 pango_font_description_free (default_font_desc);
1363 theme_adium_constructed (GObject *object)
1365 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1367 const gchar *font_family = NULL;
1369 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1370 WebKitWebSettings *webkit_settings;
1371 WebKitWebInspector *webkit_inspector;
1373 /* Set default settings */
1374 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1375 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1376 webkit_settings = webkit_web_view_get_settings (webkit_view);
1378 if (font_family && font_size) {
1379 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1381 theme_adium_set_default_font (webkit_settings);
1384 /* Setup webkit inspector */
1385 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1386 g_signal_connect (webkit_inspector, "inspect-web-view",
1387 G_CALLBACK (theme_adium_inspect_web_view_cb),
1389 g_signal_connect (webkit_inspector, "show-window",
1390 G_CALLBACK (theme_adium_inspector_show_window_cb),
1392 g_signal_connect (webkit_inspector, "close-window",
1393 G_CALLBACK (theme_adium_inspector_close_window_cb),
1397 priv->pages_loading = 1;
1399 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
1400 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object),
1401 priv->data->template_html,
1403 g_free (basedir_uri);
1407 theme_adium_get_property (GObject *object,
1412 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1415 case PROP_ADIUM_DATA:
1416 g_value_set_boxed (value, priv->data);
1419 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1425 theme_adium_set_property (GObject *object,
1427 const GValue *value,
1430 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1433 case PROP_ADIUM_DATA:
1434 g_assert (priv->data == NULL);
1435 priv->data = g_value_dup_boxed (value);
1438 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1444 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1446 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1447 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1449 object_class->finalize = theme_adium_finalize;
1450 object_class->dispose = theme_adium_dispose;
1451 object_class->constructed = theme_adium_constructed;
1452 object_class->get_property = theme_adium_get_property;
1453 object_class->set_property = theme_adium_set_property;
1455 widget_class->button_press_event = theme_adium_button_press_event;
1457 g_object_class_install_property (object_class,
1459 g_param_spec_boxed ("adium-data",
1461 "Data for the adium theme",
1462 EMPATHY_TYPE_ADIUM_DATA,
1463 G_PARAM_CONSTRUCT_ONLY |
1465 G_PARAM_STATIC_STRINGS));
1467 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1471 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1473 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1474 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1478 g_queue_init (&priv->message_queue);
1479 priv->allow_scrolling = TRUE;
1480 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1482 g_signal_connect (theme, "load-finished",
1483 G_CALLBACK (theme_adium_load_finished_cb),
1485 g_signal_connect (theme, "navigation-policy-decision-requested",
1486 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1489 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1490 g_signal_connect (priv->gsettings_chat,
1491 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1492 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1495 theme_adium_update_enable_webkit_developer_tools (theme);
1499 empathy_theme_adium_new (EmpathyAdiumData *data)
1501 g_return_val_if_fail (data != NULL, NULL);
1503 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1509 empathy_adium_path_is_valid (const gchar *path)
1514 /* The theme is not valid if there is no Info.plist */
1515 file = g_build_filename (path, "Contents", "Info.plist",
1517 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1523 /* We ship a default Template.html as fallback if there is any problem
1524 * with the one inside the theme. The only other required file is
1525 * Content.html OR Incoming/Content.html*/
1526 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1528 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1532 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1533 "Content.html", NULL);
1534 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1542 empathy_adium_info_new (const gchar *path)
1546 GHashTable *info = NULL;
1548 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1550 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1551 value = empathy_plist_parse_from_file (file);
1557 info = g_value_dup_boxed (value);
1558 tp_g_value_slice_free (value);
1560 /* Insert the theme's path into the hash table,
1561 * keys have to be dupped */
1562 tp_asv_set_string (info, g_strdup ("path"), path);
1568 adium_info_get_version (GHashTable *info)
1570 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1573 static const gchar *
1574 adium_info_get_no_variant_name (GHashTable *info)
1576 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1577 return name ? name : _("Normal");
1580 static const gchar *
1581 adium_info_get_default_or_first_variant (GHashTable *info)
1584 GPtrArray *variants;
1586 name = empathy_adium_info_get_default_variant (info);
1591 variants = empathy_adium_info_get_available_variants (info);
1592 g_assert (variants->len > 0);
1593 return g_ptr_array_index (variants, 0);
1597 adium_info_dup_path_for_variant (GHashTable *info,
1598 const gchar *variant)
1600 guint version = adium_info_get_version (info);
1601 const gchar *no_variant = adium_info_get_no_variant_name (info);
1603 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1604 return g_strdup ("main.css");
1607 return g_strdup_printf ("Variants/%s.css", variant);
1612 empathy_adium_info_get_default_variant (GHashTable *info)
1614 if (adium_info_get_version (info) <= 2) {
1615 return adium_info_get_no_variant_name (info);
1618 return tp_asv_get_string (info, "DefaultVariant");
1622 empathy_adium_info_get_available_variants (GHashTable *info)
1624 GPtrArray *variants;
1629 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1630 if (variants != NULL) {
1634 variants = g_ptr_array_new_with_free_func (g_free);
1635 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1636 G_TYPE_PTR_ARRAY, variants);
1638 path = tp_asv_get_string (info, "path");
1639 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1640 dir = g_dir_open (dirpath, 0, NULL);
1644 for (name = g_dir_read_name (dir);
1646 name = g_dir_read_name (dir)) {
1647 gchar *display_name;
1649 if (!g_str_has_suffix (name, ".css")) {
1653 display_name = g_strdup (name);
1654 strstr (display_name, ".css")[0] = '\0';
1655 g_ptr_array_add (variants, display_name);
1661 if (adium_info_get_version (info) <= 2) {
1662 g_ptr_array_add (variants,
1663 g_strdup (adium_info_get_no_variant_name (info)));
1670 empathy_adium_data_get_type (void)
1672 static GType type_id = 0;
1676 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1677 (GBoxedCopyFunc) empathy_adium_data_ref,
1678 (GBoxedFreeFunc) empathy_adium_data_unref);
1685 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1687 EmpathyAdiumData *data;
1688 gchar *template_html = NULL;
1689 gchar *footer_html = NULL;
1690 gchar *variant_path;
1693 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1695 data = g_slice_new0 (EmpathyAdiumData);
1696 data->ref_count = 1;
1697 data->path = g_strdup (path);
1698 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1699 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1700 data->info = g_hash_table_ref (info);
1701 data->version = adium_info_get_version (info);
1702 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1704 DEBUG ("Loading theme at %s", path);
1706 #define LOAD(path, var) \
1707 tmp = g_build_filename (data->basedir, path, NULL); \
1708 g_file_get_contents (tmp, &var, NULL, NULL); \
1711 #define LOAD_CONST(path, var) \
1714 LOAD (path, content); \
1715 if (content != NULL) { \
1716 g_ptr_array_add (data->strings_to_free, content); \
1721 /* Load html files */
1722 LOAD_CONST ("Content.html", data->content_html);
1723 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1724 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1725 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1726 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1727 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1728 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1729 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1730 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1731 LOAD_CONST ("Status.html", data->status_html);
1732 LOAD ("Template.html", template_html);
1733 LOAD ("Footer.html", footer_html);
1738 /* HTML fallbacks: If we have at least content OR in_content, then
1739 * everything else gets a fallback */
1741 #define FALLBACK(html, fallback) \
1742 if (html == NULL) { \
1746 /* in_nextcontent -> in_content -> content */
1747 FALLBACK (data->in_content_html, data->content_html);
1748 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1750 /* context -> content */
1751 FALLBACK (data->in_context_html, data->in_content_html);
1752 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1753 FALLBACK (data->out_context_html, data->out_content_html);
1754 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1757 FALLBACK (data->out_content_html, data->in_content_html);
1758 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1759 FALLBACK (data->out_context_html, data->in_context_html);
1760 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1762 /* status -> in_content */
1763 FALLBACK (data->status_html, data->in_content_html);
1767 /* template -> empathy's template */
1768 data->custom_template = (template_html != NULL);
1769 if (template_html == NULL) {
1770 tmp = empathy_file_lookup ("Template.html", "data");
1771 g_file_get_contents (tmp, &template_html, NULL, NULL);
1775 /* Default avatar */
1776 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1777 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1778 data->default_incoming_avatar_filename = tmp;
1782 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1783 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1784 data->default_outgoing_avatar_filename = tmp;
1789 variant_path = adium_info_dup_path_for_variant (info,
1790 adium_info_get_default_or_first_variant (info));
1792 /* Old custom templates had only 4 parameters.
1793 * New templates have 5 parameters */
1794 if (data->version <= 2 && data->custom_template) {
1795 tmp = string_with_format (template_html,
1798 "", /* The header */
1799 footer_html ? footer_html : "",
1802 tmp = string_with_format (template_html,
1804 data->version <= 2 ? "" : "@import url( \"main.css\" );",
1806 "", /* The header */
1807 footer_html ? footer_html : "",
1810 g_ptr_array_add (data->strings_to_free, tmp);
1811 data->template_html = tmp;
1813 g_free (template_html);
1814 g_free (footer_html);
1815 g_free (variant_path);
1821 empathy_adium_data_new (const gchar *path)
1823 EmpathyAdiumData *data;
1826 info = empathy_adium_info_new (path);
1827 data = empathy_adium_data_new_with_info (path, info);
1828 g_hash_table_unref (info);
1834 empathy_adium_data_ref (EmpathyAdiumData *data)
1836 g_return_val_if_fail (data != NULL, NULL);
1838 g_atomic_int_inc (&data->ref_count);
1844 empathy_adium_data_unref (EmpathyAdiumData *data)
1846 g_return_if_fail (data != NULL);
1848 if (g_atomic_int_dec_and_test (&data->ref_count)) {
1849 g_free (data->path);
1850 g_free (data->basedir);
1851 g_free (data->default_avatar_filename);
1852 g_free (data->default_incoming_avatar_filename);
1853 g_free (data->default_outgoing_avatar_filename);
1854 g_hash_table_unref (data->info);
1855 g_ptr_array_unref (data->strings_to_free);
1857 g_slice_free (EmpathyAdiumData, data);
1862 empathy_adium_data_get_info (EmpathyAdiumData *data)
1864 g_return_val_if_fail (data != NULL, NULL);
1870 empathy_adium_data_get_path (EmpathyAdiumData *data)
1872 g_return_val_if_fail (data != NULL, NULL);