Port empathy-call to GtkApplication
[empathy-mirror.git] / libempathy-gtk / empathy-theme-adium.c
blob0370b90551ccb8fda272160c32fdcc5fab651e1f
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
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>
22 #include "config.h"
24 #include <string.h>
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>
32 #include <gdk/gdk.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
55 typedef struct {
56 EmpathyAdiumData *data;
57 EmpathySmileyManager *smiley_manager;
58 EmpathyContact *last_contact;
59 gint64 last_timestamp;
60 gboolean last_is_backlog;
61 guint pages_loading;
62 /* Queue of GValue* containing an EmpathyMessage or string */
63 GQueue message_queue;
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;
69 gboolean has_focus;
70 gboolean has_unread_message;
71 gboolean allow_scrolling;
72 gchar *variant;
73 gboolean in_construction;
74 } EmpathyThemeAdiumPriv;
76 struct _EmpathyAdiumData {
77 gint ref_count;
78 gchar *path;
79 gchar *basedir;
80 gchar *default_avatar_filename;
81 gchar *default_incoming_avatar_filename;
82 gchar *default_outgoing_avatar_filename;
83 GHashTable *info;
84 guint version;
85 gboolean custom_template;
86 /* gchar* -> gchar* both owned */
87 GHashTable *date_format_cache;
89 /* HTML bits */
90 const gchar *template_html;
91 const gchar *content_html;
92 const gchar *in_content_html;
93 const gchar *in_context_html;
94 const gchar *in_nextcontent_html;
95 const gchar *in_nextcontext_html;
96 const gchar *out_content_html;
97 const gchar *out_context_html;
98 const gchar *out_nextcontent_html;
99 const gchar *out_nextcontext_html;
100 const gchar *status_html;
102 /* Above html strings are pointers to strings stored in this array.
103 * We do this because of fallbacks, some htmls could be pointing the
104 * same string. */
105 GPtrArray *strings_to_free;
108 static void theme_adium_iface_init (EmpathyChatViewIface *iface);
109 static gchar * adium_info_dup_path_for_variant (GHashTable *info, const gchar *variant);
111 enum {
112 PROP_0,
113 PROP_ADIUM_DATA,
114 PROP_VARIANT,
117 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium, empathy_theme_adium,
118 WEBKIT_TYPE_WEB_VIEW,
119 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
120 theme_adium_iface_init));
122 static void
123 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium *theme)
125 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
126 WebKitWebView *web_view = WEBKIT_WEB_VIEW (theme);
127 gboolean enable_webkit_developer_tools;
129 enable_webkit_developer_tools = g_settings_get_boolean (
130 priv->gsettings_chat,
131 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS);
133 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view)),
134 "enable-developer-extras",
135 enable_webkit_developer_tools,
136 NULL);
139 static void
140 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings *gsettings,
141 const gchar *key,
142 gpointer user_data)
144 EmpathyThemeAdium *theme = user_data;
146 theme_adium_update_enable_webkit_developer_tools (theme);
149 static gboolean
150 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView *view,
151 WebKitWebFrame *web_frame,
152 WebKitNetworkRequest *request,
153 WebKitWebNavigationAction *action,
154 WebKitWebPolicyDecision *decision,
155 gpointer data)
157 const gchar *uri;
159 /* Only call url_show on clicks */
160 if (webkit_web_navigation_action_get_reason (action) !=
161 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED) {
162 webkit_web_policy_decision_use (decision);
163 return TRUE;
166 uri = webkit_network_request_get_uri (request);
167 empathy_url_show (GTK_WIDGET (view), uri);
169 webkit_web_policy_decision_ignore (decision);
170 return TRUE;
173 static void
174 theme_adium_copy_address_cb (GtkMenuItem *menuitem,
175 gpointer user_data)
177 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
178 gchar *uri;
179 GtkClipboard *clipboard;
181 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
183 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
184 gtk_clipboard_set_text (clipboard, uri, -1);
186 clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
187 gtk_clipboard_set_text (clipboard, uri, -1);
189 g_free (uri);
192 static void
193 theme_adium_open_address_cb (GtkMenuItem *menuitem,
194 gpointer user_data)
196 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
197 gchar *uri;
199 g_object_get (G_OBJECT (hit_test_result), "link-uri", &uri, NULL);
201 empathy_url_show (GTK_WIDGET (menuitem), uri);
203 g_free (uri);
206 /* Replace each %@ in format with string passed in args */
207 static gchar *
208 string_with_format (const gchar *format,
209 const gchar *first_string,
210 ...)
212 va_list args;
213 const gchar *str;
214 GString *result;
216 va_start (args, first_string);
217 result = g_string_sized_new (strlen (format));
218 for (str = first_string; str != NULL; str = va_arg (args, const gchar *)) {
219 const gchar *next;
221 next = strstr (format, "%@");
222 if (next == NULL) {
223 break;
226 g_string_append_len (result, format, next - format);
227 g_string_append (result, str);
228 format = next + 2;
230 g_string_append (result, format);
231 va_end (args);
233 return g_string_free (result, FALSE);
236 static void
237 theme_adium_load_template (EmpathyThemeAdium *theme)
239 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
240 gchar *basedir_uri;
241 gchar *variant_path;
242 gchar *template;
244 priv->pages_loading++;
245 basedir_uri = g_strconcat ("file://", priv->data->basedir, NULL);
246 variant_path = adium_info_dup_path_for_variant (priv->data->info,
247 priv->variant);
248 template = string_with_format (priv->data->template_html,
249 variant_path, NULL);
250 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme),
251 template, basedir_uri);
252 g_free (basedir_uri);
253 g_free (variant_path);
254 g_free (template);
257 static void
258 theme_adium_match_newline (const gchar *text,
259 gssize len,
260 EmpathyStringReplace replace_func,
261 EmpathyStringParser *sub_parsers,
262 gpointer user_data)
264 GString *string = user_data;
265 gint i;
266 gint prev = 0;
268 if (len < 0) {
269 len = G_MAXSSIZE;
272 /* Replace \n by <br/> */
273 for (i = 0; i < len && text[i] != '\0'; i++) {
274 if (text[i] == '\n') {
275 empathy_string_parser_substr (text + prev,
276 i - prev, sub_parsers,
277 user_data);
278 g_string_append (string, "<br/>");
279 prev = i + 1;
282 empathy_string_parser_substr (text + prev, i - prev,
283 sub_parsers, user_data);
286 static void
287 theme_adium_replace_smiley (const gchar *text,
288 gssize len,
289 gpointer match_data,
290 gpointer user_data)
292 EmpathySmileyHit *hit = match_data;
293 GString *string = user_data;
295 /* Replace smiley by a <img/> tag */
296 g_string_append_printf (string,
297 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
298 hit->path, (int)len, text, (int)len, text);
301 static EmpathyStringParser string_parsers[] = {
302 {empathy_string_match_link, empathy_string_replace_link},
303 {theme_adium_match_newline, NULL},
304 {empathy_string_match_all, empathy_string_replace_escaped},
305 {NULL, NULL}
308 static EmpathyStringParser string_parsers_with_smiley[] = {
309 {empathy_string_match_link, empathy_string_replace_link},
310 {empathy_string_match_smiley, theme_adium_replace_smiley},
311 {theme_adium_match_newline, NULL},
312 {empathy_string_match_all, empathy_string_replace_escaped},
313 {NULL, NULL}
316 static gchar *
317 theme_adium_parse_body (EmpathyThemeAdium *self,
318 const gchar *text)
320 EmpathyThemeAdiumPriv *priv = GET_PRIV (self);
321 EmpathyStringParser *parsers;
322 GString *string;
324 /* Check if we have to parse smileys */
325 if (g_settings_get_boolean (priv->gsettings_chat,
326 EMPATHY_PREFS_CHAT_SHOW_SMILEYS))
327 parsers = string_parsers_with_smiley;
328 else
329 parsers = string_parsers;
331 /* Parse text and construct string with links and smileys replaced
332 * by html tags. Also escape text to make sure html code is
333 * displayed verbatim. */
334 string = g_string_sized_new (strlen (text));
335 empathy_string_parser_substr (text, -1, parsers, string);
337 /* Wrap body in order to make tabs and multiple spaces displayed
338 * properly. See bug #625745. */
339 g_string_prepend (string, "<div style=\"display: inline; "
340 "white-space: pre-wrap\"'>");
341 g_string_append (string, "</div>");
343 return g_string_free (string, FALSE);
346 static void
347 escape_and_append_len (GString *string, const gchar *str, gint len)
349 while (str != NULL && *str != '\0' && len != 0) {
350 switch (*str) {
351 case '\\':
352 /* \ becomes \\ */
353 g_string_append (string, "\\\\");
354 break;
355 case '\"':
356 /* " becomes \" */
357 g_string_append (string, "\\\"");
358 break;
359 case '\n':
360 /* Remove end of lines */
361 break;
362 default:
363 g_string_append_c (string, *str);
366 str++;
367 len--;
371 /* If *str starts with match, returns TRUE and move pointer to the end */
372 static gboolean
373 theme_adium_match (const gchar **str,
374 const gchar *match)
376 gint len;
378 len = strlen (match);
379 if (strncmp (*str, match, len) == 0) {
380 *str += len - 1;
381 return TRUE;
384 return FALSE;
387 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
388 static gboolean
389 theme_adium_match_with_format (const gchar **str,
390 const gchar *match,
391 gchar **format)
393 const gchar *cur = *str;
394 const gchar *end;
396 if (!theme_adium_match (&cur, match)) {
397 return FALSE;
399 cur++;
401 end = strstr (cur, "}%");
402 if (!end) {
403 return FALSE;
406 *format = g_strndup (cur , end - cur);
407 *str = end + 1;
408 return TRUE;
411 /* List of colors used by %senderColor%. Copied from
412 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
414 static gchar *colors[] = {
415 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
416 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
417 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
418 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
419 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
420 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
421 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
422 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
423 "lightblue", "lightcoral",
424 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
425 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
426 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
427 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
428 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
429 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
430 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
431 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
432 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
433 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
434 "yellowgreen",
437 static const gchar *
438 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
440 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
441 * to strftime supported by g_date_time_format.
442 * FIXME: table is incomplete, doc of g_date_time_format has a table of
443 * supported tags.
444 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
445 * in 2.29.x we have to explictely request padding with %0x */
446 static const gchar *convert_table[] = {
447 "a", "%p", // AM/PM
448 "A", NULL, // 0~86399999 (Millisecond of Day)
450 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
451 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
452 "cc", "%u", // 1~7 (Day of Week)
453 "c", "%u", // 1~7 (Day of Week)
455 "dd", "%d", // 1~31 (0 padded Day of Month)
456 "d", "%d", // 1~31 (0 padded Day of Month)
457 "D", "%j", // 1~366 (0 padded Day of Year)
459 "e", "%u", // 1~7 (0 padded Day of Week)
460 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
461 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
462 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
463 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
465 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
467 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
468 "GGGG", NULL, // Before Christ/Anno Domini
469 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
470 "GG", NULL, // BC/AD (Era Designator Abbreviated)
471 "G", NULL, // BC/AD (Era Designator Abbreviated)
473 "h", "%I", // 1~12 (0 padded Hour (12hr))
474 "H", "%H", // 0~23 (0 padded Hour (24hr))
476 "k", NULL, // 1~24 (0 padded Hour (24hr)
477 "K", NULL, // 0~11 (0 padded Hour (12hr))
479 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
480 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
481 "LL", "%m", // 1~12 (0 padded Month)
482 "L", "%m", // 1~12 (0 padded Month)
484 "m", "%M", // 0~59 (0 padded Minute)
485 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
486 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
487 "MM", "%m", // 1~12 (0 padded Month)
488 "M", "%m", // 1~12 (0 padded Month)
490 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
491 "qqq", NULL, // Q1/Q2/Q3/Q4
492 "qq", NULL, // 1~4 (0 padded Quarter)
493 "q", NULL, // 1~4 (0 padded Quarter)
494 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
495 "QQQ", NULL, // Q1/Q2/Q3/Q4
496 "QQ", NULL, // 1~4 (0 padded Quarter)
497 "Q", NULL, // 1~4 (0 padded Quarter)
499 "s", "%S", // 0~59 (0 padded Second)
500 "S", NULL, // (rounded Sub-Second)
502 "u", "%Y", // (0 padded Year)
504 "vvvv", "%Z", // (General GMT Timezone Name)
505 "vvv", "%Z", // (General GMT Timezone Abbreviation)
506 "vv", "%Z", // (General GMT Timezone Abbreviation)
507 "v", "%Z", // (General GMT Timezone Abbreviation)
509 "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)
510 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
512 "yyyy", "%Y", // (Full Year)
513 "yyy", "%y", // (2 Digits Year)
514 "yy", "%y", // (2 Digits Year)
515 "y", "%Y", // (Full Year)
516 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
517 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
518 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
519 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
521 "zzzz", NULL, // (Specific GMT Timezone Name)
522 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
523 "zz", NULL, // (Specific GMT Timezone Abbreviation)
524 "z", NULL, // (Specific GMT Timezone Abbreviation)
525 "Z", "%z", // +0000 (RFC 822 Timezone)
527 const gchar *str;
528 GString *string;
529 guint i, j;
531 if (nsdate == NULL) {
532 return NULL;
535 str = g_hash_table_lookup (data->date_format_cache, nsdate);
536 if (str != NULL) {
537 return str;
540 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
541 * by corresponding strftime tag. */
542 string = g_string_sized_new (strlen (nsdate));
543 for (i = 0; nsdate[i] != '\0'; i++) {
544 gboolean found = FALSE;
546 /* even indexes are NSDateFormatter tag, odd indexes are the
547 * corresponding strftime tag */
548 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2) {
549 if (g_str_has_prefix (nsdate + i, convert_table[j])) {
550 found = TRUE;
551 break;
554 if (found) {
555 /* If we don't have a replacement, just ignore that tag */
556 if (convert_table[j + 1] != NULL) {
557 g_string_append (string, convert_table[j + 1]);
559 i += strlen (convert_table[j]) - 1;
560 } else {
561 g_string_append_c (string, nsdate[i]);
565 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
567 /* The cache takes ownership of string->str */
568 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
569 return g_string_free (string, FALSE);
573 static void
574 theme_adium_append_html (EmpathyThemeAdium *theme,
575 const gchar *func,
576 const gchar *html,
577 const gchar *message,
578 const gchar *avatar_filename,
579 const gchar *name,
580 const gchar *contact_id,
581 const gchar *service_name,
582 const gchar *message_classes,
583 gint64 timestamp,
584 gboolean is_backlog)
586 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
587 GString *string;
588 const gchar *cur = NULL;
589 gchar *script;
591 /* Make some search-and-replace in the html code */
592 string = g_string_sized_new (strlen (html) + strlen (message));
593 g_string_append_printf (string, "%s(\"", func);
594 for (cur = html; *cur != '\0'; cur++) {
595 const gchar *replace = NULL;
596 gchar *dup_replace = NULL;
597 gchar *format = NULL;
599 /* Those are all well known keywords that needs replacement in
600 * html files. Please keep them in the same order than the adium
601 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
602 if (theme_adium_match (&cur, "%userIconPath%")) {
603 replace = avatar_filename;
604 } else if (theme_adium_match (&cur, "%senderScreenName%")) {
605 replace = contact_id;
606 } else if (theme_adium_match (&cur, "%sender%")) {
607 replace = name;
608 } else if (theme_adium_match (&cur, "%senderColor%")) {
609 /* A color derived from the user's name.
610 * FIXME: If a colon separated list of HTML colors is at
611 * Incoming/SenderColors.txt it will be used instead of
612 * the default colors.
614 if (contact_id != NULL) {
615 guint hash = g_str_hash (contact_id);
616 replace = colors[hash % G_N_ELEMENTS (colors)];
618 } else if (theme_adium_match (&cur, "%senderStatusIcon%")) {
619 /* FIXME: The path to the status icon of the sender
620 * (available, away, etc...)
622 } else if (theme_adium_match (&cur, "%messageDirection%")) {
623 /* FIXME: The text direction of the message
624 * (either rtl or ltr)
626 } else if (theme_adium_match (&cur, "%senderDisplayName%")) {
627 /* FIXME: The serverside (remotely set) name of the
628 * sender, such as an MSN display name.
630 * We don't have access to that yet so we use
631 * local alias instead.
633 replace = name;
634 } else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{", &format)) {
635 /* FIXME: This keyword is used to represent the
636 * highlight background color. "X" is the opacity of the
637 * background, ranges from 0 to 1 and can be any decimal
638 * between.
640 } else if (theme_adium_match (&cur, "%message%")) {
641 replace = message;
642 } else if (theme_adium_match (&cur, "%time%") ||
643 theme_adium_match_with_format (&cur, "%time{", &format)) {
644 const gchar *strftime_format;
646 strftime_format = nsdate_to_strftime (priv->data, format);
647 if (is_backlog) {
648 dup_replace = empathy_time_to_string_local (timestamp,
649 strftime_format ? strftime_format :
650 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT);
651 } else {
652 dup_replace = empathy_time_to_string_local (timestamp,
653 strftime_format ? strftime_format :
654 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
656 replace = dup_replace;
657 } else if (theme_adium_match (&cur, "%shortTime%")) {
658 dup_replace = empathy_time_to_string_local (timestamp,
659 EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
660 replace = dup_replace;
661 } else if (theme_adium_match (&cur, "%service%")) {
662 replace = service_name;
663 } else if (theme_adium_match (&cur, "%variant%")) {
664 /* FIXME: The name of the active message style variant,
665 * with all spaces replaced with an underscore.
666 * A variant named "Alternating Messages - Blue Red"
667 * will become "Alternating_Messages_-_Blue_Red".
669 } else if (theme_adium_match (&cur, "%userIcons%")) {
670 /* FIXME: mus t be "hideIcons" if use preference is set
671 * to hide avatars */
672 replace = "showIcons";
673 } else if (theme_adium_match (&cur, "%messageClasses%")) {
674 replace = message_classes;
675 } else if (theme_adium_match (&cur, "%status%")) {
676 /* FIXME: A description of the status event. This is
677 * neither in the user's local language nor expected to
678 * be displayed; it may be useful to use a different div
679 * class to present different types of status messages.
680 * The following is a list of some of the more important
681 * status messages; your message style should be able to
682 * handle being shown a status message not in this list,
683 * as even at present the list is incomplete and is
684 * certain to become out of date in the future:
685 * online
686 * offline
687 * away
688 * away_message
689 * return_away
690 * idle
691 * return_idle
692 * date_separator
693 * contact_joined (group chats)
694 * contact_left
695 * error
696 * timed_out
697 * encryption (all OTR messages use this status)
698 * purple (all IRC topic and join/part messages use this status)
699 * fileTransferStarted
700 * fileTransferCompleted
702 } else {
703 escape_and_append_len (string, cur, 1);
704 continue;
707 /* Here we have a replacement to make */
708 escape_and_append_len (string, replace, -1);
710 g_free (dup_replace);
711 g_free (format);
713 g_string_append (string, "\")");
715 script = g_string_free (string, FALSE);
716 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
717 g_free (script);
720 static void
721 theme_adium_append_event_escaped (EmpathyChatView *view,
722 const gchar *escaped)
724 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
725 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
727 theme_adium_append_html (theme, "appendMessage",
728 priv->data->status_html, escaped, NULL, NULL, NULL,
729 NULL, "event",
730 empathy_time_get_current (), FALSE);
732 /* There is no last contact */
733 if (priv->last_contact) {
734 g_object_unref (priv->last_contact);
735 priv->last_contact = NULL;
739 static void
740 theme_adium_remove_focus_marks (EmpathyThemeAdium *theme,
741 WebKitDOMNodeList *nodes)
743 guint i;
745 /* Remove focus and firstFocus class */
746 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++) {
747 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
748 WebKitDOMHTMLElement *element = WEBKIT_DOM_HTML_ELEMENT (node);
749 gchar *class_name;
750 gchar **classes, **iter;
751 GString *new_class_name;
752 gboolean first = TRUE;
754 if (element == NULL) {
755 continue;
758 class_name = webkit_dom_html_element_get_class_name (element);
759 classes = g_strsplit (class_name, " ", -1);
760 new_class_name = g_string_sized_new (strlen (class_name));
761 for (iter = classes; *iter != NULL; iter++) {
762 if (tp_strdiff (*iter, "focus") &&
763 tp_strdiff (*iter, "firstFocus")) {
764 if (!first) {
765 g_string_append_c (new_class_name, ' ');
767 g_string_append (new_class_name, *iter);
768 first = FALSE;
772 webkit_dom_html_element_set_class_name (element, new_class_name->str);
774 g_free (class_name);
775 g_strfreev (classes);
776 g_string_free (new_class_name, TRUE);
780 static void
781 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *theme)
783 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
784 WebKitDOMDocument *dom;
785 WebKitDOMNodeList *nodes;
786 GError *error = NULL;
788 if (!priv->has_unread_message)
789 return;
791 priv->has_unread_message = FALSE;
793 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme));
794 if (dom == NULL) {
795 return;
798 /* Get all nodes with focus class */
799 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
800 if (nodes == NULL) {
801 DEBUG ("Error getting focus nodes: %s",
802 error ? error->message : "No error");
803 g_clear_error (&error);
804 return;
807 theme_adium_remove_focus_marks (theme, nodes);
810 static void
811 theme_adium_append_message (EmpathyChatView *view,
812 EmpathyMessage *msg)
814 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (view);
815 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
816 EmpathyContact *sender;
817 TpMessage *tp_msg;
818 TpAccount *account;
819 gchar *body_escaped;
820 const gchar *body;
821 const gchar *name;
822 const gchar *contact_id;
823 EmpathyAvatar *avatar;
824 const gchar *avatar_filename = NULL;
825 gint64 timestamp;
826 const gchar *html = NULL;
827 const gchar *func;
828 const gchar *service_name;
829 GString *message_classes = NULL;
830 gboolean is_backlog;
831 gboolean consecutive;
832 gboolean action;
834 if (priv->pages_loading != 0) {
835 GValue *value = tp_g_value_slice_new (EMPATHY_TYPE_MESSAGE);
836 g_value_set_object (value, msg);
837 g_queue_push_tail (&priv->message_queue, value);
838 return;
841 /* Get information */
842 sender = empathy_message_get_sender (msg);
843 account = empathy_contact_get_account (sender);
844 service_name = empathy_protocol_name_to_display_name
845 (tp_account_get_protocol (account));
846 if (service_name == NULL)
847 service_name = tp_account_get_protocol (account);
848 timestamp = empathy_message_get_timestamp (msg);
849 body = empathy_message_get_body (msg);
850 body_escaped = theme_adium_parse_body (theme, body);
851 name = empathy_contact_get_alias (sender);
852 contact_id = empathy_contact_get_id (sender);
853 action = (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
855 /* If this is a /me probably */
856 if (action) {
857 gchar *str;
859 if (priv->data->version >= 4 || !priv->data->custom_template) {
860 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
861 "<span class='actionMessageBody'>%s</span>",
862 name, body_escaped);
863 } else {
864 str = g_strdup_printf ("*%s*", body_escaped);
866 g_free (body_escaped);
867 body_escaped = str;
870 /* Get the avatar filename, or a fallback */
871 avatar = empathy_contact_get_avatar (sender);
872 if (avatar) {
873 avatar_filename = avatar->filename;
875 if (!avatar_filename) {
876 if (empathy_contact_is_user (sender)) {
877 avatar_filename = priv->data->default_outgoing_avatar_filename;
878 } else {
879 avatar_filename = priv->data->default_incoming_avatar_filename;
881 if (!avatar_filename) {
882 if (!priv->data->default_avatar_filename) {
883 priv->data->default_avatar_filename =
884 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT,
885 GTK_ICON_SIZE_DIALOG);
887 avatar_filename = priv->data->default_avatar_filename;
891 /* We want to join this message with the last one if
892 * - senders are the same contact,
893 * - last message was recieved recently,
894 * - last message and this message both are/aren't backlog, and
895 * - DisableCombineConsecutive is not set in theme's settings */
896 is_backlog = empathy_message_is_backlog (msg);
897 consecutive = empathy_contact_equal (priv->last_contact, sender) &&
898 (timestamp - priv->last_timestamp < MESSAGE_JOIN_PERIOD) &&
899 (is_backlog == priv->last_is_backlog) &&
900 !tp_asv_get_boolean (priv->data->info,
901 "DisableCombineConsecutive", NULL);
903 /* Define message classes */
904 message_classes = g_string_new ("message");
905 if (!priv->has_focus && !is_backlog) {
906 if (!priv->has_unread_message) {
907 g_string_append (message_classes, " firstFocus");
908 priv->has_unread_message = TRUE;
910 g_string_append (message_classes, " focus");
912 if (is_backlog) {
913 g_string_append (message_classes, " history");
915 if (consecutive) {
916 g_string_append (message_classes, " consecutive");
918 if (empathy_contact_is_user (sender)) {
919 g_string_append (message_classes, " outgoing");
920 } else {
921 g_string_append (message_classes, " incoming");
923 if (empathy_message_should_highlight (msg)) {
924 g_string_append (message_classes, " mention");
926 if (empathy_message_get_tptype (msg) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY) {
927 g_string_append (message_classes, " autoreply");
929 if (action) {
930 g_string_append (message_classes, " action");
932 /* FIXME: other classes:
933 * status - the message is a status change
934 * event - the message is a notification of something happening
935 * (for example, encryption being turned on)
936 * %status% - See %status% in theme_adium_append_html ()
939 /* This is slightly a hack, but it's the only way to add
940 * arbitrary data to messages in the HTML. We add another
941 * class called "x-empathy-message-id-*" to the message. This
942 * way, we can remove the unread marker for this specific
943 * message later. */
944 tp_msg = empathy_message_get_tp_message (msg);
945 if (tp_msg != NULL) {
946 gchar *tmp = tp_escape_as_identifier (
947 tp_message_get_token (tp_msg));
948 g_string_append_printf (message_classes,
949 " x-empathy-message-id-%s", tmp);
950 g_free (tmp);
953 /* Define javascript function to use */
954 if (consecutive) {
955 func = priv->allow_scrolling ? "appendNextMessage" : "appendNextMessageNoScroll";
956 } else {
957 func = priv->allow_scrolling ? "appendMessage" : "appendMessageNoScroll";
960 if (empathy_contact_is_user (sender)) {
961 /* out */
962 if (is_backlog) {
963 /* context */
964 html = consecutive ? priv->data->out_nextcontext_html : priv->data->out_context_html;
965 } else {
966 /* content */
967 html = consecutive ? priv->data->out_nextcontent_html : priv->data->out_content_html;
970 /* remove all the unread marks when we are sending a message */
971 theme_adium_remove_all_focus_marks (theme);
972 } else {
973 /* in */
974 if (is_backlog) {
975 /* context */
976 html = consecutive ? priv->data->in_nextcontext_html : priv->data->in_context_html;
977 } else {
978 /* content */
979 html = consecutive ? priv->data->in_nextcontent_html : priv->data->in_content_html;
983 theme_adium_append_html (theme, func, html, body_escaped,
984 avatar_filename, name, contact_id,
985 service_name, message_classes->str,
986 timestamp, is_backlog);
988 /* Keep the sender of the last displayed message */
989 if (priv->last_contact) {
990 g_object_unref (priv->last_contact);
992 priv->last_contact = g_object_ref (sender);
993 priv->last_timestamp = timestamp;
994 priv->last_is_backlog = is_backlog;
996 g_free (body_escaped);
997 g_string_free (message_classes, TRUE);
1000 static void
1001 theme_adium_append_event (EmpathyChatView *view,
1002 const gchar *str)
1004 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1005 gchar *str_escaped;
1007 if (priv->pages_loading != 0) {
1008 g_queue_push_tail (&priv->message_queue,
1009 tp_g_value_slice_new_string (str));
1010 return;
1013 str_escaped = g_markup_escape_text (str, -1);
1014 theme_adium_append_event_escaped (view, str_escaped);
1015 g_free (str_escaped);
1018 static void
1019 theme_adium_scroll (EmpathyChatView *view,
1020 gboolean allow_scrolling)
1022 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1024 priv->allow_scrolling = allow_scrolling;
1025 if (allow_scrolling) {
1026 empathy_chat_view_scroll_down (view);
1030 static void
1031 theme_adium_scroll_down (EmpathyChatView *view)
1033 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view), "alignChat(true);");
1036 static gboolean
1037 theme_adium_get_has_selection (EmpathyChatView *view)
1039 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view));
1042 static void
1043 theme_adium_clear (EmpathyChatView *view)
1045 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1047 theme_adium_load_template (EMPATHY_THEME_ADIUM (view));
1049 /* Clear last contact to avoid trying to add a 'joined'
1050 * message when we don't have an insertion point. */
1051 if (priv->last_contact) {
1052 g_object_unref (priv->last_contact);
1053 priv->last_contact = NULL;
1057 static gboolean
1058 theme_adium_find_previous (EmpathyChatView *view,
1059 const gchar *search_criteria,
1060 gboolean new_search,
1061 gboolean match_case)
1063 /* FIXME: Doesn't respect new_search */
1064 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1065 search_criteria, match_case,
1066 FALSE, TRUE);
1069 static gboolean
1070 theme_adium_find_next (EmpathyChatView *view,
1071 const gchar *search_criteria,
1072 gboolean new_search,
1073 gboolean match_case)
1075 /* FIXME: Doesn't respect new_search */
1076 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view),
1077 search_criteria, match_case,
1078 TRUE, TRUE);
1081 static void
1082 theme_adium_find_abilities (EmpathyChatView *view,
1083 const gchar *search_criteria,
1084 gboolean match_case,
1085 gboolean *can_do_previous,
1086 gboolean *can_do_next)
1088 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1089 * find_next and find_previous to work around this problem. */
1090 if (can_do_previous)
1091 *can_do_previous = TRUE;
1092 if (can_do_next)
1093 *can_do_next = TRUE;
1096 static void
1097 theme_adium_highlight (EmpathyChatView *view,
1098 const gchar *text,
1099 gboolean match_case)
1101 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view));
1102 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view),
1103 text, match_case, 0);
1104 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view),
1105 TRUE);
1108 static void
1109 theme_adium_copy_clipboard (EmpathyChatView *view)
1111 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view));
1114 static void
1115 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1116 const gchar *token)
1118 WebKitDOMDocument *dom;
1119 WebKitDOMNodeList *nodes;
1120 gchar *class, *tmp;
1121 GError *error = NULL;
1123 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1124 if (dom == NULL) {
1125 return;
1128 tmp = tp_escape_as_identifier (token);
1129 class = g_strdup_printf (".x-empathy-message-id-%s", tmp);
1130 g_free (tmp);
1132 /* Get all nodes with focus class */
1133 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1134 g_free (class);
1136 if (nodes == NULL) {
1137 DEBUG ("Error getting focus nodes: %s",
1138 error ? error->message : "No error");
1139 g_clear_error (&error);
1140 return;
1143 theme_adium_remove_focus_marks (self, nodes);
1146 static void
1147 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1148 gpointer user_data)
1150 EmpathyThemeAdium *self = user_data;
1151 gchar *token = data;
1153 theme_adium_remove_mark_from_message (self, token);
1154 g_free (token);
1157 static void
1158 theme_adium_focus_toggled (EmpathyChatView *view,
1159 gboolean has_focus)
1161 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1163 priv->has_focus = has_focus;
1164 if (!priv->has_focus) {
1165 /* We've lost focus, so let's make sure all the acked
1166 * messages have lost their unread marker. */
1167 g_queue_foreach (&priv->acked_messages,
1168 theme_adium_remove_acked_message_unread_mark_foreach,
1169 view);
1170 g_queue_clear (&priv->acked_messages);
1172 priv->has_unread_message = FALSE;
1176 static void
1177 theme_adium_message_acknowledged (EmpathyChatView *view,
1178 EmpathyMessage *message)
1180 EmpathyThemeAdium *self = (EmpathyThemeAdium *) view;
1181 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1182 TpMessage *tp_msg;
1184 tp_msg = empathy_message_get_tp_message (message);
1186 if (tp_msg == NULL) {
1187 return;
1190 /* We only want to actually remove the unread marker if the
1191 * view doesn't have focus. If we did it all the time we would
1192 * never see the unread markers, ever! So, we'll queue these
1193 * up, and when we lose focus, we'll remove the markers. */
1194 if (priv->has_focus) {
1195 g_queue_push_tail (&priv->acked_messages,
1196 g_strdup (tp_message_get_token (tp_msg)));
1197 return;
1200 theme_adium_remove_mark_from_message (self,
1201 tp_message_get_token (tp_msg));
1204 static void
1205 theme_adium_context_menu_selection_done_cb (GtkMenuShell *menu, gpointer user_data)
1207 WebKitHitTestResult *hit_test_result = WEBKIT_HIT_TEST_RESULT (user_data);
1209 g_object_unref (hit_test_result);
1212 static void
1213 theme_adium_context_menu_for_event (EmpathyThemeAdium *theme, GdkEventButton *event)
1215 WebKitWebView *view = WEBKIT_WEB_VIEW (theme);
1216 WebKitHitTestResult *hit_test_result;
1217 WebKitHitTestResultContext context;
1218 GtkWidget *menu;
1219 GtkWidget *item;
1221 hit_test_result = webkit_web_view_get_hit_test_result (view, event);
1222 g_object_get (G_OBJECT (hit_test_result), "context", &context, NULL);
1224 /* The menu */
1225 menu = empathy_context_menu_new (GTK_WIDGET (view));
1227 /* Select all item */
1228 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL, NULL);
1229 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1231 g_signal_connect_swapped (item, "activate",
1232 G_CALLBACK (webkit_web_view_select_all),
1233 view);
1235 /* Copy menu item */
1236 if (webkit_web_view_can_copy_clipboard (view)) {
1237 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY, NULL);
1238 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1240 g_signal_connect_swapped (item, "activate",
1241 G_CALLBACK (webkit_web_view_copy_clipboard),
1242 view);
1245 /* Clear menu item */
1246 item = gtk_separator_menu_item_new ();
1247 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1249 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
1250 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1252 g_signal_connect_swapped (item, "activate",
1253 G_CALLBACK (empathy_chat_view_clear),
1254 view);
1256 /* We will only add the following menu items if we are
1257 * right-clicking a link */
1258 if (context & WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK) {
1259 /* Separator */
1260 item = gtk_separator_menu_item_new ();
1261 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1263 /* Copy Link Address menu item */
1264 item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1265 g_signal_connect (item, "activate",
1266 G_CALLBACK (theme_adium_copy_address_cb),
1267 hit_test_result);
1268 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1270 /* Open Link menu item */
1271 item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1272 g_signal_connect (item, "activate",
1273 G_CALLBACK (theme_adium_open_address_cb),
1274 hit_test_result);
1275 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1278 g_signal_connect (GTK_MENU_SHELL (menu), "selection-done",
1279 G_CALLBACK (theme_adium_context_menu_selection_done_cb),
1280 hit_test_result);
1282 /* Display the menu */
1283 gtk_widget_show_all (menu);
1284 gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
1285 event->button, event->time);
1288 static gboolean
1289 theme_adium_button_press_event (GtkWidget *widget, GdkEventButton *event)
1291 if (event->button == 3) {
1292 gboolean developer_tools_enabled;
1294 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget))),
1295 "enable-developer-extras", &developer_tools_enabled, NULL);
1297 /* We currently have no way to add an inspector menu
1298 * item ourselves, so we disable our customized menu
1299 * if the developer extras are enabled. */
1300 if (!developer_tools_enabled) {
1301 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget), event);
1302 return TRUE;
1306 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class)->button_press_event (widget, event);
1309 static void
1310 theme_adium_iface_init (EmpathyChatViewIface *iface)
1312 iface->append_message = theme_adium_append_message;
1313 iface->append_event = theme_adium_append_event;
1314 iface->scroll = theme_adium_scroll;
1315 iface->scroll_down = theme_adium_scroll_down;
1316 iface->get_has_selection = theme_adium_get_has_selection;
1317 iface->clear = theme_adium_clear;
1318 iface->find_previous = theme_adium_find_previous;
1319 iface->find_next = theme_adium_find_next;
1320 iface->find_abilities = theme_adium_find_abilities;
1321 iface->highlight = theme_adium_highlight;
1322 iface->copy_clipboard = theme_adium_copy_clipboard;
1323 iface->focus_toggled = theme_adium_focus_toggled;
1324 iface->message_acknowledged = theme_adium_message_acknowledged;
1327 static void
1328 theme_adium_load_finished_cb (WebKitWebView *view,
1329 WebKitWebFrame *frame,
1330 gpointer user_data)
1332 EmpathyThemeAdiumPriv *priv = GET_PRIV (view);
1333 EmpathyChatView *chat_view = EMPATHY_CHAT_VIEW (view);
1334 GList *l;
1336 DEBUG ("Page loaded");
1337 priv->pages_loading--;
1339 if (priv->pages_loading != 0)
1340 return;
1342 /* Display queued messages */
1343 for (l = priv->message_queue.head; l != NULL; l = l->next) {
1344 GValue *value = l->data;
1346 if (G_VALUE_HOLDS_OBJECT (value)) {
1347 theme_adium_append_message (chat_view,
1348 g_value_get_object (value));
1349 } else {
1350 theme_adium_append_event (chat_view,
1351 g_value_get_string (value));
1354 tp_g_value_slice_free (value);
1356 g_queue_clear (&priv->message_queue);
1359 static void
1360 theme_adium_finalize (GObject *object)
1362 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1364 empathy_adium_data_unref (priv->data);
1365 g_object_unref (priv->gsettings_chat);
1367 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1370 static void
1371 theme_adium_dispose (GObject *object)
1373 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1375 if (priv->smiley_manager) {
1376 g_object_unref (priv->smiley_manager);
1377 priv->smiley_manager = NULL;
1380 if (priv->last_contact) {
1381 g_object_unref (priv->last_contact);
1382 priv->last_contact = NULL;
1385 if (priv->inspector_window) {
1386 gtk_widget_destroy (priv->inspector_window);
1387 priv->inspector_window = NULL;
1390 if (priv->acked_messages.length > 0) {
1391 g_queue_foreach (&priv->acked_messages, (GFunc) g_free, NULL);
1392 g_queue_clear (&priv->acked_messages);
1395 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1398 static gboolean
1399 theme_adium_inspector_show_window_cb (WebKitWebInspector *inspector,
1400 EmpathyThemeAdium *theme)
1402 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1404 if (priv->inspector_window) {
1405 gtk_widget_show_all (priv->inspector_window);
1408 return TRUE;
1411 static gboolean
1412 theme_adium_inspector_close_window_cb (WebKitWebInspector *inspector,
1413 EmpathyThemeAdium *theme)
1415 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1417 if (priv->inspector_window) {
1418 gtk_widget_hide (priv->inspector_window);
1421 return TRUE;
1424 static WebKitWebView *
1425 theme_adium_inspect_web_view_cb (WebKitWebInspector *inspector,
1426 WebKitWebView *web_view,
1427 EmpathyThemeAdium *theme)
1429 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1430 GtkWidget *scrolled_window;
1431 GtkWidget *inspector_web_view;
1433 if (!priv->inspector_window) {
1434 /* Create main window */
1435 priv->inspector_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
1436 gtk_window_set_default_size (GTK_WINDOW (priv->inspector_window),
1437 800, 600);
1438 g_signal_connect (priv->inspector_window, "delete-event",
1439 G_CALLBACK (gtk_widget_hide_on_delete), NULL);
1441 /* Pack a scrolled window */
1442 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1443 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1444 GTK_POLICY_AUTOMATIC,
1445 GTK_POLICY_AUTOMATIC);
1446 gtk_container_add (GTK_CONTAINER (priv->inspector_window),
1447 scrolled_window);
1448 gtk_widget_show (scrolled_window);
1450 /* Pack a webview in the scrolled window. That webview will be
1451 * used to render the inspector tool. */
1452 inspector_web_view = webkit_web_view_new ();
1453 gtk_container_add (GTK_CONTAINER (scrolled_window),
1454 inspector_web_view);
1455 gtk_widget_show (scrolled_window);
1457 return WEBKIT_WEB_VIEW (inspector_web_view);
1460 return NULL;
1463 static PangoFontDescription *
1464 theme_adium_get_default_font (void)
1466 GSettings *gsettings;
1467 PangoFontDescription *pango_fd;
1468 gchar *font_family;
1470 gsettings = g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1472 font_family = g_settings_get_string (gsettings,
1473 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1475 if (font_family == NULL)
1476 return NULL;
1478 pango_fd = pango_font_description_from_string (font_family);
1479 g_free (font_family);
1480 g_object_unref (gsettings);
1481 return pango_fd;
1484 static void
1485 theme_adium_set_webkit_font (WebKitWebSettings *w_settings,
1486 const gchar *name,
1487 gint size)
1489 g_object_set (w_settings, "default-font-family", name, NULL);
1490 g_object_set (w_settings, "default-font-size", size, NULL);
1493 static void
1494 theme_adium_set_default_font (WebKitWebSettings *w_settings)
1496 PangoFontDescription *default_font_desc;
1497 GdkScreen *current_screen;
1498 gdouble dpi = 0;
1499 gint pango_font_size = 0;
1501 default_font_desc = theme_adium_get_default_font ();
1502 if (default_font_desc == NULL)
1503 return ;
1504 pango_font_size = pango_font_description_get_size (default_font_desc)
1505 / PANGO_SCALE ;
1506 if (pango_font_description_get_size_is_absolute (default_font_desc)) {
1507 current_screen = gdk_screen_get_default ();
1508 if (current_screen != NULL) {
1509 dpi = gdk_screen_get_resolution (current_screen);
1510 } else {
1511 dpi = BORING_DPI_DEFAULT;
1513 pango_font_size = (gint) (pango_font_size / (dpi / 72));
1515 theme_adium_set_webkit_font (w_settings,
1516 pango_font_description_get_family (default_font_desc),
1517 pango_font_size);
1518 pango_font_description_free (default_font_desc);
1521 static void
1522 theme_adium_constructed (GObject *object)
1524 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1525 const gchar *font_family = NULL;
1526 gint font_size = 0;
1527 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1528 WebKitWebSettings *webkit_settings;
1529 WebKitWebInspector *webkit_inspector;
1531 /* Set default settings */
1532 font_family = tp_asv_get_string (priv->data->info, "DefaultFontFamily");
1533 font_size = tp_asv_get_int32 (priv->data->info, "DefaultFontSize", NULL);
1534 webkit_settings = webkit_web_view_get_settings (webkit_view);
1536 if (font_family && font_size) {
1537 theme_adium_set_webkit_font (webkit_settings, font_family, font_size);
1538 } else {
1539 theme_adium_set_default_font (webkit_settings);
1542 /* Setup webkit inspector */
1543 webkit_inspector = webkit_web_view_get_inspector (webkit_view);
1544 g_signal_connect (webkit_inspector, "inspect-web-view",
1545 G_CALLBACK (theme_adium_inspect_web_view_cb),
1546 object);
1547 g_signal_connect (webkit_inspector, "show-window",
1548 G_CALLBACK (theme_adium_inspector_show_window_cb),
1549 object);
1550 g_signal_connect (webkit_inspector, "close-window",
1551 G_CALLBACK (theme_adium_inspector_close_window_cb),
1552 object);
1554 /* Load template */
1555 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1557 priv->in_construction = FALSE;
1560 static void
1561 theme_adium_get_property (GObject *object,
1562 guint param_id,
1563 GValue *value,
1564 GParamSpec *pspec)
1566 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1568 switch (param_id) {
1569 case PROP_ADIUM_DATA:
1570 g_value_set_boxed (value, priv->data);
1571 break;
1572 case PROP_VARIANT:
1573 g_value_set_string (value, priv->variant);
1574 break;
1575 default:
1576 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1577 break;
1581 static void
1582 theme_adium_set_property (GObject *object,
1583 guint param_id,
1584 const GValue *value,
1585 GParamSpec *pspec)
1587 EmpathyThemeAdium *theme = EMPATHY_THEME_ADIUM (object);
1588 EmpathyThemeAdiumPriv *priv = GET_PRIV (object);
1590 switch (param_id) {
1591 case PROP_ADIUM_DATA:
1592 g_assert (priv->data == NULL);
1593 priv->data = g_value_dup_boxed (value);
1594 break;
1595 case PROP_VARIANT:
1596 empathy_theme_adium_set_variant (theme, g_value_get_string (value));
1597 break;
1598 default:
1599 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1600 break;
1604 static void
1605 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1607 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1608 GtkWidgetClass* widget_class = GTK_WIDGET_CLASS (klass);
1610 object_class->finalize = theme_adium_finalize;
1611 object_class->dispose = theme_adium_dispose;
1612 object_class->constructed = theme_adium_constructed;
1613 object_class->get_property = theme_adium_get_property;
1614 object_class->set_property = theme_adium_set_property;
1616 widget_class->button_press_event = theme_adium_button_press_event;
1618 g_object_class_install_property (object_class,
1619 PROP_ADIUM_DATA,
1620 g_param_spec_boxed ("adium-data",
1621 "The theme data",
1622 "Data for the adium theme",
1623 EMPATHY_TYPE_ADIUM_DATA,
1624 G_PARAM_CONSTRUCT_ONLY |
1625 G_PARAM_READWRITE |
1626 G_PARAM_STATIC_STRINGS));
1627 g_object_class_install_property (object_class,
1628 PROP_VARIANT,
1629 g_param_spec_string ("variant",
1630 "The theme variant",
1631 "Variant name for the theme",
1632 NULL,
1633 G_PARAM_CONSTRUCT |
1634 G_PARAM_READWRITE |
1635 G_PARAM_STATIC_STRINGS));
1637 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1640 static void
1641 empathy_theme_adium_init (EmpathyThemeAdium *theme)
1643 EmpathyThemeAdiumPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (theme,
1644 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1646 theme->priv = priv;
1648 priv->in_construction = TRUE;
1649 g_queue_init (&priv->message_queue);
1650 priv->allow_scrolling = TRUE;
1651 priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1653 g_signal_connect (theme, "load-finished",
1654 G_CALLBACK (theme_adium_load_finished_cb),
1655 NULL);
1656 g_signal_connect (theme, "navigation-policy-decision-requested",
1657 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb),
1658 NULL);
1660 priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1661 g_signal_connect (priv->gsettings_chat,
1662 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS,
1663 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb),
1664 theme);
1666 theme_adium_update_enable_webkit_developer_tools (theme);
1669 EmpathyThemeAdium *
1670 empathy_theme_adium_new (EmpathyAdiumData *data,
1671 const gchar *variant)
1673 g_return_val_if_fail (data != NULL, NULL);
1675 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1676 "adium-data", data,
1677 "variant", variant,
1678 NULL);
1681 void
1682 empathy_theme_adium_set_variant (EmpathyThemeAdium *theme,
1683 const gchar *variant)
1685 EmpathyThemeAdiumPriv *priv = GET_PRIV (theme);
1686 gchar *variant_path;
1687 gchar *script;
1689 if (!tp_strdiff (priv->variant, variant)) {
1690 return;
1693 g_free (priv->variant);
1694 priv->variant = g_strdup (variant);
1696 if (priv->in_construction) {
1697 return;
1700 DEBUG ("Update view with variant: '%s'", variant);
1701 variant_path = adium_info_dup_path_for_variant (priv->data->info,
1702 priv->variant);
1703 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path);
1705 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme), script);
1707 g_free (variant_path);
1708 g_free (script);
1710 g_object_notify (G_OBJECT (theme), "variant");
1713 gboolean
1714 empathy_adium_path_is_valid (const gchar *path)
1716 gboolean ret;
1717 gchar *file;
1719 /* The theme is not valid if there is no Info.plist */
1720 file = g_build_filename (path, "Contents", "Info.plist",
1721 NULL);
1722 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1723 g_free (file);
1725 if (!ret)
1726 return FALSE;
1728 /* We ship a default Template.html as fallback if there is any problem
1729 * with the one inside the theme. The only other required file is
1730 * Content.html OR Incoming/Content.html*/
1731 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1732 NULL);
1733 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1734 g_free (file);
1736 if (!ret) {
1737 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1738 "Content.html", NULL);
1739 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1740 g_free (file);
1743 return ret;
1746 GHashTable *
1747 empathy_adium_info_new (const gchar *path)
1749 gchar *file;
1750 GValue *value;
1751 GHashTable *info = NULL;
1753 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1755 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1756 value = empathy_plist_parse_from_file (file);
1757 g_free (file);
1759 if (value == NULL)
1760 return NULL;
1762 info = g_value_dup_boxed (value);
1763 tp_g_value_slice_free (value);
1765 /* Insert the theme's path into the hash table,
1766 * keys have to be dupped */
1767 tp_asv_set_string (info, g_strdup ("path"), path);
1769 return info;
1772 static guint
1773 adium_info_get_version (GHashTable *info)
1775 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1778 static const gchar *
1779 adium_info_get_no_variant_name (GHashTable *info)
1781 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1782 return name ? name : _("Normal");
1785 static gchar *
1786 adium_info_dup_path_for_variant (GHashTable *info,
1787 const gchar *variant)
1789 guint version = adium_info_get_version (info);
1790 const gchar *no_variant = adium_info_get_no_variant_name (info);
1791 GPtrArray *variants;
1792 guint i;
1794 if (version <= 2 && !tp_strdiff (variant, no_variant)) {
1795 return g_strdup ("main.css");
1798 /* Verify the variant exists, fallback to the first one */
1799 variants = empathy_adium_info_get_available_variants (info);
1800 for (i = 0; i < variants->len; i++) {
1801 if (!tp_strdiff (variant, g_ptr_array_index (variants, i))) {
1802 break;
1805 if (i == variants->len) {
1806 DEBUG ("Variant %s does not exist", variant);
1807 variant = g_ptr_array_index (variants, 0);
1810 return g_strdup_printf ("Variants/%s.css", variant);
1814 const gchar *
1815 empathy_adium_info_get_default_variant (GHashTable *info)
1817 if (adium_info_get_version (info) <= 2) {
1818 return adium_info_get_no_variant_name (info);
1821 return tp_asv_get_string (info, "DefaultVariant");
1824 GPtrArray *
1825 empathy_adium_info_get_available_variants (GHashTable *info)
1827 GPtrArray *variants;
1828 const gchar *path;
1829 gchar *dirpath;
1830 GDir *dir;
1832 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1833 if (variants != NULL) {
1834 return variants;
1837 variants = g_ptr_array_new_with_free_func (g_free);
1838 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1839 G_TYPE_PTR_ARRAY, variants);
1841 path = tp_asv_get_string (info, "path");
1842 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1843 dir = g_dir_open (dirpath, 0, NULL);
1844 if (dir != NULL) {
1845 const gchar *name;
1847 for (name = g_dir_read_name (dir);
1848 name != NULL;
1849 name = g_dir_read_name (dir)) {
1850 gchar *display_name;
1852 if (!g_str_has_suffix (name, ".css")) {
1853 continue;
1856 display_name = g_strdup (name);
1857 strstr (display_name, ".css")[0] = '\0';
1858 g_ptr_array_add (variants, display_name);
1860 g_dir_close (dir);
1862 g_free (dirpath);
1864 if (adium_info_get_version (info) <= 2) {
1865 g_ptr_array_add (variants,
1866 g_strdup (adium_info_get_no_variant_name (info)));
1869 return variants;
1872 GType
1873 empathy_adium_data_get_type (void)
1875 static GType type_id = 0;
1877 if (!type_id)
1879 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1880 (GBoxedCopyFunc) empathy_adium_data_ref,
1881 (GBoxedFreeFunc) empathy_adium_data_unref);
1884 return type_id;
1887 EmpathyAdiumData *
1888 empathy_adium_data_new_with_info (const gchar *path, GHashTable *info)
1890 EmpathyAdiumData *data;
1891 gchar *template_html = NULL;
1892 gchar *footer_html = NULL;
1893 gchar *tmp;
1895 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1897 data = g_slice_new0 (EmpathyAdiumData);
1898 data->ref_count = 1;
1899 data->path = g_strdup (path);
1900 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1901 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1902 data->info = g_hash_table_ref (info);
1903 data->version = adium_info_get_version (info);
1904 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1905 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1906 g_str_equal, g_free, g_free);
1908 DEBUG ("Loading theme at %s", path);
1910 #define LOAD(path, var) \
1911 tmp = g_build_filename (data->basedir, path, NULL); \
1912 g_file_get_contents (tmp, &var, NULL, NULL); \
1913 g_free (tmp); \
1915 #define LOAD_CONST(path, var) \
1917 gchar *content; \
1918 LOAD (path, content); \
1919 if (content != NULL) { \
1920 g_ptr_array_add (data->strings_to_free, content); \
1922 var = content; \
1925 /* Load html files */
1926 LOAD_CONST ("Content.html", data->content_html);
1927 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
1928 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
1929 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
1930 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
1931 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
1932 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
1933 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
1934 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
1935 LOAD_CONST ("Status.html", data->status_html);
1936 LOAD ("Template.html", template_html);
1937 LOAD ("Footer.html", footer_html);
1939 #undef LOAD_CONST
1940 #undef LOAD
1942 /* HTML fallbacks: If we have at least content OR in_content, then
1943 * everything else gets a fallback */
1945 #define FALLBACK(html, fallback) \
1946 if (html == NULL) { \
1947 html = fallback; \
1950 /* in_nextcontent -> in_content -> content */
1951 FALLBACK (data->in_content_html, data->content_html);
1952 FALLBACK (data->in_nextcontent_html, data->in_content_html);
1954 /* context -> content */
1955 FALLBACK (data->in_context_html, data->in_content_html);
1956 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
1957 FALLBACK (data->out_context_html, data->out_content_html);
1958 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
1960 /* out -> in */
1961 FALLBACK (data->out_content_html, data->in_content_html);
1962 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
1963 FALLBACK (data->out_context_html, data->in_context_html);
1964 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
1966 /* status -> in_content */
1967 FALLBACK (data->status_html, data->in_content_html);
1969 #undef FALLBACK
1971 /* template -> empathy's template */
1972 data->custom_template = (template_html != NULL);
1973 if (template_html == NULL) {
1974 tmp = empathy_file_lookup ("Template.html", "data");
1975 g_file_get_contents (tmp, &template_html, NULL, NULL);
1976 g_free (tmp);
1979 /* Default avatar */
1980 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
1981 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1982 data->default_incoming_avatar_filename = tmp;
1983 } else {
1984 g_free (tmp);
1986 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
1987 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
1988 data->default_outgoing_avatar_filename = tmp;
1989 } else {
1990 g_free (tmp);
1993 /* Old custom templates had only 4 parameters.
1994 * New templates have 5 parameters */
1995 if (data->version <= 2 && data->custom_template) {
1996 tmp = string_with_format (template_html,
1997 data->basedir,
1998 "%@", /* Leave variant unset */
1999 "", /* The header */
2000 footer_html ? footer_html : "",
2001 NULL);
2002 } else {
2003 tmp = string_with_format (template_html,
2004 data->basedir,
2005 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2006 "%@", /* Leave variant unset */
2007 "", /* The header */
2008 footer_html ? footer_html : "",
2009 NULL);
2011 g_ptr_array_add (data->strings_to_free, tmp);
2012 data->template_html = tmp;
2014 g_free (template_html);
2015 g_free (footer_html);
2017 return data;
2020 EmpathyAdiumData *
2021 empathy_adium_data_new (const gchar *path)
2023 EmpathyAdiumData *data;
2024 GHashTable *info;
2026 info = empathy_adium_info_new (path);
2027 data = empathy_adium_data_new_with_info (path, info);
2028 g_hash_table_unref (info);
2030 return data;
2033 EmpathyAdiumData *
2034 empathy_adium_data_ref (EmpathyAdiumData *data)
2036 g_return_val_if_fail (data != NULL, NULL);
2038 g_atomic_int_inc (&data->ref_count);
2040 return data;
2043 void
2044 empathy_adium_data_unref (EmpathyAdiumData *data)
2046 g_return_if_fail (data != NULL);
2048 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2049 g_free (data->path);
2050 g_free (data->basedir);
2051 g_free (data->default_avatar_filename);
2052 g_free (data->default_incoming_avatar_filename);
2053 g_free (data->default_outgoing_avatar_filename);
2054 g_hash_table_unref (data->info);
2055 g_ptr_array_unref (data->strings_to_free);
2056 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2058 g_slice_free (EmpathyAdiumData, data);
2062 GHashTable *
2063 empathy_adium_data_get_info (EmpathyAdiumData *data)
2065 g_return_val_if_fail (data != NULL, NULL);
2067 return data->info;
2070 const gchar *
2071 empathy_adium_data_get_path (EmpathyAdiumData *data)
2073 g_return_val_if_fail (data != NULL, NULL);
2075 return data->path;