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.h>
27 #include <webkit/webkit.h>
28 #include <telepathy-glib/dbus.h>
29 #include <telepathy-glib/util.h>
31 #include <gconf/gconf-client.h>
32 #include <pango/pango.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-conf.h"
41 #include "empathy-ui-utils.h"
42 #include "empathy-plist.h"
44 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
45 #include <libempathy/empathy-debug.h>
47 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
49 /* GConf key containing current value of font */
50 #define EMPATHY_GCONF_FONT_KEY_NAME "/desktop/gnome/interface/font_name"
51 #define BORING_DPI_DEFAULT 96
53 /* "Join" consecutive messages with timestamps within five minutes */
54 #define MESSAGE_JOIN_PERIOD 5*60
57 EmpathyAdiumData
*data
;
58 EmpathySmileyManager
*smiley_manager
;
59 EmpathyContact
*last_contact
;
60 time_t last_timestamp
;
61 gboolean last_is_backlog
;
64 guint notify_enable_webkit_developer_tools_id
;
65 GtkWidget
*inspector_window
;
66 } EmpathyThemeAdiumPriv
;
68 struct _EmpathyAdiumData
{
72 gchar
*default_avatar_filename
;
73 gchar
*default_incoming_avatar_filename
;
74 gchar
*default_outgoing_avatar_filename
;
76 gchar
*in_content_html
;
78 gchar
*in_context_html
;
80 gchar
*in_nextcontent_html
;
81 gsize in_nextcontent_len
;
82 gchar
*in_nextcontext_html
;
83 gsize in_nextcontext_len
;
84 gchar
*out_content_html
;
85 gsize out_content_len
;
86 gchar
*out_context_html
;
87 gsize out_context_len
;
88 gchar
*out_nextcontent_html
;
89 gsize out_nextcontent_len
;
90 gchar
*out_nextcontext_html
;
91 gsize out_nextcontext_len
;
97 static void theme_adium_iface_init (EmpathyChatViewIface
*iface
);
104 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium
, empathy_theme_adium
,
105 WEBKIT_TYPE_WEB_VIEW
,
106 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW
,
107 theme_adium_iface_init
));
110 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium
*theme
)
112 WebKitWebView
*web_view
= WEBKIT_WEB_VIEW (theme
);
113 gboolean enable_webkit_developer_tools
;
115 if (!empathy_conf_get_bool (empathy_conf_get (),
116 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
,
117 &enable_webkit_developer_tools
)) {
121 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view
)),
122 "enable-developer-extras",
123 enable_webkit_developer_tools
,
128 theme_adium_notify_enable_webkit_developer_tools_cb (EmpathyConf
*conf
,
132 EmpathyThemeAdium
*theme
= user_data
;
134 theme_adium_update_enable_webkit_developer_tools (theme
);
138 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView
*view
,
139 WebKitWebFrame
*web_frame
,
140 WebKitNetworkRequest
*request
,
141 WebKitWebNavigationAction
*action
,
142 WebKitWebPolicyDecision
*decision
,
147 /* Only call url_show on clicks */
148 if (webkit_web_navigation_action_get_reason (action
) !=
149 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED
) {
150 webkit_web_policy_decision_use (decision
);
154 uri
= webkit_network_request_get_uri (request
);
155 empathy_url_show (GTK_WIDGET (view
), uri
);
157 webkit_web_policy_decision_ignore (decision
);
162 theme_adium_copy_address_cb (GtkMenuItem
*menuitem
,
165 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
167 GtkClipboard
*clipboard
;
169 g_object_get (G_OBJECT (hit_test_result
), "link-uri", &uri
, NULL
);
171 clipboard
= gtk_clipboard_get (GDK_SELECTION_CLIPBOARD
);
172 gtk_clipboard_set_text (clipboard
, uri
, -1);
174 clipboard
= gtk_clipboard_get (GDK_SELECTION_PRIMARY
);
175 gtk_clipboard_set_text (clipboard
, uri
, -1);
181 theme_adium_open_address_cb (GtkMenuItem
*menuitem
,
184 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
187 g_object_get (G_OBJECT (hit_test_result
), "link-uri", &uri
, NULL
);
189 empathy_url_show (GTK_WIDGET (menuitem
), uri
);
195 theme_adium_match_newline (const gchar
*text
,
197 EmpathyStringReplace replace_func
,
198 EmpathyStringParser
*sub_parsers
,
201 GString
*string
= user_data
;
209 /* Replace \n by <br/> */
210 for (i
= 0; i
< len
&& text
[i
] != '\0'; i
++) {
211 if (text
[i
] == '\n') {
212 empathy_string_parser_substr (text
+ prev
,
213 i
- prev
, sub_parsers
,
215 g_string_append (string
, "<br/>");
219 empathy_string_parser_substr (text
+ prev
, i
- prev
,
220 sub_parsers
, user_data
);
224 theme_adium_replace_link (const gchar
*text
,
229 GString
*string
= user_data
;
233 real_url
= empathy_make_absolute_url_len (text
, len
);
235 /* The thing we are making a link of may contain
236 * characters which need escaping */
237 escaped
= g_markup_escape_text (text
, len
);
239 /* Append the link inside <a href=""></a> tag */
240 g_string_append_printf (string
, "<a href=\"%s\">%s</a>",
248 theme_adium_replace_smiley (const gchar
*text
,
253 EmpathySmileyHit
*hit
= match_data
;
254 GString
*string
= user_data
;
256 /* Replace smiley by a <img/> tag */
257 g_string_append_printf (string
,
258 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
259 hit
->path
, (int)len
, text
, (int)len
, text
);
263 theme_adium_replace_escaped (const gchar
*text
,
268 GString
*string
= user_data
;
271 escaped
= g_markup_escape_text (text
, len
);
272 g_string_append (string
, escaped
);
276 static EmpathyStringParser string_parsers
[] = {
277 {empathy_string_match_link
, theme_adium_replace_link
},
278 {theme_adium_match_newline
, NULL
},
279 {empathy_string_match_all
, theme_adium_replace_escaped
},
283 static EmpathyStringParser string_parsers_with_smiley
[] = {
284 {empathy_string_match_link
, theme_adium_replace_link
},
285 {empathy_string_match_smiley
, theme_adium_replace_smiley
},
286 {theme_adium_match_newline
, NULL
},
287 {empathy_string_match_all
, theme_adium_replace_escaped
},
292 theme_adium_parse_body (const gchar
*text
)
294 EmpathyStringParser
*parsers
;
296 gboolean use_smileys
;
298 /* Check if we have to parse smileys */
299 empathy_conf_get_bool (empathy_conf_get (),
300 EMPATHY_PREFS_CHAT_SHOW_SMILEYS
,
303 parsers
= string_parsers_with_smiley
;
305 parsers
= string_parsers
;
307 /* Parse text and construct string with links and smileys replaced
308 * by html tags. Also escape text to make sure html code is
309 * displayed verbatim. */
310 string
= g_string_sized_new (strlen (text
));
311 empathy_string_parser_substr (text
, -1, parsers
, string
);
313 return g_string_free (string
, FALSE
);
317 escape_and_append_len (GString
*string
, const gchar
*str
, gint len
)
319 while (*str
!= '\0' && len
!= 0) {
323 g_string_append (string
, "\\\\");
327 g_string_append (string
, "\\\"");
330 /* Remove end of lines */
333 g_string_append_c (string
, *str
);
342 theme_adium_match (const gchar
**str
, const gchar
*match
)
346 len
= strlen (match
);
347 if (strncmp (*str
, match
, len
) == 0) {
356 theme_adium_append_html (EmpathyThemeAdium
*theme
,
358 const gchar
*html
, gsize len
,
359 const gchar
*message
,
360 const gchar
*avatar_filename
,
362 const gchar
*contact_id
,
363 const gchar
*service_name
,
364 const gchar
*message_classes
,
368 const gchar
*cur
= NULL
;
371 /* Make some search-and-replace in the html code */
372 string
= g_string_sized_new (len
+ strlen (message
));
373 g_string_append_printf (string
, "%s(\"", func
);
374 for (cur
= html
; *cur
!= '\0'; cur
++) {
375 const gchar
*replace
= NULL
;
376 gchar
*dup_replace
= NULL
;
378 if (theme_adium_match (&cur
, "%message%")) {
380 } else if (theme_adium_match (&cur
, "%messageClasses%")) {
381 replace
= message_classes
;
382 } else if (theme_adium_match (&cur
, "%userIconPath%")) {
383 replace
= avatar_filename
;
384 } else if (theme_adium_match (&cur
, "%sender%")) {
386 } else if (theme_adium_match (&cur
, "%senderScreenName%")) {
387 replace
= contact_id
;
388 } else if (theme_adium_match (&cur
, "%senderDisplayName%")) {
389 /* %senderDisplayName% -
390 * "The serverside (remotely set) name of the sender,
391 * such as an MSN display name."
393 * We don't have access to that yet so we use local
396 } else if (theme_adium_match (&cur
, "%service%")) {
397 replace
= service_name
;
398 } else if (theme_adium_match (&cur
, "%shortTime%")) {
399 dup_replace
= empathy_time_to_string_local (timestamp
,
400 EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
401 replace
= dup_replace
;
402 } else if (theme_adium_match (&cur
, "%time")) {
403 gchar
*format
= NULL
;
405 /* Time can be in 2 formats:
406 * %time% or %time{strftime format}%
407 * Extract the time format if provided. */
410 end
= strstr (cur
, "}%");
415 format
= g_strndup (cur
, end
- cur
);
421 dup_replace
= empathy_time_to_string_local (timestamp
,
422 format
? format
: EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
423 replace
= dup_replace
;
426 escape_and_append_len (string
, cur
, 1);
430 /* Here we have a replacement to make */
431 escape_and_append_len (string
, replace
, -1);
432 g_free (dup_replace
);
434 g_string_append (string
, "\")");
436 script
= g_string_free (string
, FALSE
);
437 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme
), script
);
442 theme_adium_append_event_escaped (EmpathyChatView
*view
,
443 const gchar
*escaped
)
445 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (view
);
446 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
448 if (priv
->data
->status_html
) {
449 theme_adium_append_html (theme
, "appendMessage",
450 priv
->data
->status_html
,
451 priv
->data
->status_len
,
452 escaped
, NULL
, NULL
, NULL
, NULL
,
453 "event", empathy_time_get_current ());
456 /* There is no last contact */
457 if (priv
->last_contact
) {
458 g_object_unref (priv
->last_contact
);
459 priv
->last_contact
= NULL
;
464 theme_adium_append_message (EmpathyChatView
*view
,
467 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (view
);
468 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
469 EmpathyContact
*sender
;
474 const gchar
*contact_id
;
475 EmpathyAvatar
*avatar
;
476 const gchar
*avatar_filename
= NULL
;
481 const gchar
*service_name
;
482 GString
*message_classes
= NULL
;
484 gboolean consecutive
;
486 if (!priv
->page_loaded
) {
487 priv
->message_queue
= g_list_prepend (priv
->message_queue
,
492 /* Get information */
493 sender
= empathy_message_get_sender (msg
);
494 account
= empathy_contact_get_account (sender
);
495 service_name
= empathy_protocol_name_to_display_name
496 (tp_account_get_protocol (account
));
497 if (service_name
== NULL
)
498 service_name
= tp_account_get_protocol (account
);
499 timestamp
= empathy_message_get_timestamp (msg
);
500 body
= empathy_message_get_body (msg
);
501 body_escaped
= theme_adium_parse_body (body
);
502 name
= empathy_contact_get_name (sender
);
503 contact_id
= empathy_contact_get_id (sender
);
505 /* If this is a /me, append an event */
506 if (empathy_message_get_tptype (msg
) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION
) {
509 str
= g_strdup_printf ("%s %s", name
, body_escaped
);
510 theme_adium_append_event_escaped (view
, str
);
513 g_free (body_escaped
);
517 /* Get the avatar filename, or a fallback */
518 avatar
= empathy_contact_get_avatar (sender
);
520 avatar_filename
= avatar
->filename
;
522 if (!avatar_filename
) {
523 if (empathy_contact_is_user (sender
)) {
524 avatar_filename
= priv
->data
->default_outgoing_avatar_filename
;
526 avatar_filename
= priv
->data
->default_incoming_avatar_filename
;
528 if (!avatar_filename
) {
529 if (!priv
->data
->default_avatar_filename
) {
530 priv
->data
->default_avatar_filename
=
531 empathy_filename_from_icon_name ("stock_person",
532 GTK_ICON_SIZE_DIALOG
);
534 avatar_filename
= priv
->data
->default_avatar_filename
;
538 /* We want to join this message with the last one if
539 * - senders are the same contact,
540 * - last message was recieved recently,
541 * - last message and this message both are/aren't backlog, and
542 * - DisableCombineConsecutive is not set in theme's settings */
543 is_backlog
= empathy_message_is_backlog (msg
);
544 consecutive
= empathy_contact_equal (priv
->last_contact
, sender
) &&
545 (timestamp
- priv
->last_timestamp
< MESSAGE_JOIN_PERIOD
) &&
546 (is_backlog
== priv
->last_is_backlog
) &&
547 !tp_asv_get_boolean (priv
->data
->info
,
548 "DisableCombineConsecutive", NULL
);
550 /* Define message classes */
551 message_classes
= g_string_new ("message");
553 g_string_append (message_classes
, " history");
556 g_string_append (message_classes
, " consecutive");
558 if (empathy_contact_is_user (sender
)) {
559 g_string_append (message_classes
, " outgoing");
561 g_string_append (message_classes
, " incoming");
564 /* Define javascript function to use */
566 func
= "appendNextMessage";
568 func
= "appendMessage";
572 if (empathy_contact_is_user (sender
)) {
575 html
= priv
->data
->out_nextcontext_html
;
576 len
= priv
->data
->out_nextcontext_len
;
579 /* Not backlog, or fallback if NextContext.html
582 html
= priv
->data
->out_nextcontent_html
;
583 len
= priv
->data
->out_nextcontent_len
;
587 /* Not consecutive, or fallback if NextContext.html and/or
588 * NextContent.html are missing */
591 html
= priv
->data
->out_context_html
;
592 len
= priv
->data
->out_context_len
;
596 html
= priv
->data
->out_content_html
;
597 len
= priv
->data
->out_content_len
;
602 /* Incoming, or fallback if outgoing files are missing */
606 html
= priv
->data
->in_nextcontext_html
;
607 len
= priv
->data
->in_nextcontext_len
;
610 /* Note backlog, or fallback if NextContext.html
613 html
= priv
->data
->in_nextcontent_html
;
614 len
= priv
->data
->in_nextcontent_len
;
618 /* Not consecutive, or fallback if NextContext.html and/or
619 * NextContent.html are missing */
622 html
= priv
->data
->in_context_html
;
623 len
= priv
->data
->in_context_len
;
627 html
= priv
->data
->in_content_html
;
628 len
= priv
->data
->in_content_len
;
634 theme_adium_append_html (theme
, func
, html
, len
, body_escaped
,
635 avatar_filename
, name
, contact_id
,
636 service_name
, message_classes
->str
,
639 DEBUG ("Couldn't find HTML file for this message");
642 /* Keep the sender of the last displayed message */
643 if (priv
->last_contact
) {
644 g_object_unref (priv
->last_contact
);
646 priv
->last_contact
= g_object_ref (sender
);
647 priv
->last_timestamp
= timestamp
;
648 priv
->last_is_backlog
= is_backlog
;
650 g_free (body_escaped
);
651 g_string_free (message_classes
, TRUE
);
655 theme_adium_append_event (EmpathyChatView
*view
,
660 str_escaped
= g_markup_escape_text (str
, -1);
661 theme_adium_append_event_escaped (view
, str_escaped
);
662 g_free (str_escaped
);
666 theme_adium_scroll (EmpathyChatView
*view
,
667 gboolean allow_scrolling
)
669 /* FIXME: Is it possible? I guess we need a js function, but I don't
674 theme_adium_scroll_down (EmpathyChatView
*view
)
676 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view
), "scrollToBottom()");
680 theme_adium_get_has_selection (EmpathyChatView
*view
)
682 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view
));
686 theme_adium_clear (EmpathyChatView
*view
)
688 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
691 priv
->page_loaded
= FALSE
;
692 basedir_uri
= g_strconcat ("file://", priv
->data
->basedir
, NULL
);
693 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view
),
694 priv
->data
->template_html
,
696 g_free (basedir_uri
);
698 /* Clear last contact to avoid trying to add a 'joined'
699 * message when we don't have an insertion point. */
700 if (priv
->last_contact
) {
701 g_object_unref (priv
->last_contact
);
702 priv
->last_contact
= NULL
;
707 theme_adium_find_previous (EmpathyChatView
*view
,
708 const gchar
*search_criteria
,
711 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view
),
712 search_criteria
, FALSE
,
717 theme_adium_find_next (EmpathyChatView
*view
,
718 const gchar
*search_criteria
,
721 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view
),
722 search_criteria
, FALSE
,
727 theme_adium_find_abilities (EmpathyChatView
*view
,
728 const gchar
*search_criteria
,
729 gboolean
*can_do_previous
,
730 gboolean
*can_do_next
)
732 /* FIXME: Does webkit provide an API for that? We have wrap=true in
733 * find_next and find_previous to work around this problem. */
735 *can_do_previous
= TRUE
;
741 theme_adium_highlight (EmpathyChatView
*view
,
744 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view
));
745 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view
),
747 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view
),
752 theme_adium_copy_clipboard (EmpathyChatView
*view
)
754 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view
));
758 theme_adium_context_menu_selection_done_cb (GtkMenuShell
*menu
, gpointer user_data
)
760 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
762 g_object_unref (hit_test_result
);
766 theme_adium_context_menu_for_event (EmpathyThemeAdium
*theme
, GdkEventButton
*event
)
768 WebKitWebView
*view
= WEBKIT_WEB_VIEW (theme
);
769 WebKitHitTestResult
*hit_test_result
;
770 WebKitHitTestResultContext context
;
774 hit_test_result
= webkit_web_view_get_hit_test_result (view
, event
);
775 g_object_get (G_OBJECT (hit_test_result
), "context", &context
, NULL
);
778 menu
= gtk_menu_new ();
780 /* Select all item */
781 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL
, NULL
);
782 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
784 g_signal_connect_swapped (item
, "activate",
785 G_CALLBACK (webkit_web_view_select_all
),
789 if (webkit_web_view_can_copy_clipboard (view
)) {
790 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY
, NULL
);
791 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
793 g_signal_connect_swapped (item
, "activate",
794 G_CALLBACK (webkit_web_view_copy_clipboard
),
798 /* Clear menu item */
799 item
= gtk_separator_menu_item_new ();
800 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
802 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR
, NULL
);
803 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
805 g_signal_connect_swapped (item
, "activate",
806 G_CALLBACK (empathy_chat_view_clear
),
809 /* We will only add the following menu items if we are
810 * right-clicking a link */
811 if (context
& WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK
) {
813 item
= gtk_separator_menu_item_new ();
814 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
816 /* Copy Link Address menu item */
817 item
= gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
818 g_signal_connect (item
, "activate",
819 G_CALLBACK (theme_adium_copy_address_cb
),
821 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
823 /* Open Link menu item */
824 item
= gtk_menu_item_new_with_mnemonic (_("_Open Link"));
825 g_signal_connect (item
, "activate",
826 G_CALLBACK (theme_adium_open_address_cb
),
828 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
831 g_signal_connect (GTK_MENU_SHELL (menu
), "selection-done",
832 G_CALLBACK (theme_adium_context_menu_selection_done_cb
),
835 /* Display the menu */
836 gtk_widget_show_all (menu
);
837 gtk_menu_popup (GTK_MENU (menu
), NULL
, NULL
, NULL
, NULL
,
838 event
->button
, event
->time
);
842 theme_adium_button_press_event (GtkWidget
*widget
, GdkEventButton
*event
)
844 if (event
->button
== 3) {
845 gboolean developer_tools_enabled
;
847 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget
))),
848 "enable-developer-extras", &developer_tools_enabled
, NULL
);
850 /* We currently have no way to add an inspector menu
851 * item ourselves, so we disable our customized menu
852 * if the developer extras are enabled. */
853 if (!developer_tools_enabled
) {
854 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget
), event
);
859 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class
)->button_press_event (widget
, event
);
863 theme_adium_iface_init (EmpathyChatViewIface
*iface
)
865 iface
->append_message
= theme_adium_append_message
;
866 iface
->append_event
= theme_adium_append_event
;
867 iface
->scroll
= theme_adium_scroll
;
868 iface
->scroll_down
= theme_adium_scroll_down
;
869 iface
->get_has_selection
= theme_adium_get_has_selection
;
870 iface
->clear
= theme_adium_clear
;
871 iface
->find_previous
= theme_adium_find_previous
;
872 iface
->find_next
= theme_adium_find_next
;
873 iface
->find_abilities
= theme_adium_find_abilities
;
874 iface
->highlight
= theme_adium_highlight
;
875 iface
->copy_clipboard
= theme_adium_copy_clipboard
;
879 theme_adium_load_finished_cb (WebKitWebView
*view
,
880 WebKitWebFrame
*frame
,
883 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
884 EmpathyChatView
*chat_view
= EMPATHY_CHAT_VIEW (view
);
886 DEBUG ("Page loaded");
887 priv
->page_loaded
= TRUE
;
889 /* Display queued messages */
890 priv
->message_queue
= g_list_reverse (priv
->message_queue
);
891 while (priv
->message_queue
) {
892 EmpathyMessage
*message
= priv
->message_queue
->data
;
894 theme_adium_append_message (chat_view
, message
);
895 priv
->message_queue
= g_list_remove (priv
->message_queue
, message
);
896 g_object_unref (message
);
901 theme_adium_finalize (GObject
*object
)
903 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
905 empathy_adium_data_unref (priv
->data
);
907 empathy_conf_notify_remove (empathy_conf_get (),
908 priv
->notify_enable_webkit_developer_tools_id
);
910 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->finalize (object
);
914 theme_adium_dispose (GObject
*object
)
916 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
918 if (priv
->smiley_manager
) {
919 g_object_unref (priv
->smiley_manager
);
920 priv
->smiley_manager
= NULL
;
923 if (priv
->last_contact
) {
924 g_object_unref (priv
->last_contact
);
925 priv
->last_contact
= NULL
;
928 if (priv
->inspector_window
) {
929 gtk_widget_destroy (priv
->inspector_window
);
930 priv
->inspector_window
= NULL
;
933 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->dispose (object
);
937 theme_adium_inspector_show_window_cb (WebKitWebInspector
*inspector
,
938 EmpathyThemeAdium
*theme
)
940 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
942 if (priv
->inspector_window
) {
943 gtk_widget_show_all (priv
->inspector_window
);
950 theme_adium_inspector_close_window_cb (WebKitWebInspector
*inspector
,
951 EmpathyThemeAdium
*theme
)
953 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
955 if (priv
->inspector_window
) {
956 gtk_widget_hide (priv
->inspector_window
);
962 static WebKitWebView
*
963 theme_adium_inspect_web_view_cb (WebKitWebInspector
*inspector
,
964 WebKitWebView
*web_view
,
965 EmpathyThemeAdium
*theme
)
967 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
968 GtkWidget
*scrolled_window
;
969 GtkWidget
*inspector_web_view
;
971 if (!priv
->inspector_window
) {
972 /* Create main window */
973 priv
->inspector_window
= gtk_window_new (GTK_WINDOW_TOPLEVEL
);
974 gtk_window_set_default_size (GTK_WINDOW (priv
->inspector_window
),
976 g_signal_connect (priv
->inspector_window
, "delete-event",
977 G_CALLBACK (gtk_widget_hide_on_delete
), NULL
);
979 /* Pack a scrolled window */
980 scrolled_window
= gtk_scrolled_window_new (NULL
, NULL
);
981 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window
),
982 GTK_POLICY_AUTOMATIC
,
983 GTK_POLICY_AUTOMATIC
);
984 gtk_container_add (GTK_CONTAINER (priv
->inspector_window
),
986 gtk_widget_show (scrolled_window
);
988 /* Pack a webview in the scrolled window. That webview will be
989 * used to render the inspector tool. */
990 inspector_web_view
= webkit_web_view_new ();
991 gtk_container_add (GTK_CONTAINER (scrolled_window
),
993 gtk_widget_show (scrolled_window
);
995 return WEBKIT_WEB_VIEW (inspector_web_view
);
1001 static PangoFontDescription
*
1002 theme_adium_get_default_font (void)
1004 GConfClient
*gconf_client
;
1005 PangoFontDescription
*pango_fd
;
1006 gchar
*gconf_font_family
;
1008 gconf_client
= gconf_client_get_default ();
1009 if (gconf_client
== NULL
) {
1012 gconf_font_family
= gconf_client_get_string (gconf_client
,
1013 EMPATHY_GCONF_FONT_KEY_NAME
,
1015 if (gconf_font_family
== NULL
) {
1016 g_object_unref (gconf_client
);
1019 pango_fd
= pango_font_description_from_string (gconf_font_family
);
1020 g_free (gconf_font_family
);
1021 g_object_unref (gconf_client
);
1026 theme_adium_set_webkit_font (WebKitWebSettings
*w_settings
,
1030 g_object_set (w_settings
, "default-font-family", name
, NULL
);
1031 g_object_set (w_settings
, "default-font-size", size
, NULL
);
1035 theme_adium_set_default_font (WebKitWebSettings
*w_settings
)
1037 PangoFontDescription
*default_font_desc
;
1038 GdkScreen
*current_screen
;
1040 gint pango_font_size
= 0;
1042 default_font_desc
= theme_adium_get_default_font ();
1043 if (default_font_desc
== NULL
)
1045 pango_font_size
= pango_font_description_get_size (default_font_desc
)
1047 if (pango_font_description_get_size_is_absolute (default_font_desc
)) {
1048 current_screen
= gdk_screen_get_default ();
1049 if (current_screen
!= NULL
) {
1050 dpi
= gdk_screen_get_resolution (current_screen
);
1052 dpi
= BORING_DPI_DEFAULT
;
1054 pango_font_size
= (gint
) (pango_font_size
/ (dpi
/ 72));
1056 theme_adium_set_webkit_font (w_settings
,
1057 pango_font_description_get_family (default_font_desc
),
1059 pango_font_description_free (default_font_desc
);
1063 theme_adium_constructed (GObject
*object
)
1065 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1067 const gchar
*font_family
= NULL
;
1069 WebKitWebView
*webkit_view
= WEBKIT_WEB_VIEW (object
);
1070 WebKitWebSettings
*webkit_settings
;
1071 WebKitWebInspector
*webkit_inspector
;
1073 /* Set default settings */
1074 font_family
= tp_asv_get_string (priv
->data
->info
, "DefaultFontFamily");
1075 font_size
= tp_asv_get_int32 (priv
->data
->info
, "DefaultFontSize", NULL
);
1076 webkit_settings
= webkit_web_view_get_settings (webkit_view
);
1078 if (font_family
&& font_size
) {
1079 theme_adium_set_webkit_font (webkit_settings
, font_family
, font_size
);
1081 theme_adium_set_default_font (webkit_settings
);
1084 /* Setup webkit inspector */
1085 webkit_inspector
= webkit_web_view_get_inspector (webkit_view
);
1086 g_signal_connect (webkit_inspector
, "inspect-web-view",
1087 G_CALLBACK (theme_adium_inspect_web_view_cb
),
1089 g_signal_connect (webkit_inspector
, "show-window",
1090 G_CALLBACK (theme_adium_inspector_show_window_cb
),
1092 g_signal_connect (webkit_inspector
, "close-window",
1093 G_CALLBACK (theme_adium_inspector_close_window_cb
),
1097 basedir_uri
= g_strconcat ("file://", priv
->data
->basedir
, NULL
);
1098 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object
),
1099 priv
->data
->template_html
,
1101 g_free (basedir_uri
);
1105 theme_adium_get_property (GObject
*object
,
1110 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1113 case PROP_ADIUM_DATA
:
1114 g_value_set_boxed (value
, priv
->data
);
1117 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1123 theme_adium_set_property (GObject
*object
,
1125 const GValue
*value
,
1128 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1131 case PROP_ADIUM_DATA
:
1132 g_assert (priv
->data
== NULL
);
1133 priv
->data
= g_value_dup_boxed (value
);
1136 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1142 empathy_theme_adium_class_init (EmpathyThemeAdiumClass
*klass
)
1144 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
1145 GtkWidgetClass
* widget_class
= GTK_WIDGET_CLASS (klass
);
1147 object_class
->finalize
= theme_adium_finalize
;
1148 object_class
->dispose
= theme_adium_dispose
;
1149 object_class
->constructed
= theme_adium_constructed
;
1150 object_class
->get_property
= theme_adium_get_property
;
1151 object_class
->set_property
= theme_adium_set_property
;
1153 widget_class
->button_press_event
= theme_adium_button_press_event
;
1155 g_object_class_install_property (object_class
,
1157 g_param_spec_boxed ("adium-data",
1159 "Data for the adium theme",
1160 EMPATHY_TYPE_ADIUM_DATA
,
1161 G_PARAM_CONSTRUCT_ONLY
|
1163 G_PARAM_STATIC_STRINGS
));
1165 g_type_class_add_private (object_class
, sizeof (EmpathyThemeAdiumPriv
));
1169 empathy_theme_adium_init (EmpathyThemeAdium
*theme
)
1171 EmpathyThemeAdiumPriv
*priv
= G_TYPE_INSTANCE_GET_PRIVATE (theme
,
1172 EMPATHY_TYPE_THEME_ADIUM
, EmpathyThemeAdiumPriv
);
1176 priv
->smiley_manager
= empathy_smiley_manager_dup_singleton ();
1178 g_signal_connect (theme
, "load-finished",
1179 G_CALLBACK (theme_adium_load_finished_cb
),
1181 g_signal_connect (theme
, "navigation-policy-decision-requested",
1182 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb
),
1185 priv
->notify_enable_webkit_developer_tools_id
=
1186 empathy_conf_notify_add (empathy_conf_get (),
1187 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
,
1188 theme_adium_notify_enable_webkit_developer_tools_cb
,
1191 theme_adium_update_enable_webkit_developer_tools (theme
);
1195 empathy_theme_adium_new (EmpathyAdiumData
*data
)
1197 g_return_val_if_fail (data
!= NULL
, NULL
);
1199 return g_object_new (EMPATHY_TYPE_THEME_ADIUM
,
1205 empathy_adium_path_is_valid (const gchar
*path
)
1210 /* The theme is not valid if there is no Info.plist */
1211 file
= g_build_filename (path
, "Contents", "Info.plist",
1213 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1219 /* We ship a default Template.html as fallback if there is any problem
1220 * with the one inside the theme. The only other required file is
1221 * Content.html for incoming messages (outgoing fallback to use
1223 file
= g_build_filename (path
, "Contents", "Resources", "Incoming",
1224 "Content.html", NULL
);
1225 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1232 empathy_adium_info_new (const gchar
*path
)
1236 GHashTable
*info
= NULL
;
1238 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1240 file
= g_build_filename (path
, "Contents", "Info.plist", NULL
);
1241 value
= empathy_plist_parse_from_file (file
);
1247 info
= g_value_dup_boxed (value
);
1248 tp_g_value_slice_free (value
);
1250 /* Insert the theme's path into the hash table,
1251 * keys have to be dupped */
1252 tp_asv_set_string (info
, g_strdup ("path"), path
);
1258 empathy_adium_data_get_type (void)
1260 static GType type_id
= 0;
1264 type_id
= g_boxed_type_register_static ("EmpathyAdiumData",
1265 (GBoxedCopyFunc
) empathy_adium_data_ref
,
1266 (GBoxedFreeFunc
) empathy_adium_data_unref
);
1273 empathy_adium_data_new_with_info (const gchar
*path
, GHashTable
*info
)
1275 EmpathyAdiumData
*data
;
1277 gchar
*template_html
= NULL
;
1279 gchar
*footer_html
= NULL
;
1282 gchar
**strv
= NULL
;
1287 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1289 data
= g_slice_new0 (EmpathyAdiumData
);
1290 data
->ref_count
= 1;
1291 data
->path
= g_strdup (path
);
1292 data
->basedir
= g_strconcat (path
, G_DIR_SEPARATOR_S
"Contents"
1293 G_DIR_SEPARATOR_S
"Resources" G_DIR_SEPARATOR_S
, NULL
);
1294 data
->info
= g_hash_table_ref (info
);
1296 /* Load html files */
1297 file
= g_build_filename (data
->basedir
, "Incoming", "Content.html", NULL
);
1298 g_file_get_contents (file
, &data
->in_content_html
, &data
->in_content_len
, NULL
);
1301 file
= g_build_filename (data
->basedir
, "Incoming", "NextContent.html", NULL
);
1302 g_file_get_contents (file
, &data
->in_nextcontent_html
, &data
->in_nextcontent_len
, NULL
);
1305 file
= g_build_filename (data
->basedir
, "Incoming", "Context.html", NULL
);
1306 g_file_get_contents (file
, &data
->in_context_html
, &data
->in_context_len
, NULL
);
1309 file
= g_build_filename (data
->basedir
, "Incoming", "NextContext.html", NULL
);
1310 g_file_get_contents (file
, &data
->in_nextcontext_html
, &data
->in_nextcontext_len
, NULL
);
1313 file
= g_build_filename (data
->basedir
, "Outgoing", "Content.html", NULL
);
1314 g_file_get_contents (file
, &data
->out_content_html
, &data
->out_content_len
, NULL
);
1317 file
= g_build_filename (data
->basedir
, "Outgoing", "NextContent.html", NULL
);
1318 g_file_get_contents (file
, &data
->out_nextcontent_html
, &data
->out_nextcontent_len
, NULL
);
1321 file
= g_build_filename (data
->basedir
, "Outgoing", "Context.html", NULL
);
1322 g_file_get_contents (file
, &data
->out_context_html
, &data
->out_context_len
, NULL
);
1325 file
= g_build_filename (data
->basedir
, "Outgoing", "NextContext.html", NULL
);
1326 g_file_get_contents (file
, &data
->out_nextcontext_html
, &data
->out_nextcontext_len
, NULL
);
1329 file
= g_build_filename (data
->basedir
, "Status.html", NULL
);
1330 g_file_get_contents (file
, &data
->status_html
, &data
->status_len
, NULL
);
1333 file
= g_build_filename (data
->basedir
, "Footer.html", NULL
);
1334 g_file_get_contents (file
, &footer_html
, &footer_len
, NULL
);
1337 file
= g_build_filename (data
->basedir
, "Incoming", "buddy_icon.png", NULL
);
1338 if (g_file_test (file
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
)) {
1339 data
->default_incoming_avatar_filename
= file
;
1344 file
= g_build_filename (data
->basedir
, "Outgoing", "buddy_icon.png", NULL
);
1345 if (g_file_test (file
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
)) {
1346 data
->default_outgoing_avatar_filename
= file
;
1351 css_path
= g_build_filename (data
->basedir
, "main.css", NULL
);
1353 /* There is 2 formats for Template.html: The old one has 4 parameters,
1354 * the new one has 5 parameters. */
1355 file
= g_build_filename (data
->basedir
, "Template.html", NULL
);
1356 if (g_file_get_contents (file
, &template_html
, &template_len
, NULL
)) {
1357 strv
= g_strsplit (template_html
, "%@", -1);
1358 len
= g_strv_length (strv
);
1362 if (len
!= 5 && len
!= 6) {
1363 /* Either the theme has no template or it don't have the good
1364 * number of parameters. Fallback to use our own template. */
1365 g_free (template_html
);
1368 file
= empathy_file_lookup ("Template.html", "data");
1369 g_file_get_contents (file
, &template_html
, &template_len
, NULL
);
1371 strv
= g_strsplit (template_html
, "%@", -1);
1372 len
= g_strv_length (strv
);
1375 /* Replace %@ with the needed information in the template html. */
1376 string
= g_string_sized_new (template_len
);
1377 g_string_append (string
, strv
[i
++]);
1378 g_string_append (string
, data
->basedir
);
1379 g_string_append (string
, strv
[i
++]);
1381 const gchar
*variant
;
1383 /* We include main.css by default */
1384 g_string_append_printf (string
, "@import url(\"%s\");", css_path
);
1385 g_string_append (string
, strv
[i
++]);
1386 variant
= tp_asv_get_string (data
->info
, "DefaultVariant");
1388 g_string_append (string
, "Variants/");
1389 g_string_append (string
, variant
);
1390 g_string_append (string
, ".css");
1393 /* FIXME: We should set main.css OR the variant css */
1394 g_string_append (string
, css_path
);
1396 g_string_append (string
, strv
[i
++]);
1397 g_string_append (string
, ""); /* We don't want header */
1398 g_string_append (string
, strv
[i
++]);
1399 /* FIXME: We should replace adium %macros% in footer */
1401 g_string_append (string
, footer_html
);
1403 g_string_append (string
, strv
[i
++]);
1404 data
->template_html
= g_string_free (string
, FALSE
);
1406 g_free (footer_html
);
1407 g_free (template_html
);
1415 empathy_adium_data_new (const gchar
*path
)
1417 EmpathyAdiumData
*data
;
1420 info
= empathy_adium_info_new (path
);
1421 data
= empathy_adium_data_new_with_info (path
, info
);
1422 g_hash_table_unref (info
);
1428 empathy_adium_data_ref (EmpathyAdiumData
*data
)
1430 g_return_val_if_fail (data
!= NULL
, NULL
);
1432 g_atomic_int_inc (&data
->ref_count
);
1438 empathy_adium_data_unref (EmpathyAdiumData
*data
)
1440 g_return_if_fail (data
!= NULL
);
1442 if (g_atomic_int_dec_and_test (&data
->ref_count
)) {
1443 g_free (data
->path
);
1444 g_free (data
->basedir
);
1445 g_free (data
->template_html
);
1446 g_free (data
->in_content_html
);
1447 g_free (data
->in_nextcontent_html
);
1448 g_free (data
->in_context_html
);
1449 g_free (data
->in_nextcontext_html
);
1450 g_free (data
->out_content_html
);
1451 g_free (data
->out_nextcontent_html
);
1452 g_free (data
->out_context_html
);
1453 g_free (data
->out_nextcontext_html
);
1454 g_free (data
->default_avatar_filename
);
1455 g_free (data
->default_incoming_avatar_filename
);
1456 g_free (data
->default_outgoing_avatar_filename
);
1457 g_free (data
->status_html
);
1458 g_hash_table_unref (data
->info
);
1459 g_slice_free (EmpathyAdiumData
, data
);
1464 empathy_adium_data_get_info (EmpathyAdiumData
*data
)
1466 g_return_val_if_fail (data
!= NULL
, NULL
);
1472 empathy_adium_data_get_path (EmpathyAdiumData
*data
)
1474 g_return_val_if_fail (data
!= NULL
, NULL
);