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>
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
;
58 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
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
;
69 gboolean has_unread_message
;
70 gboolean allow_scrolling
;
72 gboolean in_construction
;
73 gboolean show_avatars
;
76 struct _EmpathyAdiumData
81 gchar
*default_avatar_filename
;
82 gchar
*default_incoming_avatar_filename
;
83 gchar
*default_outgoing_avatar_filename
;
86 gboolean custom_template
;
87 /* gchar* -> gchar* both owned */
88 GHashTable
*date_format_cache
;
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
106 GPtrArray
*strings_to_free
;
109 static gchar
* adium_info_dup_path_for_variant (GHashTable
*info
,
110 const gchar
*variant
);
119 G_DEFINE_TYPE (EmpathyThemeAdium
, empathy_theme_adium
,
120 WEBKIT_TYPE_WEB_VIEW
)
134 gboolean should_highlight
;
138 queue_item (GQueue
*queue
,
142 gboolean should_highlight
,
145 QueuedItem
*item
= g_slice_new0 (QueuedItem
);
149 item
->msg
= g_object_ref (msg
);
150 item
->str
= g_strdup (str
);
151 item
->should_highlight
= should_highlight
;
154 g_queue_push_head (queue
, item
);
156 g_queue_push_tail (queue
, item
);
162 free_queued_item (QueuedItem
*item
)
164 tp_clear_object (&item
->msg
);
167 g_slice_free (QueuedItem
, item
);
171 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView
*view
,
172 WebKitWebFrame
*web_frame
,
173 WebKitNetworkRequest
*request
,
174 WebKitWebNavigationAction
*action
,
175 WebKitWebPolicyDecision
*decision
,
180 /* Only call url_show on clicks */
181 if (webkit_web_navigation_action_get_reason (action
) !=
182 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED
)
184 webkit_web_policy_decision_use (decision
);
188 uri
= webkit_network_request_get_uri (request
);
189 empathy_url_show (GTK_WIDGET (view
), uri
);
191 webkit_web_policy_decision_ignore (decision
);
195 /* Replace each %@ in format with string passed in args */
197 string_with_format (const gchar
*format
,
198 const gchar
*first_string
,
205 va_start (args
, first_string
);
206 result
= g_string_sized_new (strlen (format
));
207 for (str
= first_string
; str
!= NULL
; str
= va_arg (args
, const gchar
*))
211 next
= strstr (format
, "%@");
215 g_string_append_len (result
, format
, next
- format
);
216 g_string_append (result
, str
);
219 g_string_append (result
, format
);
222 return g_string_free (result
, FALSE
);
226 theme_adium_load_template (EmpathyThemeAdium
*self
)
232 self
->priv
->pages_loading
++;
233 basedir_uri
= g_strconcat ("file://", self
->priv
->data
->basedir
, NULL
);
235 variant_path
= adium_info_dup_path_for_variant (self
->priv
->data
->info
,
236 self
->priv
->variant
);
238 template = string_with_format (self
->priv
->data
->template_html
,
241 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (self
),
242 template, basedir_uri
);
244 g_free (basedir_uri
);
245 g_free (variant_path
);
250 theme_adium_parse_body (EmpathyThemeAdium
*self
,
254 TpawStringParser
*parsers
;
257 /* Check if we have to parse smileys */
258 parsers
= empathy_webkit_get_string_parser (
259 g_settings_get_boolean (self
->priv
->gsettings_chat
,
260 EMPATHY_PREFS_CHAT_SHOW_SMILEYS
));
262 /* Parse text and construct string with links and smileys replaced
263 * by html tags. Also escape text to make sure html code is
264 * displayed verbatim. */
265 string
= g_string_sized_new (strlen (text
));
267 /* wrap this in HTML that allows us to find the message for later
269 if (!tp_str_empty (token
))
270 g_string_append_printf (string
,
271 "<span id=\"message-token-%s\">",
274 tpaw_string_parser_substr (text
, -1, parsers
, string
);
276 if (!tp_str_empty (token
))
277 g_string_append (string
, "</span>");
279 /* Wrap body in order to make tabs and multiple spaces displayed
280 * properly. See bug #625745. */
281 g_string_prepend (string
, "<div style=\"display: inline; "
282 "white-space: pre-wrap\"'>");
283 g_string_append (string
, "</div>");
285 return g_string_free (string
, FALSE
);
289 escape_and_append_len (GString
*string
, const gchar
*str
, gint len
)
291 while (str
!= NULL
&& *str
!= '\0' && len
!= 0)
297 g_string_append (string
, "\\\\");
301 g_string_append (string
, "\\\"");
304 /* Remove end of lines */
307 g_string_append_c (string
, *str
);
315 /* If *str starts with match, returns TRUE and move pointer to the end */
317 theme_adium_match (const gchar
**str
,
322 len
= strlen (match
);
323 if (strncmp (*str
, match
, len
) == 0)
332 /* Like theme_adium_match() but also return the X part if match is
335 theme_adium_match_with_format (const gchar
**str
,
339 const gchar
*cur
= *str
;
342 if (!theme_adium_match (&cur
, match
))
347 end
= strstr (cur
, "}%");
351 *format
= g_strndup (cur
, end
- cur
);
356 /* List of colors used by %senderColor%. Copied from
357 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
359 static gchar
*colors
[] = {
360 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
361 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
362 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
363 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
364 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
365 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
366 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
367 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
368 "lightblue", "lightcoral",
369 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
370 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
371 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
372 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
373 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
374 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
375 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
376 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
377 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
378 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
383 nsdate_to_strftime (EmpathyAdiumData
*data
, const gchar
*nsdate
)
385 /* Convert from NSDateFormatter
386 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
387 * to strftime supported by g_date_time_format.
388 * FIXME: table is incomplete, doc of g_date_time_format has a table of
390 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
391 * in 2.29.x we have to explictely request padding with %0x */
392 static const gchar
*convert_table
[] = {
394 "A", NULL
, // 0~86399999 (Millisecond of Day)
396 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
397 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
398 "cc", "%u", // 1~7 (Day of Week)
399 "c", "%u", // 1~7 (Day of Week)
401 "dd", "%d", // 1~31 (0 padded Day of Month)
402 "d", "%d", // 1~31 (0 padded Day of Month)
403 "D", "%j", // 1~366 (0 padded Day of Year)
405 "e", "%u", // 1~7 (0 padded Day of Week)
406 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
407 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
408 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
409 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
411 "F", NULL
, // 1~5 (0 padded Week of Month, first day of week = Monday)
413 "g", NULL
, // Julian Day Number (number of days since 4713 BC January 1)
414 "GGGG", NULL
, // Before Christ/Anno Domini
415 "GGG", NULL
, // BC/AD (Era Designator Abbreviated)
416 "GG", NULL
, // BC/AD (Era Designator Abbreviated)
417 "G", NULL
, // BC/AD (Era Designator Abbreviated)
419 "h", "%I", // 1~12 (0 padded Hour (12hr))
420 "H", "%H", // 0~23 (0 padded Hour (24hr))
422 "k", NULL
, // 1~24 (0 padded Hour (24hr)
423 "K", NULL
, // 0~11 (0 padded Hour (12hr))
425 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
426 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
427 "LL", "%m", // 1~12 (0 padded Month)
428 "L", "%m", // 1~12 (0 padded Month)
430 "m", "%M", // 0~59 (0 padded Minute)
431 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
432 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
433 "MM", "%m", // 1~12 (0 padded Month)
434 "M", "%m", // 1~12 (0 padded Month)
436 "qqqq", NULL
, // 1st quarter/2nd quarter/3rd quarter/4th quarter
437 "qqq", NULL
, // Q1/Q2/Q3/Q4
438 "qq", NULL
, // 1~4 (0 padded Quarter)
439 "q", NULL
, // 1~4 (0 padded Quarter)
440 "QQQQ", NULL
, // 1st quarter/2nd quarter/3rd quarter/4th quarter
441 "QQQ", NULL
, // Q1/Q2/Q3/Q4
442 "QQ", NULL
, // 1~4 (0 padded Quarter)
443 "Q", NULL
, // 1~4 (0 padded Quarter)
445 "s", "%S", // 0~59 (0 padded Second)
446 "S", NULL
, // (rounded Sub-Second)
448 "u", "%Y", // (0 padded Year)
450 "vvvv", "%Z", // (General GMT Timezone Name)
451 "vvv", "%Z", // (General GMT Timezone Abbreviation)
452 "vv", "%Z", // (General GMT Timezone Abbreviation)
453 "v", "%Z", // (General GMT Timezone Abbreviation)
455 "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)
456 "W", NULL
, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
458 "yyyy", "%Y", // (Full Year)
459 "yyy", "%y", // (2 Digits Year)
460 "yy", "%y", // (2 Digits Year)
461 "y", "%Y", // (Full Year)
462 "YYYY", NULL
, // (Full Year, starting from the Sunday of the 1st week of year)
463 "YYY", NULL
, // (2 Digits Year, starting from the Sunday of the 1st week of year)
464 "YY", NULL
, // (2 Digits Year, starting from the Sunday of the 1st week of year)
465 "Y", NULL
, // (Full Year, starting from the Sunday of the 1st week of year)
467 "zzzz", NULL
, // (Specific GMT Timezone Name)
468 "zzz", NULL
, // (Specific GMT Timezone Abbreviation)
469 "zz", NULL
, // (Specific GMT Timezone Abbreviation)
470 "z", NULL
, // (Specific GMT Timezone Abbreviation)
471 "Z", "%z", // +0000 (RFC 822 Timezone)
480 str
= g_hash_table_lookup (data
->date_format_cache
, nsdate
);
485 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
486 * by corresponding strftime tag. */
487 string
= g_string_sized_new (strlen (nsdate
));
488 for (i
= 0; nsdate
[i
] != '\0'; i
++)
490 gboolean found
= FALSE
;
492 /* even indexes are NSDateFormatter tag, odd indexes are the
493 * corresponding strftime tag */
494 for (j
= 0; j
< G_N_ELEMENTS (convert_table
); j
+= 2)
496 if (g_str_has_prefix (nsdate
+ i
, convert_table
[j
]))
505 /* If we don't have a replacement, just ignore that tag */
506 if (convert_table
[j
+ 1] != NULL
)
507 g_string_append (string
, convert_table
[j
+ 1]);
509 i
+= strlen (convert_table
[j
]) - 1;
513 g_string_append_c (string
, nsdate
[i
]);
517 DEBUG ("Date format converted '%s' → '%s'", nsdate
, string
->str
);
519 /* The cache takes ownership of string->str */
520 g_hash_table_insert (data
->date_format_cache
, g_strdup (nsdate
), string
->str
);
521 return g_string_free (string
, FALSE
);
525 theme_adium_add_html (EmpathyThemeAdium
*self
,
528 const gchar
*message
,
529 const gchar
*avatar_filename
,
531 const gchar
*contact_id
,
532 const gchar
*service_name
,
533 const gchar
*message_classes
,
537 PangoDirection direction
)
541 const gchar
*cur
= NULL
;
545 /* Make some search-and-replace in the html code */
546 string
= g_string_sized_new (strlen (html
) + strlen (message
));
547 g_string_append_printf (string
, "%s(\"", func
);
549 for (cur
= html
; *cur
!= '\0'; cur
++)
551 const gchar
*replace
= NULL
;
552 gchar
*dup_replace
= NULL
;
553 gchar
*format
= NULL
;
555 /* Those are all well known keywords that needs replacement in
556 * html files. Please keep them in the same order than the adium
557 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
558 if (theme_adium_match (&cur
, "%userIconPath%"))
560 replace
= avatar_filename
;
562 else if (theme_adium_match (&cur
, "%senderScreenName%"))
564 replace
= contact_id
;
566 else if (theme_adium_match (&cur
, "%sender%"))
570 else if (theme_adium_match (&cur
, "%senderColor%"))
572 /* A color derived from the user's name.
573 * FIXME: If a colon separated list of HTML colors is at
574 * Incoming/SenderColors.txt it will be used instead of
575 * the default colors.
578 /* Ensure we always use the same color when sending messages
584 else if (contact_id
!= NULL
)
586 guint hash
= g_str_hash (contact_id
);
587 replace
= colors
[hash
% G_N_ELEMENTS (colors
)];
590 else if (theme_adium_match (&cur
, "%senderStatusIcon%"))
592 /* FIXME: The path to the status icon of the sender
593 * (available, away, etc...)
596 else if (theme_adium_match (&cur
, "%messageDirection%"))
600 case PANGO_DIRECTION_LTR
:
601 case PANGO_DIRECTION_TTB_LTR
:
602 case PANGO_DIRECTION_WEAK_LTR
:
605 case PANGO_DIRECTION_RTL
:
606 case PANGO_DIRECTION_TTB_RTL
:
607 case PANGO_DIRECTION_WEAK_RTL
:
610 case PANGO_DIRECTION_NEUTRAL
:
615 else if (theme_adium_match (&cur
, "%senderDisplayName%"))
617 /* FIXME: The serverside (remotely set) name of the
618 * sender, such as an MSN display name.
620 * We don't have access to that yet so we use
621 * local alias instead.
625 else if (theme_adium_match (&cur
, "%senderPrefix%"))
627 /* FIXME: If we supported IRC user mode flags, this
628 * would be replaced with @ if the user is an op, + if
629 * the user has voice, etc. as per
630 * http://hg.adium.im/adium/rev/b586b027de42. But we
631 * don't, so for now we just strip it. */
633 else if (theme_adium_match_with_format (&cur
, "%textbackgroundcolor{",
636 /* FIXME: This keyword is used to represent the
637 * highlight background color. "X" is the opacity of the
638 * background, ranges from 0 to 1 and can be any decimal
642 else if (theme_adium_match (&cur
, "%message%"))
646 else if (theme_adium_match (&cur
, "%time%") ||
647 theme_adium_match_with_format (&cur
, "%time{", &format
))
649 const gchar
*strftime_format
;
651 strftime_format
= nsdate_to_strftime (self
->priv
->data
, format
);
653 dup_replace
= tpaw_time_to_string_local (timestamp
,
654 strftime_format
? strftime_format
:
655 TPAW_TIME_DATE_FORMAT_DISPLAY_SHORT
);
657 dup_replace
= tpaw_time_to_string_local (timestamp
,
658 strftime_format
? strftime_format
:
659 TPAW_TIME_FORMAT_DISPLAY_SHORT
);
661 replace
= dup_replace
;
663 else if (theme_adium_match (&cur
, "%shortTime%"))
665 dup_replace
= tpaw_time_to_string_local (timestamp
,
666 TPAW_TIME_FORMAT_DISPLAY_SHORT
);
667 replace
= dup_replace
;
669 else if (theme_adium_match (&cur
, "%service%"))
671 replace
= service_name
;
673 else if (theme_adium_match (&cur
, "%variant%"))
675 /* FIXME: The name of the active message style variant,
676 * with all spaces replaced with an underscore.
677 * A variant named "Alternating Messages - Blue Red"
678 * will become "Alternating_Messages_-_Blue_Red".
681 else if (theme_adium_match (&cur
, "%userIcons%"))
683 replace
= self
->priv
->show_avatars
? "showIcons" : "hideIcons";
685 else if (theme_adium_match (&cur
, "%messageClasses%"))
687 replace
= message_classes
;
689 else if (theme_adium_match (&cur
, "%status%"))
691 /* FIXME: A description of the status event. This is
692 * neither in the user's local language nor expected to
693 * be displayed; it may be useful to use a different div
694 * class to present different types of status messages.
695 * The following is a list of some of the more important
696 * status messages; your message style should be able to
697 * handle being shown a status message not in this list,
698 * as even at present the list is incomplete and is
699 * certain to become out of date in the future:
708 * contact_joined (group chats)
712 * encryption (all OTR messages use this status)
713 * purple (all IRC topic and join/part messages use this status)
714 * fileTransferStarted
715 * fileTransferCompleted
720 escape_and_append_len (string
, cur
, 1);
724 /* Here we have a replacement to make */
725 escape_and_append_len (string
, replace
, -1);
727 g_free (dup_replace
);
730 g_string_append (string
, "\")");
732 bytes
= g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
733 G_RESOURCE_LOOKUP_FLAGS_NONE
,
738 js
= (const gchar
*) g_bytes_get_data (bytes
, NULL
);
739 g_string_prepend (string
, js
);
740 g_bytes_unref (bytes
);
743 script
= g_string_free (string
, FALSE
);
744 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self
), script
);
749 theme_adium_append_event_escaped (EmpathyThemeAdium
*self
,
750 const gchar
*escaped
,
751 PangoDirection direction
)
753 theme_adium_add_html (self
, "appendMessage",
754 self
->priv
->data
->status_html
, escaped
, NULL
, NULL
, NULL
,
755 NULL
, "event", tpaw_time_get_current (), FALSE
, FALSE
, direction
);
757 /* There is no last contact */
758 if (self
->priv
->last_contact
)
760 g_object_unref (self
->priv
->last_contact
);
761 self
->priv
->last_contact
= NULL
;
766 theme_adium_remove_focus_marks (EmpathyThemeAdium
*self
,
767 WebKitDOMNodeList
*nodes
)
771 /* Remove focus and firstFocus class */
772 for (i
= 0; i
< webkit_dom_node_list_get_length (nodes
); i
++)
774 WebKitDOMNode
*node
= webkit_dom_node_list_item (nodes
, i
);
775 WebKitDOMHTMLElement
*element
= WEBKIT_DOM_HTML_ELEMENT (node
);
777 gchar
**classes
, **iter
;
778 GString
*new_class_name
;
779 gboolean first
= TRUE
;
784 class_name
= webkit_dom_html_element_get_class_name (element
);
785 classes
= g_strsplit (class_name
, " ", -1);
786 new_class_name
= g_string_sized_new (strlen (class_name
));
788 for (iter
= classes
; *iter
!= NULL
; iter
++)
790 if (tp_strdiff (*iter
, "focus") &&
791 tp_strdiff (*iter
, "firstFocus"))
794 g_string_append_c (new_class_name
, ' ');
796 g_string_append (new_class_name
, *iter
);
801 webkit_dom_html_element_set_class_name (element
, new_class_name
->str
);
804 g_strfreev (classes
);
805 g_string_free (new_class_name
, TRUE
);
810 theme_adium_remove_all_focus_marks (EmpathyThemeAdium
*self
)
812 WebKitDOMDocument
*dom
;
813 WebKitDOMNodeList
*nodes
;
814 GError
*error
= NULL
;
816 if (!self
->priv
->has_unread_message
)
819 self
->priv
->has_unread_message
= FALSE
;
821 dom
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
825 /* Get all nodes with focus class */
826 nodes
= webkit_dom_document_query_selector_all (dom
, ".focus", &error
);
830 DEBUG ("Error getting focus nodes: %s",
831 error
? error
->message
: "No error");
832 g_clear_error (&error
);
836 theme_adium_remove_focus_marks (self
, nodes
);
841 ADD_CONSECUTIVE_MSG_SCROLL
= 0,
842 ADD_CONSECUTIVE_MSG_NO_SCROLL
= 1,
844 ADD_MSG_NO_SCROLL
= 3
848 * theme_adium_add_message:
849 * @self: The #EmpathyThemeAdium used by the view.
850 * @msg: An #EmpathyMessage that is to be added to the view.
851 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
852 * @prev_timestamp: (out): Timestamp of the previous message.
853 * @prev_is_backlog: (out): Whether the previous message was fetched
855 * @should_highlight: Whether the message should be highlighted. eg.,
856 * if it matches the user's username in multi-user chat.
857 * @js_funcs: An array of JavaScript function names
859 * Shows @msg in the chat view by adding to @self. Addition is defined
860 * by the JavaScript functions listed in @js_funcs. Common examples
861 * are appending new incoming messages or prepending old messages from
864 * @js_funcs should be an array with exactly 4 entries. The entries
865 * should be the names of JavaScript functions that take the raw HTML
866 * that is to be added to the view as an argument and take the following
867 * actions, in this order:
868 * - add a new consecutive message and scroll to it if needed,
869 * - add a new consecutive message and do not scroll,
870 * - add a new non-consecutive message and scroll to it if needed, and
871 * - add a new non-consecutive message and do not scroll
873 * A message is considered to be consecutive with the previous one if
874 * all the following conditions are met:
875 * - senders are the same contact,
876 * - last message was recieved recently,
877 * - last message and this message both are/aren't backlog, and
878 * - DisableCombineConsecutive is not set in theme's settings
881 theme_adium_add_message (EmpathyThemeAdium
*self
,
883 EmpathyContact
**prev_contact
,
884 gint64
*prev_timestamp
,
885 gboolean
*prev_is_backlog
,
886 gboolean should_highlight
,
887 const gchar
*js_funcs
[])
889 EmpathyContact
*sender
;
892 gchar
*body_escaped
, *name_escaped
;
894 const gchar
*contact_id
;
895 EmpathyAvatar
*avatar
;
896 const gchar
*avatar_filename
= NULL
;
898 const gchar
*html
= NULL
;
900 const gchar
*service_name
;
901 GString
*message_classes
= NULL
;
903 gboolean consecutive
;
905 PangoDirection direction
;
908 /* Get information */
909 sender
= empathy_message_get_sender (msg
);
910 account
= empathy_contact_get_account (sender
);
911 service_name
= tpaw_protocol_name_to_display_name
912 (tp_account_get_protocol_name (account
));
913 if (service_name
== NULL
)
914 service_name
= tp_account_get_protocol_name (account
);
915 timestamp
= empathy_message_get_timestamp (msg
);
916 body_escaped
= theme_adium_parse_body (self
,
917 empathy_message_get_body (msg
),
918 empathy_message_get_token (msg
));
919 name
= empathy_contact_get_logged_alias (sender
);
920 contact_id
= empathy_contact_get_id (sender
);
921 action
= (empathy_message_get_tptype (msg
) ==
922 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION
);
924 name_escaped
= g_markup_escape_text (name
, -1);
926 /* If this is a /me probably */
931 if (self
->priv
->data
->version
>= 4 || !self
->priv
->data
->custom_template
)
933 str
= g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
934 "<span class='actionMessageBody'>%s</span>",
935 name_escaped
, body_escaped
);
939 str
= g_strdup_printf ("*%s*", body_escaped
);
942 g_free (body_escaped
);
946 /* Get the avatar filename, or a fallback */
947 avatar
= empathy_contact_get_avatar (sender
);
949 avatar_filename
= avatar
->filename
;
951 if (!avatar_filename
)
953 if (empathy_contact_is_user (sender
))
954 avatar_filename
= self
->priv
->data
->default_outgoing_avatar_filename
;
956 avatar_filename
= self
->priv
->data
->default_incoming_avatar_filename
;
958 if (!avatar_filename
)
960 if (!self
->priv
->data
->default_avatar_filename
)
961 self
->priv
->data
->default_avatar_filename
=
962 tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT
,
963 GTK_ICON_SIZE_DIALOG
);
965 avatar_filename
= self
->priv
->data
->default_avatar_filename
;
969 is_backlog
= empathy_message_is_backlog (msg
);
970 consecutive
= empathy_contact_equal (*prev_contact
, sender
) &&
971 (ABS (timestamp
- *prev_timestamp
) < MESSAGE_JOIN_PERIOD
) &&
972 (is_backlog
== *prev_is_backlog
) &&
973 !tp_asv_get_boolean (self
->priv
->data
->info
,
974 "DisableCombineConsecutive", NULL
);
976 /* Define message classes */
977 message_classes
= g_string_new ("message");
978 if (!self
->priv
->has_focus
&& !is_backlog
)
980 if (!self
->priv
->has_unread_message
)
982 g_string_append (message_classes
, " firstFocus");
983 self
->priv
->has_unread_message
= TRUE
;
985 g_string_append (message_classes
, " focus");
989 g_string_append (message_classes
, " history");
992 g_string_append (message_classes
, " consecutive");
994 if (empathy_contact_is_user (sender
))
995 g_string_append (message_classes
, " outgoing");
997 g_string_append (message_classes
, " incoming");
999 if (should_highlight
)
1000 g_string_append (message_classes
, " mention");
1002 if (empathy_message_get_tptype (msg
) ==
1003 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY
)
1004 g_string_append (message_classes
, " autoreply");
1007 g_string_append (message_classes
, " action");
1009 /* FIXME: other classes:
1010 * status - the message is a status change
1011 * event - the message is a notification of something happening
1012 * (for example, encryption being turned on)
1013 * %status% - See %status% in theme_adium_add_html ()
1016 /* This is slightly a hack, but it's the only way to add
1017 * arbitrary data to messages in the HTML. We add another
1018 * class called "x-empathy-message-id-*" to the message. This
1019 * way, we can remove the unread marker for this specific
1021 tp_msg
= empathy_message_get_tp_message (msg
);
1027 id
= tp_message_get_pending_message_id (tp_msg
, &valid
);
1029 g_string_append_printf (message_classes
,
1030 " x-empathy-message-id-%u", id
);
1033 /* Define javascript function to use */
1035 func
= self
->priv
->allow_scrolling
? js_funcs
[ADD_CONSECUTIVE_MSG_SCROLL
] :
1036 js_funcs
[ADD_CONSECUTIVE_MSG_NO_SCROLL
];
1038 func
= self
->priv
->allow_scrolling
? js_funcs
[ADD_MSG_SCROLL
] :
1039 js_funcs
[ADD_MSG_NO_SCROLL
];
1041 if (empathy_contact_is_user (sender
))
1046 html
= consecutive
? self
->priv
->data
->out_nextcontext_html
:
1047 self
->priv
->data
->out_context_html
;
1050 html
= consecutive
? self
->priv
->data
->out_nextcontent_html
:
1051 self
->priv
->data
->out_content_html
;
1053 /* remove all the unread marks when we are sending a message */
1054 theme_adium_remove_all_focus_marks (self
);
1061 html
= consecutive
? self
->priv
->data
->in_nextcontext_html
:
1062 self
->priv
->data
->in_context_html
;
1065 html
= consecutive
? self
->priv
->data
->in_nextcontent_html
:
1066 self
->priv
->data
->in_content_html
;
1069 direction
= pango_find_base_dir (empathy_message_get_body (msg
), -1);
1071 theme_adium_add_html (self
, func
, html
, body_escaped
,
1072 avatar_filename
, name_escaped
, contact_id
,
1073 service_name
, message_classes
->str
,
1074 timestamp
, is_backlog
, empathy_contact_is_user (sender
), direction
);
1076 /* Keep the sender of the last displayed message */
1078 g_object_unref (*prev_contact
);
1080 *prev_contact
= g_object_ref (sender
);
1081 *prev_timestamp
= timestamp
;
1082 *prev_is_backlog
= is_backlog
;
1084 g_free (body_escaped
);
1085 g_free (name_escaped
);
1086 g_string_free (message_classes
, TRUE
);
1090 empathy_theme_adium_append_message (EmpathyThemeAdium
*self
,
1091 EmpathyMessage
*msg
,
1092 gboolean should_highlight
)
1094 const gchar
*js_funcs
[] = { "appendNextMessage",
1095 "appendNextMessageNoScroll",
1097 "appendMessageNoScroll" };
1099 if (self
->priv
->pages_loading
!= 0)
1101 queue_item (&self
->priv
->message_queue
, QUEUED_MESSAGE
, msg
, NULL
,
1102 should_highlight
, FALSE
);
1106 theme_adium_add_message (self
, msg
, &self
->priv
->last_contact
,
1107 &self
->priv
->last_timestamp
, &self
->priv
->last_is_backlog
,
1108 should_highlight
, js_funcs
);
1112 empathy_theme_adium_append_event (EmpathyThemeAdium
*self
,
1116 PangoDirection direction
;
1118 if (self
->priv
->pages_loading
!= 0)
1120 queue_item (&self
->priv
->message_queue
, QUEUED_EVENT
, NULL
, str
, FALSE
, FALSE
);
1124 direction
= pango_find_base_dir (str
, -1);
1125 str_escaped
= g_markup_escape_text (str
, -1);
1126 theme_adium_append_event_escaped (self
, str_escaped
, direction
);
1127 g_free (str_escaped
);
1131 empathy_theme_adium_append_event_markup (EmpathyThemeAdium
*self
,
1132 const gchar
*markup_text
,
1133 const gchar
*fallback_text
)
1135 PangoDirection direction
;
1137 direction
= pango_find_base_dir (fallback_text
, -1);
1138 theme_adium_append_event_escaped (self
, markup_text
, direction
);
1142 empathy_theme_adium_prepend_message (EmpathyThemeAdium
*self
,
1143 EmpathyMessage
*msg
,
1144 gboolean should_highlight
)
1146 const gchar
*js_funcs
[] = { "prependPrev",
1151 if (self
->priv
->pages_loading
!= 0)
1153 queue_item (&self
->priv
->message_queue
, QUEUED_MESSAGE
, msg
, NULL
,
1154 should_highlight
, TRUE
);
1158 theme_adium_add_message (self
, msg
, &self
->priv
->first_contact
,
1159 &self
->priv
->first_timestamp
, &self
->priv
->first_is_backlog
,
1160 should_highlight
, js_funcs
);
1164 empathy_theme_adium_edit_message (EmpathyThemeAdium
*self
,
1165 EmpathyMessage
*message
)
1167 WebKitDOMDocument
*doc
;
1168 WebKitDOMElement
*span
;
1169 gchar
*id
, *parsed_body
;
1170 gchar
*tooltip
, *timestamp
;
1171 GtkIconInfo
*icon_info
;
1172 GError
*error
= NULL
;
1174 if (self
->priv
->pages_loading
!= 0)
1176 queue_item (&self
->priv
->message_queue
, QUEUED_EDIT
, message
, NULL
, FALSE
, FALSE
);
1180 id
= g_strdup_printf ("message-token-%s",
1181 empathy_message_get_supersedes (message
));
1182 /* we don't pass a token here, because doing so will return another
1183 * <span> element, and we don't want nested <span> elements */
1184 parsed_body
= theme_adium_parse_body (self
,
1185 empathy_message_get_body (message
), NULL
);
1187 /* find the element */
1188 doc
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
1189 span
= webkit_dom_document_get_element_by_id (doc
, id
);
1193 DEBUG ("Failed to find id '%s'", id
);
1197 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span
))
1199 DEBUG ("Not a HTML element");
1203 /* update the HTML */
1204 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span
),
1205 parsed_body
, &error
);
1209 DEBUG ("Error setting new inner-HTML: %s", error
->message
);
1210 g_error_free (error
);
1215 timestamp
= tpaw_time_to_string_local (
1216 empathy_message_get_timestamp (message
),
1218 tooltip
= g_strdup_printf (_("Message edited at %s"), timestamp
);
1220 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span
),
1226 /* mark this message as edited */
1227 icon_info
= gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1228 EMPATHY_IMAGE_EDIT_MESSAGE
, 16, 0);
1230 if (icon_info
!= NULL
)
1232 /* set the icon as a background image using CSS
1233 * FIXME: the icon won't update in response to theme changes */
1234 gchar
*style
= g_strdup_printf (
1235 "background-image:url('%s');"
1236 "background-repeat:no-repeat;"
1237 "background-position:left center;"
1238 "padding-left:19px;", /* 16px icon + 3px padding */
1239 gtk_icon_info_get_filename (icon_info
));
1241 webkit_dom_element_set_attribute (span
, "style", style
, &error
);
1245 DEBUG ("Error setting element style: %s",
1247 g_clear_error (&error
);
1252 g_object_unref (icon_info
);
1258 DEBUG ("Could not find message to edit with: %s",
1259 empathy_message_get_body (message
));
1263 g_free (parsed_body
);
1267 empathy_theme_adium_scroll (EmpathyThemeAdium
*self
,
1268 gboolean allow_scrolling
)
1270 self
->priv
->allow_scrolling
= allow_scrolling
;
1272 if (allow_scrolling
)
1273 empathy_theme_adium_scroll_down (self
);
1277 empathy_theme_adium_scroll_down (EmpathyThemeAdium
*self
)
1279 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self
), "alignChat(true);");
1283 empathy_theme_adium_get_has_selection (EmpathyThemeAdium
*self
)
1285 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (self
));
1289 empathy_theme_adium_clear (EmpathyThemeAdium
*self
)
1291 theme_adium_load_template (self
);
1293 /* Clear last contact to avoid trying to add a 'joined'
1294 * message when we don't have an insertion point. */
1295 if (self
->priv
->last_contact
)
1297 g_object_unref (self
->priv
->last_contact
);
1298 self
->priv
->last_contact
= NULL
;
1303 empathy_theme_adium_find_previous (EmpathyThemeAdium
*self
,
1304 const gchar
*search_criteria
,
1305 gboolean new_search
,
1306 gboolean match_case
)
1308 /* FIXME: Doesn't respect new_search */
1309 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self
),
1310 search_criteria
, match_case
, FALSE
, TRUE
);
1314 empathy_theme_adium_find_next (EmpathyThemeAdium
*self
,
1315 const gchar
*search_criteria
,
1316 gboolean new_search
,
1317 gboolean match_case
)
1319 /* FIXME: Doesn't respect new_search */
1320 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (self
),
1321 search_criteria
, match_case
, TRUE
, TRUE
);
1325 empathy_theme_adium_find_abilities (EmpathyThemeAdium
*self
,
1326 const gchar
*search_criteria
,
1327 gboolean match_case
,
1328 gboolean
*can_do_previous
,
1329 gboolean
*can_do_next
)
1331 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1332 * find_next and find_previous to work around this problem. */
1333 if (can_do_previous
)
1334 *can_do_previous
= TRUE
;
1336 *can_do_next
= TRUE
;
1340 empathy_theme_adium_highlight (EmpathyThemeAdium
*self
,
1342 gboolean match_case
)
1344 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (self
));
1345 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (self
),
1346 text
, match_case
, 0);
1347 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (self
),
1352 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium
*self
)
1354 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (self
));
1358 theme_adium_remove_mark_from_message (EmpathyThemeAdium
*self
,
1361 WebKitDOMDocument
*dom
;
1362 WebKitDOMNodeList
*nodes
;
1364 GError
*error
= NULL
;
1366 dom
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
1370 class = g_strdup_printf (".x-empathy-message-id-%u", id
);
1372 /* Get all nodes with focus class */
1373 nodes
= webkit_dom_document_query_selector_all (dom
, class, &error
);
1378 DEBUG ("Error getting focus nodes: %s",
1379 error
? error
->message
: "No error");
1380 g_clear_error (&error
);
1384 theme_adium_remove_focus_marks (self
, nodes
);
1388 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data
,
1391 EmpathyThemeAdium
*self
= user_data
;
1392 guint32 id
= GPOINTER_TO_UINT (data
);
1394 theme_adium_remove_mark_from_message (self
, id
);
1398 empathy_theme_adium_focus_toggled (EmpathyThemeAdium
*self
,
1401 self
->priv
->has_focus
= has_focus
;
1402 if (!self
->priv
->has_focus
)
1404 /* We've lost focus, so let's make sure all the acked
1405 * messages have lost their unread marker. */
1406 g_queue_foreach (&self
->priv
->acked_messages
,
1407 theme_adium_remove_acked_message_unread_mark_foreach
, self
);
1408 g_queue_clear (&self
->priv
->acked_messages
);
1410 self
->priv
->has_unread_message
= FALSE
;
1415 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium
*self
,
1416 EmpathyMessage
*message
)
1422 tp_msg
= empathy_message_get_tp_message (message
);
1427 id
= tp_message_get_pending_message_id (tp_msg
, &valid
);
1430 g_warning ("Acknoledged message doesn't have a pending ID");
1434 /* We only want to actually remove the unread marker if the
1435 * view doesn't have focus. If we did it all the time we would
1436 * never see the unread markers, ever! So, we'll queue these
1437 * up, and when we lose focus, we'll remove the markers. */
1438 if (self
->priv
->has_focus
)
1440 g_queue_push_tail (&self
->priv
->acked_messages
,
1441 GUINT_TO_POINTER (id
));
1445 theme_adium_remove_mark_from_message (self
, id
);
1449 theme_adium_context_menu_cb (EmpathyThemeAdium
*self
,
1450 GtkWidget
*default_menu
,
1451 WebKitHitTestResult
*hit_test_result
,
1452 gboolean triggered_with_keyboard
,
1456 EmpathyWebKitMenuFlags flags
= EMPATHY_WEBKIT_MENU_CLEAR
;
1458 if (g_settings_get_boolean (self
->priv
->gsettings_chat
,
1459 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
))
1460 flags
|= EMPATHY_WEBKIT_MENU_INSPECT
;
1462 menu
= empathy_webkit_create_context_menu (
1463 WEBKIT_WEB_VIEW (self
), hit_test_result
, flags
);
1465 gtk_widget_show_all (menu
);
1467 gtk_menu_popup (GTK_MENU (menu
), NULL
, NULL
, NULL
, NULL
, 3,
1468 gtk_get_current_event_time ());
1474 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium
*self
,
1475 gboolean show_avatars
)
1477 self
->priv
->show_avatars
= show_avatars
;
1481 theme_adium_load_finished_cb (WebKitWebView
*view
,
1482 WebKitWebFrame
*frame
,
1485 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (view
);
1488 DEBUG ("Page loaded");
1489 self
->priv
->pages_loading
--;
1491 if (self
->priv
->pages_loading
!= 0)
1494 /* Display queued messages */
1495 for (l
= self
->priv
->message_queue
.head
; l
!= NULL
; l
= l
->next
)
1497 QueuedItem
*item
= l
->data
;
1501 case QUEUED_MESSAGE
:
1502 empathy_theme_adium_append_message (self
, item
->msg
,
1503 item
->should_highlight
);
1507 empathy_theme_adium_edit_message (self
, item
->msg
);
1511 empathy_theme_adium_append_event (self
, item
->str
);
1515 free_queued_item (item
);
1518 g_queue_clear (&self
->priv
->message_queue
);
1522 theme_adium_finalize (GObject
*object
)
1524 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1526 empathy_adium_data_unref (self
->priv
->data
);
1528 g_object_unref (self
->priv
->gsettings_chat
);
1529 g_object_unref (self
->priv
->gsettings_desktop
);
1531 g_free (self
->priv
->variant
);
1533 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->finalize (object
);
1537 theme_adium_dispose (GObject
*object
)
1539 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1541 if (self
->priv
->smiley_manager
)
1543 g_object_unref (self
->priv
->smiley_manager
);
1544 self
->priv
->smiley_manager
= NULL
;
1547 g_clear_object (&self
->priv
->first_contact
);
1549 if (self
->priv
->last_contact
)
1551 g_object_unref (self
->priv
->last_contact
);
1552 self
->priv
->last_contact
= NULL
;
1555 if (self
->priv
->inspector_window
)
1557 gtk_widget_destroy (self
->priv
->inspector_window
);
1558 self
->priv
->inspector_window
= NULL
;
1561 if (self
->priv
->acked_messages
.length
> 0)
1563 g_queue_clear (&self
->priv
->acked_messages
);
1566 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->dispose (object
);
1570 theme_adium_inspector_show_window_cb (WebKitWebInspector
*inspector
,
1571 EmpathyThemeAdium
*self
)
1573 if (self
->priv
->inspector_window
)
1575 gtk_widget_show_all (self
->priv
->inspector_window
);
1582 theme_adium_inspector_close_window_cb (WebKitWebInspector
*inspector
,
1583 EmpathyThemeAdium
*self
)
1585 if (self
->priv
->inspector_window
)
1587 gtk_widget_hide (self
->priv
->inspector_window
);
1593 static WebKitWebView
*
1594 theme_adium_inspect_web_view_cb (WebKitWebInspector
*inspector
,
1595 WebKitWebView
*web_view
,
1596 EmpathyThemeAdium
*self
)
1598 GtkWidget
*scrolled_window
;
1599 GtkWidget
*inspector_web_view
;
1601 if (!self
->priv
->inspector_window
)
1603 /* Create main window */
1604 self
->priv
->inspector_window
= gtk_window_new (GTK_WINDOW_TOPLEVEL
);
1606 gtk_window_set_default_size (GTK_WINDOW (self
->priv
->inspector_window
),
1609 g_signal_connect (self
->priv
->inspector_window
, "delete-event",
1610 G_CALLBACK (gtk_widget_hide_on_delete
), NULL
);
1612 /* Pack a scrolled window */
1613 scrolled_window
= gtk_scrolled_window_new (NULL
, NULL
);
1615 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window
),
1616 GTK_POLICY_AUTOMATIC
,
1617 GTK_POLICY_AUTOMATIC
);
1618 gtk_container_add (GTK_CONTAINER (self
->priv
->inspector_window
),
1620 gtk_widget_show (scrolled_window
);
1622 /* Pack a webview in the scrolled window. That webview will be
1623 * used to render the inspector tool. */
1624 inspector_web_view
= webkit_web_view_new ();
1625 gtk_container_add (GTK_CONTAINER (scrolled_window
),
1626 inspector_web_view
);
1627 gtk_widget_show (scrolled_window
);
1629 return WEBKIT_WEB_VIEW (inspector_web_view
);
1636 theme_adium_constructed (GObject
*object
)
1638 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1639 const gchar
*font_family
= NULL
;
1641 WebKitWebView
*webkit_view
= WEBKIT_WEB_VIEW (object
);
1642 WebKitWebInspector
*webkit_inspector
;
1644 /* Set default settings */
1645 font_family
= tp_asv_get_string (self
->priv
->data
->info
, "DefaultFontFamily");
1646 font_size
= tp_asv_get_int32 (self
->priv
->data
->info
, "DefaultFontSize", NULL
);
1648 if (font_family
&& font_size
)
1650 g_object_set (webkit_web_view_get_settings (webkit_view
),
1651 "default-font-family", font_family
,
1652 "default-font-size", font_size
,
1657 empathy_webkit_bind_font_setting (webkit_view
,
1658 self
->priv
->gsettings_desktop
,
1659 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME
);
1662 /* Setup webkit inspector */
1663 webkit_inspector
= webkit_web_view_get_inspector (webkit_view
);
1664 g_signal_connect (webkit_inspector
, "inspect-web-view",
1665 G_CALLBACK (theme_adium_inspect_web_view_cb
), object
);
1666 g_signal_connect (webkit_inspector
, "show-window",
1667 G_CALLBACK (theme_adium_inspector_show_window_cb
), object
);
1668 g_signal_connect (webkit_inspector
, "close-window",
1669 G_CALLBACK (theme_adium_inspector_close_window_cb
), object
);
1672 theme_adium_load_template (EMPATHY_THEME_ADIUM (object
));
1674 self
->priv
->in_construction
= FALSE
;
1678 theme_adium_get_property (GObject
*object
,
1683 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1687 case PROP_ADIUM_DATA
:
1688 g_value_set_boxed (value
, self
->priv
->data
);
1691 g_value_set_string (value
, self
->priv
->variant
);
1694 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1700 theme_adium_set_property (GObject
*object
,
1702 const GValue
*value
,
1705 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1709 case PROP_ADIUM_DATA
:
1710 g_assert (self
->priv
->data
== NULL
);
1711 self
->priv
->data
= g_value_dup_boxed (value
);
1714 empathy_theme_adium_set_variant (self
, g_value_get_string (value
));
1717 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1723 empathy_theme_adium_class_init (EmpathyThemeAdiumClass
*klass
)
1725 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
1727 object_class
->finalize
= theme_adium_finalize
;
1728 object_class
->dispose
= theme_adium_dispose
;
1729 object_class
->constructed
= theme_adium_constructed
;
1730 object_class
->get_property
= theme_adium_get_property
;
1731 object_class
->set_property
= theme_adium_set_property
;
1733 g_object_class_install_property (object_class
, PROP_ADIUM_DATA
,
1734 g_param_spec_boxed ("adium-data",
1736 "Data for the adium theme",
1737 EMPATHY_TYPE_ADIUM_DATA
,
1738 G_PARAM_CONSTRUCT_ONLY
|
1740 G_PARAM_STATIC_STRINGS
));
1742 g_object_class_install_property (object_class
, PROP_VARIANT
,
1743 g_param_spec_string ("variant",
1744 "The theme variant",
1745 "Variant name for the theme",
1749 G_PARAM_STATIC_STRINGS
));
1751 g_type_class_add_private (object_class
, sizeof (EmpathyThemeAdiumPriv
));
1755 empathy_theme_adium_init (EmpathyThemeAdium
*self
)
1757 self
->priv
= G_TYPE_INSTANCE_GET_PRIVATE (self
,
1758 EMPATHY_TYPE_THEME_ADIUM
, EmpathyThemeAdiumPriv
);
1760 self
->priv
->in_construction
= TRUE
;
1761 g_queue_init (&self
->priv
->message_queue
);
1762 self
->priv
->allow_scrolling
= TRUE
;
1763 self
->priv
->smiley_manager
= empathy_smiley_manager_dup_singleton ();
1765 /* Show avatars by default. */
1766 self
->priv
->show_avatars
= TRUE
;
1768 g_signal_connect (self
, "load-finished",
1769 G_CALLBACK (theme_adium_load_finished_cb
), NULL
);
1770 g_signal_connect (self
, "navigation-policy-decision-requested",
1771 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb
), NULL
);
1772 g_signal_connect (self
, "context-menu",
1773 G_CALLBACK (theme_adium_context_menu_cb
), NULL
);
1775 self
->priv
->gsettings_chat
= g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA
);
1776 self
->priv
->gsettings_desktop
= g_settings_new (
1777 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA
);
1781 empathy_theme_adium_new (EmpathyAdiumData
*data
,
1782 const gchar
*variant
)
1784 g_return_val_if_fail (data
!= NULL
, NULL
);
1786 return g_object_new (EMPATHY_TYPE_THEME_ADIUM
,
1793 empathy_theme_adium_set_variant (EmpathyThemeAdium
*self
,
1794 const gchar
*variant
)
1796 gchar
*variant_path
;
1799 if (!tp_strdiff (self
->priv
->variant
, variant
))
1802 g_free (self
->priv
->variant
);
1803 self
->priv
->variant
= g_strdup (variant
);
1805 if (self
->priv
->in_construction
)
1808 DEBUG ("Update view with variant: '%s'", variant
);
1809 variant_path
= adium_info_dup_path_for_variant (self
->priv
->data
->info
,
1810 self
->priv
->variant
);
1811 script
= g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1814 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (self
), script
);
1816 g_free (variant_path
);
1819 g_object_notify (G_OBJECT (self
), "variant");
1823 empathy_theme_adium_show_inspector (EmpathyThemeAdium
*self
)
1825 WebKitWebView
*web_view
= WEBKIT_WEB_VIEW (self
);
1827 empathy_webkit_show_inspector (web_view
);
1831 empathy_adium_path_is_valid (const gchar
*path
)
1841 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1842 tmp
= g_strsplit (path
, "/", 0);
1846 dir
= tmp
[g_strv_length (tmp
) - 1];
1848 if (!g_str_has_suffix (dir
, ".AdiumMessageStyle"))
1856 /* The theme is not valid if there is no Info.plist */
1857 file
= g_build_filename (path
, "Contents", "Info.plist",
1859 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1865 /* We ship a default Template.html as fallback if there is any problem
1866 * with the one inside the theme. The only other required file is
1867 * Content.html OR Incoming/Content.html*/
1868 file
= g_build_filename (path
, "Contents", "Resources", "Content.html",
1870 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1875 file
= g_build_filename (path
, "Contents", "Resources", "Incoming",
1876 "Content.html", NULL
);
1877 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1885 empathy_adium_info_new (const gchar
*path
)
1889 GHashTable
*info
= NULL
;
1891 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1893 file
= g_build_filename (path
, "Contents", "Info.plist", NULL
);
1894 value
= empathy_plist_parse_from_file (file
);
1900 info
= g_value_dup_boxed (value
);
1901 tp_g_value_slice_free (value
);
1903 /* Insert the theme's path into the hash table,
1904 * keys have to be dupped */
1905 tp_asv_set_string (info
, g_strdup ("path"), path
);
1911 adium_info_get_version (GHashTable
*info
)
1913 return tp_asv_get_int32 (info
, "MessageViewVersion", NULL
);
1916 static const gchar
*
1917 adium_info_get_no_variant_name (GHashTable
*info
)
1919 const gchar
*name
= tp_asv_get_string (info
, "DisplayNameForNoVariant");
1920 return name
? name
: _("Normal");
1924 adium_info_dup_path_for_variant (GHashTable
*info
,
1925 const gchar
*variant
)
1927 guint version
= adium_info_get_version (info
);
1928 const gchar
*no_variant
= adium_info_get_no_variant_name (info
);
1929 GPtrArray
*variants
;
1932 if (version
<= 2 && !tp_strdiff (variant
, no_variant
))
1933 return g_strdup ("main.css");
1935 variants
= empathy_adium_info_get_available_variants (info
);
1936 if (variants
->len
== 0)
1937 return g_strdup ("main.css");
1939 /* Verify the variant exists, fallback to the first one */
1940 for (i
= 0; i
< variants
->len
; i
++)
1942 if (!tp_strdiff (variant
, g_ptr_array_index (variants
, i
)))
1946 if (i
== variants
->len
)
1948 DEBUG ("Variant %s does not exist", variant
);
1949 variant
= g_ptr_array_index (variants
, 0);
1952 return g_strdup_printf ("Variants/%s.css", variant
);
1957 empathy_adium_info_get_default_variant (GHashTable
*info
)
1959 if (adium_info_get_version (info
) <= 2)
1960 return adium_info_get_no_variant_name (info
);
1962 return tp_asv_get_string (info
, "DefaultVariant");
1966 empathy_adium_info_get_available_variants (GHashTable
*info
)
1968 GPtrArray
*variants
;
1973 variants
= tp_asv_get_boxed (info
, "AvailableVariants", G_TYPE_PTR_ARRAY
);
1974 if (variants
!= NULL
)
1977 variants
= g_ptr_array_new_with_free_func (g_free
);
1978 tp_asv_take_boxed (info
, g_strdup ("AvailableVariants"),
1979 G_TYPE_PTR_ARRAY
, variants
);
1981 path
= tp_asv_get_string (info
, "path");
1982 dirpath
= g_build_filename (path
, "Contents", "Resources", "Variants", NULL
);
1983 dir
= g_dir_open (dirpath
, 0, NULL
);
1988 for (name
= g_dir_read_name (dir
);
1990 name
= g_dir_read_name (dir
))
1992 gchar
*display_name
;
1994 if (!g_str_has_suffix (name
, ".css"))
1997 display_name
= g_strdup (name
);
1998 strstr (display_name
, ".css")[0] = '\0';
1999 g_ptr_array_add (variants
, display_name
);
2006 if (adium_info_get_version (info
) <= 2)
2007 g_ptr_array_add (variants
,
2008 g_strdup (adium_info_get_no_variant_name (info
)));
2014 empathy_adium_data_get_type (void)
2016 static GType type_id
= 0;
2020 type_id
= g_boxed_type_register_static ("EmpathyAdiumData",
2021 (GBoxedCopyFunc
) empathy_adium_data_ref
,
2022 (GBoxedFreeFunc
) empathy_adium_data_unref
);
2029 empathy_adium_data_new_with_info (const gchar
*path
,
2032 EmpathyAdiumData
*data
;
2033 gchar
*template_html
= NULL
;
2034 gchar
*footer_html
= NULL
;
2037 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
2039 data
= g_slice_new0 (EmpathyAdiumData
);
2040 data
->ref_count
= 1;
2041 data
->path
= g_strdup (path
);
2042 data
->basedir
= g_strconcat (path
, G_DIR_SEPARATOR_S
"Contents"
2043 G_DIR_SEPARATOR_S
"Resources" G_DIR_SEPARATOR_S
, NULL
);
2044 data
->info
= g_hash_table_ref (info
);
2045 data
->version
= adium_info_get_version (info
);
2046 data
->strings_to_free
= g_ptr_array_new_with_free_func (g_free
);
2047 data
->date_format_cache
= g_hash_table_new_full (g_str_hash
,
2048 g_str_equal
, g_free
, g_free
);
2050 DEBUG ("Loading theme at %s", path
);
2052 #define LOAD(path, var) \
2053 tmp = g_build_filename (data->basedir, path, NULL); \
2054 g_file_get_contents (tmp, &var, NULL, NULL); \
2057 #define LOAD_CONST(path, var) \
2060 LOAD (path, content); \
2061 if (content != NULL) { \
2062 g_ptr_array_add (data->strings_to_free, content); \
2067 /* Load html files */
2068 LOAD_CONST ("Content.html", data
->content_html
);
2069 LOAD_CONST ("Incoming/Content.html", data
->in_content_html
);
2070 LOAD_CONST ("Incoming/NextContent.html", data
->in_nextcontent_html
);
2071 LOAD_CONST ("Incoming/Context.html", data
->in_context_html
);
2072 LOAD_CONST ("Incoming/NextContext.html", data
->in_nextcontext_html
);
2073 LOAD_CONST ("Outgoing/Content.html", data
->out_content_html
);
2074 LOAD_CONST ("Outgoing/NextContent.html", data
->out_nextcontent_html
);
2075 LOAD_CONST ("Outgoing/Context.html", data
->out_context_html
);
2076 LOAD_CONST ("Outgoing/NextContext.html", data
->out_nextcontext_html
);
2077 LOAD_CONST ("Status.html", data
->status_html
);
2078 LOAD ("Template.html", template_html
);
2079 LOAD ("Footer.html", footer_html
);
2084 /* HTML fallbacks: If we have at least content OR in_content, then
2085 * everything else gets a fallback */
2087 #define FALLBACK(html, fallback) \
2088 if (html == NULL) { \
2092 /* in_nextcontent -> in_content -> content */
2093 FALLBACK (data
->in_content_html
, data
->content_html
);
2094 FALLBACK (data
->in_nextcontent_html
, data
->in_content_html
);
2096 /* context -> content */
2097 FALLBACK (data
->in_context_html
, data
->in_content_html
);
2098 FALLBACK (data
->in_nextcontext_html
, data
->in_nextcontent_html
);
2099 FALLBACK (data
->out_context_html
, data
->out_content_html
);
2100 FALLBACK (data
->out_nextcontext_html
, data
->out_nextcontent_html
);
2103 FALLBACK (data
->out_content_html
, data
->in_content_html
);
2104 FALLBACK (data
->out_nextcontent_html
, data
->in_nextcontent_html
);
2105 FALLBACK (data
->out_context_html
, data
->in_context_html
);
2106 FALLBACK (data
->out_nextcontext_html
, data
->in_nextcontext_html
);
2108 /* status -> in_content */
2109 FALLBACK (data
->status_html
, data
->in_content_html
);
2113 /* template -> empathy's template */
2114 data
->custom_template
= (template_html
!= NULL
);
2115 if (template_html
== NULL
)
2117 GError
*error
= NULL
;
2119 tmp
= empathy_file_lookup ("Template.html", "data");
2121 if (!g_file_get_contents (tmp
, &template_html
, NULL
, &error
)) {
2122 g_warning ("couldn't load Empathy's default theme "
2123 "template: %s", error
->message
);
2124 g_return_val_if_reached (data
);
2130 /* Default avatar */
2131 tmp
= g_build_filename (data
->basedir
, "Incoming", "buddy_icon.png", NULL
);
2132 if (g_file_test (tmp
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
))
2134 data
->default_incoming_avatar_filename
= tmp
;
2141 tmp
= g_build_filename (data
->basedir
, "Outgoing", "buddy_icon.png", NULL
);
2142 if (g_file_test (tmp
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
))
2144 data
->default_outgoing_avatar_filename
= tmp
;
2151 /* Old custom templates had only 4 parameters.
2152 * New templates have 5 parameters */
2153 if (data
->version
<= 2 && data
->custom_template
)
2155 tmp
= string_with_format (template_html
,
2157 "%@", /* Leave variant unset */
2158 "", /* The header */
2159 footer_html
? footer_html
: "",
2164 tmp
= string_with_format (template_html
,
2166 data
->version
<= 2 ? "" : "@import url( \"main.css\" );",
2167 "%@", /* Leave variant unset */
2168 "", /* The header */
2169 footer_html
? footer_html
: "",
2172 g_ptr_array_add (data
->strings_to_free
, tmp
);
2173 data
->template_html
= tmp
;
2175 g_free (template_html
);
2176 g_free (footer_html
);
2182 empathy_adium_data_new (const gchar
*path
)
2184 EmpathyAdiumData
*data
;
2187 info
= empathy_adium_info_new (path
);
2188 data
= empathy_adium_data_new_with_info (path
, info
);
2189 g_hash_table_unref (info
);
2195 empathy_adium_data_ref (EmpathyAdiumData
*data
)
2197 g_return_val_if_fail (data
!= NULL
, NULL
);
2199 g_atomic_int_inc (&data
->ref_count
);
2205 empathy_adium_data_unref (EmpathyAdiumData
*data
)
2207 g_return_if_fail (data
!= NULL
);
2209 if (g_atomic_int_dec_and_test (&data
->ref_count
)) {
2210 g_free (data
->path
);
2211 g_free (data
->basedir
);
2212 g_free (data
->default_avatar_filename
);
2213 g_free (data
->default_incoming_avatar_filename
);
2214 g_free (data
->default_outgoing_avatar_filename
);
2215 g_hash_table_unref (data
->info
);
2216 g_ptr_array_unref (data
->strings_to_free
);
2217 tp_clear_pointer (&data
->date_format_cache
, g_hash_table_unref
);
2219 g_slice_free (EmpathyAdiumData
, data
);
2224 empathy_adium_data_get_info (EmpathyAdiumData
*data
)
2226 g_return_val_if_fail (data
!= NULL
, NULL
);
2232 empathy_adium_data_get_path (EmpathyAdiumData
*data
)
2234 g_return_val_if_fail (data
!= NULL
, NULL
);