1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2008-2009 Collabora Ltd.
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 * Authors: Xavier Claessens <xclaesse@gmail.com>
25 #include <glib/gi18n-lib.h>
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
31 #include <pango/pango.h>
34 #include <libempathy/empathy-gsettings.h>
35 #include <libempathy/empathy-time.h>
36 #include <libempathy/empathy-utils.h>
38 #include "empathy-theme-adium.h"
39 #include "empathy-smiley-manager.h"
40 #include "empathy-ui-utils.h"
41 #include "empathy-plist.h"
42 #include "empathy-string-parser.h"
43 #include "empathy-images.h"
45 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
46 #include <libempathy/empathy-debug.h>
48 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
50 #define BORING_DPI_DEFAULT 96
52 /* "Join" consecutive messages with timestamps within five minutes */
53 #define MESSAGE_JOIN_PERIOD 5*60
56 EmpathyAdiumData
*data
;
57 EmpathySmileyManager
*smiley_manager
;
58 EmpathyContact
*last_contact
;
59 gint64 last_timestamp
;
60 gboolean last_is_backlog
;
62 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
64 /* Queue of owned gchar* of message token to remove unread
65 * marker for when we lose focus. */
66 GQueue acked_messages
;
67 GtkWidget
*inspector_window
;
68 GSettings
*gsettings_chat
;
70 gboolean has_unread_message
;
71 gboolean allow_scrolling
;
73 gboolean in_construction
;
74 } EmpathyThemeAdiumPriv
;
76 struct _EmpathyAdiumData
{
80 gchar
*default_avatar_filename
;
81 gchar
*default_incoming_avatar_filename
;
82 gchar
*default_outgoing_avatar_filename
;
85 gboolean custom_template
;
86 /* gchar* -> gchar* both owned */
87 GHashTable
*date_format_cache
;
90 const gchar
*template_html
;
91 const gchar
*content_html
;
92 const gchar
*in_content_html
;
93 const gchar
*in_context_html
;
94 const gchar
*in_nextcontent_html
;
95 const gchar
*in_nextcontext_html
;
96 const gchar
*out_content_html
;
97 const gchar
*out_context_html
;
98 const gchar
*out_nextcontent_html
;
99 const gchar
*out_nextcontext_html
;
100 const gchar
*status_html
;
102 /* Above html strings are pointers to strings stored in this array.
103 * We do this because of fallbacks, some htmls could be pointing the
105 GPtrArray
*strings_to_free
;
108 static void theme_adium_iface_init (EmpathyChatViewIface
*iface
);
109 static gchar
* adium_info_dup_path_for_variant (GHashTable
*info
, const gchar
*variant
);
117 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium
, empathy_theme_adium
,
118 WEBKIT_TYPE_WEB_VIEW
,
119 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW
,
120 theme_adium_iface_init
));
135 queue_item (GQueue
*queue
,
140 QueuedItem
*item
= g_slice_new0 (QueuedItem
);
144 item
->msg
= g_object_ref (msg
);
145 item
->str
= g_strdup (str
);
147 g_queue_push_tail (queue
, item
);
153 free_queued_item (QueuedItem
*item
)
155 tp_clear_object (&item
->msg
);
158 g_slice_free (QueuedItem
, item
);
162 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium
*theme
)
164 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
165 WebKitWebView
*web_view
= WEBKIT_WEB_VIEW (theme
);
166 gboolean enable_webkit_developer_tools
;
168 enable_webkit_developer_tools
= g_settings_get_boolean (
169 priv
->gsettings_chat
,
170 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
);
172 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view
)),
173 "enable-developer-extras",
174 enable_webkit_developer_tools
,
179 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings
*gsettings
,
183 EmpathyThemeAdium
*theme
= user_data
;
185 theme_adium_update_enable_webkit_developer_tools (theme
);
189 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView
*view
,
190 WebKitWebFrame
*web_frame
,
191 WebKitNetworkRequest
*request
,
192 WebKitWebNavigationAction
*action
,
193 WebKitWebPolicyDecision
*decision
,
198 /* Only call url_show on clicks */
199 if (webkit_web_navigation_action_get_reason (action
) !=
200 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED
) {
201 webkit_web_policy_decision_use (decision
);
205 uri
= webkit_network_request_get_uri (request
);
206 empathy_url_show (GTK_WIDGET (view
), uri
);
208 webkit_web_policy_decision_ignore (decision
);
213 theme_adium_copy_address_cb (GtkMenuItem
*menuitem
,
216 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
218 GtkClipboard
*clipboard
;
220 g_object_get (G_OBJECT (hit_test_result
), "link-uri", &uri
, NULL
);
222 clipboard
= gtk_clipboard_get (GDK_SELECTION_CLIPBOARD
);
223 gtk_clipboard_set_text (clipboard
, uri
, -1);
225 clipboard
= gtk_clipboard_get (GDK_SELECTION_PRIMARY
);
226 gtk_clipboard_set_text (clipboard
, uri
, -1);
232 theme_adium_open_address_cb (GtkMenuItem
*menuitem
,
235 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
238 g_object_get (G_OBJECT (hit_test_result
), "link-uri", &uri
, NULL
);
240 empathy_url_show (GTK_WIDGET (menuitem
), uri
);
245 /* Replace each %@ in format with string passed in args */
247 string_with_format (const gchar
*format
,
248 const gchar
*first_string
,
255 va_start (args
, first_string
);
256 result
= g_string_sized_new (strlen (format
));
257 for (str
= first_string
; str
!= NULL
; str
= va_arg (args
, const gchar
*)) {
260 next
= strstr (format
, "%@");
265 g_string_append_len (result
, format
, next
- format
);
266 g_string_append (result
, str
);
269 g_string_append (result
, format
);
272 return g_string_free (result
, FALSE
);
276 theme_adium_load_template (EmpathyThemeAdium
*theme
)
278 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
283 priv
->pages_loading
++;
284 basedir_uri
= g_strconcat ("file://", priv
->data
->basedir
, NULL
);
285 variant_path
= adium_info_dup_path_for_variant (priv
->data
->info
,
287 template = string_with_format (priv
->data
->template_html
,
289 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (theme
),
290 template, basedir_uri
);
291 g_free (basedir_uri
);
292 g_free (variant_path
);
297 theme_adium_match_newline (const gchar
*text
,
299 EmpathyStringReplace replace_func
,
300 EmpathyStringParser
*sub_parsers
,
303 GString
*string
= user_data
;
311 /* Replace \n by <br/> */
312 for (i
= 0; i
< len
&& text
[i
] != '\0'; i
++) {
313 if (text
[i
] == '\n') {
314 empathy_string_parser_substr (text
+ prev
,
315 i
- prev
, sub_parsers
,
317 g_string_append (string
, "<br/>");
321 empathy_string_parser_substr (text
+ prev
, i
- prev
,
322 sub_parsers
, user_data
);
326 theme_adium_replace_smiley (const gchar
*text
,
331 EmpathySmileyHit
*hit
= match_data
;
332 GString
*string
= user_data
;
334 /* Replace smiley by a <img/> tag */
335 g_string_append_printf (string
,
336 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
337 hit
->path
, (int)len
, text
, (int)len
, text
);
340 static EmpathyStringParser string_parsers
[] = {
341 {empathy_string_match_link
, empathy_string_replace_link
},
342 {theme_adium_match_newline
, NULL
},
343 {empathy_string_match_all
, empathy_string_replace_escaped
},
347 static EmpathyStringParser string_parsers_with_smiley
[] = {
348 {empathy_string_match_link
, empathy_string_replace_link
},
349 {empathy_string_match_smiley
, theme_adium_replace_smiley
},
350 {theme_adium_match_newline
, NULL
},
351 {empathy_string_match_all
, empathy_string_replace_escaped
},
356 theme_adium_parse_body (EmpathyThemeAdium
*self
,
360 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (self
);
361 EmpathyStringParser
*parsers
;
364 /* Check if we have to parse smileys */
365 if (g_settings_get_boolean (priv
->gsettings_chat
,
366 EMPATHY_PREFS_CHAT_SHOW_SMILEYS
))
367 parsers
= string_parsers_with_smiley
;
369 parsers
= string_parsers
;
371 /* Parse text and construct string with links and smileys replaced
372 * by html tags. Also escape text to make sure html code is
373 * displayed verbatim. */
374 string
= g_string_sized_new (strlen (text
));
376 /* wrap this in HTML that allows us to find the message for later
378 if (!tp_str_empty (token
))
379 g_string_append_printf (string
,
380 "<span id=\"message-token-%s\">",
383 empathy_string_parser_substr (text
, -1, parsers
, string
);
385 if (!tp_str_empty (token
))
386 g_string_append (string
, "</span>");
388 /* Wrap body in order to make tabs and multiple spaces displayed
389 * properly. See bug #625745. */
390 g_string_prepend (string
, "<div style=\"display: inline; "
391 "white-space: pre-wrap\"'>");
392 g_string_append (string
, "</div>");
394 return g_string_free (string
, FALSE
);
398 escape_and_append_len (GString
*string
, const gchar
*str
, gint len
)
400 while (str
!= NULL
&& *str
!= '\0' && len
!= 0) {
404 g_string_append (string
, "\\\\");
408 g_string_append (string
, "\\\"");
411 /* Remove end of lines */
414 g_string_append_c (string
, *str
);
422 /* If *str starts with match, returns TRUE and move pointer to the end */
424 theme_adium_match (const gchar
**str
,
429 len
= strlen (match
);
430 if (strncmp (*str
, match
, len
) == 0) {
438 /* Like theme_adium_match() but also return the X part if match is like %foo{X}% */
440 theme_adium_match_with_format (const gchar
**str
,
444 const gchar
*cur
= *str
;
447 if (!theme_adium_match (&cur
, match
)) {
452 end
= strstr (cur
, "}%");
457 *format
= g_strndup (cur
, end
- cur
);
462 /* List of colors used by %senderColor%. Copied from
463 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
465 static gchar
*colors
[] = {
466 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
467 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
468 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
469 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
470 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
471 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
472 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
473 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
474 "lightblue", "lightcoral",
475 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
476 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
477 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
478 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
479 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
480 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
481 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
482 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
483 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
484 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
489 nsdate_to_strftime (EmpathyAdiumData
*data
, const gchar
*nsdate
)
491 /* Convert from NSDateFormatter (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
492 * to strftime supported by g_date_time_format.
493 * FIXME: table is incomplete, doc of g_date_time_format has a table of
495 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
496 * in 2.29.x we have to explictely request padding with %0x */
497 static const gchar
*convert_table
[] = {
499 "A", NULL
, // 0~86399999 (Millisecond of Day)
501 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
502 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
503 "cc", "%u", // 1~7 (Day of Week)
504 "c", "%u", // 1~7 (Day of Week)
506 "dd", "%d", // 1~31 (0 padded Day of Month)
507 "d", "%d", // 1~31 (0 padded Day of Month)
508 "D", "%j", // 1~366 (0 padded Day of Year)
510 "e", "%u", // 1~7 (0 padded Day of Week)
511 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
512 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
513 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
514 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
516 "F", NULL
, // 1~5 (0 padded Week of Month, first day of week = Monday)
518 "g", NULL
, // Julian Day Number (number of days since 4713 BC January 1)
519 "GGGG", NULL
, // Before Christ/Anno Domini
520 "GGG", NULL
, // BC/AD (Era Designator Abbreviated)
521 "GG", NULL
, // BC/AD (Era Designator Abbreviated)
522 "G", NULL
, // BC/AD (Era Designator Abbreviated)
524 "h", "%I", // 1~12 (0 padded Hour (12hr))
525 "H", "%H", // 0~23 (0 padded Hour (24hr))
527 "k", NULL
, // 1~24 (0 padded Hour (24hr)
528 "K", NULL
, // 0~11 (0 padded Hour (12hr))
530 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
531 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
532 "LL", "%m", // 1~12 (0 padded Month)
533 "L", "%m", // 1~12 (0 padded Month)
535 "m", "%M", // 0~59 (0 padded Minute)
536 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
537 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
538 "MM", "%m", // 1~12 (0 padded Month)
539 "M", "%m", // 1~12 (0 padded Month)
541 "qqqq", NULL
, // 1st quarter/2nd quarter/3rd quarter/4th quarter
542 "qqq", NULL
, // Q1/Q2/Q3/Q4
543 "qq", NULL
, // 1~4 (0 padded Quarter)
544 "q", NULL
, // 1~4 (0 padded Quarter)
545 "QQQQ", NULL
, // 1st quarter/2nd quarter/3rd quarter/4th quarter
546 "QQQ", NULL
, // Q1/Q2/Q3/Q4
547 "QQ", NULL
, // 1~4 (0 padded Quarter)
548 "Q", NULL
, // 1~4 (0 padded Quarter)
550 "s", "%S", // 0~59 (0 padded Second)
551 "S", NULL
, // (rounded Sub-Second)
553 "u", "%Y", // (0 padded Year)
555 "vvvv", "%Z", // (General GMT Timezone Name)
556 "vvv", "%Z", // (General GMT Timezone Abbreviation)
557 "vv", "%Z", // (General GMT Timezone Abbreviation)
558 "v", "%Z", // (General GMT Timezone Abbreviation)
560 "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)
561 "W", NULL
, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
563 "yyyy", "%Y", // (Full Year)
564 "yyy", "%y", // (2 Digits Year)
565 "yy", "%y", // (2 Digits Year)
566 "y", "%Y", // (Full Year)
567 "YYYY", NULL
, // (Full Year, starting from the Sunday of the 1st week of year)
568 "YYY", NULL
, // (2 Digits Year, starting from the Sunday of the 1st week of year)
569 "YY", NULL
, // (2 Digits Year, starting from the Sunday of the 1st week of year)
570 "Y", NULL
, // (Full Year, starting from the Sunday of the 1st week of year)
572 "zzzz", NULL
, // (Specific GMT Timezone Name)
573 "zzz", NULL
, // (Specific GMT Timezone Abbreviation)
574 "zz", NULL
, // (Specific GMT Timezone Abbreviation)
575 "z", NULL
, // (Specific GMT Timezone Abbreviation)
576 "Z", "%z", // +0000 (RFC 822 Timezone)
582 if (nsdate
== NULL
) {
586 str
= g_hash_table_lookup (data
->date_format_cache
, nsdate
);
591 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
592 * by corresponding strftime tag. */
593 string
= g_string_sized_new (strlen (nsdate
));
594 for (i
= 0; nsdate
[i
] != '\0'; i
++) {
595 gboolean found
= FALSE
;
597 /* even indexes are NSDateFormatter tag, odd indexes are the
598 * corresponding strftime tag */
599 for (j
= 0; j
< G_N_ELEMENTS (convert_table
); j
+= 2) {
600 if (g_str_has_prefix (nsdate
+ i
, convert_table
[j
])) {
606 /* If we don't have a replacement, just ignore that tag */
607 if (convert_table
[j
+ 1] != NULL
) {
608 g_string_append (string
, convert_table
[j
+ 1]);
610 i
+= strlen (convert_table
[j
]) - 1;
612 g_string_append_c (string
, nsdate
[i
]);
616 DEBUG ("Date format converted '%s' → '%s'", nsdate
, string
->str
);
618 /* The cache takes ownership of string->str */
619 g_hash_table_insert (data
->date_format_cache
, g_strdup (nsdate
), string
->str
);
620 return g_string_free (string
, FALSE
);
625 theme_adium_append_html (EmpathyThemeAdium
*theme
,
628 const gchar
*message
,
629 const gchar
*avatar_filename
,
631 const gchar
*contact_id
,
632 const gchar
*service_name
,
633 const gchar
*message_classes
,
637 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
639 const gchar
*cur
= NULL
;
642 /* Make some search-and-replace in the html code */
643 string
= g_string_sized_new (strlen (html
) + strlen (message
));
644 g_string_append_printf (string
, "%s(\"", func
);
645 for (cur
= html
; *cur
!= '\0'; cur
++) {
646 const gchar
*replace
= NULL
;
647 gchar
*dup_replace
= NULL
;
648 gchar
*format
= NULL
;
650 /* Those are all well known keywords that needs replacement in
651 * html files. Please keep them in the same order than the adium
652 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
653 if (theme_adium_match (&cur
, "%userIconPath%")) {
654 replace
= avatar_filename
;
655 } else if (theme_adium_match (&cur
, "%senderScreenName%")) {
656 replace
= contact_id
;
657 } else if (theme_adium_match (&cur
, "%sender%")) {
659 } else if (theme_adium_match (&cur
, "%senderColor%")) {
660 /* A color derived from the user's name.
661 * FIXME: If a colon separated list of HTML colors is at
662 * Incoming/SenderColors.txt it will be used instead of
663 * the default colors.
665 if (contact_id
!= NULL
) {
666 guint hash
= g_str_hash (contact_id
);
667 replace
= colors
[hash
% G_N_ELEMENTS (colors
)];
669 } else if (theme_adium_match (&cur
, "%senderStatusIcon%")) {
670 /* FIXME: The path to the status icon of the sender
671 * (available, away, etc...)
673 } else if (theme_adium_match (&cur
, "%messageDirection%")) {
674 /* FIXME: The text direction of the message
675 * (either rtl or ltr)
677 } else if (theme_adium_match (&cur
, "%senderDisplayName%")) {
678 /* FIXME: The serverside (remotely set) name of the
679 * sender, such as an MSN display name.
681 * We don't have access to that yet so we use
682 * local alias instead.
685 } else if (theme_adium_match_with_format (&cur
, "%textbackgroundcolor{", &format
)) {
686 /* FIXME: This keyword is used to represent the
687 * highlight background color. "X" is the opacity of the
688 * background, ranges from 0 to 1 and can be any decimal
691 } else if (theme_adium_match (&cur
, "%message%")) {
693 } else if (theme_adium_match (&cur
, "%time%") ||
694 theme_adium_match_with_format (&cur
, "%time{", &format
)) {
695 const gchar
*strftime_format
;
697 strftime_format
= nsdate_to_strftime (priv
->data
, format
);
699 dup_replace
= empathy_time_to_string_local (timestamp
,
700 strftime_format
? strftime_format
:
701 EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT
);
703 dup_replace
= empathy_time_to_string_local (timestamp
,
704 strftime_format
? strftime_format
:
705 EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
707 replace
= dup_replace
;
708 } else if (theme_adium_match (&cur
, "%shortTime%")) {
709 dup_replace
= empathy_time_to_string_local (timestamp
,
710 EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
711 replace
= dup_replace
;
712 } else if (theme_adium_match (&cur
, "%service%")) {
713 replace
= service_name
;
714 } else if (theme_adium_match (&cur
, "%variant%")) {
715 /* FIXME: The name of the active message style variant,
716 * with all spaces replaced with an underscore.
717 * A variant named "Alternating Messages - Blue Red"
718 * will become "Alternating_Messages_-_Blue_Red".
720 } else if (theme_adium_match (&cur
, "%userIcons%")) {
721 /* FIXME: mus t be "hideIcons" if use preference is set
723 replace
= "showIcons";
724 } else if (theme_adium_match (&cur
, "%messageClasses%")) {
725 replace
= message_classes
;
726 } else if (theme_adium_match (&cur
, "%status%")) {
727 /* FIXME: A description of the status event. This is
728 * neither in the user's local language nor expected to
729 * be displayed; it may be useful to use a different div
730 * class to present different types of status messages.
731 * The following is a list of some of the more important
732 * status messages; your message style should be able to
733 * handle being shown a status message not in this list,
734 * as even at present the list is incomplete and is
735 * certain to become out of date in the future:
744 * contact_joined (group chats)
748 * encryption (all OTR messages use this status)
749 * purple (all IRC topic and join/part messages use this status)
750 * fileTransferStarted
751 * fileTransferCompleted
754 escape_and_append_len (string
, cur
, 1);
758 /* Here we have a replacement to make */
759 escape_and_append_len (string
, replace
, -1);
761 g_free (dup_replace
);
764 g_string_append (string
, "\")");
766 script
= g_string_free (string
, FALSE
);
767 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme
), script
);
772 theme_adium_append_event_escaped (EmpathyChatView
*view
,
773 const gchar
*escaped
)
775 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (view
);
776 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
778 theme_adium_append_html (theme
, "appendMessage",
779 priv
->data
->status_html
, escaped
, NULL
, NULL
, NULL
,
781 empathy_time_get_current (), FALSE
);
783 /* There is no last contact */
784 if (priv
->last_contact
) {
785 g_object_unref (priv
->last_contact
);
786 priv
->last_contact
= NULL
;
791 theme_adium_remove_focus_marks (EmpathyThemeAdium
*theme
,
792 WebKitDOMNodeList
*nodes
)
796 /* Remove focus and firstFocus class */
797 for (i
= 0; i
< webkit_dom_node_list_get_length (nodes
); i
++) {
798 WebKitDOMNode
*node
= webkit_dom_node_list_item (nodes
, i
);
799 WebKitDOMHTMLElement
*element
= WEBKIT_DOM_HTML_ELEMENT (node
);
801 gchar
**classes
, **iter
;
802 GString
*new_class_name
;
803 gboolean first
= TRUE
;
805 if (element
== NULL
) {
809 class_name
= webkit_dom_html_element_get_class_name (element
);
810 classes
= g_strsplit (class_name
, " ", -1);
811 new_class_name
= g_string_sized_new (strlen (class_name
));
812 for (iter
= classes
; *iter
!= NULL
; iter
++) {
813 if (tp_strdiff (*iter
, "focus") &&
814 tp_strdiff (*iter
, "firstFocus")) {
816 g_string_append_c (new_class_name
, ' ');
818 g_string_append (new_class_name
, *iter
);
823 webkit_dom_html_element_set_class_name (element
, new_class_name
->str
);
826 g_strfreev (classes
);
827 g_string_free (new_class_name
, TRUE
);
832 theme_adium_remove_all_focus_marks (EmpathyThemeAdium
*theme
)
834 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
835 WebKitDOMDocument
*dom
;
836 WebKitDOMNodeList
*nodes
;
837 GError
*error
= NULL
;
839 if (!priv
->has_unread_message
)
842 priv
->has_unread_message
= FALSE
;
844 dom
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (theme
));
849 /* Get all nodes with focus class */
850 nodes
= webkit_dom_document_query_selector_all (dom
, ".focus", &error
);
852 DEBUG ("Error getting focus nodes: %s",
853 error
? error
->message
: "No error");
854 g_clear_error (&error
);
858 theme_adium_remove_focus_marks (theme
, nodes
);
862 theme_adium_append_message (EmpathyChatView
*view
,
865 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (view
);
866 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
867 EmpathyContact
*sender
;
872 const gchar
*contact_id
;
873 EmpathyAvatar
*avatar
;
874 const gchar
*avatar_filename
= NULL
;
876 const gchar
*html
= NULL
;
878 const gchar
*service_name
;
879 GString
*message_classes
= NULL
;
881 gboolean consecutive
;
884 if (priv
->pages_loading
!= 0) {
885 queue_item (&priv
->message_queue
, QUEUED_MESSAGE
, msg
, NULL
);
889 /* Get information */
890 sender
= empathy_message_get_sender (msg
);
891 account
= empathy_contact_get_account (sender
);
892 service_name
= empathy_protocol_name_to_display_name
893 (tp_account_get_protocol (account
));
894 if (service_name
== NULL
)
895 service_name
= tp_account_get_protocol (account
);
896 timestamp
= empathy_message_get_timestamp (msg
);
897 body_escaped
= theme_adium_parse_body (theme
,
898 empathy_message_get_body (msg
),
899 empathy_message_get_token (msg
));
900 name
= empathy_contact_get_alias (sender
);
901 contact_id
= empathy_contact_get_id (sender
);
902 action
= (empathy_message_get_tptype (msg
) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION
);
904 /* If this is a /me probably */
908 if (priv
->data
->version
>= 4 || !priv
->data
->custom_template
) {
909 str
= g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
910 "<span class='actionMessageBody'>%s</span>",
913 str
= g_strdup_printf ("*%s*", body_escaped
);
915 g_free (body_escaped
);
919 /* Get the avatar filename, or a fallback */
920 avatar
= empathy_contact_get_avatar (sender
);
922 avatar_filename
= avatar
->filename
;
924 if (!avatar_filename
) {
925 if (empathy_contact_is_user (sender
)) {
926 avatar_filename
= priv
->data
->default_outgoing_avatar_filename
;
928 avatar_filename
= priv
->data
->default_incoming_avatar_filename
;
930 if (!avatar_filename
) {
931 if (!priv
->data
->default_avatar_filename
) {
932 priv
->data
->default_avatar_filename
=
933 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT
,
934 GTK_ICON_SIZE_DIALOG
);
936 avatar_filename
= priv
->data
->default_avatar_filename
;
940 /* We want to join this message with the last one if
941 * - senders are the same contact,
942 * - last message was recieved recently,
943 * - last message and this message both are/aren't backlog, and
944 * - DisableCombineConsecutive is not set in theme's settings */
945 is_backlog
= empathy_message_is_backlog (msg
);
946 consecutive
= empathy_contact_equal (priv
->last_contact
, sender
) &&
947 (timestamp
- priv
->last_timestamp
< MESSAGE_JOIN_PERIOD
) &&
948 (is_backlog
== priv
->last_is_backlog
) &&
949 !tp_asv_get_boolean (priv
->data
->info
,
950 "DisableCombineConsecutive", NULL
);
952 /* Define message classes */
953 message_classes
= g_string_new ("message");
954 if (!priv
->has_focus
&& !is_backlog
) {
955 if (!priv
->has_unread_message
) {
956 g_string_append (message_classes
, " firstFocus");
957 priv
->has_unread_message
= TRUE
;
959 g_string_append (message_classes
, " focus");
962 g_string_append (message_classes
, " history");
965 g_string_append (message_classes
, " consecutive");
967 if (empathy_contact_is_user (sender
)) {
968 g_string_append (message_classes
, " outgoing");
970 g_string_append (message_classes
, " incoming");
972 if (empathy_message_should_highlight (msg
)) {
973 g_string_append (message_classes
, " mention");
975 if (empathy_message_get_tptype (msg
) == TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY
) {
976 g_string_append (message_classes
, " autoreply");
979 g_string_append (message_classes
, " action");
981 /* FIXME: other classes:
982 * status - the message is a status change
983 * event - the message is a notification of something happening
984 * (for example, encryption being turned on)
985 * %status% - See %status% in theme_adium_append_html ()
988 /* This is slightly a hack, but it's the only way to add
989 * arbitrary data to messages in the HTML. We add another
990 * class called "x-empathy-message-id-*" to the message. This
991 * way, we can remove the unread marker for this specific
993 tp_msg
= empathy_message_get_tp_message (msg
);
994 if (tp_msg
!= NULL
) {
995 gchar
*tmp
= tp_escape_as_identifier (
996 tp_message_get_token (tp_msg
));
997 g_string_append_printf (message_classes
,
998 " x-empathy-message-id-%s", tmp
);
1002 /* Define javascript function to use */
1004 func
= priv
->allow_scrolling
? "appendNextMessage" : "appendNextMessageNoScroll";
1006 func
= priv
->allow_scrolling
? "appendMessage" : "appendMessageNoScroll";
1009 if (empathy_contact_is_user (sender
)) {
1013 html
= consecutive
? priv
->data
->out_nextcontext_html
: priv
->data
->out_context_html
;
1016 html
= consecutive
? priv
->data
->out_nextcontent_html
: priv
->data
->out_content_html
;
1019 /* remove all the unread marks when we are sending a message */
1020 theme_adium_remove_all_focus_marks (theme
);
1025 html
= consecutive
? priv
->data
->in_nextcontext_html
: priv
->data
->in_context_html
;
1028 html
= consecutive
? priv
->data
->in_nextcontent_html
: priv
->data
->in_content_html
;
1032 theme_adium_append_html (theme
, func
, html
, body_escaped
,
1033 avatar_filename
, name
, contact_id
,
1034 service_name
, message_classes
->str
,
1035 timestamp
, is_backlog
);
1037 /* Keep the sender of the last displayed message */
1038 if (priv
->last_contact
) {
1039 g_object_unref (priv
->last_contact
);
1041 priv
->last_contact
= g_object_ref (sender
);
1042 priv
->last_timestamp
= timestamp
;
1043 priv
->last_is_backlog
= is_backlog
;
1045 g_free (body_escaped
);
1046 g_string_free (message_classes
, TRUE
);
1050 theme_adium_append_event (EmpathyChatView
*view
,
1053 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1056 if (priv
->pages_loading
!= 0) {
1057 queue_item (&priv
->message_queue
, QUEUED_EVENT
, NULL
, str
);
1061 str_escaped
= g_markup_escape_text (str
, -1);
1062 theme_adium_append_event_escaped (view
, str_escaped
);
1063 g_free (str_escaped
);
1067 theme_adium_edit_message (EmpathyChatView
*view
,
1068 EmpathyMessage
*message
)
1070 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1071 WebKitDOMDocument
*doc
;
1072 WebKitDOMElement
*span
;
1073 gchar
*id
, *parsed_body
;
1074 gchar
*tooltip
, *timestamp
;
1075 GtkIconInfo
*icon_info
;
1076 GError
*error
= NULL
;
1078 if (priv
->pages_loading
!= 0) {
1079 queue_item (&priv
->message_queue
, QUEUED_EDIT
, message
, NULL
);
1083 id
= g_strdup_printf ("message-token-%s",
1084 empathy_message_get_supersedes (message
));
1085 /* we don't pass a token here, because doing so will return another
1086 * <span> element, and we don't want nested <span> elements */
1087 parsed_body
= theme_adium_parse_body (EMPATHY_THEME_ADIUM (view
),
1088 empathy_message_get_body (message
), NULL
);
1090 /* find the element */
1091 doc
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (view
));
1092 span
= webkit_dom_document_get_element_by_id (doc
, id
);
1095 DEBUG ("Failed to find id '%s'", id
);
1099 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span
)) {
1100 DEBUG ("Not a HTML element");
1104 /* update the HTML */
1105 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span
),
1106 parsed_body
, &error
);
1108 if (error
!= NULL
) {
1109 DEBUG ("Error setting new inner-HTML: %s", error
->message
);
1110 g_error_free (error
);
1115 timestamp
= empathy_time_to_string_local (
1116 empathy_message_get_timestamp (message
),
1118 tooltip
= g_strdup_printf (_("Message edited at %s"), timestamp
);
1120 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span
),
1126 /* mark this message as edited */
1127 icon_info
= gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1128 EMPATHY_IMAGE_EDIT_MESSAGE
, 16, 0);
1130 if (icon_info
!= NULL
) {
1131 /* set the icon as a background image using CSS
1132 * FIXME: the icon won't update in response to theme changes */
1133 gchar
*style
= g_strdup_printf (
1134 "background-image:url('%s');"
1135 "background-repeat:no-repeat;"
1136 "background-position:left center;"
1137 "padding-left:19px;", /* 16px icon + 3px padding */
1138 gtk_icon_info_get_filename (icon_info
));
1140 webkit_dom_element_set_attribute (span
, "style", style
, &error
);
1142 if (error
!= NULL
) {
1143 DEBUG ("Error setting element style: %s",
1145 g_clear_error (&error
);
1150 gtk_icon_info_free (icon_info
);
1156 DEBUG ("Could not find message to edit with: %s",
1157 empathy_message_get_body (message
));
1161 g_free (parsed_body
);
1165 theme_adium_scroll (EmpathyChatView
*view
,
1166 gboolean allow_scrolling
)
1168 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1170 priv
->allow_scrolling
= allow_scrolling
;
1171 if (allow_scrolling
) {
1172 empathy_chat_view_scroll_down (view
);
1177 theme_adium_scroll_down (EmpathyChatView
*view
)
1179 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view
), "alignChat(true);");
1183 theme_adium_get_has_selection (EmpathyChatView
*view
)
1185 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view
));
1189 theme_adium_clear (EmpathyChatView
*view
)
1191 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1193 theme_adium_load_template (EMPATHY_THEME_ADIUM (view
));
1195 /* Clear last contact to avoid trying to add a 'joined'
1196 * message when we don't have an insertion point. */
1197 if (priv
->last_contact
) {
1198 g_object_unref (priv
->last_contact
);
1199 priv
->last_contact
= NULL
;
1204 theme_adium_find_previous (EmpathyChatView
*view
,
1205 const gchar
*search_criteria
,
1206 gboolean new_search
,
1207 gboolean match_case
)
1209 /* FIXME: Doesn't respect new_search */
1210 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view
),
1211 search_criteria
, match_case
,
1216 theme_adium_find_next (EmpathyChatView
*view
,
1217 const gchar
*search_criteria
,
1218 gboolean new_search
,
1219 gboolean match_case
)
1221 /* FIXME: Doesn't respect new_search */
1222 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view
),
1223 search_criteria
, match_case
,
1228 theme_adium_find_abilities (EmpathyChatView
*view
,
1229 const gchar
*search_criteria
,
1230 gboolean match_case
,
1231 gboolean
*can_do_previous
,
1232 gboolean
*can_do_next
)
1234 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1235 * find_next and find_previous to work around this problem. */
1236 if (can_do_previous
)
1237 *can_do_previous
= TRUE
;
1239 *can_do_next
= TRUE
;
1243 theme_adium_highlight (EmpathyChatView
*view
,
1245 gboolean match_case
)
1247 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view
));
1248 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view
),
1249 text
, match_case
, 0);
1250 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view
),
1255 theme_adium_copy_clipboard (EmpathyChatView
*view
)
1257 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view
));
1261 theme_adium_remove_mark_from_message (EmpathyThemeAdium
*self
,
1264 WebKitDOMDocument
*dom
;
1265 WebKitDOMNodeList
*nodes
;
1267 GError
*error
= NULL
;
1269 dom
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
1274 tmp
= tp_escape_as_identifier (token
);
1275 class = g_strdup_printf (".x-empathy-message-id-%s", tmp
);
1278 /* Get all nodes with focus class */
1279 nodes
= webkit_dom_document_query_selector_all (dom
, class, &error
);
1282 if (nodes
== NULL
) {
1283 DEBUG ("Error getting focus nodes: %s",
1284 error
? error
->message
: "No error");
1285 g_clear_error (&error
);
1289 theme_adium_remove_focus_marks (self
, nodes
);
1293 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data
,
1296 EmpathyThemeAdium
*self
= user_data
;
1297 gchar
*token
= data
;
1299 theme_adium_remove_mark_from_message (self
, token
);
1304 theme_adium_focus_toggled (EmpathyChatView
*view
,
1307 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1309 priv
->has_focus
= has_focus
;
1310 if (!priv
->has_focus
) {
1311 /* We've lost focus, so let's make sure all the acked
1312 * messages have lost their unread marker. */
1313 g_queue_foreach (&priv
->acked_messages
,
1314 theme_adium_remove_acked_message_unread_mark_foreach
,
1316 g_queue_clear (&priv
->acked_messages
);
1318 priv
->has_unread_message
= FALSE
;
1323 theme_adium_message_acknowledged (EmpathyChatView
*view
,
1324 EmpathyMessage
*message
)
1326 EmpathyThemeAdium
*self
= (EmpathyThemeAdium
*) view
;
1327 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1330 tp_msg
= empathy_message_get_tp_message (message
);
1332 if (tp_msg
== NULL
) {
1336 /* We only want to actually remove the unread marker if the
1337 * view doesn't have focus. If we did it all the time we would
1338 * never see the unread markers, ever! So, we'll queue these
1339 * up, and when we lose focus, we'll remove the markers. */
1340 if (priv
->has_focus
) {
1341 g_queue_push_tail (&priv
->acked_messages
,
1342 g_strdup (tp_message_get_token (tp_msg
)));
1346 theme_adium_remove_mark_from_message (self
,
1347 tp_message_get_token (tp_msg
));
1351 theme_adium_context_menu_selection_done_cb (GtkMenuShell
*menu
, gpointer user_data
)
1353 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
1355 g_object_unref (hit_test_result
);
1359 theme_adium_context_menu_for_event (EmpathyThemeAdium
*theme
, GdkEventButton
*event
)
1361 WebKitWebView
*view
= WEBKIT_WEB_VIEW (theme
);
1362 WebKitHitTestResult
*hit_test_result
;
1363 WebKitHitTestResultContext context
;
1367 hit_test_result
= webkit_web_view_get_hit_test_result (view
, event
);
1368 g_object_get (G_OBJECT (hit_test_result
), "context", &context
, NULL
);
1371 menu
= empathy_context_menu_new (GTK_WIDGET (view
));
1373 /* Select all item */
1374 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL
, NULL
);
1375 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1377 g_signal_connect_swapped (item
, "activate",
1378 G_CALLBACK (webkit_web_view_select_all
),
1381 /* Copy menu item */
1382 if (webkit_web_view_can_copy_clipboard (view
)) {
1383 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY
, NULL
);
1384 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1386 g_signal_connect_swapped (item
, "activate",
1387 G_CALLBACK (webkit_web_view_copy_clipboard
),
1391 /* Clear menu item */
1392 item
= gtk_separator_menu_item_new ();
1393 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1395 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR
, NULL
);
1396 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1398 g_signal_connect_swapped (item
, "activate",
1399 G_CALLBACK (empathy_chat_view_clear
),
1402 /* We will only add the following menu items if we are
1403 * right-clicking a link */
1404 if (context
& WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK
) {
1406 item
= gtk_separator_menu_item_new ();
1407 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1409 /* Copy Link Address menu item */
1410 item
= gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
1411 g_signal_connect (item
, "activate",
1412 G_CALLBACK (theme_adium_copy_address_cb
),
1414 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1416 /* Open Link menu item */
1417 item
= gtk_menu_item_new_with_mnemonic (_("_Open Link"));
1418 g_signal_connect (item
, "activate",
1419 G_CALLBACK (theme_adium_open_address_cb
),
1421 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
1424 g_signal_connect (GTK_MENU_SHELL (menu
), "selection-done",
1425 G_CALLBACK (theme_adium_context_menu_selection_done_cb
),
1428 /* Display the menu */
1429 gtk_widget_show_all (menu
);
1430 gtk_menu_popup (GTK_MENU (menu
), NULL
, NULL
, NULL
, NULL
,
1431 event
->button
, event
->time
);
1435 theme_adium_button_press_event (GtkWidget
*widget
, GdkEventButton
*event
)
1437 if (event
->button
== 3) {
1438 gboolean developer_tools_enabled
;
1440 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget
))),
1441 "enable-developer-extras", &developer_tools_enabled
, NULL
);
1443 /* We currently have no way to add an inspector menu
1444 * item ourselves, so we disable our customized menu
1445 * if the developer extras are enabled. */
1446 if (!developer_tools_enabled
) {
1447 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget
), event
);
1452 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class
)->button_press_event (widget
, event
);
1456 theme_adium_iface_init (EmpathyChatViewIface
*iface
)
1458 iface
->append_message
= theme_adium_append_message
;
1459 iface
->append_event
= theme_adium_append_event
;
1460 iface
->edit_message
= theme_adium_edit_message
;
1461 iface
->scroll
= theme_adium_scroll
;
1462 iface
->scroll_down
= theme_adium_scroll_down
;
1463 iface
->get_has_selection
= theme_adium_get_has_selection
;
1464 iface
->clear
= theme_adium_clear
;
1465 iface
->find_previous
= theme_adium_find_previous
;
1466 iface
->find_next
= theme_adium_find_next
;
1467 iface
->find_abilities
= theme_adium_find_abilities
;
1468 iface
->highlight
= theme_adium_highlight
;
1469 iface
->copy_clipboard
= theme_adium_copy_clipboard
;
1470 iface
->focus_toggled
= theme_adium_focus_toggled
;
1471 iface
->message_acknowledged
= theme_adium_message_acknowledged
;
1475 theme_adium_load_finished_cb (WebKitWebView
*view
,
1476 WebKitWebFrame
*frame
,
1479 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
1480 EmpathyChatView
*chat_view
= EMPATHY_CHAT_VIEW (view
);
1483 DEBUG ("Page loaded");
1484 priv
->pages_loading
--;
1486 if (priv
->pages_loading
!= 0)
1489 /* Display queued messages */
1490 for (l
= priv
->message_queue
.head
; l
!= NULL
; l
= l
->next
) {
1491 QueuedItem
*item
= l
->data
;
1495 case QUEUED_MESSAGE
:
1496 theme_adium_append_message (chat_view
, item
->msg
);
1500 theme_adium_edit_message (chat_view
, item
->msg
);
1504 theme_adium_append_event (chat_view
, item
->str
);
1508 free_queued_item (item
);
1511 g_queue_clear (&priv
->message_queue
);
1515 theme_adium_finalize (GObject
*object
)
1517 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1519 empathy_adium_data_unref (priv
->data
);
1520 g_object_unref (priv
->gsettings_chat
);
1522 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->finalize (object
);
1526 theme_adium_dispose (GObject
*object
)
1528 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1530 if (priv
->smiley_manager
) {
1531 g_object_unref (priv
->smiley_manager
);
1532 priv
->smiley_manager
= NULL
;
1535 if (priv
->last_contact
) {
1536 g_object_unref (priv
->last_contact
);
1537 priv
->last_contact
= NULL
;
1540 if (priv
->inspector_window
) {
1541 gtk_widget_destroy (priv
->inspector_window
);
1542 priv
->inspector_window
= NULL
;
1545 if (priv
->acked_messages
.length
> 0) {
1546 g_queue_foreach (&priv
->acked_messages
, (GFunc
) g_free
, NULL
);
1547 g_queue_clear (&priv
->acked_messages
);
1550 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->dispose (object
);
1554 theme_adium_inspector_show_window_cb (WebKitWebInspector
*inspector
,
1555 EmpathyThemeAdium
*theme
)
1557 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
1559 if (priv
->inspector_window
) {
1560 gtk_widget_show_all (priv
->inspector_window
);
1567 theme_adium_inspector_close_window_cb (WebKitWebInspector
*inspector
,
1568 EmpathyThemeAdium
*theme
)
1570 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
1572 if (priv
->inspector_window
) {
1573 gtk_widget_hide (priv
->inspector_window
);
1579 static WebKitWebView
*
1580 theme_adium_inspect_web_view_cb (WebKitWebInspector
*inspector
,
1581 WebKitWebView
*web_view
,
1582 EmpathyThemeAdium
*theme
)
1584 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
1585 GtkWidget
*scrolled_window
;
1586 GtkWidget
*inspector_web_view
;
1588 if (!priv
->inspector_window
) {
1589 /* Create main window */
1590 priv
->inspector_window
= gtk_window_new (GTK_WINDOW_TOPLEVEL
);
1591 gtk_window_set_default_size (GTK_WINDOW (priv
->inspector_window
),
1593 g_signal_connect (priv
->inspector_window
, "delete-event",
1594 G_CALLBACK (gtk_widget_hide_on_delete
), NULL
);
1596 /* Pack a scrolled window */
1597 scrolled_window
= gtk_scrolled_window_new (NULL
, NULL
);
1598 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window
),
1599 GTK_POLICY_AUTOMATIC
,
1600 GTK_POLICY_AUTOMATIC
);
1601 gtk_container_add (GTK_CONTAINER (priv
->inspector_window
),
1603 gtk_widget_show (scrolled_window
);
1605 /* Pack a webview in the scrolled window. That webview will be
1606 * used to render the inspector tool. */
1607 inspector_web_view
= webkit_web_view_new ();
1608 gtk_container_add (GTK_CONTAINER (scrolled_window
),
1609 inspector_web_view
);
1610 gtk_widget_show (scrolled_window
);
1612 return WEBKIT_WEB_VIEW (inspector_web_view
);
1618 static PangoFontDescription
*
1619 theme_adium_get_default_font (void)
1621 GSettings
*gsettings
;
1622 PangoFontDescription
*pango_fd
;
1625 gsettings
= g_settings_new (EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA
);
1627 font_family
= g_settings_get_string (gsettings
,
1628 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME
);
1630 if (font_family
== NULL
)
1633 pango_fd
= pango_font_description_from_string (font_family
);
1634 g_free (font_family
);
1635 g_object_unref (gsettings
);
1640 theme_adium_set_webkit_font (WebKitWebSettings
*w_settings
,
1644 g_object_set (w_settings
, "default-font-family", name
, NULL
);
1645 g_object_set (w_settings
, "default-font-size", size
, NULL
);
1649 theme_adium_set_default_font (WebKitWebSettings
*w_settings
)
1651 PangoFontDescription
*default_font_desc
;
1652 GdkScreen
*current_screen
;
1654 gint pango_font_size
= 0;
1656 default_font_desc
= theme_adium_get_default_font ();
1657 if (default_font_desc
== NULL
)
1659 pango_font_size
= pango_font_description_get_size (default_font_desc
)
1661 if (pango_font_description_get_size_is_absolute (default_font_desc
)) {
1662 current_screen
= gdk_screen_get_default ();
1663 if (current_screen
!= NULL
) {
1664 dpi
= gdk_screen_get_resolution (current_screen
);
1666 dpi
= BORING_DPI_DEFAULT
;
1668 pango_font_size
= (gint
) (pango_font_size
/ (dpi
/ 72));
1670 theme_adium_set_webkit_font (w_settings
,
1671 pango_font_description_get_family (default_font_desc
),
1673 pango_font_description_free (default_font_desc
);
1677 theme_adium_constructed (GObject
*object
)
1679 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1680 const gchar
*font_family
= NULL
;
1682 WebKitWebView
*webkit_view
= WEBKIT_WEB_VIEW (object
);
1683 WebKitWebSettings
*webkit_settings
;
1684 WebKitWebInspector
*webkit_inspector
;
1686 /* Set default settings */
1687 font_family
= tp_asv_get_string (priv
->data
->info
, "DefaultFontFamily");
1688 font_size
= tp_asv_get_int32 (priv
->data
->info
, "DefaultFontSize", NULL
);
1689 webkit_settings
= webkit_web_view_get_settings (webkit_view
);
1691 if (font_family
&& font_size
) {
1692 theme_adium_set_webkit_font (webkit_settings
, font_family
, font_size
);
1694 theme_adium_set_default_font (webkit_settings
);
1697 /* Setup webkit inspector */
1698 webkit_inspector
= webkit_web_view_get_inspector (webkit_view
);
1699 g_signal_connect (webkit_inspector
, "inspect-web-view",
1700 G_CALLBACK (theme_adium_inspect_web_view_cb
),
1702 g_signal_connect (webkit_inspector
, "show-window",
1703 G_CALLBACK (theme_adium_inspector_show_window_cb
),
1705 g_signal_connect (webkit_inspector
, "close-window",
1706 G_CALLBACK (theme_adium_inspector_close_window_cb
),
1710 theme_adium_load_template (EMPATHY_THEME_ADIUM (object
));
1712 priv
->in_construction
= FALSE
;
1716 theme_adium_get_property (GObject
*object
,
1721 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1724 case PROP_ADIUM_DATA
:
1725 g_value_set_boxed (value
, priv
->data
);
1728 g_value_set_string (value
, priv
->variant
);
1731 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1737 theme_adium_set_property (GObject
*object
,
1739 const GValue
*value
,
1742 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (object
);
1743 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1746 case PROP_ADIUM_DATA
:
1747 g_assert (priv
->data
== NULL
);
1748 priv
->data
= g_value_dup_boxed (value
);
1751 empathy_theme_adium_set_variant (theme
, g_value_get_string (value
));
1754 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1760 empathy_theme_adium_class_init (EmpathyThemeAdiumClass
*klass
)
1762 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
1763 GtkWidgetClass
* widget_class
= GTK_WIDGET_CLASS (klass
);
1765 object_class
->finalize
= theme_adium_finalize
;
1766 object_class
->dispose
= theme_adium_dispose
;
1767 object_class
->constructed
= theme_adium_constructed
;
1768 object_class
->get_property
= theme_adium_get_property
;
1769 object_class
->set_property
= theme_adium_set_property
;
1771 widget_class
->button_press_event
= theme_adium_button_press_event
;
1773 g_object_class_install_property (object_class
,
1775 g_param_spec_boxed ("adium-data",
1777 "Data for the adium theme",
1778 EMPATHY_TYPE_ADIUM_DATA
,
1779 G_PARAM_CONSTRUCT_ONLY
|
1781 G_PARAM_STATIC_STRINGS
));
1782 g_object_class_install_property (object_class
,
1784 g_param_spec_string ("variant",
1785 "The theme variant",
1786 "Variant name for the theme",
1790 G_PARAM_STATIC_STRINGS
));
1792 g_type_class_add_private (object_class
, sizeof (EmpathyThemeAdiumPriv
));
1796 empathy_theme_adium_init (EmpathyThemeAdium
*theme
)
1798 EmpathyThemeAdiumPriv
*priv
= G_TYPE_INSTANCE_GET_PRIVATE (theme
,
1799 EMPATHY_TYPE_THEME_ADIUM
, EmpathyThemeAdiumPriv
);
1803 priv
->in_construction
= TRUE
;
1804 g_queue_init (&priv
->message_queue
);
1805 priv
->allow_scrolling
= TRUE
;
1806 priv
->smiley_manager
= empathy_smiley_manager_dup_singleton ();
1808 g_signal_connect (theme
, "load-finished",
1809 G_CALLBACK (theme_adium_load_finished_cb
),
1811 g_signal_connect (theme
, "navigation-policy-decision-requested",
1812 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb
),
1815 priv
->gsettings_chat
= g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA
);
1816 g_signal_connect (priv
->gsettings_chat
,
1817 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
,
1818 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb
),
1821 theme_adium_update_enable_webkit_developer_tools (theme
);
1825 empathy_theme_adium_new (EmpathyAdiumData
*data
,
1826 const gchar
*variant
)
1828 g_return_val_if_fail (data
!= NULL
, NULL
);
1830 return g_object_new (EMPATHY_TYPE_THEME_ADIUM
,
1837 empathy_theme_adium_set_variant (EmpathyThemeAdium
*theme
,
1838 const gchar
*variant
)
1840 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
1841 gchar
*variant_path
;
1844 if (!tp_strdiff (priv
->variant
, variant
)) {
1848 g_free (priv
->variant
);
1849 priv
->variant
= g_strdup (variant
);
1851 if (priv
->in_construction
) {
1855 DEBUG ("Update view with variant: '%s'", variant
);
1856 variant_path
= adium_info_dup_path_for_variant (priv
->data
->info
,
1858 script
= g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");", variant_path
);
1860 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme
), script
);
1862 g_free (variant_path
);
1865 g_object_notify (G_OBJECT (theme
), "variant");
1869 empathy_adium_path_is_valid (const gchar
*path
)
1874 /* The theme is not valid if there is no Info.plist */
1875 file
= g_build_filename (path
, "Contents", "Info.plist",
1877 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1883 /* We ship a default Template.html as fallback if there is any problem
1884 * with the one inside the theme. The only other required file is
1885 * Content.html OR Incoming/Content.html*/
1886 file
= g_build_filename (path
, "Contents", "Resources", "Content.html",
1888 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1892 file
= g_build_filename (path
, "Contents", "Resources", "Incoming",
1893 "Content.html", NULL
);
1894 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1902 empathy_adium_info_new (const gchar
*path
)
1906 GHashTable
*info
= NULL
;
1908 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1910 file
= g_build_filename (path
, "Contents", "Info.plist", NULL
);
1911 value
= empathy_plist_parse_from_file (file
);
1917 info
= g_value_dup_boxed (value
);
1918 tp_g_value_slice_free (value
);
1920 /* Insert the theme's path into the hash table,
1921 * keys have to be dupped */
1922 tp_asv_set_string (info
, g_strdup ("path"), path
);
1928 adium_info_get_version (GHashTable
*info
)
1930 return tp_asv_get_int32 (info
, "MessageViewVersion", NULL
);
1933 static const gchar
*
1934 adium_info_get_no_variant_name (GHashTable
*info
)
1936 const gchar
*name
= tp_asv_get_string (info
, "DisplayNameForNoVariant");
1937 return name
? name
: _("Normal");
1941 adium_info_dup_path_for_variant (GHashTable
*info
,
1942 const gchar
*variant
)
1944 guint version
= adium_info_get_version (info
);
1945 const gchar
*no_variant
= adium_info_get_no_variant_name (info
);
1946 GPtrArray
*variants
;
1949 if (version
<= 2 && !tp_strdiff (variant
, no_variant
)) {
1950 return g_strdup ("main.css");
1953 /* Verify the variant exists, fallback to the first one */
1954 variants
= empathy_adium_info_get_available_variants (info
);
1955 for (i
= 0; i
< variants
->len
; i
++) {
1956 if (!tp_strdiff (variant
, g_ptr_array_index (variants
, i
))) {
1960 if (i
== variants
->len
) {
1961 DEBUG ("Variant %s does not exist", variant
);
1962 variant
= g_ptr_array_index (variants
, 0);
1965 return g_strdup_printf ("Variants/%s.css", variant
);
1970 empathy_adium_info_get_default_variant (GHashTable
*info
)
1972 if (adium_info_get_version (info
) <= 2) {
1973 return adium_info_get_no_variant_name (info
);
1976 return tp_asv_get_string (info
, "DefaultVariant");
1980 empathy_adium_info_get_available_variants (GHashTable
*info
)
1982 GPtrArray
*variants
;
1987 variants
= tp_asv_get_boxed (info
, "AvailableVariants", G_TYPE_PTR_ARRAY
);
1988 if (variants
!= NULL
) {
1992 variants
= g_ptr_array_new_with_free_func (g_free
);
1993 tp_asv_take_boxed (info
, g_strdup ("AvailableVariants"),
1994 G_TYPE_PTR_ARRAY
, variants
);
1996 path
= tp_asv_get_string (info
, "path");
1997 dirpath
= g_build_filename (path
, "Contents", "Resources", "Variants", NULL
);
1998 dir
= g_dir_open (dirpath
, 0, NULL
);
2002 for (name
= g_dir_read_name (dir
);
2004 name
= g_dir_read_name (dir
)) {
2005 gchar
*display_name
;
2007 if (!g_str_has_suffix (name
, ".css")) {
2011 display_name
= g_strdup (name
);
2012 strstr (display_name
, ".css")[0] = '\0';
2013 g_ptr_array_add (variants
, display_name
);
2019 if (adium_info_get_version (info
) <= 2) {
2020 g_ptr_array_add (variants
,
2021 g_strdup (adium_info_get_no_variant_name (info
)));
2028 empathy_adium_data_get_type (void)
2030 static GType type_id
= 0;
2034 type_id
= g_boxed_type_register_static ("EmpathyAdiumData",
2035 (GBoxedCopyFunc
) empathy_adium_data_ref
,
2036 (GBoxedFreeFunc
) empathy_adium_data_unref
);
2043 empathy_adium_data_new_with_info (const gchar
*path
, GHashTable
*info
)
2045 EmpathyAdiumData
*data
;
2046 gchar
*template_html
= NULL
;
2047 gchar
*footer_html
= NULL
;
2050 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
2052 data
= g_slice_new0 (EmpathyAdiumData
);
2053 data
->ref_count
= 1;
2054 data
->path
= g_strdup (path
);
2055 data
->basedir
= g_strconcat (path
, G_DIR_SEPARATOR_S
"Contents"
2056 G_DIR_SEPARATOR_S
"Resources" G_DIR_SEPARATOR_S
, NULL
);
2057 data
->info
= g_hash_table_ref (info
);
2058 data
->version
= adium_info_get_version (info
);
2059 data
->strings_to_free
= g_ptr_array_new_with_free_func (g_free
);
2060 data
->date_format_cache
= g_hash_table_new_full (g_str_hash
,
2061 g_str_equal
, g_free
, g_free
);
2063 DEBUG ("Loading theme at %s", path
);
2065 #define LOAD(path, var) \
2066 tmp = g_build_filename (data->basedir, path, NULL); \
2067 g_file_get_contents (tmp, &var, NULL, NULL); \
2070 #define LOAD_CONST(path, var) \
2073 LOAD (path, content); \
2074 if (content != NULL) { \
2075 g_ptr_array_add (data->strings_to_free, content); \
2080 /* Load html files */
2081 LOAD_CONST ("Content.html", data
->content_html
);
2082 LOAD_CONST ("Incoming/Content.html", data
->in_content_html
);
2083 LOAD_CONST ("Incoming/NextContent.html", data
->in_nextcontent_html
);
2084 LOAD_CONST ("Incoming/Context.html", data
->in_context_html
);
2085 LOAD_CONST ("Incoming/NextContext.html", data
->in_nextcontext_html
);
2086 LOAD_CONST ("Outgoing/Content.html", data
->out_content_html
);
2087 LOAD_CONST ("Outgoing/NextContent.html", data
->out_nextcontent_html
);
2088 LOAD_CONST ("Outgoing/Context.html", data
->out_context_html
);
2089 LOAD_CONST ("Outgoing/NextContext.html", data
->out_nextcontext_html
);
2090 LOAD_CONST ("Status.html", data
->status_html
);
2091 LOAD ("Template.html", template_html
);
2092 LOAD ("Footer.html", footer_html
);
2097 /* HTML fallbacks: If we have at least content OR in_content, then
2098 * everything else gets a fallback */
2100 #define FALLBACK(html, fallback) \
2101 if (html == NULL) { \
2105 /* in_nextcontent -> in_content -> content */
2106 FALLBACK (data
->in_content_html
, data
->content_html
);
2107 FALLBACK (data
->in_nextcontent_html
, data
->in_content_html
);
2109 /* context -> content */
2110 FALLBACK (data
->in_context_html
, data
->in_content_html
);
2111 FALLBACK (data
->in_nextcontext_html
, data
->in_nextcontent_html
);
2112 FALLBACK (data
->out_context_html
, data
->out_content_html
);
2113 FALLBACK (data
->out_nextcontext_html
, data
->out_nextcontent_html
);
2116 FALLBACK (data
->out_content_html
, data
->in_content_html
);
2117 FALLBACK (data
->out_nextcontent_html
, data
->in_nextcontent_html
);
2118 FALLBACK (data
->out_context_html
, data
->in_context_html
);
2119 FALLBACK (data
->out_nextcontext_html
, data
->in_nextcontext_html
);
2121 /* status -> in_content */
2122 FALLBACK (data
->status_html
, data
->in_content_html
);
2126 /* template -> empathy's template */
2127 data
->custom_template
= (template_html
!= NULL
);
2128 if (template_html
== NULL
) {
2129 tmp
= empathy_file_lookup ("Template.html", "data");
2130 g_file_get_contents (tmp
, &template_html
, NULL
, NULL
);
2134 /* Default avatar */
2135 tmp
= g_build_filename (data
->basedir
, "Incoming", "buddy_icon.png", NULL
);
2136 if (g_file_test (tmp
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
)) {
2137 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
)) {
2143 data
->default_outgoing_avatar_filename
= tmp
;
2148 /* Old custom templates had only 4 parameters.
2149 * New templates have 5 parameters */
2150 if (data
->version
<= 2 && data
->custom_template
) {
2151 tmp
= string_with_format (template_html
,
2153 "%@", /* Leave variant unset */
2154 "", /* The header */
2155 footer_html
? footer_html
: "",
2158 tmp
= string_with_format (template_html
,
2160 data
->version
<= 2 ? "" : "@import url( \"main.css\" );",
2161 "%@", /* Leave variant unset */
2162 "", /* The header */
2163 footer_html
? footer_html
: "",
2166 g_ptr_array_add (data
->strings_to_free
, tmp
);
2167 data
->template_html
= tmp
;
2169 g_free (template_html
);
2170 g_free (footer_html
);
2176 empathy_adium_data_new (const gchar
*path
)
2178 EmpathyAdiumData
*data
;
2181 info
= empathy_adium_info_new (path
);
2182 data
= empathy_adium_data_new_with_info (path
, info
);
2183 g_hash_table_unref (info
);
2189 empathy_adium_data_ref (EmpathyAdiumData
*data
)
2191 g_return_val_if_fail (data
!= NULL
, NULL
);
2193 g_atomic_int_inc (&data
->ref_count
);
2199 empathy_adium_data_unref (EmpathyAdiumData
*data
)
2201 g_return_if_fail (data
!= NULL
);
2203 if (g_atomic_int_dec_and_test (&data
->ref_count
)) {
2204 g_free (data
->path
);
2205 g_free (data
->basedir
);
2206 g_free (data
->default_avatar_filename
);
2207 g_free (data
->default_incoming_avatar_filename
);
2208 g_free (data
->default_outgoing_avatar_filename
);
2209 g_hash_table_unref (data
->info
);
2210 g_ptr_array_unref (data
->strings_to_free
);
2211 tp_clear_pointer (&data
->date_format_cache
, g_hash_table_unref
);
2213 g_slice_free (EmpathyAdiumData
, data
);
2218 empathy_adium_data_get_info (EmpathyAdiumData
*data
)
2220 g_return_val_if_fail (data
!= NULL
, NULL
);
2226 empathy_adium_data_get_path (EmpathyAdiumData
*data
)
2228 g_return_val_if_fail (data
!= NULL
, NULL
);