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-gsettings.h>
36 #include <libempathy/empathy-time.h>
37 #include <libempathy/empathy-utils.h>
39 #include "empathy-theme-adium.h"
40 #include "empathy-smiley-manager.h"
41 #include "empathy-ui-utils.h"
42 #include "empathy-plist.h"
43 #include "empathy-string-parser.h"
44 #include "empathy-images.h"
46 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
47 #include <libempathy/empathy-debug.h>
49 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyThemeAdium)
51 /* GConf key containing current value of font */
52 #define EMPATHY_GCONF_FONT_KEY_NAME "/desktop/gnome/interface/document_font_name"
53 #define BORING_DPI_DEFAULT 96
55 /* "Join" consecutive messages with timestamps within five minutes */
56 #define MESSAGE_JOIN_PERIOD 5*60
59 EmpathyAdiumData
*data
;
60 EmpathySmileyManager
*smiley_manager
;
61 EmpathyContact
*last_contact
;
62 time_t last_timestamp
;
63 gboolean last_is_backlog
;
66 GtkWidget
*inspector_window
;
67 GSettings
*gsettings_chat
;
68 } EmpathyThemeAdiumPriv
;
70 struct _EmpathyAdiumData
{
74 gchar
*default_avatar_filename
;
75 gchar
*default_incoming_avatar_filename
;
76 gchar
*default_outgoing_avatar_filename
;
78 gchar
*in_content_html
;
80 gchar
*in_context_html
;
82 gchar
*in_nextcontent_html
;
83 gsize in_nextcontent_len
;
84 gchar
*in_nextcontext_html
;
85 gsize in_nextcontext_len
;
86 gchar
*out_content_html
;
87 gsize out_content_len
;
88 gchar
*out_context_html
;
89 gsize out_context_len
;
90 gchar
*out_nextcontent_html
;
91 gsize out_nextcontent_len
;
92 gchar
*out_nextcontext_html
;
93 gsize out_nextcontext_len
;
99 static void theme_adium_iface_init (EmpathyChatViewIface
*iface
);
106 G_DEFINE_TYPE_WITH_CODE (EmpathyThemeAdium
, empathy_theme_adium
,
107 WEBKIT_TYPE_WEB_VIEW
,
108 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW
,
109 theme_adium_iface_init
));
112 theme_adium_update_enable_webkit_developer_tools (EmpathyThemeAdium
*theme
)
114 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
115 WebKitWebView
*web_view
= WEBKIT_WEB_VIEW (theme
);
116 gboolean enable_webkit_developer_tools
;
118 enable_webkit_developer_tools
= g_settings_get_boolean (
119 priv
->gsettings_chat
,
120 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
);
122 g_object_set (G_OBJECT (webkit_web_view_get_settings (web_view
)),
123 "enable-developer-extras",
124 enable_webkit_developer_tools
,
129 theme_adium_notify_enable_webkit_developer_tools_cb (GSettings
*gsettings
,
133 EmpathyThemeAdium
*theme
= user_data
;
135 theme_adium_update_enable_webkit_developer_tools (theme
);
139 theme_adium_navigation_policy_decision_requested_cb (WebKitWebView
*view
,
140 WebKitWebFrame
*web_frame
,
141 WebKitNetworkRequest
*request
,
142 WebKitWebNavigationAction
*action
,
143 WebKitWebPolicyDecision
*decision
,
148 /* Only call url_show on clicks */
149 if (webkit_web_navigation_action_get_reason (action
) !=
150 WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED
) {
151 webkit_web_policy_decision_use (decision
);
155 uri
= webkit_network_request_get_uri (request
);
156 empathy_url_show (GTK_WIDGET (view
), uri
);
158 webkit_web_policy_decision_ignore (decision
);
163 theme_adium_copy_address_cb (GtkMenuItem
*menuitem
,
166 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
168 GtkClipboard
*clipboard
;
170 g_object_get (G_OBJECT (hit_test_result
), "link-uri", &uri
, NULL
);
172 clipboard
= gtk_clipboard_get (GDK_SELECTION_CLIPBOARD
);
173 gtk_clipboard_set_text (clipboard
, uri
, -1);
175 clipboard
= gtk_clipboard_get (GDK_SELECTION_PRIMARY
);
176 gtk_clipboard_set_text (clipboard
, uri
, -1);
182 theme_adium_open_address_cb (GtkMenuItem
*menuitem
,
185 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
188 g_object_get (G_OBJECT (hit_test_result
), "link-uri", &uri
, NULL
);
190 empathy_url_show (GTK_WIDGET (menuitem
), uri
);
196 theme_adium_match_newline (const gchar
*text
,
198 EmpathyStringReplace replace_func
,
199 EmpathyStringParser
*sub_parsers
,
202 GString
*string
= user_data
;
210 /* Replace \n by <br/> */
211 for (i
= 0; i
< len
&& text
[i
] != '\0'; i
++) {
212 if (text
[i
] == '\n') {
213 empathy_string_parser_substr (text
+ prev
,
214 i
- prev
, sub_parsers
,
216 g_string_append (string
, "<br/>");
220 empathy_string_parser_substr (text
+ prev
, i
- prev
,
221 sub_parsers
, user_data
);
225 theme_adium_replace_smiley (const gchar
*text
,
230 EmpathySmileyHit
*hit
= match_data
;
231 GString
*string
= user_data
;
233 /* Replace smiley by a <img/> tag */
234 g_string_append_printf (string
,
235 "<img src=\"%s\" alt=\"%.*s\" title=\"%.*s\"/>",
236 hit
->path
, (int)len
, text
, (int)len
, text
);
239 static EmpathyStringParser string_parsers
[] = {
240 {empathy_string_match_link
, empathy_string_replace_link
},
241 {theme_adium_match_newline
, NULL
},
242 {empathy_string_match_all
, empathy_string_replace_escaped
},
246 static EmpathyStringParser string_parsers_with_smiley
[] = {
247 {empathy_string_match_link
, empathy_string_replace_link
},
248 {empathy_string_match_smiley
, theme_adium_replace_smiley
},
249 {theme_adium_match_newline
, NULL
},
250 {empathy_string_match_all
, empathy_string_replace_escaped
},
255 theme_adium_parse_body (const gchar
*text
)
257 EmpathyStringParser
*parsers
;
259 GSettings
*gsettings
= g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA
);
261 /* Check if we have to parse smileys */
262 if (g_settings_get_boolean (gsettings
, EMPATHY_PREFS_CHAT_SHOW_SMILEYS
))
263 parsers
= string_parsers_with_smiley
;
265 parsers
= string_parsers
;
267 /* Parse text and construct string with links and smileys replaced
268 * by html tags. Also escape text to make sure html code is
269 * displayed verbatim. */
270 string
= g_string_sized_new (strlen (text
));
271 empathy_string_parser_substr (text
, -1, parsers
, string
);
273 g_object_unref (gsettings
);
275 return g_string_free (string
, FALSE
);
279 escape_and_append_len (GString
*string
, const gchar
*str
, gint len
)
281 while (*str
!= '\0' && len
!= 0) {
285 g_string_append (string
, "\\\\");
289 g_string_append (string
, "\\\"");
292 /* Remove end of lines */
295 g_string_append_c (string
, *str
);
304 theme_adium_match (const gchar
**str
, const gchar
*match
)
308 len
= strlen (match
);
309 if (strncmp (*str
, match
, len
) == 0) {
318 theme_adium_append_html (EmpathyThemeAdium
*theme
,
320 const gchar
*html
, gsize len
,
321 const gchar
*message
,
322 const gchar
*avatar_filename
,
324 const gchar
*contact_id
,
325 const gchar
*service_name
,
326 const gchar
*message_classes
,
331 const gchar
*cur
= NULL
;
334 /* Make some search-and-replace in the html code */
335 string
= g_string_sized_new (len
+ strlen (message
));
336 g_string_append_printf (string
, "%s(\"", func
);
337 for (cur
= html
; *cur
!= '\0'; cur
++) {
338 const gchar
*replace
= NULL
;
339 gchar
*dup_replace
= NULL
;
341 if (theme_adium_match (&cur
, "%message%")) {
343 } else if (theme_adium_match (&cur
, "%messageClasses%")) {
344 replace
= message_classes
;
345 } else if (theme_adium_match (&cur
, "%userIconPath%")) {
346 replace
= avatar_filename
;
347 } else if (theme_adium_match (&cur
, "%sender%")) {
349 } else if (theme_adium_match (&cur
, "%senderScreenName%")) {
350 replace
= contact_id
;
351 } else if (theme_adium_match (&cur
, "%senderDisplayName%")) {
352 /* %senderDisplayName% -
353 * "The serverside (remotely set) name of the sender,
354 * such as an MSN display name."
356 * We don't have access to that yet so we use local
359 } else if (theme_adium_match (&cur
, "%service%")) {
360 replace
= service_name
;
361 } else if (theme_adium_match (&cur
, "%shortTime%")) {
362 dup_replace
= empathy_time_to_string_local (timestamp
,
363 EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
364 replace
= dup_replace
;
365 } else if (theme_adium_match (&cur
, "%time")) {
366 gchar
*format
= NULL
;
368 /* Time can be in 2 formats:
369 * %time% or %time{strftime format}%
370 * Extract the time format if provided. */
373 end
= strstr (cur
, "}%");
378 format
= g_strndup (cur
, end
- cur
);
385 dup_replace
= empathy_time_to_string_local (timestamp
,
386 format
? format
: EMPATHY_TIME_DATE_FORMAT_DISPLAY_SHORT
);
388 dup_replace
= empathy_time_to_string_local (timestamp
,
389 format
? format
: EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
391 replace
= dup_replace
;
394 escape_and_append_len (string
, cur
, 1);
398 /* Here we have a replacement to make */
399 escape_and_append_len (string
, replace
, -1);
400 g_free (dup_replace
);
402 g_string_append (string
, "\")");
404 script
= g_string_free (string
, FALSE
);
405 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (theme
), script
);
410 theme_adium_append_event_escaped (EmpathyChatView
*view
,
411 const gchar
*escaped
)
413 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (view
);
414 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
416 if (priv
->data
->status_html
) {
417 theme_adium_append_html (theme
, "appendMessage",
418 priv
->data
->status_html
,
419 priv
->data
->status_len
,
420 escaped
, NULL
, NULL
, NULL
, NULL
,
421 "event", empathy_time_get_current (), FALSE
);
424 /* There is no last contact */
425 if (priv
->last_contact
) {
426 g_object_unref (priv
->last_contact
);
427 priv
->last_contact
= NULL
;
432 theme_adium_append_message (EmpathyChatView
*view
,
435 EmpathyThemeAdium
*theme
= EMPATHY_THEME_ADIUM (view
);
436 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
437 EmpathyContact
*sender
;
442 const gchar
*contact_id
;
443 EmpathyAvatar
*avatar
;
444 const gchar
*avatar_filename
= NULL
;
449 const gchar
*service_name
;
450 GString
*message_classes
= NULL
;
452 gboolean consecutive
;
454 if (!priv
->page_loaded
) {
455 priv
->message_queue
= g_list_prepend (priv
->message_queue
,
460 /* Get information */
461 sender
= empathy_message_get_sender (msg
);
462 account
= empathy_contact_get_account (sender
);
463 service_name
= empathy_protocol_name_to_display_name
464 (tp_account_get_protocol (account
));
465 if (service_name
== NULL
)
466 service_name
= tp_account_get_protocol (account
);
467 timestamp
= empathy_message_get_timestamp (msg
);
468 body
= empathy_message_get_body (msg
);
469 body_escaped
= theme_adium_parse_body (body
);
470 name
= empathy_contact_get_alias (sender
);
471 contact_id
= empathy_contact_get_id (sender
);
473 /* If this is a /me, append an event */
474 if (empathy_message_get_tptype (msg
) == TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION
) {
477 str
= g_strdup_printf ("%s %s", name
, body_escaped
);
478 theme_adium_append_event_escaped (view
, str
);
481 g_free (body_escaped
);
485 /* Get the avatar filename, or a fallback */
486 avatar
= empathy_contact_get_avatar (sender
);
488 avatar_filename
= avatar
->filename
;
490 if (!avatar_filename
) {
491 if (empathy_contact_is_user (sender
)) {
492 avatar_filename
= priv
->data
->default_outgoing_avatar_filename
;
494 avatar_filename
= priv
->data
->default_incoming_avatar_filename
;
496 if (!avatar_filename
) {
497 if (!priv
->data
->default_avatar_filename
) {
498 priv
->data
->default_avatar_filename
=
499 empathy_filename_from_icon_name (EMPATHY_IMAGE_AVATAR_DEFAULT
,
500 GTK_ICON_SIZE_DIALOG
);
502 avatar_filename
= priv
->data
->default_avatar_filename
;
506 /* We want to join this message with the last one if
507 * - senders are the same contact,
508 * - last message was recieved recently,
509 * - last message and this message both are/aren't backlog, and
510 * - DisableCombineConsecutive is not set in theme's settings */
511 is_backlog
= empathy_message_is_backlog (msg
);
512 consecutive
= empathy_contact_equal (priv
->last_contact
, sender
) &&
513 (timestamp
- priv
->last_timestamp
< MESSAGE_JOIN_PERIOD
) &&
514 (is_backlog
== priv
->last_is_backlog
) &&
515 !tp_asv_get_boolean (priv
->data
->info
,
516 "DisableCombineConsecutive", NULL
);
518 /* Define message classes */
519 message_classes
= g_string_new ("message");
521 g_string_append (message_classes
, " history");
524 g_string_append (message_classes
, " consecutive");
526 if (empathy_contact_is_user (sender
)) {
527 g_string_append (message_classes
, " outgoing");
529 g_string_append (message_classes
, " incoming");
532 /* Define javascript function to use */
534 func
= "appendNextMessage";
536 func
= "appendMessage";
540 if (empathy_contact_is_user (sender
)) {
543 html
= priv
->data
->out_nextcontext_html
;
544 len
= priv
->data
->out_nextcontext_len
;
547 /* Not backlog, or fallback if NextContext.html
550 html
= priv
->data
->out_nextcontent_html
;
551 len
= priv
->data
->out_nextcontent_len
;
555 /* Not consecutive, or fallback if NextContext.html and/or
556 * NextContent.html are missing */
559 html
= priv
->data
->out_context_html
;
560 len
= priv
->data
->out_context_len
;
564 html
= priv
->data
->out_content_html
;
565 len
= priv
->data
->out_content_len
;
570 /* Incoming, or fallback if outgoing files are missing */
574 html
= priv
->data
->in_nextcontext_html
;
575 len
= priv
->data
->in_nextcontext_len
;
578 /* Note backlog, or fallback if NextContext.html
581 html
= priv
->data
->in_nextcontent_html
;
582 len
= priv
->data
->in_nextcontent_len
;
586 /* Not consecutive, or fallback if NextContext.html and/or
587 * NextContent.html are missing */
590 html
= priv
->data
->in_context_html
;
591 len
= priv
->data
->in_context_len
;
595 html
= priv
->data
->in_content_html
;
596 len
= priv
->data
->in_content_len
;
602 theme_adium_append_html (theme
, func
, html
, len
, body_escaped
,
603 avatar_filename
, name
, contact_id
,
604 service_name
, message_classes
->str
,
605 timestamp
, is_backlog
);
607 DEBUG ("Couldn't find HTML file for this message");
610 /* Keep the sender of the last displayed message */
611 if (priv
->last_contact
) {
612 g_object_unref (priv
->last_contact
);
614 priv
->last_contact
= g_object_ref (sender
);
615 priv
->last_timestamp
= timestamp
;
616 priv
->last_is_backlog
= is_backlog
;
618 g_free (body_escaped
);
619 g_string_free (message_classes
, TRUE
);
623 theme_adium_append_event (EmpathyChatView
*view
,
628 str_escaped
= g_markup_escape_text (str
, -1);
629 theme_adium_append_event_escaped (view
, str_escaped
);
630 g_free (str_escaped
);
634 theme_adium_scroll (EmpathyChatView
*view
,
635 gboolean allow_scrolling
)
637 /* FIXME: Is it possible? I guess we need a js function, but I don't
642 theme_adium_scroll_down (EmpathyChatView
*view
)
644 webkit_web_view_execute_script (WEBKIT_WEB_VIEW (view
), "scrollToBottom()");
648 theme_adium_get_has_selection (EmpathyChatView
*view
)
650 return webkit_web_view_has_selection (WEBKIT_WEB_VIEW (view
));
654 theme_adium_clear (EmpathyChatView
*view
)
656 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
659 priv
->page_loaded
= FALSE
;
660 basedir_uri
= g_strconcat ("file://", priv
->data
->basedir
, NULL
);
661 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (view
),
662 priv
->data
->template_html
,
664 g_free (basedir_uri
);
666 /* Clear last contact to avoid trying to add a 'joined'
667 * message when we don't have an insertion point. */
668 if (priv
->last_contact
) {
669 g_object_unref (priv
->last_contact
);
670 priv
->last_contact
= NULL
;
675 theme_adium_find_previous (EmpathyChatView
*view
,
676 const gchar
*search_criteria
,
680 /* FIXME: Doesn't respect new_search */
681 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view
),
682 search_criteria
, match_case
,
687 theme_adium_find_next (EmpathyChatView
*view
,
688 const gchar
*search_criteria
,
692 /* FIXME: Doesn't respect new_search */
693 return webkit_web_view_search_text (WEBKIT_WEB_VIEW (view
),
694 search_criteria
, match_case
,
699 theme_adium_find_abilities (EmpathyChatView
*view
,
700 const gchar
*search_criteria
,
702 gboolean
*can_do_previous
,
703 gboolean
*can_do_next
)
705 /* FIXME: Does webkit provide an API for that? We have wrap=true in
706 * find_next and find_previous to work around this problem. */
708 *can_do_previous
= TRUE
;
714 theme_adium_highlight (EmpathyChatView
*view
,
718 webkit_web_view_unmark_text_matches (WEBKIT_WEB_VIEW (view
));
719 webkit_web_view_mark_text_matches (WEBKIT_WEB_VIEW (view
),
720 text
, match_case
, 0);
721 webkit_web_view_set_highlight_text_matches (WEBKIT_WEB_VIEW (view
),
726 theme_adium_copy_clipboard (EmpathyChatView
*view
)
728 webkit_web_view_copy_clipboard (WEBKIT_WEB_VIEW (view
));
732 theme_adium_context_menu_selection_done_cb (GtkMenuShell
*menu
, gpointer user_data
)
734 WebKitHitTestResult
*hit_test_result
= WEBKIT_HIT_TEST_RESULT (user_data
);
736 g_object_unref (hit_test_result
);
740 theme_adium_context_menu_for_event (EmpathyThemeAdium
*theme
, GdkEventButton
*event
)
742 WebKitWebView
*view
= WEBKIT_WEB_VIEW (theme
);
743 WebKitHitTestResult
*hit_test_result
;
744 WebKitHitTestResultContext context
;
748 hit_test_result
= webkit_web_view_get_hit_test_result (view
, event
);
749 g_object_get (G_OBJECT (hit_test_result
), "context", &context
, NULL
);
752 menu
= gtk_menu_new ();
754 /* Select all item */
755 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_SELECT_ALL
, NULL
);
756 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
758 g_signal_connect_swapped (item
, "activate",
759 G_CALLBACK (webkit_web_view_select_all
),
763 if (webkit_web_view_can_copy_clipboard (view
)) {
764 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_COPY
, NULL
);
765 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
767 g_signal_connect_swapped (item
, "activate",
768 G_CALLBACK (webkit_web_view_copy_clipboard
),
772 /* Clear menu item */
773 item
= gtk_separator_menu_item_new ();
774 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
776 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR
, NULL
);
777 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
779 g_signal_connect_swapped (item
, "activate",
780 G_CALLBACK (empathy_chat_view_clear
),
783 /* We will only add the following menu items if we are
784 * right-clicking a link */
785 if (context
& WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK
) {
787 item
= gtk_separator_menu_item_new ();
788 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
790 /* Copy Link Address menu item */
791 item
= gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
792 g_signal_connect (item
, "activate",
793 G_CALLBACK (theme_adium_copy_address_cb
),
795 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
797 /* Open Link menu item */
798 item
= gtk_menu_item_new_with_mnemonic (_("_Open Link"));
799 g_signal_connect (item
, "activate",
800 G_CALLBACK (theme_adium_open_address_cb
),
802 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
805 g_signal_connect (GTK_MENU_SHELL (menu
), "selection-done",
806 G_CALLBACK (theme_adium_context_menu_selection_done_cb
),
809 /* Display the menu */
810 gtk_widget_show_all (menu
);
811 gtk_menu_popup (GTK_MENU (menu
), NULL
, NULL
, NULL
, NULL
,
812 event
->button
, event
->time
);
813 g_object_ref_sink (menu
);
814 g_object_unref (menu
);
818 theme_adium_button_press_event (GtkWidget
*widget
, GdkEventButton
*event
)
820 if (event
->button
== 3) {
821 gboolean developer_tools_enabled
;
823 g_object_get (G_OBJECT (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget
))),
824 "enable-developer-extras", &developer_tools_enabled
, NULL
);
826 /* We currently have no way to add an inspector menu
827 * item ourselves, so we disable our customized menu
828 * if the developer extras are enabled. */
829 if (!developer_tools_enabled
) {
830 theme_adium_context_menu_for_event (EMPATHY_THEME_ADIUM (widget
), event
);
835 return GTK_WIDGET_CLASS (empathy_theme_adium_parent_class
)->button_press_event (widget
, event
);
839 theme_adium_iface_init (EmpathyChatViewIface
*iface
)
841 iface
->append_message
= theme_adium_append_message
;
842 iface
->append_event
= theme_adium_append_event
;
843 iface
->scroll
= theme_adium_scroll
;
844 iface
->scroll_down
= theme_adium_scroll_down
;
845 iface
->get_has_selection
= theme_adium_get_has_selection
;
846 iface
->clear
= theme_adium_clear
;
847 iface
->find_previous
= theme_adium_find_previous
;
848 iface
->find_next
= theme_adium_find_next
;
849 iface
->find_abilities
= theme_adium_find_abilities
;
850 iface
->highlight
= theme_adium_highlight
;
851 iface
->copy_clipboard
= theme_adium_copy_clipboard
;
855 theme_adium_load_finished_cb (WebKitWebView
*view
,
856 WebKitWebFrame
*frame
,
859 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (view
);
860 EmpathyChatView
*chat_view
= EMPATHY_CHAT_VIEW (view
);
862 DEBUG ("Page loaded");
863 priv
->page_loaded
= TRUE
;
865 /* Display queued messages */
866 priv
->message_queue
= g_list_reverse (priv
->message_queue
);
867 while (priv
->message_queue
) {
868 EmpathyMessage
*message
= priv
->message_queue
->data
;
870 theme_adium_append_message (chat_view
, message
);
871 priv
->message_queue
= g_list_remove (priv
->message_queue
, message
);
872 g_object_unref (message
);
877 theme_adium_finalize (GObject
*object
)
879 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
881 empathy_adium_data_unref (priv
->data
);
882 g_object_unref (priv
->gsettings_chat
);
884 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->finalize (object
);
888 theme_adium_dispose (GObject
*object
)
890 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
892 if (priv
->smiley_manager
) {
893 g_object_unref (priv
->smiley_manager
);
894 priv
->smiley_manager
= NULL
;
897 if (priv
->last_contact
) {
898 g_object_unref (priv
->last_contact
);
899 priv
->last_contact
= NULL
;
902 if (priv
->inspector_window
) {
903 gtk_widget_destroy (priv
->inspector_window
);
904 priv
->inspector_window
= NULL
;
907 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->dispose (object
);
911 theme_adium_inspector_show_window_cb (WebKitWebInspector
*inspector
,
912 EmpathyThemeAdium
*theme
)
914 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
916 if (priv
->inspector_window
) {
917 gtk_widget_show_all (priv
->inspector_window
);
924 theme_adium_inspector_close_window_cb (WebKitWebInspector
*inspector
,
925 EmpathyThemeAdium
*theme
)
927 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
929 if (priv
->inspector_window
) {
930 gtk_widget_hide (priv
->inspector_window
);
936 static WebKitWebView
*
937 theme_adium_inspect_web_view_cb (WebKitWebInspector
*inspector
,
938 WebKitWebView
*web_view
,
939 EmpathyThemeAdium
*theme
)
941 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (theme
);
942 GtkWidget
*scrolled_window
;
943 GtkWidget
*inspector_web_view
;
945 if (!priv
->inspector_window
) {
946 /* Create main window */
947 priv
->inspector_window
= gtk_window_new (GTK_WINDOW_TOPLEVEL
);
948 gtk_window_set_default_size (GTK_WINDOW (priv
->inspector_window
),
950 g_signal_connect (priv
->inspector_window
, "delete-event",
951 G_CALLBACK (gtk_widget_hide_on_delete
), NULL
);
953 /* Pack a scrolled window */
954 scrolled_window
= gtk_scrolled_window_new (NULL
, NULL
);
955 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window
),
956 GTK_POLICY_AUTOMATIC
,
957 GTK_POLICY_AUTOMATIC
);
958 gtk_container_add (GTK_CONTAINER (priv
->inspector_window
),
960 gtk_widget_show (scrolled_window
);
962 /* Pack a webview in the scrolled window. That webview will be
963 * used to render the inspector tool. */
964 inspector_web_view
= webkit_web_view_new ();
965 gtk_container_add (GTK_CONTAINER (scrolled_window
),
967 gtk_widget_show (scrolled_window
);
969 return WEBKIT_WEB_VIEW (inspector_web_view
);
975 static PangoFontDescription
*
976 theme_adium_get_default_font (void)
978 GConfClient
*gconf_client
;
979 PangoFontDescription
*pango_fd
;
980 gchar
*gconf_font_family
;
982 gconf_client
= gconf_client_get_default ();
983 if (gconf_client
== NULL
) {
986 gconf_font_family
= gconf_client_get_string (gconf_client
,
987 EMPATHY_GCONF_FONT_KEY_NAME
,
989 if (gconf_font_family
== NULL
) {
990 g_object_unref (gconf_client
);
993 pango_fd
= pango_font_description_from_string (gconf_font_family
);
994 g_free (gconf_font_family
);
995 g_object_unref (gconf_client
);
1000 theme_adium_set_webkit_font (WebKitWebSettings
*w_settings
,
1004 g_object_set (w_settings
, "default-font-family", name
, NULL
);
1005 g_object_set (w_settings
, "default-font-size", size
, NULL
);
1009 theme_adium_set_default_font (WebKitWebSettings
*w_settings
)
1011 PangoFontDescription
*default_font_desc
;
1012 GdkScreen
*current_screen
;
1014 gint pango_font_size
= 0;
1016 default_font_desc
= theme_adium_get_default_font ();
1017 if (default_font_desc
== NULL
)
1019 pango_font_size
= pango_font_description_get_size (default_font_desc
)
1021 if (pango_font_description_get_size_is_absolute (default_font_desc
)) {
1022 current_screen
= gdk_screen_get_default ();
1023 if (current_screen
!= NULL
) {
1024 dpi
= gdk_screen_get_resolution (current_screen
);
1026 dpi
= BORING_DPI_DEFAULT
;
1028 pango_font_size
= (gint
) (pango_font_size
/ (dpi
/ 72));
1030 theme_adium_set_webkit_font (w_settings
,
1031 pango_font_description_get_family (default_font_desc
),
1033 pango_font_description_free (default_font_desc
);
1037 theme_adium_constructed (GObject
*object
)
1039 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1041 const gchar
*font_family
= NULL
;
1043 WebKitWebView
*webkit_view
= WEBKIT_WEB_VIEW (object
);
1044 WebKitWebSettings
*webkit_settings
;
1045 WebKitWebInspector
*webkit_inspector
;
1047 /* Set default settings */
1048 font_family
= tp_asv_get_string (priv
->data
->info
, "DefaultFontFamily");
1049 font_size
= tp_asv_get_int32 (priv
->data
->info
, "DefaultFontSize", NULL
);
1050 webkit_settings
= webkit_web_view_get_settings (webkit_view
);
1052 if (font_family
&& font_size
) {
1053 theme_adium_set_webkit_font (webkit_settings
, font_family
, font_size
);
1055 theme_adium_set_default_font (webkit_settings
);
1058 /* Setup webkit inspector */
1059 webkit_inspector
= webkit_web_view_get_inspector (webkit_view
);
1060 g_signal_connect (webkit_inspector
, "inspect-web-view",
1061 G_CALLBACK (theme_adium_inspect_web_view_cb
),
1063 g_signal_connect (webkit_inspector
, "show-window",
1064 G_CALLBACK (theme_adium_inspector_show_window_cb
),
1066 g_signal_connect (webkit_inspector
, "close-window",
1067 G_CALLBACK (theme_adium_inspector_close_window_cb
),
1071 basedir_uri
= g_strconcat ("file://", priv
->data
->basedir
, NULL
);
1072 webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (object
),
1073 priv
->data
->template_html
,
1075 g_free (basedir_uri
);
1079 theme_adium_get_property (GObject
*object
,
1084 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1087 case PROP_ADIUM_DATA
:
1088 g_value_set_boxed (value
, priv
->data
);
1091 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1097 theme_adium_set_property (GObject
*object
,
1099 const GValue
*value
,
1102 EmpathyThemeAdiumPriv
*priv
= GET_PRIV (object
);
1105 case PROP_ADIUM_DATA
:
1106 g_assert (priv
->data
== NULL
);
1107 priv
->data
= g_value_dup_boxed (value
);
1110 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1116 empathy_theme_adium_class_init (EmpathyThemeAdiumClass
*klass
)
1118 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
1119 GtkWidgetClass
* widget_class
= GTK_WIDGET_CLASS (klass
);
1121 object_class
->finalize
= theme_adium_finalize
;
1122 object_class
->dispose
= theme_adium_dispose
;
1123 object_class
->constructed
= theme_adium_constructed
;
1124 object_class
->get_property
= theme_adium_get_property
;
1125 object_class
->set_property
= theme_adium_set_property
;
1127 widget_class
->button_press_event
= theme_adium_button_press_event
;
1129 g_object_class_install_property (object_class
,
1131 g_param_spec_boxed ("adium-data",
1133 "Data for the adium theme",
1134 EMPATHY_TYPE_ADIUM_DATA
,
1135 G_PARAM_CONSTRUCT_ONLY
|
1137 G_PARAM_STATIC_STRINGS
));
1139 g_type_class_add_private (object_class
, sizeof (EmpathyThemeAdiumPriv
));
1143 empathy_theme_adium_init (EmpathyThemeAdium
*theme
)
1145 EmpathyThemeAdiumPriv
*priv
= G_TYPE_INSTANCE_GET_PRIVATE (theme
,
1146 EMPATHY_TYPE_THEME_ADIUM
, EmpathyThemeAdiumPriv
);
1150 priv
->smiley_manager
= empathy_smiley_manager_dup_singleton ();
1152 g_signal_connect (theme
, "load-finished",
1153 G_CALLBACK (theme_adium_load_finished_cb
),
1155 g_signal_connect (theme
, "navigation-policy-decision-requested",
1156 G_CALLBACK (theme_adium_navigation_policy_decision_requested_cb
),
1159 priv
->gsettings_chat
= g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA
);
1160 g_signal_connect (priv
->gsettings_chat
,
1161 "changed::" EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
,
1162 G_CALLBACK (theme_adium_notify_enable_webkit_developer_tools_cb
),
1165 theme_adium_update_enable_webkit_developer_tools (theme
);
1169 empathy_theme_adium_new (EmpathyAdiumData
*data
)
1171 g_return_val_if_fail (data
!= NULL
, NULL
);
1173 return g_object_new (EMPATHY_TYPE_THEME_ADIUM
,
1179 empathy_adium_path_is_valid (const gchar
*path
)
1184 /* The theme is not valid if there is no Info.plist */
1185 file
= g_build_filename (path
, "Contents", "Info.plist",
1187 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1193 /* We ship a default Template.html as fallback if there is any problem
1194 * with the one inside the theme. The only other required file is
1195 * Content.html for incoming messages (outgoing fallback to use
1197 file
= g_build_filename (path
, "Contents", "Resources", "Incoming",
1198 "Content.html", NULL
);
1199 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1206 empathy_adium_info_new (const gchar
*path
)
1210 GHashTable
*info
= NULL
;
1212 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1214 file
= g_build_filename (path
, "Contents", "Info.plist", NULL
);
1215 value
= empathy_plist_parse_from_file (file
);
1221 info
= g_value_dup_boxed (value
);
1222 tp_g_value_slice_free (value
);
1224 /* Insert the theme's path into the hash table,
1225 * keys have to be dupped */
1226 tp_asv_set_string (info
, g_strdup ("path"), path
);
1232 empathy_adium_data_get_type (void)
1234 static GType type_id
= 0;
1238 type_id
= g_boxed_type_register_static ("EmpathyAdiumData",
1239 (GBoxedCopyFunc
) empathy_adium_data_ref
,
1240 (GBoxedFreeFunc
) empathy_adium_data_unref
);
1247 empathy_adium_data_new_with_info (const gchar
*path
, GHashTable
*info
)
1249 EmpathyAdiumData
*data
;
1251 gchar
*template_html
= NULL
;
1253 gchar
*footer_html
= NULL
;
1256 gchar
**strv
= NULL
;
1261 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1263 data
= g_slice_new0 (EmpathyAdiumData
);
1264 data
->ref_count
= 1;
1265 data
->path
= g_strdup (path
);
1266 data
->basedir
= g_strconcat (path
, G_DIR_SEPARATOR_S
"Contents"
1267 G_DIR_SEPARATOR_S
"Resources" G_DIR_SEPARATOR_S
, NULL
);
1268 data
->info
= g_hash_table_ref (info
);
1270 /* Load html files */
1271 file
= g_build_filename (data
->basedir
, "Incoming", "Content.html", NULL
);
1272 g_file_get_contents (file
, &data
->in_content_html
, &data
->in_content_len
, NULL
);
1275 file
= g_build_filename (data
->basedir
, "Incoming", "NextContent.html", NULL
);
1276 g_file_get_contents (file
, &data
->in_nextcontent_html
, &data
->in_nextcontent_len
, NULL
);
1279 file
= g_build_filename (data
->basedir
, "Incoming", "Context.html", NULL
);
1280 g_file_get_contents (file
, &data
->in_context_html
, &data
->in_context_len
, NULL
);
1283 file
= g_build_filename (data
->basedir
, "Incoming", "NextContext.html", NULL
);
1284 g_file_get_contents (file
, &data
->in_nextcontext_html
, &data
->in_nextcontext_len
, NULL
);
1287 file
= g_build_filename (data
->basedir
, "Outgoing", "Content.html", NULL
);
1288 g_file_get_contents (file
, &data
->out_content_html
, &data
->out_content_len
, NULL
);
1291 file
= g_build_filename (data
->basedir
, "Outgoing", "NextContent.html", NULL
);
1292 g_file_get_contents (file
, &data
->out_nextcontent_html
, &data
->out_nextcontent_len
, NULL
);
1295 file
= g_build_filename (data
->basedir
, "Outgoing", "Context.html", NULL
);
1296 g_file_get_contents (file
, &data
->out_context_html
, &data
->out_context_len
, NULL
);
1299 file
= g_build_filename (data
->basedir
, "Outgoing", "NextContext.html", NULL
);
1300 g_file_get_contents (file
, &data
->out_nextcontext_html
, &data
->out_nextcontext_len
, NULL
);
1303 file
= g_build_filename (data
->basedir
, "Status.html", NULL
);
1304 g_file_get_contents (file
, &data
->status_html
, &data
->status_len
, NULL
);
1307 file
= g_build_filename (data
->basedir
, "Footer.html", NULL
);
1308 g_file_get_contents (file
, &footer_html
, &footer_len
, NULL
);
1311 file
= g_build_filename (data
->basedir
, "Incoming", "buddy_icon.png", NULL
);
1312 if (g_file_test (file
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
)) {
1313 data
->default_incoming_avatar_filename
= file
;
1318 file
= g_build_filename (data
->basedir
, "Outgoing", "buddy_icon.png", NULL
);
1319 if (g_file_test (file
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
)) {
1320 data
->default_outgoing_avatar_filename
= file
;
1325 css_path
= g_build_filename (data
->basedir
, "main.css", NULL
);
1327 /* There is 2 formats for Template.html: The old one has 4 parameters,
1328 * the new one has 5 parameters. */
1329 file
= g_build_filename (data
->basedir
, "Template.html", NULL
);
1330 if (g_file_get_contents (file
, &template_html
, &template_len
, NULL
)) {
1331 strv
= g_strsplit (template_html
, "%@", -1);
1332 len
= g_strv_length (strv
);
1336 if (len
!= 5 && len
!= 6) {
1337 /* Either the theme has no template or it don't have the good
1338 * number of parameters. Fallback to use our own template. */
1339 g_free (template_html
);
1342 file
= empathy_file_lookup ("Template.html", "data");
1343 g_file_get_contents (file
, &template_html
, &template_len
, NULL
);
1345 strv
= g_strsplit (template_html
, "%@", -1);
1346 len
= g_strv_length (strv
);
1349 /* Replace %@ with the needed information in the template html. */
1350 string
= g_string_sized_new (template_len
);
1351 g_string_append (string
, strv
[i
++]);
1352 g_string_append (string
, data
->basedir
);
1353 g_string_append (string
, strv
[i
++]);
1355 const gchar
*variant
;
1357 /* We include main.css by default */
1358 g_string_append_printf (string
, "@import url(\"%s\");", css_path
);
1359 g_string_append (string
, strv
[i
++]);
1360 variant
= tp_asv_get_string (data
->info
, "DefaultVariant");
1362 g_string_append (string
, "Variants/");
1363 g_string_append (string
, variant
);
1364 g_string_append (string
, ".css");
1367 /* FIXME: We should set main.css OR the variant css */
1368 g_string_append (string
, css_path
);
1370 g_string_append (string
, strv
[i
++]);
1371 g_string_append (string
, ""); /* We don't want header */
1372 g_string_append (string
, strv
[i
++]);
1373 /* FIXME: We should replace adium %macros% in footer */
1375 g_string_append (string
, footer_html
);
1377 g_string_append (string
, strv
[i
++]);
1378 data
->template_html
= g_string_free (string
, FALSE
);
1380 g_free (footer_html
);
1381 g_free (template_html
);
1389 empathy_adium_data_new (const gchar
*path
)
1391 EmpathyAdiumData
*data
;
1394 info
= empathy_adium_info_new (path
);
1395 data
= empathy_adium_data_new_with_info (path
, info
);
1396 g_hash_table_unref (info
);
1402 empathy_adium_data_ref (EmpathyAdiumData
*data
)
1404 g_return_val_if_fail (data
!= NULL
, NULL
);
1406 g_atomic_int_inc (&data
->ref_count
);
1412 empathy_adium_data_unref (EmpathyAdiumData
*data
)
1414 g_return_if_fail (data
!= NULL
);
1416 if (g_atomic_int_dec_and_test (&data
->ref_count
)) {
1417 g_free (data
->path
);
1418 g_free (data
->basedir
);
1419 g_free (data
->template_html
);
1420 g_free (data
->in_content_html
);
1421 g_free (data
->in_nextcontent_html
);
1422 g_free (data
->in_context_html
);
1423 g_free (data
->in_nextcontext_html
);
1424 g_free (data
->out_content_html
);
1425 g_free (data
->out_nextcontent_html
);
1426 g_free (data
->out_context_html
);
1427 g_free (data
->out_nextcontext_html
);
1428 g_free (data
->default_avatar_filename
);
1429 g_free (data
->default_incoming_avatar_filename
);
1430 g_free (data
->default_outgoing_avatar_filename
);
1431 g_free (data
->status_html
);
1432 g_hash_table_unref (data
->info
);
1433 g_slice_free (EmpathyAdiumData
, data
);
1438 empathy_adium_data_get_info (EmpathyAdiumData
*data
)
1440 g_return_val_if_fail (data
!= NULL
, NULL
);
1446 empathy_adium_data_get_path (EmpathyAdiumData
*data
)
1448 g_return_val_if_fail (data
!= NULL
, NULL
);