/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2008-2009 Collabora Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * Authors: Xavier Claessens */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "empathy-theme-adium.h" #include "empathy-smiley-manager.h" #include "empathy-ui-utils.h" #include "empathy-plist.h" #include "empathy-images.h" #include "empathy-webkit-utils.h" #define DEBUG_FLAG EMPATHY_DEBUG_CHAT #include #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium) #define BORING_DPI_DEFAULT 96 /* "Join" consecutive messages with timestamps within five minutes */ #define MESSAGE_JOIN_PERIOD 5*60 typedef struct { EmpathyAdiumData *data; EmpathySmileyManager *smiley_manager; EmpathyContact *last_contact; gint64 last_timestamp; gboolean last_is_backlog; guint pages_loading; /* Queue of QueuedItem*s containing an EmpathyMessage or string */ GQueue message_queue; /* Queue of guint32 of pending message id to remove unread * marker for when we lose focus. */ GQueue acked_messages; GtkWidget *inspector_window; GSettings *gsettings_chat; gboolean has_focus; gboolean has_unread_message; gboolean allow_scrolling; gchar *variant; gboolean in_construction; } EmpathyThemeAdiumPriv; struct _EmpathyAdiumData { gint ref_count; gchar *path; gchar *basedir; gchar *default_avatar_filename; gchar *default_incoming_avatar_filename; gchar *default_outgoing_avatar_filename; GHashTable *info; guint version; gboolean custom_template; /* gchar* -> gchar* both owned */ GHashTable *date_format_cache; /* HTML bits */ const gchar *template_html; const gchar *content_html; const gchar *in_content_html; const gchar *in_context_html; const gchar *in_nextcontent_html; const gchar *in_nextcontext_html; const gchar *out_content_html; const gchar *out_context_html; const gchar *out_nextcontent_html; const gchar *out_nextcontext_html; const gchar *status_html; /* Above html strings are pointers to strings stored in this array. * We do this because of fallbacks, some htmls could be pointing the * same string. */ GPtrArray *strings_to_free; }; static void theme_adium_iface_init (EmpathyChatViewIface *iface); static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant); enum { PROP_0, PROP_ADIUM_DATA, PROP_VARIANT, }; G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium, WEBKIT_TYPE_WEB_VIEW, G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW, theme_adium_iface_init)); enum { QUEUED_EVENT, QUEUED_MESSAGE, QUEUED_EDIT }; typedef struct { guint type; EmpathyMessage *msg; char *str; } QueuedItem; static QueuedItem * queue_item (GQueue *queue, guint type, EmpathyMessage *msg, const char *str) { QueuedItem *item = g_slice_new0 (QueuedItem); item->type = type; if (msg != NULL) item->msg = g_object_ref (msg); item->str = g_strdup (str); g_queue_push_tail (queue, item); return item; } static void free_queued_item (QueuedItem *item) { tp_clear_object (&item->msg); g_free (item->str); g_slice_free (QueuedItem, item); } static void theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme); gboolean enable_webkit_developer_tools; enable_webkit_developer_tools = g_settings_get_boolean ( priv->gsettings_chat, EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS); g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)), "enable-developer-extras", enable_webkit_developer_tools, NULL); } static void theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings, const gchar *key, gpointer user_data) { EmpathyThemeAdium *theme = user_data; theme_adium_update_enable_webkit_developer_tools (theme); } static gboolean theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view, WebKitWebFrame *web_frame, WebKitNetworkRequest *request, WebKitWebNavigationAction *action, WebKitWebPolicyDecision *decision, gpointer data) { const gchar *uri; /* Only call url_show on clicks */ if (webkit_web_navigation_action_get_reason (action) != WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) { webkit_web_policy_decision_use (decision); return TRUE; } uri = webkit_network_request_get_uri (request); empathy_url_show (GTK_WIDGET (view), uri); webkit_web_policy_decision_ignore (decision); return TRUE; } static void theme_adium_copy_address_cb (GtkMenuItem *menuitem, gpointer user_data) { WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data); gchar *uri; GtkClipboard *clipboard; g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL); clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); gtk_clipboard_set_text (clipboard, uri, -1); clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); gtk_clipboard_set_text (clipboard, uri, -1); g_free (uri); } static void theme_adium_open_address_cb (GtkMenuItem *menuitem, gpointer user_data) { WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data); gchar *uri; g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL); empathy_url_show (GTK_WIDGET (menuitem), uri); g_free (uri); } /* Replace each %@ in format with string passed in args */ static gchar * string_with_format (const gchar *format, const gchar *first_string, ...) { va_list args; const gchar *str; GString *result; va_start (args, first_string); result = g_string_sized_new (strlen (format)); for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) { const gchar *next; next = strstr (format, "%@"); if (next == NULL) { break; } g_string_append_len (result, format, next - format); g_string_append (result, str); format = next + 2; } g_string_append (result, format); va_end (args); return g_string_free (result, FALSE); } static void theme_adium_load_template (EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); gchar *basedir_uri; gchar *variant_path; gchar *template; priv->pages_loading++; basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL); variant_path = adium_info_dup_path_for_variant (priv->data->info, priv->variant); template = string_with_format (priv->data->template_html, variant_path, NULL); webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme), template, basedir_uri); g_free (basedir_uri); g_free (variant_path); g_free (template); } static gchar * theme_adium_parse_body (EmpathyThemeAdium *self, const gchar *text, const gchar *token) { EmpathyThemeAdiumPriv *priv = GET_PRIV (self); EmpathyStringParser *parsers; GString *string; /* Check if we have to parse smileys */ parsers = empathy_webkit_get_string_parser ( g_settings_get_boolean (priv->gsettings_chat, EMPATHY_PREFS_CHAT_SHOW_SMILEYS)); /* Parse text and construct string with links and smileys replaced * by html tags. Also escape text to make sure html code is * displayed verbatim. */ string = g_string_sized_new (strlen (text)); /* wrap this in HTML that allows us to find the message for later * editing */ if (!tp_str_empty (token)) g_string_append_printf (string, "", token); empathy_string_parser_substr (text, -1, parsers, string); if (!tp_str_empty (token)) g_string_append (string, ""); /* Wrap body in order to make tabs and multiple spaces displayed * properly. See bug #625745. */ g_string_prepend (string, "
"); g_string_append (string, "
"); return g_string_free (string, FALSE); } static void escape_and_append_len (GString *string, const gchar *str, gint len) { while (str != NULL && *str != '\0' && len != 0) { switch (*str) { case '\\': /* \ becomes \\ */ g_string_append (string, "\\\\"); break; case '\"': /* " becomes \" */ g_string_append (string, "\\\""); break; case '\n': /* Remove end of lines */ break; default: g_string_append_c (string, *str); } str++; len--; } } /* If *str starts with match, returns TRUE and move pointer to the end */ static gboolean theme_adium_match (const gchar **str, const gchar *match) { gint len; len = strlen (match); if (strncmp (*str, match, len) == 0) { *str += len - 1; return TRUE; } return FALSE; } /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */ static gboolean theme_adium_match_with_format (const gchar **str, const gchar *match, gchar **format) { const gchar *cur = *str; const gchar *end; if (!theme_adium_match (&cur, match)) { return FALSE; } cur++; end = strstr (cur, "}%"); if (!end) { return FALSE; } *format = g_strndup (cur , end - cur); *str = end + 1; return TRUE; } /* List of colors used by %senderColor%. Copied from * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m */ static gchar *colors[] = { "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey", "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod", "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen", "lightblue", "lightcoral", "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive", "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise", "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "yellowgreen", }; static const gchar * nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate) { /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/) * to strftime supported by g_date_time_format. * FIXME: table is incomplete, doc of g_date_time_format has a table of * supported tags. * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but * in 2.29.x we have to explictely request padding with %0x */ static const gchar *convert_table[] = { "a", "%p", // AM/PM "A", NULL, // 0~86399999 (Millisecond of Day) "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat "cc", "%u", // 1~7 (Day of Week) "c", "%u", // 1~7 (Day of Week) "dd", "%d", // 1~31 (0 padded Day of Month) "d", "%d", // 1~31 (0 padded Day of Month) "D", "%j", // 1~366 (0 padded Day of Year) "e", "%u", // 1~7 (0 padded Day of Week) "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday) "g", NULL, // Julian Day Number (number of days since 4713 BC January 1) "GGGG", NULL, // Before Christ/Anno Domini "GGG", NULL, // BC/AD (Era Designator Abbreviated) "GG", NULL, // BC/AD (Era Designator Abbreviated) "G", NULL, // BC/AD (Era Designator Abbreviated) "h", "%I", // 1~12 (0 padded Hour (12hr)) "H", "%H", // 0~23 (0 padded Hour (24hr)) "k", NULL, // 1~24 (0 padded Hour (24hr) "K", NULL, // 0~11 (0 padded Hour (12hr)) "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec "LL", "%m", // 1~12 (0 padded Month) "L", "%m", // 1~12 (0 padded Month) "m", "%M", // 0~59 (0 padded Minute) "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec "MM", "%m", // 1~12 (0 padded Month) "M", "%m", // 1~12 (0 padded Month) "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter "qqq", NULL, // Q1/Q2/Q3/Q4 "qq", NULL, // 1~4 (0 padded Quarter) "q", NULL, // 1~4 (0 padded Quarter) "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter "QQQ", NULL, // Q1/Q2/Q3/Q4 "QQ", NULL, // 1~4 (0 padded Quarter) "Q", NULL, // 1~4 (0 padded Quarter) "s", "%S", // 0~59 (0 padded Second) "S", NULL, // (rounded Sub-Second) "u", "%Y", // (0 padded Year) "vvvv", "%Z", // (General GMT Timezone Name) "vvv", "%Z", // (General GMT Timezone Abbreviation) "vv", "%Z", // (General GMT Timezone Abbreviation) "v", "%Z", // (General GMT Timezone Abbreviation) "w", "%W", // 1~53 (0 padded Week of Year, 1st day of week = Sunday, NB, 1st week of year starts from the last Sunday of last year) "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday) "yyyy", "%Y", // (Full Year) "yyy", "%y", // (2 Digits Year) "yy", "%y", // (2 Digits Year) "y", "%Y", // (Full Year) "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year) "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year) "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year) "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year) "zzzz", NULL, // (Specific GMT Timezone Name) "zzz", NULL, // (Specific GMT Timezone Abbreviation) "zz", NULL, // (Specific GMT Timezone Abbreviation) "z", NULL, // (Specific GMT Timezone Abbreviation) "Z", "%z", // +0000 (RFC 822 Timezone) }; const gchar *str; GString *string; guint i, j; if (nsdate == NULL) { return NULL; } str = g_hash_table_lookup (data->date_format_cache, nsdate); if (str != NULL) { return str; } /* Copy nsdate into string, replacing occurences of NSDateFormatter tags * by corresponding strftime tag. */ string = g_string_sized_new (strlen (nsdate)); for (i = 0; nsdate[i] != '\0'; i++) { gboolean found = FALSE; /* even indexes are NSDateFormatter tag, odd indexes are the * corresponding strftime tag */ for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) { if (g_str_has_prefix (nsdate + i, convert_table[j])) { found = TRUE; break; } } if (found) { /* If we don't have a replacement, just ignore that tag */ if (convert_table[j + 1] != NULL) { g_string_append (string, convert_table[j + 1]); } i += strlen (convert_table[j]) - 1; } else { g_string_append_c (string, nsdate[i]); } } DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str); /* The cache takes ownership of string->str */ g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str); return g_string_free (string, FALSE); } static void theme_adium_append_html (EmpathyThemeAdium *theme, const gchar *func, const gchar *html, const gchar *message, const gchar *avatar_filename, const gchar *name, const gchar *contact_id, const gchar *service_name, const gchar *message_classes, gint64 timestamp, gboolean is_backlog) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); GString *string; const gchar *cur = NULL; gchar *script; /* Make some search-and-replace in the html code */ string = g_string_sized_new (strlen (html) + strlen (message)); g_string_append_printf (string, "%s(\"", func); for (cur = html; *cur != '\0'; cur++) { const gchar *replace = NULL; gchar *dup_replace = NULL; gchar *format = NULL; /* Those are all well known keywords that needs replacement in * html files. Please keep them in the same order than the adium * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */ if (theme_adium_match (&cur, "%userIconPath%")) { replace = avatar_filename; } else if (theme_adium_match (&cur, "%senderScreenName%")) { replace = contact_id; } else if (theme_adium_match (&cur, "%sender%")) { replace = name; } else if (theme_adium_match (&cur, "%senderColor%")) { /* A color derived from the user's name. * FIXME: If a colon separated list of HTML colors is at * Incoming/SenderColors.txt it will be used instead of * the default colors. */ if (contact_id != NULL) { guint hash = g_str_hash (contact_id); replace = colors[hash % G_N_ELEMENTS (colors)]; } } else if (theme_adium_match (&cur, "%senderStatusIcon%")) { /* FIXME: The path to the status icon of the sender * (available, away, etc...) */ } else if (theme_adium_match (&cur, "%messageDirection%")) { /* FIXME: The text direction of the message * (either rtl or ltr) */ } else if (theme_adium_match (&cur, "%senderDisplayName%")) { /* FIXME: The serverside (remotely set) name of the * sender, such as an MSN display name. * * We don't have access to that yet so we use * local alias instead. */ replace = name; } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) { /* FIXME: This keyword is used to represent the * highlight background color. "X" is the opacity of the * background, ranges from 0 to 1 and can be any decimal * between. */ } else if (theme_adium_match (&cur, "%message%")) { replace = message; } else if (theme_adium_match (&cur, "%time%") || theme_adium_match_with_format (&cur, "%time{", &format)) { const gchar *strftime_format; strftime_format = nsdate_to_strftime (priv->data, format); if (is_backlog) { dup_replace = empathy_time_to_string_local (timestamp, strftime_format ? strftime_format : EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT); } else { dup_replace = empathy_time_to_string_local (timestamp, strftime_format ? strftime_format : EMPATHY_TIME_FORMAT_DISPLAY_SHORT); } replace = dup_replace; } else if (theme_adium_match (&cur, "%shortTime%")) { dup_replace = empathy_time_to_string_local (timestamp, EMPATHY_TIME_FORMAT_DISPLAY_SHORT); replace = dup_replace; } else if (theme_adium_match (&cur, "%service%")) { replace = service_name; } else if (theme_adium_match (&cur, "%variant%")) { /* FIXME: The name of the active message style variant, * with all spaces replaced with an underscore. * A variant named "Alternating Messages - Blue Red" * will become "Alternating_Messages_-_Blue_Red". */ } else if (theme_adium_match (&cur, "%userIcons%")) { /* FIXME: mus t be "hideIcons" if use preference is set * to hide avatars */ replace = "showIcons"; } else if (theme_adium_match (&cur, "%messageClasses%")) { replace = message_classes; } else if (theme_adium_match (&cur, "%status%")) { /* FIXME: A description of the status event. This is * neither in the user's local language nor expected to * be displayed; it may be useful to use a different div * class to present different types of status messages. * The following is a list of some of the more important * status messages; your message style should be able to * handle being shown a status message not in this list, * as even at present the list is incomplete and is * certain to become out of date in the future: * online * offline * away * away_message * return_away * idle * return_idle * date_separator * contact_joined (group chats) * contact_left * error * timed_out * encryption (all OTR messages use this status) * purple (all IRC topic and join/part messages use this status) * fileTransferStarted * fileTransferCompleted */ } else { escape_and_append_len (string, cur, 1); continue; } /* Here we have a replacement to make */ escape_and_append_len (string, replace, -1); g_free (dup_replace); g_free (format); } g_string_append (string, "\")"); script = g_string_free (string, FALSE); webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script); g_free (script); } static void theme_adium_append_event_escaped (EmpathyChatView *view, const gchar *escaped) { EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view); EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); theme_adium_append_html (theme, "appendMessage", priv->data->status_html, escaped, NULL, NULL, NULL, NULL, "event", empathy_time_get_current (), FALSE); /* There is no last contact */ if (priv->last_contact) { g_object_unref (priv->last_contact); priv->last_contact = NULL; } } static void theme_adium_remove_focus_marks (EmpathyThemeAdium *theme, WebKitDOMNodeList *nodes) { guint i; /* Remove focus and firstFocus class */ for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) { WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i); WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node); gchar *class_name; gchar **classes, **iter; GString *new_class_name; gboolean first = TRUE; if (element == NULL) { continue; } class_name = webkit_dom_html_element_get_class_name (element); classes = g_strsplit (class_name, " ", -1); new_class_name = g_string_sized_new (strlen (class_name)); for (iter = classes; *iter != NULL; iter++) { if (tp_strdiff (*iter, "focus") && tp_strdiff (*iter, "firstFocus")) { if (!first) { g_string_append_c (new_class_name, ' '); } g_string_append (new_class_name, *iter); first = FALSE; } } webkit_dom_html_element_set_class_name (element, new_class_name->str); g_free (class_name); g_strfreev (classes); g_string_free (new_class_name, TRUE); } } static void theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); WebKitDOMDocument *dom; WebKitDOMNodeList *nodes; GError *error = NULL; if (!priv->has_unread_message) return; priv->has_unread_message = FALSE; dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme)); if (dom == NULL) { return; } /* Get all nodes with focus class */ nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error); if (nodes == NULL) { DEBUG ("Error getting focus nodes: %s", error ? error->message : "No error"); g_clear_error (&error); return; } theme_adium_remove_focus_marks (theme, nodes); } static void theme_adium_append_message (EmpathyChatView *view, EmpathyMessage *msg) { EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view); EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); EmpathyContact *sender; TpMessage *tp_msg; TpAccount *account; gchar *body_escaped; const gchar *name; const gchar *contact_id; EmpathyAvatar *avatar; const gchar *avatar_filename = NULL; gint64 timestamp; const gchar *html = NULL; const gchar *func; const gchar *service_name; GString *message_classes = NULL; gboolean is_backlog; gboolean consecutive; gboolean action; if (priv->pages_loading != 0) { queue_item (&priv->message_queue, QUEUED_MESSAGE, msg, NULL); return; } /* Get information */ sender = empathy_message_get_sender (msg); account = empathy_contact_get_account (sender); service_name = empathy_protocol_name_to_display_name (tp_account_get_protocol (account)); if (service_name == NULL) service_name = tp_account_get_protocol (account); timestamp = empathy_message_get_timestamp (msg); body_escaped = theme_adium_parse_body (theme, empathy_message_get_body (msg), empathy_message_get_token (msg)); name = empathy_contact_get_logged_alias (sender); contact_id = empathy_contact_get_id (sender); action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION); /* If this is a /me probably */ if (action) { gchar *str; if (priv->data->version >= 4 || !priv->data->custom_template) { str = g_strdup_printf ("%s" "%s", name, body_escaped); } else { str = g_strdup_printf ("*%s*", body_escaped); } g_free (body_escaped); body_escaped = str; } /* Get the avatar filename, or a fallback */ avatar = empathy_contact_get_avatar (sender); if (avatar) { avatar_filename = avatar->filename; } if (!avatar_filename) { if (empathy_contact_is_user (sender)) { avatar_filename = priv->data->default_outgoing_avatar_filename; } else { avatar_filename = priv->data->default_incoming_avatar_filename; } if (!avatar_filename) { if (!priv->data->default_avatar_filename) { priv->data->default_avatar_filename = empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT, GTK_ICON_SIZE_DIALOG); } avatar_filename = priv->data->default_avatar_filename; } } /* We want to join this message with the last one if * - senders are the same contact, * - last message was recieved recently, * - last message and this message both are/aren't backlog, and * - DisableCombineConsecutive is not set in theme's settings */ is_backlog = empathy_message_is_backlog (msg); consecutive = empathy_contact_equal (priv->last_contact, sender) && (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) && (is_backlog == priv->last_is_backlog) && !tp_asv_get_boolean (priv->data->info, "DisableCombineConsecutive", NULL); /* Define message classes */ message_classes = g_string_new ("message"); if (!priv->has_focus && !is_backlog) { if (!priv->has_unread_message) { g_string_append (message_classes, " firstFocus"); priv->has_unread_message = TRUE; } g_string_append (message_classes, " focus"); } if (is_backlog) { g_string_append (message_classes, " history"); } if (consecutive) { g_string_append (message_classes, " consecutive"); } if (empathy_contact_is_user (sender)) { g_string_append (message_classes, " outgoing"); } else { g_string_append (message_classes, " incoming"); } if (empathy_message_should_highlight (msg)) { g_string_append (message_classes, " mention"); } if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) { g_string_append (message_classes, " autoreply"); } if (action) { g_string_append (message_classes, " action"); } /* FIXME: other classes: * status - the message is a status change * event - the message is a notification of something happening * (for example, encryption being turned on) * %status% - See %status% in theme_adium_append_html () */ /* This is slightly a hack, but it's the only way to add * arbitrary data to messages in the HTML. We add another * class called "x-empathy-message-id-*" to the message. This * way, we can remove the unread marker for this specific * message later. */ tp_msg = empathy_message_get_tp_message (msg); if (tp_msg != NULL) { guint32 id; gboolean valid; id = tp_message_get_pending_message_id (tp_msg, &valid); if (valid) { g_string_append_printf (message_classes, " x-empathy-message-id-%u", id); } } /* Define javascript function to use */ if (consecutive) { func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll"; } else { func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll"; } if (empathy_contact_is_user (sender)) { /* out */ if (is_backlog) { /* context */ html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html; } else { /* content */ html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html; } /* remove all the unread marks when we are sending a message */ theme_adium_remove_all_focus_marks (theme); } else { /* in */ if (is_backlog) { /* context */ html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html; } else { /* content */ html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html; } } theme_adium_append_html (theme, func, html, body_escaped, avatar_filename, name, contact_id, service_name, message_classes->str, timestamp, is_backlog); /* Keep the sender of the last displayed message */ if (priv->last_contact) { g_object_unref (priv->last_contact); } priv->last_contact = g_object_ref (sender); priv->last_timestamp = timestamp; priv->last_is_backlog = is_backlog; g_free (body_escaped); g_string_free (message_classes, TRUE); } static void theme_adium_append_event (EmpathyChatView *view, const gchar *str) { EmpathyThemeAdiumPriv *priv = GET_PRIV (view); gchar *str_escaped; if (priv->pages_loading != 0) { queue_item (&priv->message_queue, QUEUED_EVENT, NULL, str); return; } str_escaped = g_markup_escape_text (str, -1); theme_adium_append_event_escaped (view, str_escaped); g_free (str_escaped); } static void theme_adium_edit_message (EmpathyChatView *view, EmpathyMessage *message) { EmpathyThemeAdiumPriv *priv = GET_PRIV (view); WebKitDOMDocument *doc; WebKitDOMElement *span; gchar *id, *parsed_body; gchar *tooltip, *timestamp; GtkIconInfo *icon_info; GError *error = NULL; if (priv->pages_loading != 0) { queue_item (&priv->message_queue, QUEUED_EDIT, message, NULL); return; } id = g_strdup_printf ("message-token-%s", empathy_message_get_supersedes (message)); /* we don't pass a token here, because doing so will return another * element, and we don't want nested elements */ parsed_body = theme_adium_parse_body (EMPATHY_THEME_ADIUM (view), empathy_message_get_body (message), NULL); /* find the element */ doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view)); span = webkit_dom_document_get_element_by_id (doc, id); if (span == NULL) { DEBUG ("Failed to find id '%s'", id); goto except; } if (!WEBKIT_DOM_IS_HTML_ELEMENT (span)) { DEBUG ("Not a HTML element"); goto except; } /* update the HTML */ webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span), parsed_body, &error); if (error != NULL) { DEBUG ("Error setting new inner-HTML: %s", error->message); g_error_free (error); goto except; } /* set a tooltip */ timestamp = empathy_time_to_string_local ( empathy_message_get_timestamp (message), "%H:%M:%S"); tooltip = g_strdup_printf (_("Message edited at %s"), timestamp); webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span), tooltip); g_free (tooltip); g_free (timestamp); /* mark this message as edited */ icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (), EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0); if (icon_info != NULL) { /* set the icon as a background image using CSS * FIXME: the icon won't update in response to theme changes */ gchar *style = g_strdup_printf ( "background-image:url('%s');" "background-repeat:no-repeat;" "background-position:left center;" "padding-left:19px;", /* 16px icon + 3px padding */ gtk_icon_info_get_filename (icon_info)); webkit_dom_element_set_attribute (span, "style", style, &error); if (error != NULL) { DEBUG ("Error setting element style: %s", error->message); g_clear_error (&error); /* not fatal */ } g_free (style); gtk_icon_info_free (icon_info); } goto finally; except: DEBUG ("Could not find message to edit with: %s", empathy_message_get_body (message)); finally: g_free (id); g_free (parsed_body); } static void theme_adium_scroll (EmpathyChatView *view, gboolean allow_scrolling) { EmpathyThemeAdiumPriv *priv = GET_PRIV (view); priv->allow_scrolling = allow_scrolling; if (allow_scrolling) { empathy_chat_view_scroll_down (view); } } static void theme_adium_scroll_down (EmpathyChatView *view) { webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);"); } static gboolean theme_adium_get_has_selection (EmpathyChatView *view) { return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view)); } static void theme_adium_clear (EmpathyChatView *view) { EmpathyThemeAdiumPriv *priv = GET_PRIV (view); theme_adium_load_template (EMPATHY_THEME_ADIUM (view)); /* Clear last contact to avoid trying to add a 'joined' * message when we don't have an insertion point. */ if (priv->last_contact) { g_object_unref (priv->last_contact); priv->last_contact = NULL; } } static gboolean theme_adium_find_previous (EmpathyChatView *view, const gchar *search_criteria, gboolean new_search, gboolean match_case) { /* FIXME: Doesn't respect new_search */ return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view), search_criteria, match_case, FALSE, TRUE); } static gboolean theme_adium_find_next (EmpathyChatView *view, const gchar *search_criteria, gboolean new_search, gboolean match_case) { /* FIXME: Doesn't respect new_search */ return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view), search_criteria, match_case, TRUE, TRUE); } static void theme_adium_find_abilities (EmpathyChatView *view, const gchar *search_criteria, gboolean match_case, gboolean *can_do_previous, gboolean *can_do_next) { /* FIXME: Does webkit provide an API for that? We have wrap=true in * find_next and find_previous to work around this problem. */ if (can_do_previous) *can_do_previous = TRUE; if (can_do_next) *can_do_next = TRUE; } static void theme_adium_highlight (EmpathyChatView *view, const gchar *text, gboolean match_case) { webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view)); webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view), text, match_case, 0); webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view), TRUE); } static void theme_adium_copy_clipboard (EmpathyChatView *view) { webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view)); } static void theme_adium_remove_mark_from_message (EmpathyThemeAdium *self, guint32 id) { WebKitDOMDocument *dom; WebKitDOMNodeList *nodes; gchar *class; GError *error = NULL; dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self)); if (dom == NULL) { return; } class = g_strdup_printf (".x-empathy-message-id-%u", id); /* Get all nodes with focus class */ nodes = webkit_dom_document_query_selector_all (dom, class, &error); g_free (class); if (nodes == NULL) { DEBUG ("Error getting focus nodes: %s", error ? error->message : "No error"); g_clear_error (&error); return; } theme_adium_remove_focus_marks (self, nodes); } static void theme_adium_remove_acked_message_unread_mark_foreach (gpointer data, gpointer user_data) { EmpathyThemeAdium *self = user_data; guint32 id = GPOINTER_TO_UINT (data); theme_adium_remove_mark_from_message (self, id); } static void theme_adium_focus_toggled (EmpathyChatView *view, gboolean has_focus) { EmpathyThemeAdiumPriv *priv = GET_PRIV (view); priv->has_focus = has_focus; if (!priv->has_focus) { /* We've lost focus, so let's make sure all the acked * messages have lost their unread marker. */ g_queue_foreach (&priv->acked_messages, theme_adium_remove_acked_message_unread_mark_foreach, view); g_queue_clear (&priv->acked_messages); priv->has_unread_message = FALSE; } } static void theme_adium_message_acknowledged (EmpathyChatView *view, EmpathyMessage *message) { EmpathyThemeAdium *self = (EmpathyThemeAdium *) view; EmpathyThemeAdiumPriv *priv = GET_PRIV (view); TpMessage *tp_msg; guint32 id; gboolean valid; tp_msg = empathy_message_get_tp_message (message); if (tp_msg == NULL) { return; } id = tp_message_get_pending_message_id (tp_msg, &valid); if (!valid) { g_warning ("Acknoledged message doesn't have a pending ID"); return; } /* We only want to actually remove the unread marker if the * view doesn't have focus. If we did it all the time we would * never see the unread markers, ever! So, we'll queue these * up, and when we lose focus, we'll remove the markers. */ if (priv->has_focus) { g_queue_push_tail (&priv->acked_messages, GUINT_TO_POINTER (id)); return; } theme_adium_remove_mark_from_message (self, id); } static void theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data) { WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data); g_object_unref (hit_test_result); } static void theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event) { WebKitWebView *view = WEBKIT_WEB_VIEW (theme); WebKitHitTestResult *hit_test_result; WebKitHitTestResultContext context; GtkWidget *menu; GtkWidget *item; hit_test_result = webkit_web_view_get_hit_test_result (view, event); g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL); /* The menu */ menu = empathy_context_menu_new (GTK_WIDGET (view)); /* Select all item */ item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); g_signal_connect_swapped (item, "activate", G_CALLBACK (webkit_web_view_select_all), view); /* Copy menu item */ if (webkit_web_view_can_copy_clipboard (view)) { item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); g_signal_connect_swapped (item, "activate", G_CALLBACK (webkit_web_view_copy_clipboard), view); } /* Clear menu item */ item = gtk_separator_menu_item_new (); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); g_signal_connect_swapped (item, "activate", G_CALLBACK (empathy_chat_view_clear), view); /* We will only add the following menu items if we are * right-clicking a link */ if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) { /* Separator */ item = gtk_separator_menu_item_new (); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); /* Copy Link Address menu item */ item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address")); g_signal_connect (item, "activate", G_CALLBACK (theme_adium_copy_address_cb), hit_test_result); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); /* Open Link menu item */ item = gtk_menu_item_new_with_mnemonic (_("_Open Link")); g_signal_connect (item, "activate", G_CALLBACK (theme_adium_open_address_cb), hit_test_result); gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); } g_signal_connect (GTK_MENU_SHELL (menu), "selection-done", G_CALLBACK (theme_adium_context_menu_selection_done_cb), hit_test_result); /* Display the menu */ gtk_widget_show_all (menu); gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, event->button, event->time); } static gboolean theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event) { if (event->button == 3) { gboolean developer_tools_enabled; g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))), "enable-developer-extras", &developer_tools_enabled, NULL); /* We currently have no way to add an inspector menu * item ourselves, so we disable our customized menu * if the developer extras are enabled. */ if (!developer_tools_enabled) { theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event); return TRUE; } } return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event); } static void theme_adium_iface_init (EmpathyChatViewIface *iface) { iface->append_message = theme_adium_append_message; iface->append_event = theme_adium_append_event; iface->edit_message = theme_adium_edit_message; iface->scroll = theme_adium_scroll; iface->scroll_down = theme_adium_scroll_down; iface->get_has_selection = theme_adium_get_has_selection; iface->clear = theme_adium_clear; iface->find_previous = theme_adium_find_previous; iface->find_next = theme_adium_find_next; iface->find_abilities = theme_adium_find_abilities; iface->highlight = theme_adium_highlight; iface->copy_clipboard = theme_adium_copy_clipboard; iface->focus_toggled = theme_adium_focus_toggled; iface->message_acknowledged = theme_adium_message_acknowledged; } static void theme_adium_load_finished_cb (WebKitWebView *view, WebKitWebFrame *frame, gpointer user_data) { EmpathyThemeAdiumPriv *priv = GET_PRIV (view); EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view); GList *l; DEBUG ("Page loaded"); priv->pages_loading--; if (priv->pages_loading != 0) return; /* Display queued messages */ for (l = priv->message_queue.head; l != NULL; l = l->next) { QueuedItem *item = l->data; switch (item->type) { case QUEUED_MESSAGE: theme_adium_append_message (chat_view, item->msg); break; case QUEUED_EDIT: theme_adium_edit_message (chat_view, item->msg); break; case QUEUED_EVENT: theme_adium_append_event (chat_view, item->str); break; } free_queued_item (item); } g_queue_clear (&priv->message_queue); } static void theme_adium_finalize (GObject *object) { EmpathyThemeAdiumPriv *priv = GET_PRIV (object); empathy_adium_data_unref (priv->data); g_object_unref (priv->gsettings_chat); G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object); } static void theme_adium_dispose (GObject *object) { EmpathyThemeAdiumPriv *priv = GET_PRIV (object); if (priv->smiley_manager) { g_object_unref (priv->smiley_manager); priv->smiley_manager = NULL; } if (priv->last_contact) { g_object_unref (priv->last_contact); priv->last_contact = NULL; } if (priv->inspector_window) { gtk_widget_destroy (priv->inspector_window); priv->inspector_window = NULL; } if (priv->acked_messages.length > 0) { g_queue_clear (&priv->acked_messages); } G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object); } static gboolean theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector, EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); if (priv->inspector_window) { gtk_widget_show_all (priv->inspector_window); } return TRUE; } static gboolean theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector, EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); if (priv->inspector_window) { gtk_widget_hide (priv->inspector_window); } return TRUE; } static WebKitWebView * theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector, WebKitWebView *web_view, EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); GtkWidget *scrolled_window; GtkWidget *inspector_web_view; if (!priv->inspector_window) { /* Create main window */ priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window), 800, 600); g_signal_connect (priv->inspector_window, "delete-event", G_CALLBACK (gtk_widget_hide_on_delete), NULL); /* Pack a scrolled window */ scrolled_window = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add (GTK_CONTAINER (priv->inspector_window), scrolled_window); gtk_widget_show (scrolled_window); /* Pack a webview in the scrolled window. That webview will be * used to render the inspector tool. */ inspector_web_view = webkit_web_view_new (); gtk_container_add (GTK_CONTAINER (scrolled_window), inspector_web_view); gtk_widget_show (scrolled_window); return WEBKIT_WEB_VIEW (inspector_web_view); } return NULL; } static PangoFontDescription * theme_adium_get_default_font (void) { GSettings *gsettings; PangoFontDescription *pango_fd; gchar *font_family; gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA); font_family = g_settings_get_string (gsettings, EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME); if (font_family == NULL) return NULL; pango_fd = pango_font_description_from_string (font_family); g_free (font_family); g_object_unref (gsettings); return pango_fd; } static void theme_adium_set_webkit_font (WebKitWebSettings *w_settings, const gchar *name, gint size) { g_object_set (w_settings, "default-font-family", name, NULL); g_object_set (w_settings, "default-font-size", size, NULL); } static void theme_adium_set_default_font (WebKitWebSettings *w_settings) { PangoFontDescription *default_font_desc; GdkScreen *current_screen; gdouble dpi = 0; gint pango_font_size = 0; default_font_desc = theme_adium_get_default_font (); if (default_font_desc == NULL) return ; pango_font_size = pango_font_description_get_size (default_font_desc) / PANGO_SCALE ; if (pango_font_description_get_size_is_absolute (default_font_desc)) { current_screen = gdk_screen_get_default (); if (current_screen != NULL) { dpi = gdk_screen_get_resolution (current_screen); } else { dpi = BORING_DPI_DEFAULT; } pango_font_size = (gint) (pango_font_size / (dpi / 72)); } theme_adium_set_webkit_font (w_settings, pango_font_description_get_family (default_font_desc), pango_font_size); pango_font_description_free (default_font_desc); } static void theme_adium_constructed (GObject *object) { EmpathyThemeAdiumPriv *priv = GET_PRIV (object); const gchar *font_family = NULL; gint font_size = 0; WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object); WebKitWebSettings *webkit_settings; WebKitWebInspector *webkit_inspector; /* Set default settings */ font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily"); font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL); webkit_settings = webkit_web_view_get_settings (webkit_view); if (font_family && font_size) { theme_adium_set_webkit_font (webkit_settings, font_family, font_size); } else { theme_adium_set_default_font (webkit_settings); } /* Setup webkit inspector */ webkit_inspector = webkit_web_view_get_inspector (webkit_view); g_signal_connect (webkit_inspector, "inspect-web-view", G_CALLBACK (theme_adium_inspect_web_view_cb), object); g_signal_connect (webkit_inspector, "show-window", G_CALLBACK (theme_adium_inspector_show_window_cb), object); g_signal_connect (webkit_inspector, "close-window", G_CALLBACK (theme_adium_inspector_close_window_cb), object); /* Load template */ theme_adium_load_template (EMPATHY_THEME_ADIUM (object)); priv->in_construction = FALSE; } static void theme_adium_get_property (GObject *object, guint param_id, GValue *value, GParamSpec *pspec) { EmpathyThemeAdiumPriv *priv = GET_PRIV (object); switch (param_id) { case PROP_ADIUM_DATA: g_value_set_boxed (value, priv->data); break; case PROP_VARIANT: g_value_set_string (value, priv->variant); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); break; }; } static void theme_adium_set_property (GObject *object, guint param_id, const GValue *value, GParamSpec *pspec) { EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object); EmpathyThemeAdiumPriv *priv = GET_PRIV (object); switch (param_id) { case PROP_ADIUM_DATA: g_assert (priv->data == NULL); priv->data = g_value_dup_boxed (value); break; case PROP_VARIANT: empathy_theme_adium_set_variant (theme, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); break; }; } static void empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass); object_class->finalize = theme_adium_finalize; object_class->dispose = theme_adium_dispose; object_class->constructed = theme_adium_constructed; object_class->get_property = theme_adium_get_property; object_class->set_property = theme_adium_set_property; widget_class->button_press_event = theme_adium_button_press_event; g_object_class_install_property (object_class, PROP_ADIUM_DATA, g_param_spec_boxed ("adium-data", "The theme data", "Data for the adium theme", EMPATHY_TYPE_ADIUM_DATA, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (object_class, PROP_VARIANT, g_param_spec_string ("variant", "The theme variant", "Variant name for the theme", NULL, G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv)); } static void empathy_theme_adium_init (EmpathyThemeAdium *theme) { EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme, EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv); theme->priv = priv; priv->in_construction = TRUE; g_queue_init (&priv->message_queue); priv->allow_scrolling = TRUE; priv->smiley_manager = empathy_smiley_manager_dup_singleton (); g_signal_connect (theme, "load-finished", G_CALLBACK (theme_adium_load_finished_cb), NULL); g_signal_connect (theme, "navigation-policy-decision-requested", G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb), NULL); priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA); g_signal_connect (priv->gsettings_chat, "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS, G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb), theme); theme_adium_update_enable_webkit_developer_tools (theme); } EmpathyThemeAdium * empathy_theme_adium_new (EmpathyAdiumData *data, const gchar *variant) { g_return_val_if_fail (data != NULL, NULL); return g_object_new (EMPATHY_TYPE_THEME_ADIUM, "adium-data", data, "variant", variant, NULL); } void empathy_theme_adium_set_variant (EmpathyThemeAdium *theme, const gchar *variant) { EmpathyThemeAdiumPriv *priv = GET_PRIV (theme); gchar *variant_path; gchar *script; if (!tp_strdiff (priv->variant, variant)) { return; } g_free (priv->variant); priv->variant = g_strdup (variant); if (priv->in_construction) { return; } DEBUG ("Update view with variant: '%s'", variant); variant_path = adium_info_dup_path_for_variant (priv->data->info, priv->variant); script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path); webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script); g_free (variant_path); g_free (script); g_object_notify (G_OBJECT (theme), "variant"); } gboolean empathy_adium_path_is_valid (const gchar *path) { gboolean ret; gchar *file; /* The theme is not valid if there is no Info.plist */ file = g_build_filename (path, "Contents", "Info.plist", NULL); ret = g_file_test (file, G_FILE_TEST_EXISTS); g_free (file); if (!ret) return FALSE; /* We ship a default Template.html as fallback if there is any problem * with the one inside the theme. The only other required file is * Content.html OR Incoming/Content.html*/ file = g_build_filename (path, "Contents", "Resources", "Content.html", NULL); ret = g_file_test (file, G_FILE_TEST_EXISTS); g_free (file); if (!ret) { file = g_build_filename (path, "Contents", "Resources", "Incoming", "Content.html", NULL); ret = g_file_test (file, G_FILE_TEST_EXISTS); g_free (file); } return ret; } GHashTable * empathy_adium_info_new (const gchar *path) { gchar *file; GValue *value; GHashTable *info = NULL; g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL); file = g_build_filename (path, "Contents", "Info.plist", NULL); value = empathy_plist_parse_from_file (file); g_free (file); if (value == NULL) return NULL; info = g_value_dup_boxed (value); tp_g_value_slice_free (value); /* Insert the theme's path into the hash table, * keys have to be dupped */ tp_asv_set_string (info, g_strdup ("path"), path); return info; } static guint adium_info_get_version (GHashTable *info) { return tp_asv_get_int32 (info, "MessageViewVersion", NULL); } static const gchar * adium_info_get_no_variant_name (GHashTable *info) { const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant"); return name ? name : _("Normal"); } static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant) { guint version = adium_info_get_version (info); const gchar *no_variant = adium_info_get_no_variant_name (info); GPtrArray *variants; guint i; if (version <= 2 && !tp_strdiff (variant, no_variant)) { return g_strdup ("main.css"); } /* Verify the variant exists, fallback to the first one */ variants = empathy_adium_info_get_available_variants (info); for (i = 0; i < variants->len; i++) { if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) { break; } } if (i == variants->len) { DEBUG ("Variant %s does not exist", variant); variant = g_ptr_array_index (variants, 0); } return g_strdup_printf ("Variants/%s.css", variant); } const gchar * empathy_adium_info_get_default_variant (GHashTable *info) { if (adium_info_get_version (info) <= 2) { return adium_info_get_no_variant_name (info); } return tp_asv_get_string (info, "DefaultVariant"); } GPtrArray * empathy_adium_info_get_available_variants (GHashTable *info) { GPtrArray *variants; const gchar *path; gchar *dirpath; GDir *dir; variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY); if (variants != NULL) { return variants; } variants = g_ptr_array_new_with_free_func (g_free); tp_asv_take_boxed (info, g_strdup ("AvailableVariants"), G_TYPE_PTR_ARRAY, variants); path = tp_asv_get_string (info, "path"); dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL); dir = g_dir_open (dirpath, 0, NULL); if (dir != NULL) { const gchar *name; for (name = g_dir_read_name (dir); name != NULL; name = g_dir_read_name (dir)) { gchar *display_name; if (!g_str_has_suffix (name, ".css")) { continue; } display_name = g_strdup (name); strstr (display_name, ".css")[0] = '\0'; g_ptr_array_add (variants, display_name); } g_dir_close (dir); } g_free (dirpath); if (adium_info_get_version (info) <= 2) { g_ptr_array_add (variants, g_strdup (adium_info_get_no_variant_name (info))); } return variants; } GType empathy_adium_data_get_type (void) { static GType type_id = 0; if (!type_id) { type_id = g_boxed_type_register_static ("EmpathyAdiumData", (GBoxedCopyFunc) empathy_adium_data_ref, (GBoxedFreeFunc) empathy_adium_data_unref); } return type_id; } EmpathyAdiumData * empathy_adium_data_new_with_info (const gchar *path, GHashTable *info) { EmpathyAdiumData *data; gchar *template_html = NULL; gchar *footer_html = NULL; gchar *tmp; g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL); data = g_slice_new0 (EmpathyAdiumData); data->ref_count = 1; data->path = g_strdup (path); data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents" G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL); data->info = g_hash_table_ref (info); data->version = adium_info_get_version (info); data->strings_to_free = g_ptr_array_new_with_free_func (g_free); data->date_format_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); DEBUG ("Loading theme at %s", path); #define LOAD(path, var) \ tmp = g_build_filename (data->basedir, path, NULL); \ g_file_get_contents (tmp, &var, NULL, NULL); \ g_free (tmp); \ #define LOAD_CONST(path, var) \ { \ gchar *content; \ LOAD (path, content); \ if (content != NULL) { \ g_ptr_array_add (data->strings_to_free, content); \ } \ var = content; \ } /* Load html files */ LOAD_CONST ("Content.html", data->content_html); LOAD_CONST ("Incoming/Content.html", data->in_content_html); LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html); LOAD_CONST ("Incoming/Context.html", data->in_context_html); LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html); LOAD_CONST ("Outgoing/Content.html", data->out_content_html); LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html); LOAD_CONST ("Outgoing/Context.html", data->out_context_html); LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html); LOAD_CONST ("Status.html", data->status_html); LOAD ("Template.html", template_html); LOAD ("Footer.html", footer_html); #undef LOAD_CONST #undef LOAD /* HTML fallbacks: If we have at least content OR in_content, then * everything else gets a fallback */ #define FALLBACK(html, fallback) \ if (html == NULL) { \ html = fallback; \ } /* in_nextcontent -> in_content -> content */ FALLBACK (data->in_content_html, data->content_html); FALLBACK (data->in_nextcontent_html, data->in_content_html); /* context -> content */ FALLBACK (data->in_context_html, data->in_content_html); FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html); FALLBACK (data->out_context_html, data->out_content_html); FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html); /* out -> in */ FALLBACK (data->out_content_html, data->in_content_html); FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html); FALLBACK (data->out_context_html, data->in_context_html); FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html); /* status -> in_content */ FALLBACK (data->status_html, data->in_content_html); #undef FALLBACK /* template -> empathy's template */ data->custom_template = (template_html != NULL); if (template_html == NULL) { tmp = empathy_file_lookup ("Template.html", "data"); g_file_get_contents (tmp, &template_html, NULL, NULL); g_free (tmp); } /* Default avatar */ tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL); if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { data->default_incoming_avatar_filename = tmp; } else { g_free (tmp); } tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL); if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) { data->default_outgoing_avatar_filename = tmp; } else { g_free (tmp); } /* Old custom templates had only 4 parameters. * New templates have 5 parameters */ if (data->version <= 2 && data->custom_template) { tmp = string_with_format (template_html, data->basedir, "%@", /* Leave variant unset */ "", /* The header */ footer_html ? footer_html : "", NULL); } else { tmp = string_with_format (template_html, data->basedir, data->version <= 2 ? "" : "@import url( \"main.css\" );", "%@", /* Leave variant unset */ "", /* The header */ footer_html ? footer_html : "", NULL); } g_ptr_array_add (data->strings_to_free, tmp); data->template_html = tmp; g_free (template_html); g_free (footer_html); return data; } EmpathyAdiumData * empathy_adium_data_new (const gchar *path) { EmpathyAdiumData *data; GHashTable *info; info = empathy_adium_info_new (path); data = empathy_adium_data_new_with_info (path, info); g_hash_table_unref (info); return data; } EmpathyAdiumData * empathy_adium_data_ref (EmpathyAdiumData *data) { g_return_val_if_fail (data != NULL, NULL); g_atomic_int_inc (&data->ref_count); return data; } void empathy_adium_data_unref (EmpathyAdiumData *data) { g_return_if_fail (data != NULL); if (g_atomic_int_dec_and_test (&data->ref_count)) { g_free (data->path); g_free (data->basedir); g_free (data->default_avatar_filename); g_free (data->default_incoming_avatar_filename); g_free (data->default_outgoing_avatar_filename); g_hash_table_unref (data->info); g_ptr_array_unref (data->strings_to_free); tp_clear_pointer (&data->date_format_cache, g_hash_table_unref); g_slice_free (EmpathyAdiumData, data); } } GHashTable * empathy_adium_data_get_info (EmpathyAdiumData *data) { g_return_val_if_fail (data != NULL, NULL); return data->info; } const gchar * empathy_adium_data_get_path (EmpathyAdiumData *data) { g_return_val_if_fail (data != NULL, NULL); return data->path; }