Help: Use stable 'if' namespace instead of experimental
[empathy-mirror.git] / libempathy-gtk / empathy-theme-adium.c
blobbee6ef062478dde11992c23943db528f93967a7c
1 /*
2 * Copyright (C) 2008-2012 Collabora Ltd.
3 * Copyright (C) 2012 Red Hat, Inc.
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"
23 #include "empathy-theme-adium.h"
25 #include <glib/gi18n-lib.h>
26 #include <tp-account-widgets/tpaw-images.h>
27 #include <tp-account-widgets/tpaw-time.h>
28 #include <tp-account-widgets/tpaw-pixbuf-utils.h>
29 #include <tp-account-widgets/tpaw-utils.h>
31 #include "empathy-gsettings.h"
32 #include "empathy-images.h"
33 #include "empathy-plist.h"
34 #include "empathy-smiley-manager.h"
35 #include "empathy-ui-utils.h"
36 #include "empathy-utils.h"
37 #include "empathy-webkit-utils.h"
39 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
40 #include "empathy-debug.h"
42 #define BORING_DPI_DEFAULT 96
44 /* "Join" consecutive messages with timestamps within five minutes */
45 #define MESSAGE_JOIN_PERIOD 5*60
47 struct _EmpathyThemeAdiumPriv
49 EmpathyAdiumData *data;
50 EmpathySmileyManager *smiley_manager;
51 EmpathyContact *first_contact;
52 EmpathyContact *last_contact;
53 gint64 first_timestamp;
54 gint64 last_timestamp;
55 gboolean first_is_backlog;
56 gboolean last_is_backlog;
57 guint pages_loading;
58 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
59 GQueue message_queue;
60 /* Queue of guint32 of pending message id to remove unread
61 * marker for when we lose focus. */
62 GQueue acked_messages;
63 GtkWidget *inspector_window;
65 GSettings *gsettings_chat;
66 GSettings *gsettings_desktop;
68 gboolean has_focus;
69 gboolean has_unread_message;
70 gboolean allow_scrolling;
71 gchar *variant;
72 gboolean in_construction;
73 gboolean show_avatars;
76 struct _EmpathyAdiumData
78 gint ref_count;
79 gchar *path;
80 gchar *basedir;
81 gchar *default_avatar_filename;
82 gchar *default_incoming_avatar_filename;
83 gchar *default_outgoing_avatar_filename;
84 GHashTable *info;
85 guint version;
86 gboolean custom_template;
87 /* gchar* -> gchar* both owned */
88 GHashTable *date_format_cache;
90 /* HTML bits */
91 const gchar *template_html;
92 const gchar *content_html;
93 const gchar *in_content_html;
94 const gchar *in_context_html;
95 const gchar *in_nextcontent_html;
96 const gchar *in_nextcontext_html;
97 const gchar *out_content_html;
98 const gchar *out_context_html;
99 const gchar *out_nextcontent_html;
100 const gchar *out_nextcontext_html;
101 const gchar *status_html;
103 /* Above html strings are pointers to strings stored in this array.
104 * We do this because of fallbacks, some htmls could be pointing the
105 * same string. */
106 GPtrArray *strings_to_free;
109 static gchar * adium_info_dup_path_for_variant (GHashTable *info,
110 const gchar *variant);
112 enum
114 PROP_0,
115 PROP_ADIUM_DATA,
116 PROP_VARIANT,
119 G_DEFINE_TYPE (EmpathyThemeAdium, empathy_theme_adium,
120 WEBKIT_TYPE_WEB_VIEW)
122 enum
124 QUEUED_EVENT,
125 QUEUED_MESSAGE,
126 QUEUED_EDIT
129 typedef struct
131 guint type;
132 EmpathyMessage *msg;
133 char *str;
134 gboolean should_highlight;
135 } QueuedItem;
137 static QueuedItem *
138 queue_item (GQueue *queue,
139 guint type,
140 EmpathyMessage *msg,
141 const char *str,
142 gboolean should_highlight,
143 gboolean prepend)
145 QueuedItem *item = g_slice_new0 (QueuedItem);
147 item->type = type;
148 if (msg != NULL)
149 item->msg = g_object_ref (msg);
150 item->str = g_strdup (str);
151 item->should_highlight = should_highlight;
153 if (prepend)
154 g_queue_push_head (queue, item);
155 else
156 g_queue_push_tail (queue, item);
158 return item;
161 static void
162 free_queued_item (QueuedItem *item)
164 tp_clear_object (&item->msg);
165 g_free (item->str);
167 g_slice_free (QueuedItem, item);
170 static gboolean
171 theme_adium_policy_decision_requested_cb (WebKitWebView *view,
172 WebKitPolicyDecision *decision,
173 WebKitPolicyDecisionType decision_type,
174 gpointer data)
176 if (decision_type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION)
177 return FALSE;
179 return empathy_webkit_handle_navigation (view, WEBKIT_NAVIGATION_POLICY_DECISION (decision));
182 /* Replace each %@ in format with string passed in args */
183 static gchar *
184 string_with_format (const gchar *format,
185 const gchar *first_string,
186 ...)
188 va_list args;
189 const gchar *str;
190 GString *result;
192 va_start (args, first_string);
193 result = g_string_sized_new (strlen (format));
194 for (str = first_string; str != NULL; str = va_arg (args, const gchar *))
196 const gchar *next;
198 next = strstr (format, "%@");
199 if (next == NULL)
200 break;
202 g_string_append_len (result, format, next - format);
203 g_string_append (result, str);
204 format = next + 2;
206 g_string_append (result, format);
207 va_end (args);
209 return g_string_free (result, FALSE);
212 static void
213 theme_adium_load_template (EmpathyThemeAdium *self)
215 gchar *basedir_uri;
216 gchar *variant_path;
217 gchar *template;
219 self->priv->pages_loading++;
220 basedir_uri = g_strconcat ("file://", self->priv->data->basedir, NULL);
222 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
223 self->priv->variant);
225 template = string_with_format (self->priv->data->template_html,
226 variant_path, NULL);
228 webkit_web_view_load_html (WEBKIT_WEB_VIEW (self),
229 template, basedir_uri);
231 g_free (basedir_uri);
232 g_free (variant_path);
233 g_free (template);
236 static gchar *
237 theme_adium_parse_body (EmpathyThemeAdium *self,
238 const gchar *text,
239 const gchar *token)
241 TpawStringParser *parsers;
242 GString *string;
244 /* Check if we have to parse smileys */
245 parsers = empathy_webkit_get_string_parser (
246 g_settings_get_boolean (self->priv->gsettings_chat,
247 EMPATHY_PREFS_CHAT_SHOW_SMILEYS));
249 /* Parse text and construct string with links and smileys replaced
250 * by html tags. Also escape text to make sure html code is
251 * displayed verbatim. */
252 string = g_string_sized_new (strlen (text));
254 /* wrap this in HTML that allows us to find the message for later
255 * editing */
256 if (!tp_str_empty (token))
257 g_string_append_printf (string,
258 "<span id=\"message-token-%s\">",
259 token);
261 tpaw_string_parser_substr (text, -1, parsers, string);
263 if (!tp_str_empty (token))
264 g_string_append (string, "</span>");
266 /* Wrap body in order to make tabs and multiple spaces displayed
267 * properly. See bug #625745. */
268 g_string_prepend (string, "<div style=\"display: inline; "
269 "white-space: pre-wrap\"'>");
270 g_string_append (string, "</div>");
272 return g_string_free (string, FALSE);
275 static void
276 escape_and_append_len (GString *string, const gchar *str, gint len)
278 while (str != NULL && *str != '\0' && len != 0)
280 switch (*str)
282 case '\\':
283 /* \ becomes \\ */
284 g_string_append (string, "\\\\");
285 break;
286 case '\"':
287 /* " becomes \" */
288 g_string_append (string, "\\\"");
289 break;
290 case '\n':
291 /* Remove end of lines */
292 break;
293 default:
294 g_string_append_c (string, *str);
297 str++;
298 len--;
302 /* If *str starts with match, returns TRUE and move pointer to the end */
303 static gboolean
304 theme_adium_match (const gchar **str,
305 const gchar *match)
307 gint len;
309 len = strlen (match);
310 if (strncmp (*str, match, len) == 0)
312 *str += len - 1;
313 return TRUE;
316 return FALSE;
319 /* Like theme_adium_match() but also return the X part if match is
320 * like %foo{X}% */
321 static gboolean
322 theme_adium_match_with_format (const gchar **str,
323 const gchar *match,
324 gchar **format)
326 const gchar *cur = *str;
327 const gchar *end;
329 if (!theme_adium_match (&cur, match))
330 return FALSE;
332 cur++;
334 end = strstr (cur, "}%");
335 if (!end)
336 return FALSE;
338 *format = g_strndup (cur , end - cur);
339 *str = end + 1;
340 return TRUE;
343 /* List of colors used by %senderColor%. Copied from
344 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
346 static gchar *colors[] = {
347 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
348 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
349 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
350 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
351 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
352 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
353 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
354 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
355 "lightblue", "lightcoral",
356 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
357 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
358 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
359 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
360 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
361 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
362 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
363 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
364 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
365 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
366 "yellowgreen",
369 static const gchar *
370 nsdate_to_strftime (EmpathyAdiumData *data, const gchar *nsdate)
372 /* Convert from NSDateFormatter
373 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
374 * to strftime supported by g_date_time_format.
375 * FIXME: table is incomplete, doc of g_date_time_format has a table of
376 * supported tags.
377 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
378 * in 2.29.x we have to explictely request padding with %0x */
379 static const gchar *convert_table[] = {
380 "a", "%p", // AM/PM
381 "A", NULL, // 0~86399999 (Millisecond of Day)
383 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
384 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
385 "cc", "%u", // 1~7 (Day of Week)
386 "c", "%u", // 1~7 (Day of Week)
388 "dd", "%d", // 1~31 (0 padded Day of Month)
389 "d", "%d", // 1~31 (0 padded Day of Month)
390 "D", "%j", // 1~366 (0 padded Day of Year)
392 "e", "%u", // 1~7 (0 padded Day of Week)
393 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
394 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
395 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
396 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
398 "F", NULL, // 1~5 (0 padded Week of Month, first day of week = Monday)
400 "g", NULL, // Julian Day Number (number of days since 4713 BC January 1)
401 "GGGG", NULL, // Before Christ/Anno Domini
402 "GGG", NULL, // BC/AD (Era Designator Abbreviated)
403 "GG", NULL, // BC/AD (Era Designator Abbreviated)
404 "G", NULL, // BC/AD (Era Designator Abbreviated)
406 "h", "%I", // 1~12 (0 padded Hour (12hr))
407 "H", "%H", // 0~23 (0 padded Hour (24hr))
409 "k", NULL, // 1~24 (0 padded Hour (24hr)
410 "K", NULL, // 0~11 (0 padded Hour (12hr))
412 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
413 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
414 "LL", "%m", // 1~12 (0 padded Month)
415 "L", "%m", // 1~12 (0 padded Month)
417 "m", "%M", // 0~59 (0 padded Minute)
418 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
419 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
420 "MM", "%m", // 1~12 (0 padded Month)
421 "M", "%m", // 1~12 (0 padded Month)
423 "qqqq", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
424 "qqq", NULL, // Q1/Q2/Q3/Q4
425 "qq", NULL, // 1~4 (0 padded Quarter)
426 "q", NULL, // 1~4 (0 padded Quarter)
427 "QQQQ", NULL, // 1st quarter/2nd quarter/3rd quarter/4th quarter
428 "QQQ", NULL, // Q1/Q2/Q3/Q4
429 "QQ", NULL, // 1~4 (0 padded Quarter)
430 "Q", NULL, // 1~4 (0 padded Quarter)
432 "s", "%S", // 0~59 (0 padded Second)
433 "S", NULL, // (rounded Sub-Second)
435 "u", "%Y", // (0 padded Year)
437 "vvvv", "%Z", // (General GMT Timezone Name)
438 "vvv", "%Z", // (General GMT Timezone Abbreviation)
439 "vv", "%Z", // (General GMT Timezone Abbreviation)
440 "v", "%Z", // (General GMT Timezone Abbreviation)
442 "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)
443 "W", NULL, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
445 "yyyy", "%Y", // (Full Year)
446 "yyy", "%y", // (2 Digits Year)
447 "yy", "%y", // (2 Digits Year)
448 "y", "%Y", // (Full Year)
449 "YYYY", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
450 "YYY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
451 "YY", NULL, // (2 Digits Year, starting from the Sunday of the 1st week of year)
452 "Y", NULL, // (Full Year, starting from the Sunday of the 1st week of year)
454 "zzzz", NULL, // (Specific GMT Timezone Name)
455 "zzz", NULL, // (Specific GMT Timezone Abbreviation)
456 "zz", NULL, // (Specific GMT Timezone Abbreviation)
457 "z", NULL, // (Specific GMT Timezone Abbreviation)
458 "Z", "%z", // +0000 (RFC 822 Timezone)
460 const gchar *str;
461 GString *string;
462 guint i, j;
464 if (nsdate == NULL)
465 return NULL;
467 str = g_hash_table_lookup (data->date_format_cache, nsdate);
468 if (str != NULL) {
469 return str;
472 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
473 * by corresponding strftime tag. */
474 string = g_string_sized_new (strlen (nsdate));
475 for (i = 0; nsdate[i] != '\0'; i++)
477 gboolean found = FALSE;
479 /* even indexes are NSDateFormatter tag, odd indexes are the
480 * corresponding strftime tag */
481 for (j = 0; j < G_N_ELEMENTS (convert_table); j += 2)
483 if (g_str_has_prefix (nsdate + i, convert_table[j]))
485 found = TRUE;
486 break;
490 if (found)
492 /* If we don't have a replacement, just ignore that tag */
493 if (convert_table[j + 1] != NULL)
494 g_string_append (string, convert_table[j + 1]);
496 i += strlen (convert_table[j]) - 1;
498 else
500 g_string_append_c (string, nsdate[i]);
504 DEBUG ("Date format converted '%s' → '%s'", nsdate, string->str);
506 /* The cache takes ownership of string->str */
507 g_hash_table_insert (data->date_format_cache, g_strdup (nsdate), string->str);
508 return g_string_free (string, FALSE);
511 static void
512 theme_adium_add_html (EmpathyThemeAdium *self,
513 const gchar *func,
514 const gchar *html,
515 const gchar *message,
516 const gchar *avatar_filename,
517 const gchar *name,
518 const gchar *contact_id,
519 const gchar *service_name,
520 const gchar *message_classes,
521 gint64 timestamp,
522 gboolean is_backlog,
523 gboolean outgoing,
524 PangoDirection direction)
526 GBytes *bytes;
527 GString *string;
528 const gchar *cur = NULL;
529 const gchar *js;
530 gchar *script;
532 /* Make some search-and-replace in the html code */
533 string = g_string_sized_new (strlen (html) + strlen (message));
534 g_string_append_printf (string, "%s(\"", func);
536 for (cur = html; *cur != '\0'; cur++)
538 const gchar *replace = NULL;
539 gchar *dup_replace = NULL;
540 gchar *format = NULL;
542 /* Those are all well known keywords that needs replacement in
543 * html files. Please keep them in the same order than the adium
544 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
545 if (theme_adium_match (&cur, "%userIconPath%"))
547 replace = avatar_filename;
549 else if (theme_adium_match (&cur, "%senderScreenName%"))
551 replace = contact_id;
553 else if (theme_adium_match (&cur, "%sender%"))
555 replace = name;
557 else if (theme_adium_match (&cur, "%senderColor%"))
559 /* A color derived from the user's name.
560 * FIXME: If a colon separated list of HTML colors is at
561 * Incoming/SenderColors.txt it will be used instead of
562 * the default colors.
565 /* Ensure we always use the same color when sending messages
566 * (bgo #658821) */
567 if (outgoing)
569 replace = "inherit";
571 else if (contact_id != NULL)
573 guint hash = g_str_hash (contact_id);
574 replace = colors[hash % G_N_ELEMENTS (colors)];
577 else if (theme_adium_match (&cur, "%senderStatusIcon%"))
579 /* FIXME: The path to the status icon of the sender
580 * (available, away, etc...)
583 else if (theme_adium_match (&cur, "%messageDirection%"))
585 switch (direction)
587 case PANGO_DIRECTION_LTR:
588 case PANGO_DIRECTION_TTB_LTR:
589 case PANGO_DIRECTION_WEAK_LTR:
590 replace = "ltr";
591 break;
592 case PANGO_DIRECTION_RTL:
593 case PANGO_DIRECTION_TTB_RTL:
594 case PANGO_DIRECTION_WEAK_RTL:
595 replace = "rtl";
596 break;
597 case PANGO_DIRECTION_NEUTRAL:
598 default:
599 break;
602 else if (theme_adium_match (&cur, "%senderDisplayName%"))
604 /* FIXME: The serverside (remotely set) name of the
605 * sender, such as an MSN display name.
607 * We don't have access to that yet so we use
608 * local alias instead.
610 replace = name;
612 else if (theme_adium_match (&cur, "%senderPrefix%"))
614 /* FIXME: If we supported IRC user mode flags, this
615 * would be replaced with @ if the user is an op, + if
616 * the user has voice, etc. as per
617 * http://hg.adium.im/adium/rev/b586b027de42. But we
618 * don't, so for now we just strip it. */
620 else if (theme_adium_match_with_format (&cur, "%textbackgroundcolor{",
621 &format))
623 /* FIXME: This keyword is used to represent the
624 * highlight background color. "X" is the opacity of the
625 * background, ranges from 0 to 1 and can be any decimal
626 * between.
629 else if (theme_adium_match (&cur, "%message%"))
631 replace = message;
633 else if (theme_adium_match (&cur, "%time%") ||
634 theme_adium_match_with_format (&cur, "%time{", &format))
636 const gchar *strftime_format;
638 strftime_format = nsdate_to_strftime (self->priv->data, format);
639 if (is_backlog)
640 dup_replace = tpaw_time_to_string_local (timestamp,
641 strftime_format ? strftime_format :
642 TPAW_TIME_DATE_FORMAT_DISPLAY_SHORT);
643 else
644 dup_replace = tpaw_time_to_string_local (timestamp,
645 strftime_format ? strftime_format :
646 TPAW_TIME_FORMAT_DISPLAY_SHORT);
648 replace = dup_replace;
650 else if (theme_adium_match (&cur, "%shortTime%"))
652 dup_replace = tpaw_time_to_string_local (timestamp,
653 TPAW_TIME_FORMAT_DISPLAY_SHORT);
654 replace = dup_replace;
656 else if (theme_adium_match (&cur, "%service%"))
658 replace = service_name;
660 else if (theme_adium_match (&cur, "%variant%"))
662 /* FIXME: The name of the active message style variant,
663 * with all spaces replaced with an underscore.
664 * A variant named "Alternating Messages - Blue Red"
665 * will become "Alternating_Messages_-_Blue_Red".
668 else if (theme_adium_match (&cur, "%userIcons%"))
670 replace = self->priv->show_avatars ? "showIcons" : "hideIcons";
672 else if (theme_adium_match (&cur, "%messageClasses%"))
674 replace = message_classes;
676 else if (theme_adium_match (&cur, "%status%"))
678 /* FIXME: A description of the status event. This is
679 * neither in the user's local language nor expected to
680 * be displayed; it may be useful to use a different div
681 * class to present different types of status messages.
682 * The following is a list of some of the more important
683 * status messages; your message style should be able to
684 * handle being shown a status message not in this list,
685 * as even at present the list is incomplete and is
686 * certain to become out of date in the future:
687 * online
688 * offline
689 * away
690 * away_message
691 * return_away
692 * idle
693 * return_idle
694 * date_separator
695 * contact_joined (group chats)
696 * contact_left
697 * error
698 * timed_out
699 * encryption (all OTR messages use this status)
700 * purple (all IRC topic and join/part messages use this status)
701 * fileTransferStarted
702 * fileTransferCompleted
705 else
707 escape_and_append_len (string, cur, 1);
708 continue;
711 /* Here we have a replacement to make */
712 escape_and_append_len (string, replace, -1);
714 g_free (dup_replace);
715 g_free (format);
717 g_string_append (string, "\")");
719 bytes = g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
720 G_RESOURCE_LOOKUP_FLAGS_NONE,
721 NULL);
723 if (bytes != NULL)
725 js = (const gchar *) g_bytes_get_data (bytes, NULL);
726 g_string_prepend (string, js);
727 g_bytes_unref (bytes);
730 script = g_string_free (string, FALSE);
731 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self), script, NULL, NULL, NULL);
732 g_free (script);
735 static void
736 theme_adium_append_event_escaped (EmpathyThemeAdium *self,
737 const gchar *escaped,
738 PangoDirection direction)
740 theme_adium_add_html (self, "appendMessage",
741 self->priv->data->status_html, escaped, NULL, NULL, NULL,
742 NULL, "event", tpaw_time_get_current (), FALSE, FALSE, direction);
744 /* There is no last contact */
745 if (self->priv->last_contact)
747 g_object_unref (self->priv->last_contact);
748 self->priv->last_contact = NULL;
751 #if 0
752 /* FIXME: check what this is for and port to WebKit2 */
753 static void
754 theme_adium_remove_focus_marks (EmpathyThemeAdium *self,
755 WebKitDOMNodeList *nodes)
757 guint i;
759 /* Remove focus and firstFocus class */
760 for (i = 0; i < webkit_dom_node_list_get_length (nodes); i++)
762 WebKitDOMNode *node = webkit_dom_node_list_item (nodes, i);
763 WebKitDOMElement *element = WEBKIT_DOM_ELEMENT (node);
764 gchar *class_name;
765 gchar **classes, **iter;
766 GString *new_class_name;
767 gboolean first = TRUE;
769 if (element == NULL)
770 continue;
772 class_name = webkit_dom_element_get_class_name (element);
773 classes = g_strsplit (class_name, " ", -1);
774 new_class_name = g_string_sized_new (strlen (class_name));
776 for (iter = classes; *iter != NULL; iter++)
778 if (tp_strdiff (*iter, "focus") &&
779 tp_strdiff (*iter, "firstFocus"))
781 if (!first)
782 g_string_append_c (new_class_name, ' ');
784 g_string_append (new_class_name, *iter);
785 first = FALSE;
789 webkit_dom_element_set_class_name (element, new_class_name->str);
791 g_free (class_name);
792 g_strfreev (classes);
793 g_string_free (new_class_name, TRUE);
796 #endif
797 static void
798 theme_adium_remove_all_focus_marks (EmpathyThemeAdium *self)
800 #if 0
801 /* FIXME: check what this is for and port to WebKit2 */
802 WebKitDOMDocument *dom;
803 WebKitDOMNodeList *nodes;
804 GError *error = NULL;
806 if (!self->priv->has_unread_message)
807 return;
809 self->priv->has_unread_message = FALSE;
811 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
812 if (dom == NULL)
813 return;
815 /* Get all nodes with focus class */
816 nodes = webkit_dom_document_query_selector_all (dom, ".focus", &error);
818 if (nodes == NULL)
820 DEBUG ("Error getting focus nodes: %s",
821 error ? error->message : "No error");
822 g_clear_error (&error);
823 return;
826 theme_adium_remove_focus_marks (self, nodes);
827 #endif
830 enum
832 ADD_CONSECUTIVE_MSG_SCROLL = 0,
833 ADD_CONSECUTIVE_MSG_NO_SCROLL = 1,
834 ADD_MSG_SCROLL = 2,
835 ADD_MSG_NO_SCROLL = 3
839 * theme_adium_add_message:
840 * @self: The #EmpathyThemeAdium used by the view.
841 * @msg: An #EmpathyMessage that is to be added to the view.
842 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
843 * @prev_timestamp: (out): Timestamp of the previous message.
844 * @prev_is_backlog: (out): Whether the previous message was fetched
845 * from the logs.
846 * @should_highlight: Whether the message should be highlighted. eg.,
847 * if it matches the user's username in multi-user chat.
848 * @js_funcs: An array of JavaScript function names
850 * Shows @msg in the chat view by adding to @self. Addition is defined
851 * by the JavaScript functions listed in @js_funcs. Common examples
852 * are appending new incoming messages or prepending old messages from
853 * the logs.
855 * @js_funcs should be an array with exactly 4 entries. The entries
856 * should be the names of JavaScript functions that take the raw HTML
857 * that is to be added to the view as an argument and take the following
858 * actions, in this order:
859 * - add a new consecutive message and scroll to it if needed,
860 * - add a new consecutive message and do not scroll,
861 * - add a new non-consecutive message and scroll to it if needed, and
862 * - add a new non-consecutive message and do not scroll
864 * A message is considered to be consecutive with the previous one if
865 * all the following conditions are met:
866 * - senders are the same contact,
867 * - last message was recieved recently,
868 * - last message and this message both are/aren't backlog, and
869 * - DisableCombineConsecutive is not set in theme's settings
871 static void
872 theme_adium_add_message (EmpathyThemeAdium *self,
873 EmpathyMessage *msg,
874 EmpathyContact **prev_contact,
875 gint64 *prev_timestamp,
876 gboolean *prev_is_backlog,
877 gboolean should_highlight,
878 const gchar *js_funcs[])
880 EmpathyContact *sender;
881 TpMessage *tp_msg;
882 TpAccount *account;
883 gchar *body_escaped, *name_escaped;
884 const gchar *name;
885 const gchar *contact_id;
886 EmpathyAvatar *avatar;
887 const gchar *avatar_filename = NULL;
888 gint64 timestamp;
889 const gchar *html = NULL;
890 const gchar *func;
891 const gchar *service_name;
892 GString *message_classes = NULL;
893 gboolean is_backlog;
894 gboolean consecutive;
895 gboolean action;
896 PangoDirection direction;
899 /* Get information */
900 sender = empathy_message_get_sender (msg);
901 account = empathy_contact_get_account (sender);
902 service_name = tpaw_protocol_name_to_display_name
903 (tp_account_get_protocol_name (account));
904 if (service_name == NULL)
905 service_name = tp_account_get_protocol_name (account);
906 timestamp = empathy_message_get_timestamp (msg);
907 body_escaped = theme_adium_parse_body (self,
908 empathy_message_get_body (msg),
909 empathy_message_get_token (msg));
910 name = empathy_contact_get_logged_alias (sender);
911 contact_id = empathy_contact_get_id (sender);
912 action = (empathy_message_get_tptype (msg) ==
913 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
915 name_escaped = g_markup_escape_text (name, -1);
917 /* If this is a /me probably */
918 if (action)
920 gchar *str;
922 if (self->priv->data->version >= 4 || !self->priv->data->custom_template)
924 str = g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
925 "<span class='actionMessageBody'>%s</span>",
926 name_escaped, body_escaped);
928 else
930 str = g_strdup_printf ("*%s*", body_escaped);
933 g_free (body_escaped);
934 body_escaped = str;
937 /* Get the avatar filename, or a fallback */
938 avatar = empathy_contact_get_avatar (sender);
939 if (avatar)
940 avatar_filename = avatar->filename;
942 if (!avatar_filename)
944 if (empathy_contact_is_user (sender))
945 avatar_filename = self->priv->data->default_outgoing_avatar_filename;
946 else
947 avatar_filename = self->priv->data->default_incoming_avatar_filename;
949 if (!avatar_filename)
951 if (!self->priv->data->default_avatar_filename)
952 self->priv->data->default_avatar_filename =
953 tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT,
954 GTK_ICON_SIZE_DIALOG);
956 avatar_filename = self->priv->data->default_avatar_filename;
960 is_backlog = empathy_message_is_backlog (msg);
961 consecutive = empathy_contact_equal (*prev_contact, sender) &&
962 (ABS (timestamp - *prev_timestamp) < MESSAGE_JOIN_PERIOD) &&
963 (is_backlog == *prev_is_backlog) &&
964 !tp_asv_get_boolean (self->priv->data->info,
965 "DisableCombineConsecutive", NULL);
967 /* Define message classes */
968 message_classes = g_string_new ("message");
969 if (!self->priv->has_focus && !is_backlog)
971 if (!self->priv->has_unread_message)
973 g_string_append (message_classes, " firstFocus");
974 self->priv->has_unread_message = TRUE;
976 g_string_append (message_classes, " focus");
979 if (is_backlog)
980 g_string_append (message_classes, " history");
982 if (consecutive)
983 g_string_append (message_classes, " consecutive");
985 if (empathy_contact_is_user (sender))
986 g_string_append (message_classes, " outgoing");
987 else
988 g_string_append (message_classes, " incoming");
990 if (should_highlight)
991 g_string_append (message_classes, " mention");
993 if (empathy_message_get_tptype (msg) ==
994 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY)
995 g_string_append (message_classes, " autoreply");
997 if (action)
998 g_string_append (message_classes, " action");
1000 /* FIXME: other classes:
1001 * status - the message is a status change
1002 * event - the message is a notification of something happening
1003 * (for example, encryption being turned on)
1004 * %status% - See %status% in theme_adium_add_html ()
1007 /* This is slightly a hack, but it's the only way to add
1008 * arbitrary data to messages in the HTML. We add another
1009 * class called "x-empathy-message-id-*" to the message. This
1010 * way, we can remove the unread marker for this specific
1011 * message later. */
1012 tp_msg = empathy_message_get_tp_message (msg);
1013 if (tp_msg != NULL)
1015 guint32 id;
1016 gboolean valid;
1018 id = tp_message_get_pending_message_id (tp_msg, &valid);
1019 if (valid)
1020 g_string_append_printf (message_classes,
1021 " x-empathy-message-id-%u", id);
1024 /* Define javascript function to use */
1025 if (consecutive)
1026 func = self->priv->allow_scrolling ? js_funcs[ADD_CONSECUTIVE_MSG_SCROLL] :
1027 js_funcs[ADD_CONSECUTIVE_MSG_NO_SCROLL];
1028 else
1029 func = self->priv->allow_scrolling ? js_funcs[ADD_MSG_SCROLL] :
1030 js_funcs[ADD_MSG_NO_SCROLL];
1032 if (empathy_contact_is_user (sender))
1034 /* out */
1035 if (is_backlog)
1036 /* context */
1037 html = consecutive ? self->priv->data->out_nextcontext_html :
1038 self->priv->data->out_context_html;
1039 else
1040 /* content */
1041 html = consecutive ? self->priv->data->out_nextcontent_html :
1042 self->priv->data->out_content_html;
1044 /* remove all the unread marks when we are sending a message */
1045 theme_adium_remove_all_focus_marks (self);
1047 else
1049 /* in */
1050 if (is_backlog)
1051 /* context */
1052 html = consecutive ? self->priv->data->in_nextcontext_html :
1053 self->priv->data->in_context_html;
1054 else
1055 /* content */
1056 html = consecutive ? self->priv->data->in_nextcontent_html :
1057 self->priv->data->in_content_html;
1060 direction = pango_find_base_dir (empathy_message_get_body (msg), -1);
1062 theme_adium_add_html (self, func, html, body_escaped,
1063 avatar_filename, name_escaped, contact_id,
1064 service_name, message_classes->str,
1065 timestamp, is_backlog, empathy_contact_is_user (sender), direction);
1067 /* Keep the sender of the last displayed message */
1068 if (*prev_contact)
1069 g_object_unref (*prev_contact);
1071 *prev_contact = g_object_ref (sender);
1072 *prev_timestamp = timestamp;
1073 *prev_is_backlog = is_backlog;
1075 g_free (body_escaped);
1076 g_free (name_escaped);
1077 g_string_free (message_classes, TRUE);
1080 void
1081 empathy_theme_adium_append_message (EmpathyThemeAdium *self,
1082 EmpathyMessage *msg,
1083 gboolean should_highlight)
1085 const gchar *js_funcs[] = { "appendNextMessage",
1086 "appendNextMessageNoScroll",
1087 "appendMessage",
1088 "appendMessageNoScroll" };
1090 if (self->priv->pages_loading != 0)
1092 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1093 should_highlight, FALSE);
1094 return;
1097 theme_adium_add_message (self, msg, &self->priv->last_contact,
1098 &self->priv->last_timestamp, &self->priv->last_is_backlog,
1099 should_highlight, js_funcs);
1102 void
1103 empathy_theme_adium_append_event (EmpathyThemeAdium *self,
1104 const gchar *str)
1106 gchar *str_escaped;
1107 PangoDirection direction;
1109 if (self->priv->pages_loading != 0)
1111 queue_item (&self->priv->message_queue, QUEUED_EVENT, NULL, str, FALSE, FALSE);
1112 return;
1115 direction = pango_find_base_dir (str, -1);
1116 str_escaped = g_markup_escape_text (str, -1);
1117 theme_adium_append_event_escaped (self, str_escaped, direction);
1118 g_free (str_escaped);
1121 void
1122 empathy_theme_adium_append_event_markup (EmpathyThemeAdium *self,
1123 const gchar *markup_text,
1124 const gchar *fallback_text)
1126 PangoDirection direction;
1128 direction = pango_find_base_dir (fallback_text, -1);
1129 theme_adium_append_event_escaped (self, markup_text, direction);
1132 void
1133 empathy_theme_adium_prepend_message (EmpathyThemeAdium *self,
1134 EmpathyMessage *msg,
1135 gboolean should_highlight)
1137 const gchar *js_funcs[] = { "prependPrev",
1138 "prependPrev",
1139 "prepend",
1140 "prepend" };
1142 if (self->priv->pages_loading != 0)
1144 queue_item (&self->priv->message_queue, QUEUED_MESSAGE, msg, NULL,
1145 should_highlight, TRUE);
1146 return;
1149 theme_adium_add_message (self, msg, &self->priv->first_contact,
1150 &self->priv->first_timestamp, &self->priv->first_is_backlog,
1151 should_highlight, js_funcs);
1154 void
1155 empathy_theme_adium_edit_message (EmpathyThemeAdium *self,
1156 EmpathyMessage *message)
1158 #if 0
1159 /* FIXME: this needs to be ported to WebKit2, but I have no idea what this is for */
1160 WebKitDOMDocument *doc;
1161 WebKitDOMElement *span;
1162 gchar *id, *parsed_body;
1163 gchar *tooltip, *timestamp;
1164 GtkIconInfo *icon_info;
1165 GError *error = NULL;
1167 if (self->priv->pages_loading != 0)
1169 queue_item (&self->priv->message_queue, QUEUED_EDIT, message, NULL, FALSE, FALSE);
1170 return;
1173 id = g_strdup_printf ("message-token-%s",
1174 empathy_message_get_supersedes (message));
1175 /* we don't pass a token here, because doing so will return another
1176 * <span> element, and we don't want nested <span> elements */
1177 parsed_body = theme_adium_parse_body (self,
1178 empathy_message_get_body (message), NULL);
1180 /* find the element */
1181 doc = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1182 span = webkit_dom_document_get_element_by_id (doc, id);
1184 if (span == NULL)
1186 DEBUG ("Failed to find id '%s'", id);
1187 goto except;
1190 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span))
1192 DEBUG ("Not a HTML element");
1193 goto except;
1196 /* update the HTML */
1197 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span),
1198 parsed_body, &error);
1200 if (error != NULL)
1202 DEBUG ("Error setting new inner-HTML: %s", error->message);
1203 g_error_free (error);
1204 goto except;
1207 /* set a tooltip */
1208 timestamp = tpaw_time_to_string_local (
1209 empathy_message_get_timestamp (message),
1210 "%H:%M:%S");
1211 tooltip = g_strdup_printf (_("Message edited at %s"), timestamp);
1213 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span),
1214 tooltip);
1216 g_free (tooltip);
1217 g_free (timestamp);
1219 /* mark this message as edited */
1220 icon_info = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1221 EMPATHY_IMAGE_EDIT_MESSAGE, 16, 0);
1223 if (icon_info != NULL)
1225 /* set the icon as a background image using CSS
1226 * FIXME: the icon won't update in response to theme changes */
1227 gchar *style = g_strdup_printf (
1228 "background-image:url('%s');"
1229 "background-repeat:no-repeat;"
1230 "background-position:left center;"
1231 "padding-left:19px;", /* 16px icon + 3px padding */
1232 gtk_icon_info_get_filename (icon_info));
1234 webkit_dom_element_set_attribute (span, "style", style, &error);
1236 if (error != NULL)
1238 DEBUG ("Error setting element style: %s",
1239 error->message);
1240 g_clear_error (&error);
1241 /* not fatal */
1244 g_free (style);
1245 g_object_unref (icon_info);
1248 goto finally;
1250 except:
1251 DEBUG ("Could not find message to edit with: %s",
1252 empathy_message_get_body (message));
1254 finally:
1255 g_free (id);
1256 g_free (parsed_body);
1257 #endif
1260 void
1261 empathy_theme_adium_scroll (EmpathyThemeAdium *self,
1262 gboolean allow_scrolling)
1264 self->priv->allow_scrolling = allow_scrolling;
1266 if (allow_scrolling)
1267 empathy_theme_adium_scroll_down (self);
1270 void
1271 empathy_theme_adium_scroll_down (EmpathyThemeAdium *self)
1273 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self), "alignChat(true);", NULL, NULL, NULL);
1276 static void
1277 can_copy_cb (WebKitWebView *web_view,
1278 GAsyncResult *result,
1279 GTask *task)
1281 g_task_return_boolean (task,
1282 webkit_web_view_can_execute_editing_command_finish (web_view, result, NULL));
1283 g_object_unref (task);
1286 void
1287 empathy_theme_adium_can_copy (EmpathyThemeAdium *self,
1288 GCancellable* cancellable,
1289 GAsyncReadyCallback callback,
1290 gpointer user_data)
1292 GTask *task;
1294 task = g_task_new (self, cancellable, callback, user_data);
1295 webkit_web_view_can_execute_editing_command (WEBKIT_WEB_VIEW (self), WEBKIT_EDITING_COMMAND_COPY,
1296 cancellable, (GAsyncReadyCallback)can_copy_cb, task);
1299 gboolean
1300 empathy_theme_adium_can_copy_finish (EmpathyThemeAdium *self,
1301 GAsyncResult* result,
1302 GError** error)
1304 if (!g_task_is_valid (result, self))
1305 return FALSE;
1307 return g_task_propagate_boolean (G_TASK (result), error);
1310 void
1311 empathy_theme_adium_clear (EmpathyThemeAdium *self)
1313 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self), "clearPage()", NULL, NULL, NULL);
1314 empathy_theme_adium_scroll_down (self);
1316 /* Clear last contact to avoid trying to add a 'joined'
1317 * message when we don't have an insertion point. */
1318 if (self->priv->last_contact)
1320 g_object_unref (self->priv->last_contact);
1321 self->priv->last_contact = NULL;
1325 void
1326 empathy_theme_adium_find_previous (EmpathyThemeAdium *self)
1328 webkit_find_controller_search_previous (webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (self)));
1331 void
1332 empathy_theme_adium_find_next (EmpathyThemeAdium *self)
1334 webkit_find_controller_search_next (webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (self)));
1337 void
1338 empathy_theme_adium_find_abilities (EmpathyThemeAdium *self,
1339 const gchar *search_criteria,
1340 gboolean match_case,
1341 gboolean *can_do_previous,
1342 gboolean *can_do_next)
1344 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1345 * find_next and find_previous to work around this problem. */
1346 if (can_do_previous)
1347 *can_do_previous = TRUE;
1348 if (can_do_next)
1349 *can_do_next = TRUE;
1352 void
1353 empathy_theme_adium_search (EmpathyThemeAdium *self,
1354 const gchar *text,
1355 gboolean match_case)
1357 WebKitFindController *find_controller;
1358 WebKitFindOptions options = WEBKIT_FIND_OPTIONS_NONE;
1360 find_controller = webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (self));
1361 if (!text || !*text)
1363 webkit_find_controller_search_finish (find_controller);
1364 return;
1367 if (!match_case)
1368 options |= WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE;
1370 webkit_find_controller_search (find_controller, text, options, G_MAXUINT);
1373 void
1374 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium *self)
1376 webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self),
1377 WEBKIT_EDITING_COMMAND_COPY);
1380 static void
1381 theme_adium_remove_mark_from_message (EmpathyThemeAdium *self,
1382 guint32 id)
1384 #if 0
1385 /* FIXME: check what this is for and port to WebKit2 */
1386 WebKitDOMDocument *dom;
1387 WebKitDOMNodeList *nodes;
1388 gchar *class;
1389 GError *error = NULL;
1391 dom = webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self));
1392 if (dom == NULL)
1393 return;
1395 class = g_strdup_printf (".x-empathy-message-id-%u", id);
1397 /* Get all nodes with focus class */
1398 nodes = webkit_dom_document_query_selector_all (dom, class, &error);
1399 g_free (class);
1401 if (nodes == NULL)
1403 DEBUG ("Error getting focus nodes: %s",
1404 error ? error->message : "No error");
1405 g_clear_error (&error);
1406 return;
1409 theme_adium_remove_focus_marks (self, nodes);
1410 #endif
1413 static void
1414 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data,
1415 gpointer user_data)
1417 EmpathyThemeAdium *self = user_data;
1418 guint32 id = GPOINTER_TO_UINT (data);
1420 theme_adium_remove_mark_from_message (self, id);
1423 void
1424 empathy_theme_adium_focus_toggled (EmpathyThemeAdium *self,
1425 gboolean has_focus)
1427 self->priv->has_focus = has_focus;
1428 if (!self->priv->has_focus)
1430 /* We've lost focus, so let's make sure all the acked
1431 * messages have lost their unread marker. */
1432 g_queue_foreach (&self->priv->acked_messages,
1433 theme_adium_remove_acked_message_unread_mark_foreach, self);
1434 g_queue_clear (&self->priv->acked_messages);
1436 self->priv->has_unread_message = FALSE;
1440 void
1441 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium *self,
1442 EmpathyMessage *message)
1444 TpMessage *tp_msg;
1445 guint32 id;
1446 gboolean valid;
1448 tp_msg = empathy_message_get_tp_message (message);
1450 if (tp_msg == NULL)
1451 return;
1453 id = tp_message_get_pending_message_id (tp_msg, &valid);
1454 if (!valid)
1456 g_warning ("Acknoledged message doesn't have a pending ID");
1457 return;
1460 /* We only want to actually remove the unread marker if the
1461 * view doesn't have focus. If we did it all the time we would
1462 * never see the unread markers, ever! So, we'll queue these
1463 * up, and when we lose focus, we'll remove the markers. */
1464 if (self->priv->has_focus)
1466 g_queue_push_tail (&self->priv->acked_messages,
1467 GUINT_TO_POINTER (id));
1468 return;
1471 theme_adium_remove_mark_from_message (self, id);
1474 static gboolean
1475 theme_adium_context_menu_cb (EmpathyThemeAdium *self,
1476 WebKitContextMenu *context_menu,
1477 GdkEvent *event,
1478 WebKitHitTestResult *hit_test_result,
1479 gpointer user_data)
1481 EmpathyWebKitMenuFlags flags = EMPATHY_WEBKIT_MENU_CLEAR;
1483 if (g_settings_get_boolean (self->priv->gsettings_chat,
1484 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS))
1485 flags |= EMPATHY_WEBKIT_MENU_INSPECT;
1487 empathy_webkit_populate_context_menu (WEBKIT_WEB_VIEW (self), context_menu, hit_test_result, flags);
1488 return FALSE;
1491 void
1492 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium *self,
1493 gboolean show_avatars)
1495 self->priv->show_avatars = show_avatars;
1498 static void
1499 theme_adium_load_changed_cb (WebKitWebView *view,
1500 WebKitLoadEvent event,
1501 gpointer user_data)
1503 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (view);
1504 GList *l;
1506 if (event != WEBKIT_LOAD_FINISHED)
1507 return;
1509 DEBUG ("Page loaded");
1510 self->priv->pages_loading--;
1512 if (self->priv->pages_loading != 0)
1513 return;
1515 /* Display queued messages */
1516 for (l = self->priv->message_queue.head; l != NULL; l = l->next)
1518 QueuedItem *item = l->data;
1520 switch (item->type)
1522 case QUEUED_MESSAGE:
1523 empathy_theme_adium_append_message (self, item->msg,
1524 item->should_highlight);
1525 break;
1527 case QUEUED_EDIT:
1528 empathy_theme_adium_edit_message (self, item->msg);
1529 break;
1531 case QUEUED_EVENT:
1532 empathy_theme_adium_append_event (self, item->str);
1533 break;
1536 free_queued_item (item);
1539 g_queue_clear (&self->priv->message_queue);
1542 static void
1543 theme_adium_finalize (GObject *object)
1545 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1547 empathy_adium_data_unref (self->priv->data);
1549 g_object_unref (self->priv->gsettings_chat);
1550 g_object_unref (self->priv->gsettings_desktop);
1552 g_free (self->priv->variant);
1554 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->finalize (object);
1557 static void
1558 theme_adium_dispose (GObject *object)
1560 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1562 if (self->priv->smiley_manager)
1564 g_object_unref (self->priv->smiley_manager);
1565 self->priv->smiley_manager = NULL;
1568 g_clear_object (&self->priv->first_contact);
1570 if (self->priv->last_contact)
1572 g_object_unref (self->priv->last_contact);
1573 self->priv->last_contact = NULL;
1576 if (self->priv->inspector_window)
1578 gtk_widget_destroy (self->priv->inspector_window);
1579 self->priv->inspector_window = NULL;
1582 if (self->priv->acked_messages.length > 0)
1584 g_queue_clear (&self->priv->acked_messages);
1587 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->dispose (object);
1590 static void
1591 theme_adium_constructed (GObject *object)
1593 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1594 const gchar *font_family = NULL;
1595 gint font_size = 0;
1596 WebKitWebView *webkit_view = WEBKIT_WEB_VIEW (object);
1598 G_OBJECT_CLASS (empathy_theme_adium_parent_class)->constructed (object);
1600 /* Set default settings */
1601 font_family = tp_asv_get_string (self->priv->data->info, "DefaultFontFamily");
1602 font_size = tp_asv_get_int32 (self->priv->data->info, "DefaultFontSize", NULL);
1604 if (font_family && font_size)
1606 g_object_set (webkit_web_view_get_settings (webkit_view),
1607 "default-font-family", font_family,
1608 "default-font-size", font_size,
1609 NULL);
1611 else
1613 empathy_webkit_bind_font_setting (webkit_view,
1614 self->priv->gsettings_desktop,
1615 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME);
1618 g_object_set (webkit_web_view_get_settings (webkit_view),
1619 "default-charset", "utf8",
1620 NULL);
1622 /* Load template */
1623 theme_adium_load_template (EMPATHY_THEME_ADIUM (object));
1625 self->priv->in_construction = FALSE;
1628 static void
1629 theme_adium_get_property (GObject *object,
1630 guint param_id,
1631 GValue *value,
1632 GParamSpec *pspec)
1634 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1636 switch (param_id)
1638 case PROP_ADIUM_DATA:
1639 g_value_set_boxed (value, self->priv->data);
1640 break;
1641 case PROP_VARIANT:
1642 g_value_set_string (value, self->priv->variant);
1643 break;
1644 default:
1645 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1646 break;
1650 static void
1651 theme_adium_set_property (GObject *object,
1652 guint param_id,
1653 const GValue *value,
1654 GParamSpec *pspec)
1656 EmpathyThemeAdium *self = EMPATHY_THEME_ADIUM (object);
1658 switch (param_id)
1660 case PROP_ADIUM_DATA:
1661 g_assert (self->priv->data == NULL);
1662 self->priv->data = g_value_dup_boxed (value);
1663 break;
1664 case PROP_VARIANT:
1665 empathy_theme_adium_set_variant (self, g_value_get_string (value));
1666 break;
1667 default:
1668 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
1669 break;
1673 static void
1674 empathy_theme_adium_class_init (EmpathyThemeAdiumClass *klass)
1676 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1678 object_class->finalize = theme_adium_finalize;
1679 object_class->dispose = theme_adium_dispose;
1680 object_class->constructed = theme_adium_constructed;
1681 object_class->get_property = theme_adium_get_property;
1682 object_class->set_property = theme_adium_set_property;
1684 g_object_class_install_property (object_class, PROP_ADIUM_DATA,
1685 g_param_spec_boxed ("adium-data",
1686 "The theme data",
1687 "Data for the adium theme",
1688 EMPATHY_TYPE_ADIUM_DATA,
1689 G_PARAM_CONSTRUCT_ONLY |
1690 G_PARAM_READWRITE |
1691 G_PARAM_STATIC_STRINGS));
1693 g_object_class_install_property (object_class, PROP_VARIANT,
1694 g_param_spec_string ("variant",
1695 "The theme variant",
1696 "Variant name for the theme",
1697 NULL,
1698 G_PARAM_CONSTRUCT |
1699 G_PARAM_READWRITE |
1700 G_PARAM_STATIC_STRINGS));
1702 g_type_class_add_private (object_class, sizeof (EmpathyThemeAdiumPriv));
1705 static void
1706 empathy_theme_adium_init (EmpathyThemeAdium *self)
1708 self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
1709 EMPATHY_TYPE_THEME_ADIUM, EmpathyThemeAdiumPriv);
1711 self->priv->in_construction = TRUE;
1712 g_queue_init (&self->priv->message_queue);
1713 self->priv->allow_scrolling = TRUE;
1714 self->priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
1716 /* Show avatars by default. */
1717 self->priv->show_avatars = TRUE;
1718 g_signal_connect (self, "load-changed",
1719 G_CALLBACK (theme_adium_load_changed_cb), NULL);
1720 g_signal_connect (self, "decide-policy",
1721 G_CALLBACK (theme_adium_policy_decision_requested_cb), NULL);
1722 g_signal_connect (self, "context-menu",
1723 G_CALLBACK (theme_adium_context_menu_cb), NULL);
1725 self->priv->gsettings_chat = g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA);
1726 self->priv->gsettings_desktop = g_settings_new (
1727 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA);
1730 EmpathyThemeAdium *
1731 empathy_theme_adium_new (EmpathyAdiumData *data,
1732 const gchar *variant)
1734 g_return_val_if_fail (data != NULL, NULL);
1736 return g_object_new (EMPATHY_TYPE_THEME_ADIUM,
1737 "web-context", empathy_webkit_get_web_context (),
1738 "settings", empathy_webkit_get_web_settings (),
1739 "adium-data", data,
1740 "variant", variant,
1741 NULL);
1744 void
1745 empathy_theme_adium_set_variant (EmpathyThemeAdium *self,
1746 const gchar *variant)
1748 gchar *variant_path;
1749 gchar *script;
1751 if (!tp_strdiff (self->priv->variant, variant))
1752 return;
1754 g_free (self->priv->variant);
1755 self->priv->variant = g_strdup (variant);
1757 if (self->priv->in_construction)
1758 return;
1760 DEBUG ("Update view with variant: '%s'", variant);
1761 variant_path = adium_info_dup_path_for_variant (self->priv->data->info,
1762 self->priv->variant);
1763 script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1764 variant_path);
1766 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self), script, NULL, NULL, NULL);
1768 g_free (variant_path);
1769 g_free (script);
1771 g_object_notify (G_OBJECT (self), "variant");
1774 void
1775 empathy_theme_adium_show_inspector (EmpathyThemeAdium *self)
1777 webkit_web_inspector_show (webkit_web_view_get_inspector (WEBKIT_WEB_VIEW (self)));
1780 gboolean
1781 empathy_adium_path_is_valid (const gchar *path)
1783 gboolean ret;
1784 gchar *file;
1785 gchar **tmp;
1786 const gchar *dir;
1788 if (path[0] != '/')
1789 return FALSE;
1791 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1792 tmp = g_strsplit (path, "/", 0);
1793 if (tmp == NULL)
1794 return FALSE;
1796 dir = tmp[g_strv_length (tmp) - 1];
1798 if (!g_str_has_suffix (dir, ".AdiumMessageStyle"))
1800 g_strfreev (tmp);
1801 return FALSE;
1804 g_strfreev (tmp);
1806 /* The theme is not valid if there is no Info.plist */
1807 file = g_build_filename (path, "Contents", "Info.plist",
1808 NULL);
1809 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1810 g_free (file);
1812 if (!ret)
1813 return FALSE;
1815 /* We ship a default Template.html as fallback if there is any problem
1816 * with the one inside the theme. The only other required file is
1817 * Content.html OR Incoming/Content.html*/
1818 file = g_build_filename (path, "Contents", "Resources", "Content.html",
1819 NULL);
1820 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1821 g_free (file);
1823 if (!ret)
1825 file = g_build_filename (path, "Contents", "Resources", "Incoming",
1826 "Content.html", NULL);
1827 ret = g_file_test (file, G_FILE_TEST_EXISTS);
1828 g_free (file);
1831 return ret;
1834 GHashTable *
1835 empathy_adium_info_new (const gchar *path)
1837 gchar *file;
1838 GValue *value;
1839 GHashTable *info = NULL;
1841 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1843 file = g_build_filename (path, "Contents", "Info.plist", NULL);
1844 value = empathy_plist_parse_from_file (file);
1845 g_free (file);
1847 if (value == NULL)
1848 return NULL;
1850 info = g_value_dup_boxed (value);
1851 tp_g_value_slice_free (value);
1853 /* Insert the theme's path into the hash table,
1854 * keys have to be dupped */
1855 tp_asv_set_string (info, g_strdup ("path"), path);
1857 return info;
1860 static guint
1861 adium_info_get_version (GHashTable *info)
1863 return tp_asv_get_int32 (info, "MessageViewVersion", NULL);
1866 static const gchar *
1867 adium_info_get_no_variant_name (GHashTable *info)
1869 const gchar *name = tp_asv_get_string (info, "DisplayNameForNoVariant");
1870 return name ? name : _("Normal");
1873 static gchar *
1874 adium_info_dup_path_for_variant (GHashTable *info,
1875 const gchar *variant)
1877 guint version = adium_info_get_version (info);
1878 const gchar *no_variant = adium_info_get_no_variant_name (info);
1879 GPtrArray *variants;
1880 guint i;
1882 if (version <= 2 && !tp_strdiff (variant, no_variant))
1883 return g_strdup ("main.css");
1885 variants = empathy_adium_info_get_available_variants (info);
1886 if (variants->len == 0)
1887 return g_strdup ("main.css");
1889 /* Verify the variant exists, fallback to the first one */
1890 for (i = 0; i < variants->len; i++)
1892 if (!tp_strdiff (variant, g_ptr_array_index (variants, i)))
1893 break;
1896 if (i == variants->len)
1898 DEBUG ("Variant %s does not exist", variant);
1899 variant = g_ptr_array_index (variants, 0);
1902 return g_strdup_printf ("Variants/%s.css", variant);
1906 const gchar *
1907 empathy_adium_info_get_default_variant (GHashTable *info)
1909 if (adium_info_get_version (info) <= 2)
1910 return adium_info_get_no_variant_name (info);
1912 return tp_asv_get_string (info, "DefaultVariant");
1915 GPtrArray *
1916 empathy_adium_info_get_available_variants (GHashTable *info)
1918 GPtrArray *variants;
1919 const gchar *path;
1920 gchar *dirpath;
1921 GDir *dir;
1923 variants = tp_asv_get_boxed (info, "AvailableVariants", G_TYPE_PTR_ARRAY);
1924 if (variants != NULL)
1925 return variants;
1927 variants = g_ptr_array_new_with_free_func (g_free);
1928 tp_asv_take_boxed (info, g_strdup ("AvailableVariants"),
1929 G_TYPE_PTR_ARRAY, variants);
1931 path = tp_asv_get_string (info, "path");
1932 dirpath = g_build_filename (path, "Contents", "Resources", "Variants", NULL);
1933 dir = g_dir_open (dirpath, 0, NULL);
1934 if (dir != NULL)
1936 const gchar *name;
1938 for (name = g_dir_read_name (dir);
1939 name != NULL;
1940 name = g_dir_read_name (dir))
1942 gchar *display_name;
1944 if (!g_str_has_suffix (name, ".css"))
1945 continue;
1947 display_name = g_strdup (name);
1948 strstr (display_name, ".css")[0] = '\0';
1949 g_ptr_array_add (variants, display_name);
1952 g_dir_close (dir);
1954 g_free (dirpath);
1956 if (adium_info_get_version (info) <= 2)
1957 g_ptr_array_add (variants,
1958 g_strdup (adium_info_get_no_variant_name (info)));
1960 return variants;
1963 GType
1964 empathy_adium_data_get_type (void)
1966 static GType type_id = 0;
1968 if (!type_id)
1970 type_id = g_boxed_type_register_static ("EmpathyAdiumData",
1971 (GBoxedCopyFunc) empathy_adium_data_ref,
1972 (GBoxedFreeFunc) empathy_adium_data_unref);
1975 return type_id;
1978 EmpathyAdiumData *
1979 empathy_adium_data_new_with_info (const gchar *path,
1980 GHashTable *info)
1982 EmpathyAdiumData *data;
1983 gchar *template_html = NULL;
1984 gchar *footer_html = NULL;
1985 gchar *tmp;
1987 g_return_val_if_fail (empathy_adium_path_is_valid (path), NULL);
1989 data = g_slice_new0 (EmpathyAdiumData);
1990 data->ref_count = 1;
1991 data->path = g_strdup (path);
1992 data->basedir = g_strconcat (path, G_DIR_SEPARATOR_S "Contents"
1993 G_DIR_SEPARATOR_S "Resources" G_DIR_SEPARATOR_S, NULL);
1994 data->info = g_hash_table_ref (info);
1995 data->version = adium_info_get_version (info);
1996 data->strings_to_free = g_ptr_array_new_with_free_func (g_free);
1997 data->date_format_cache = g_hash_table_new_full (g_str_hash,
1998 g_str_equal, g_free, g_free);
2000 DEBUG ("Loading theme at %s", path);
2002 #define LOAD(path, var) \
2003 tmp = g_build_filename (data->basedir, path, NULL); \
2004 g_file_get_contents (tmp, &var, NULL, NULL); \
2005 g_free (tmp); \
2007 #define LOAD_CONST(path, var) \
2009 gchar *content; \
2010 LOAD (path, content); \
2011 if (content != NULL) { \
2012 g_ptr_array_add (data->strings_to_free, content); \
2014 var = content; \
2017 /* Load html files */
2018 LOAD_CONST ("Content.html", data->content_html);
2019 LOAD_CONST ("Incoming/Content.html", data->in_content_html);
2020 LOAD_CONST ("Incoming/NextContent.html", data->in_nextcontent_html);
2021 LOAD_CONST ("Incoming/Context.html", data->in_context_html);
2022 LOAD_CONST ("Incoming/NextContext.html", data->in_nextcontext_html);
2023 LOAD_CONST ("Outgoing/Content.html", data->out_content_html);
2024 LOAD_CONST ("Outgoing/NextContent.html", data->out_nextcontent_html);
2025 LOAD_CONST ("Outgoing/Context.html", data->out_context_html);
2026 LOAD_CONST ("Outgoing/NextContext.html", data->out_nextcontext_html);
2027 LOAD_CONST ("Status.html", data->status_html);
2028 LOAD ("Template.html", template_html);
2029 LOAD ("Footer.html", footer_html);
2031 #undef LOAD_CONST
2032 #undef LOAD
2034 /* HTML fallbacks: If we have at least content OR in_content, then
2035 * everything else gets a fallback */
2037 #define FALLBACK(html, fallback) \
2038 if (html == NULL) { \
2039 html = fallback; \
2042 /* in_nextcontent -> in_content -> content */
2043 FALLBACK (data->in_content_html, data->content_html);
2044 FALLBACK (data->in_nextcontent_html, data->in_content_html);
2046 /* context -> content */
2047 FALLBACK (data->in_context_html, data->in_content_html);
2048 FALLBACK (data->in_nextcontext_html, data->in_nextcontent_html);
2049 FALLBACK (data->out_context_html, data->out_content_html);
2050 FALLBACK (data->out_nextcontext_html, data->out_nextcontent_html);
2052 /* out -> in */
2053 FALLBACK (data->out_content_html, data->in_content_html);
2054 FALLBACK (data->out_nextcontent_html, data->in_nextcontent_html);
2055 FALLBACK (data->out_context_html, data->in_context_html);
2056 FALLBACK (data->out_nextcontext_html, data->in_nextcontext_html);
2058 /* status -> in_content */
2059 FALLBACK (data->status_html, data->in_content_html);
2061 #undef FALLBACK
2063 /* template -> empathy's template */
2064 data->custom_template = (template_html != NULL);
2065 if (template_html == NULL)
2067 GError *error = NULL;
2069 tmp = empathy_file_lookup ("Template.html", "data");
2071 if (!g_file_get_contents (tmp, &template_html, NULL, &error)) {
2072 g_warning ("couldn't load Empathy's default theme "
2073 "template: %s", error->message);
2074 g_return_val_if_reached (data);
2077 g_free (tmp);
2080 /* Default avatar */
2081 tmp = g_build_filename (data->basedir, "Incoming", "buddy_icon.png", NULL);
2082 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2084 data->default_incoming_avatar_filename = tmp;
2086 else
2088 g_free (tmp);
2091 tmp = g_build_filename (data->basedir, "Outgoing", "buddy_icon.png", NULL);
2092 if (g_file_test (tmp, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR))
2094 data->default_outgoing_avatar_filename = tmp;
2096 else
2098 g_free (tmp);
2101 /* Old custom templates had only 4 parameters.
2102 * New templates have 5 parameters */
2103 if (data->version <= 2 && data->custom_template)
2105 tmp = string_with_format (template_html,
2106 data->basedir,
2107 "%@", /* Leave variant unset */
2108 "", /* The header */
2109 footer_html ? footer_html : "",
2110 NULL);
2112 else
2114 tmp = string_with_format (template_html,
2115 data->basedir,
2116 data->version <= 2 ? "" : "@import url( \"main.css\" );",
2117 "%@", /* Leave variant unset */
2118 "", /* The header */
2119 footer_html ? footer_html : "",
2120 NULL);
2122 g_ptr_array_add (data->strings_to_free, tmp);
2123 data->template_html = tmp;
2125 g_free (template_html);
2126 g_free (footer_html);
2128 return data;
2131 EmpathyAdiumData *
2132 empathy_adium_data_new (const gchar *path)
2134 EmpathyAdiumData *data;
2135 GHashTable *info;
2137 info = empathy_adium_info_new (path);
2138 data = empathy_adium_data_new_with_info (path, info);
2139 g_hash_table_unref (info);
2141 return data;
2144 EmpathyAdiumData *
2145 empathy_adium_data_ref (EmpathyAdiumData *data)
2147 g_return_val_if_fail (data != NULL, NULL);
2149 g_atomic_int_inc (&data->ref_count);
2151 return data;
2154 void
2155 empathy_adium_data_unref (EmpathyAdiumData *data)
2157 g_return_if_fail (data != NULL);
2159 if (g_atomic_int_dec_and_test (&data->ref_count)) {
2160 g_free (data->path);
2161 g_free (data->basedir);
2162 g_free (data->default_avatar_filename);
2163 g_free (data->default_incoming_avatar_filename);
2164 g_free (data->default_outgoing_avatar_filename);
2165 g_hash_table_unref (data->info);
2166 g_ptr_array_unref (data->strings_to_free);
2167 tp_clear_pointer (&data->date_format_cache, g_hash_table_unref);
2169 g_slice_free (EmpathyAdiumData, data);
2173 GHashTable *
2174 empathy_adium_data_get_info (EmpathyAdiumData *data)
2176 g_return_val_if_fail (data != NULL, NULL);
2178 return data->info;
2181 const gchar *
2182 empathy_adium_data_get_path (EmpathyAdiumData *data)
2184 g_return_val_if_fail (data != NULL, NULL);
2186 return data->path;