1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2002-2007 Imendio AB
4 * Copyright (C) 2008 Collabora Ltd.
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation; either version 2 of the
9 * License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
16 * You should have received a copy of the GNU General Public
17 * License along with this program; if not, write to the
18 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19 * Boston, MA 02110-1301 USA
21 * Authors: Mikael Hallendal <micke@imendio.com>
22 * Richard Hult <richard@imendio.com>
23 * Martyn Russell <martyn@imendio.com>
24 * Xavier Claessens <xclaesse@gmail.com>
29 #include <sys/types.h>
33 #include <glib/gi18n-lib.h>
36 #include <telepathy-glib/util.h>
38 #include <libempathy/empathy-gsettings.h>
39 #include <libempathy/empathy-utils.h>
41 #include "empathy-chat-text-view.h"
42 #include "empathy-chat.h"
43 #include "empathy-ui-utils.h"
44 #include "empathy-smiley-manager.h"
45 #include "empathy-string-parser.h"
47 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
48 #include <libempathy/empathy-debug.h>
50 /* Number of seconds between timestamps when using normal mode, 5 minutes. */
51 #define TIMESTAMP_INTERVAL (5 * G_TIME_SPAN_MINUTE)
54 #define MAX_SCROLL_TIME 0.4 /* seconds */
55 #define SCROLL_DELAY 33 /* milliseconds */
57 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyChatTextView)
60 GtkTextBuffer
*buffer
;
63 GtkTextMark
*find_mark_previous
;
64 GtkTextMark
*find_mark_next
;
65 gboolean find_wrapped
;
66 gboolean find_last_direction
;
67 EmpathyContact
*last_contact
;
68 gint64 last_timestamp
;
69 gboolean allow_scrolling
;
70 guint notify_system_fonts_id
;
71 GSettings
*gsettings_desktop
;
72 GSettings
*gsettings_chat
;
73 EmpathySmileyManager
*smiley_manager
;
74 gboolean only_if_date
;
75 } EmpathyChatTextViewPriv
;
77 static void chat_text_view_iface_init (EmpathyChatViewIface
*iface
);
79 static void chat_text_view_copy_clipboard (EmpathyChatView
*view
);
81 G_DEFINE_TYPE_WITH_CODE (EmpathyChatTextView
, empathy_chat_text_view
,
83 G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW
,
84 chat_text_view_iface_init
));
93 chat_text_view_url_event_cb (GtkTextTag
*tag
,
97 EmpathyChatTextView
*view
)
99 EmpathyChatTextViewPriv
*priv
;
100 GtkTextIter start
, end
;
103 priv
= GET_PRIV (view
);
105 /* If the link is being selected, don't do anything. */
106 gtk_text_buffer_get_selection_bounds (priv
->buffer
, &start
, &end
);
107 if (gtk_text_iter_get_offset (&start
) != gtk_text_iter_get_offset (&end
)) {
111 if (event
->type
== GDK_BUTTON_RELEASE
&& event
->button
.button
== 1) {
114 if (gtk_text_iter_backward_to_tag_toggle (&start
, tag
) &&
115 gtk_text_iter_forward_to_tag_toggle (&end
, tag
)) {
116 str
= gtk_text_buffer_get_text (priv
->buffer
,
121 empathy_url_show (GTK_WIDGET (view
), str
);
130 chat_text_view_event_cb (EmpathyChatTextView
*view
,
131 GdkEventMotion
*event
,
134 static GdkCursor
*hand
= NULL
;
135 static GdkCursor
*beam
= NULL
;
136 GtkTextWindowType type
;
139 gint x
, y
, buf_x
, buf_y
;
141 type
= gtk_text_view_get_window_type (GTK_TEXT_VIEW (view
),
144 if (type
!= GTK_TEXT_WINDOW_TEXT
) {
148 /* Get where the pointer really is. */
149 win
= gtk_text_view_get_window (GTK_TEXT_VIEW (view
), type
);
154 gdk_window_get_pointer (win
, &x
, &y
, NULL
);
156 /* Get the iter where the cursor is at */
157 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view
), type
,
161 gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view
),
165 if (gtk_text_iter_has_tag (&iter
, tag
)) {
167 hand
= gdk_cursor_new (GDK_HAND2
);
168 beam
= gdk_cursor_new (GDK_XTERM
);
170 gdk_window_set_cursor (win
, hand
);
173 beam
= gdk_cursor_new (GDK_XTERM
);
175 gdk_window_set_cursor (win
, beam
);
182 chat_text_view_create_tags (EmpathyChatTextView
*view
)
184 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
187 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_CUT
, NULL
);
188 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_HIGHLIGHT
, NULL
);
189 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_SPACING
, NULL
);
190 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_TIME
, NULL
);
191 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_ACTION
, NULL
);
192 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_BODY
, NULL
);
193 gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_EVENT
, NULL
);
195 tag
= gtk_text_buffer_create_tag (priv
->buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_LINK
, NULL
);
196 g_signal_connect (tag
, "event",
197 G_CALLBACK (chat_text_view_url_event_cb
),
200 g_signal_connect (view
, "motion-notify-event",
201 G_CALLBACK (chat_text_view_event_cb
),
206 chat_text_view_system_font_update (EmpathyChatTextView
*view
)
208 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
209 PangoFontDescription
*font_description
= NULL
;
212 font_name
= g_settings_get_string (priv
->gsettings_desktop
,
213 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME
);
215 if (font_name
!= NULL
) {
216 font_description
= pango_font_description_from_string (font_name
);
219 font_description
= NULL
;
222 gtk_widget_override_font (GTK_WIDGET (view
), font_description
);
224 if (font_description
) {
225 pango_font_description_free (font_description
);
230 chat_text_view_notify_system_font_cb (GSettings
*gsettings
,
232 EmpathyChatTextView
*self
)
234 chat_text_view_system_font_update (self
);
238 chat_text_view_open_address_cb (GtkMenuItem
*menuitem
, const gchar
*url
)
240 empathy_url_show (GTK_WIDGET (menuitem
), url
);
244 chat_text_view_copy_address_cb (GtkMenuItem
*menuitem
, const gchar
*url
)
246 GtkClipboard
*clipboard
;
248 clipboard
= gtk_clipboard_get (GDK_SELECTION_CLIPBOARD
);
249 gtk_clipboard_set_text (clipboard
, url
, -1);
251 clipboard
= gtk_clipboard_get (GDK_SELECTION_PRIMARY
);
252 gtk_clipboard_set_text (clipboard
, url
, -1);
256 chat_text_view_populate_popup (EmpathyChatTextView
*view
,
260 EmpathyChatTextViewPriv
*priv
;
261 GtkTextTagTable
*table
;
264 GtkTextIter iter
, start
, end
;
268 priv
= GET_PRIV (view
);
270 /* Clear menu item */
271 if (gtk_text_buffer_get_char_count (priv
->buffer
) > 0) {
272 item
= gtk_separator_menu_item_new ();
273 gtk_menu_shell_append (GTK_MENU_SHELL (menu
), item
);
274 gtk_widget_show (item
);
276 item
= gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR
, NULL
);
277 gtk_menu_shell_append (GTK_MENU_SHELL (menu
), item
);
278 gtk_widget_show (item
);
280 g_signal_connect_swapped (item
, "activate",
281 G_CALLBACK (empathy_chat_view_clear
),
285 /* Link context menu items */
286 table
= gtk_text_buffer_get_tag_table (priv
->buffer
);
287 tag
= gtk_text_tag_table_lookup (table
, EMPATHY_CHAT_TEXT_VIEW_TAG_LINK
);
289 gtk_widget_get_pointer (GTK_WIDGET (view
), &x
, &y
);
291 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view
),
292 GTK_TEXT_WINDOW_WIDGET
,
296 gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view
), &iter
, x
, y
);
300 if (gtk_text_iter_backward_to_tag_toggle (&start
, tag
) &&
301 gtk_text_iter_forward_to_tag_toggle (&end
, tag
)) {
302 str
= gtk_text_buffer_get_text (priv
->buffer
,
303 &start
, &end
, FALSE
);
306 if (EMP_STR_EMPTY (str
)) {
311 /* NOTE: Set data just to get the string freed when not needed. */
312 g_object_set_data_full (G_OBJECT (menu
),
314 (GDestroyNotify
) g_free
);
316 item
= gtk_separator_menu_item_new ();
317 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
318 gtk_widget_show (item
);
320 item
= gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
321 g_signal_connect (item
, "activate",
322 G_CALLBACK (chat_text_view_copy_address_cb
),
324 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
325 gtk_widget_show (item
);
327 item
= gtk_menu_item_new_with_mnemonic (_("_Open Link"));
328 g_signal_connect (item
, "activate",
329 G_CALLBACK (chat_text_view_open_address_cb
),
331 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu
), item
);
332 gtk_widget_show (item
);
336 chat_text_view_is_scrolled_down (EmpathyChatTextView
*view
)
343 vadj
= gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (view
));
344 value
= gtk_adjustment_get_value (vadj
);
345 upper
= gtk_adjustment_get_upper (vadj
);
346 page_size
= gtk_adjustment_get_page_size (vadj
);
348 if (value
< upper
- page_size
) {
356 chat_text_view_maybe_trim_buffer (EmpathyChatTextView
*view
)
358 EmpathyChatTextViewPriv
*priv
;
359 GtkTextIter top
, bottom
;
362 GtkTextTagTable
*table
;
365 priv
= GET_PRIV (view
);
367 gtk_text_buffer_get_end_iter (priv
->buffer
, &bottom
);
368 line
= gtk_text_iter_get_line (&bottom
);
369 if (line
< MAX_LINES
) {
373 remove_
= line
- MAX_LINES
;
374 gtk_text_buffer_get_start_iter (priv
->buffer
, &top
);
377 if (!gtk_text_iter_forward_lines (&bottom
, remove_
)) {
381 /* Track backwords to a place where we can safely cut, we don't do it in
382 * the middle of a tag.
384 table
= gtk_text_buffer_get_tag_table (priv
->buffer
);
385 tag
= gtk_text_tag_table_lookup (table
, EMPATHY_CHAT_TEXT_VIEW_TAG_CUT
);
390 if (!gtk_text_iter_forward_to_tag_toggle (&bottom
, tag
)) {
394 if (!gtk_text_iter_equal (&top
, &bottom
)) {
395 gtk_text_buffer_delete (priv
->buffer
, &top
, &bottom
);
400 chat_text_view_append_timestamp (EmpathyChatTextView
*view
,
404 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
409 str
= g_string_new ("- ");
411 /* Append date if needed */
413 /* Translators: timestamp displayed between conversations in
414 * chat windows (strftime format string) */
415 tmp
= empathy_time_to_string_utc (timestamp
, _("%A %B %d %Y"));
416 g_string_append (str
, tmp
);
417 g_string_append (str
, ", ");
422 tmp
= empathy_time_to_string_local (timestamp
, EMPATHY_TIME_FORMAT_DISPLAY_SHORT
);
423 g_string_append (str
, tmp
);
426 g_string_append (str
, " -\n");
428 /* Insert the string in the buffer */
429 empathy_chat_text_view_append_spacing (view
);
430 gtk_text_buffer_get_end_iter (priv
->buffer
, &iter
);
431 gtk_text_buffer_insert_with_tags_by_name (priv
->buffer
,
434 EMPATHY_CHAT_TEXT_VIEW_TAG_TIME
,
437 g_string_free (str
, TRUE
);
441 chat_text_maybe_append_date_and_time (EmpathyChatTextView
*view
,
444 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
445 GDateTime
*date
, *last_date
;
446 gboolean append_date
= FALSE
;
447 gboolean append_time
= FALSE
;
450 /* Get the date from last message */
451 last_date
= g_date_time_new_from_unix_utc (priv
->last_timestamp
);
453 /* Get the date of the message we are appending */
454 date
= g_date_time_new_from_unix_utc (timestamp
);
456 delta
= g_date_time_difference (last_date
, date
);
457 /* If last message was from another day we append date and time */
458 if (delta
>= G_TIME_SPAN_DAY
) {
463 g_date_time_unref (last_date
);
464 g_date_time_unref (date
);
466 /* If last message is 'old' append the time */
467 if (delta
>= TIMESTAMP_INTERVAL
) {
471 if (append_date
|| (!priv
->only_if_date
&& append_time
)) {
472 chat_text_view_append_timestamp (view
, timestamp
, append_date
);
477 chat_text_view_size_allocate (GtkWidget
*widget
,
478 GtkAllocation
*alloc
)
482 down
= chat_text_view_is_scrolled_down (EMPATHY_CHAT_TEXT_VIEW (widget
));
484 GTK_WIDGET_CLASS (empathy_chat_text_view_parent_class
)->size_allocate (widget
, alloc
);
489 adj
= gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (widget
));
490 gtk_adjustment_set_value (adj
,
491 gtk_adjustment_get_upper (adj
) -
492 gtk_adjustment_get_page_size (adj
));
497 chat_text_view_drag_motion (GtkWidget
*widget
,
498 GdkDragContext
*context
,
503 /* Don't handle drag motion, since we don't want the view to scroll as
504 * the result of dragging something across it. */
510 chat_text_view_get_property (GObject
*object
,
515 EmpathyChatTextViewPriv
*priv
= GET_PRIV (object
);
518 case PROP_LAST_CONTACT
:
519 g_value_set_object (value
, priv
->last_contact
);
521 case PROP_ONLY_IF_DATE
:
522 g_value_set_boolean (value
, priv
->only_if_date
);
525 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
531 chat_text_view_set_property (GObject
*object
,
536 EmpathyChatTextViewPriv
*priv
= GET_PRIV (object
);
539 case PROP_ONLY_IF_DATE
:
540 priv
->only_if_date
= g_value_get_boolean (value
);
543 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
549 chat_text_view_finalize (GObject
*object
)
551 EmpathyChatTextView
*view
;
552 EmpathyChatTextViewPriv
*priv
;
554 view
= EMPATHY_CHAT_TEXT_VIEW (object
);
555 priv
= GET_PRIV (view
);
557 DEBUG ("%p", object
);
559 g_object_unref (priv
->gsettings_chat
);
560 g_object_unref (priv
->gsettings_desktop
);
562 if (priv
->last_contact
) {
563 g_object_unref (priv
->last_contact
);
565 if (priv
->scroll_time
) {
566 g_timer_destroy (priv
->scroll_time
);
568 if (priv
->scroll_timeout
) {
569 g_source_remove (priv
->scroll_timeout
);
571 g_object_unref (priv
->smiley_manager
);
573 G_OBJECT_CLASS (empathy_chat_text_view_parent_class
)->finalize (object
);
577 text_view_copy_clipboard (GtkTextView
*text_view
)
579 chat_text_view_copy_clipboard (EMPATHY_CHAT_VIEW (text_view
));
583 empathy_chat_text_view_class_init (EmpathyChatTextViewClass
*klass
)
585 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
586 GtkWidgetClass
*widget_class
= GTK_WIDGET_CLASS (klass
);
587 GtkTextViewClass
*text_view_class
= GTK_TEXT_VIEW_CLASS (klass
);
589 object_class
->finalize
= chat_text_view_finalize
;
590 object_class
->get_property
= chat_text_view_get_property
;
591 object_class
->set_property
= chat_text_view_set_property
;
593 widget_class
->size_allocate
= chat_text_view_size_allocate
;
594 widget_class
->drag_motion
= chat_text_view_drag_motion
;
596 text_view_class
->copy_clipboard
= text_view_copy_clipboard
;
598 g_object_class_install_property (object_class
,
600 g_param_spec_object ("last-contact",
602 "The sender of the last received message",
603 EMPATHY_TYPE_CONTACT
,
605 g_object_class_install_property (object_class
,
607 g_param_spec_boolean ("only-if-date",
609 "Display timestamp only if the date changes",
614 g_type_class_add_private (object_class
, sizeof (EmpathyChatTextViewPriv
));
618 empathy_chat_text_view_init (EmpathyChatTextView
*view
)
620 EmpathyChatTextViewPriv
*priv
= G_TYPE_INSTANCE_GET_PRIVATE (view
,
621 EMPATHY_TYPE_CHAT_TEXT_VIEW
, EmpathyChatTextViewPriv
);
624 priv
->buffer
= gtk_text_view_get_buffer (GTK_TEXT_VIEW (view
));
625 priv
->last_timestamp
= 0;
626 priv
->allow_scrolling
= TRUE
;
627 priv
->smiley_manager
= empathy_smiley_manager_dup_singleton ();
630 "wrap-mode", GTK_WRAP_WORD_CHAR
,
632 "cursor-visible", FALSE
,
635 priv
->gsettings_chat
= g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA
);
637 priv
->gsettings_desktop
= g_settings_new (
638 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA
);
639 g_signal_connect (priv
->gsettings_desktop
,
640 "changed::" EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME
,
641 G_CALLBACK (chat_text_view_notify_system_font_cb
),
643 chat_text_view_system_font_update (view
);
644 chat_text_view_create_tags (view
);
646 g_signal_connect (view
,
648 G_CALLBACK (chat_text_view_populate_popup
),
653 chat_text_view_scroll_stop (EmpathyChatTextView
*view
)
655 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
657 g_timer_destroy (priv
->scroll_time
);
658 priv
->scroll_time
= NULL
;
660 g_source_remove (priv
->scroll_timeout
);
661 priv
->scroll_timeout
= 0;
664 /* Code stolen from pidgin/gtkimhtml.c */
666 chat_text_view_scroll_cb (EmpathyChatTextView
*view
)
668 EmpathyChatTextViewPriv
*priv
;
672 priv
= GET_PRIV (view
);
674 adj
= gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (view
));
675 max_val
= gtk_adjustment_get_upper (adj
) - gtk_adjustment_get_page_size (adj
);
677 g_return_val_if_fail (priv
->scroll_time
!= NULL
, FALSE
);
679 if (g_timer_elapsed (priv
->scroll_time
, NULL
) > MAX_SCROLL_TIME
) {
680 /* time's up. jump to the end and kill the timer */
681 gtk_adjustment_set_value (adj
, max_val
);
682 chat_text_view_scroll_stop (view
);
686 /* scroll by 1/3rd the remaining distance */
687 gtk_adjustment_set_value (adj
, gtk_adjustment_get_value (adj
) + ((max_val
- gtk_adjustment_get_value (adj
)) / 3));
692 chat_text_view_scroll_down (EmpathyChatView
*view
)
694 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
696 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
698 if (!priv
->allow_scrolling
) {
702 DEBUG ("Scrolling down");
704 if (priv
->scroll_time
) {
705 g_timer_reset (priv
->scroll_time
);
707 priv
->scroll_time
= g_timer_new ();
709 if (!priv
->scroll_timeout
) {
710 priv
->scroll_timeout
= g_timeout_add (SCROLL_DELAY
,
711 (GSourceFunc
) chat_text_view_scroll_cb
,
717 chat_text_view_append_message (EmpathyChatView
*view
,
720 EmpathyChatTextView
*text_view
= EMPATHY_CHAT_TEXT_VIEW (view
);
721 EmpathyChatTextViewPriv
*priv
= GET_PRIV (text_view
);
725 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
726 g_return_if_fail (EMPATHY_IS_MESSAGE (msg
));
728 if (!empathy_message_get_body (msg
)) {
732 bottom
= chat_text_view_is_scrolled_down (text_view
);
734 chat_text_view_maybe_trim_buffer (EMPATHY_CHAT_TEXT_VIEW (view
));
736 timestamp
= empathy_message_get_timestamp (msg
);
737 chat_text_maybe_append_date_and_time (text_view
, timestamp
);
738 if (EMPATHY_CHAT_TEXT_VIEW_GET_CLASS (view
)->append_message
) {
739 EMPATHY_CHAT_TEXT_VIEW_GET_CLASS (view
)->append_message (text_view
,
744 chat_text_view_scroll_down (view
);
747 if (priv
->last_contact
) {
748 g_object_unref (priv
->last_contact
);
750 priv
->last_contact
= g_object_ref (empathy_message_get_sender (msg
));
751 g_object_notify (G_OBJECT (view
), "last-contact");
753 priv
->last_timestamp
= timestamp
;
757 chat_text_view_append_event (EmpathyChatView
*view
,
760 EmpathyChatTextView
*text_view
= EMPATHY_CHAT_TEXT_VIEW (view
);
761 EmpathyChatTextViewPriv
*priv
= GET_PRIV (text_view
);
767 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
768 g_return_if_fail (!EMP_STR_EMPTY (str
));
770 bottom
= chat_text_view_is_scrolled_down (text_view
);
771 chat_text_view_maybe_trim_buffer (EMPATHY_CHAT_TEXT_VIEW (view
));
772 chat_text_maybe_append_date_and_time (text_view
,
773 empathy_time_get_current ());
775 gtk_text_buffer_get_end_iter (priv
->buffer
, &iter
);
776 msg
= g_strdup_printf (" - %s\n", str
);
777 gtk_text_buffer_insert_with_tags_by_name (priv
->buffer
, &iter
,
779 EMPATHY_CHAT_TEXT_VIEW_TAG_EVENT
,
784 chat_text_view_scroll_down (view
);
787 if (priv
->last_contact
) {
788 g_object_unref (priv
->last_contact
);
789 priv
->last_contact
= NULL
;
790 g_object_notify (G_OBJECT (view
), "last-contact");
795 chat_text_view_scroll (EmpathyChatView
*view
,
796 gboolean allow_scrolling
)
798 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
800 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
802 DEBUG ("Scrolling %s", allow_scrolling
? "enabled" : "disabled");
804 priv
->allow_scrolling
= allow_scrolling
;
805 if (allow_scrolling
) {
806 empathy_chat_view_scroll_down (view
);
811 chat_text_view_get_has_selection (EmpathyChatView
*view
)
813 GtkTextBuffer
*buffer
;
815 g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
), FALSE
);
817 buffer
= gtk_text_view_get_buffer (GTK_TEXT_VIEW (view
));
819 return gtk_text_buffer_get_has_selection (buffer
);
823 chat_text_view_clear (EmpathyChatView
*view
)
825 GtkTextBuffer
*buffer
;
826 EmpathyChatTextViewPriv
*priv
;
828 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
830 buffer
= gtk_text_view_get_buffer (GTK_TEXT_VIEW (view
));
831 gtk_text_buffer_set_text (buffer
, "", -1);
833 /* We set these back to the initial values so we get
834 * timestamps when clearing the window to know when
835 * conversations start.
837 priv
= GET_PRIV (view
);
839 priv
->last_timestamp
= 0;
840 if (priv
->last_contact
) {
841 g_object_unref (priv
->last_contact
);
842 priv
->last_contact
= NULL
;
847 chat_text_view_find_previous (EmpathyChatView
*view
,
848 const gchar
*search_criteria
,
852 EmpathyChatTextViewPriv
*priv
;
853 GtkTextBuffer
*buffer
;
854 GtkTextIter iter_at_mark
;
855 GtkTextIter iter_match_start
;
856 GtkTextIter iter_match_end
;
858 gboolean from_start
= FALSE
;
860 g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
), FALSE
);
861 g_return_val_if_fail (search_criteria
!= NULL
, FALSE
);
863 priv
= GET_PRIV (view
);
865 buffer
= priv
->buffer
;
867 if (EMP_STR_EMPTY (search_criteria
)) {
868 if (priv
->find_mark_previous
) {
869 gtk_text_buffer_get_start_iter (buffer
, &iter_at_mark
);
871 gtk_text_buffer_move_mark (buffer
,
872 priv
->find_mark_previous
,
874 gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view
),
875 priv
->find_mark_previous
,
880 gtk_text_buffer_select_range (buffer
,
892 if (!new_search
&& priv
->find_mark_previous
) {
893 gtk_text_buffer_get_iter_at_mark (buffer
,
895 priv
->find_mark_previous
);
897 gtk_text_buffer_get_end_iter (buffer
, &iter_at_mark
);
901 priv
->find_last_direction
= FALSE
;
903 /* Use the standard GTK+ method for case sensitive searches. It can't do
904 * case insensitive searches (see bug #61852), so keep the custom method
905 * around for case insensitive searches. */
907 found
= gtk_text_iter_backward_search (&iter_at_mark
,
909 0, /* no text search flags, we want exact matches */
914 found
= empathy_text_iter_backward_search (&iter_at_mark
,
922 gboolean result
= FALSE
;
928 /* Here we wrap around. */
929 if (!new_search
&& !priv
->find_wrapped
) {
930 priv
->find_wrapped
= TRUE
;
931 result
= chat_text_view_find_previous (view
,
935 priv
->find_wrapped
= FALSE
;
941 /* Set new mark and show on screen */
942 if (!priv
->find_mark_previous
) {
943 priv
->find_mark_previous
= gtk_text_buffer_create_mark (buffer
, NULL
,
947 gtk_text_buffer_move_mark (buffer
,
948 priv
->find_mark_previous
,
952 if (!priv
->find_mark_next
) {
953 priv
->find_mark_next
= gtk_text_buffer_create_mark (buffer
, NULL
,
957 gtk_text_buffer_move_mark (buffer
,
958 priv
->find_mark_next
,
962 gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view
),
963 priv
->find_mark_previous
,
969 gtk_text_buffer_move_mark_by_name (buffer
, "selection_bound", &iter_match_start
);
970 gtk_text_buffer_move_mark_by_name (buffer
, "insert", &iter_match_end
);
976 chat_text_view_find_next (EmpathyChatView
*view
,
977 const gchar
*search_criteria
,
981 EmpathyChatTextViewPriv
*priv
;
982 GtkTextBuffer
*buffer
;
983 GtkTextIter iter_at_mark
;
984 GtkTextIter iter_match_start
;
985 GtkTextIter iter_match_end
;
987 gboolean from_start
= FALSE
;
989 g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
), FALSE
);
990 g_return_val_if_fail (search_criteria
!= NULL
, FALSE
);
992 priv
= GET_PRIV (view
);
994 buffer
= priv
->buffer
;
996 if (EMP_STR_EMPTY (search_criteria
)) {
997 if (priv
->find_mark_next
) {
998 gtk_text_buffer_get_start_iter (buffer
, &iter_at_mark
);
1000 gtk_text_buffer_move_mark (buffer
,
1001 priv
->find_mark_next
,
1003 gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view
),
1004 priv
->find_mark_next
,
1009 gtk_text_buffer_select_range (buffer
,
1021 if (!new_search
&& priv
->find_mark_next
) {
1022 gtk_text_buffer_get_iter_at_mark (buffer
,
1024 priv
->find_mark_next
);
1026 gtk_text_buffer_get_start_iter (buffer
, &iter_at_mark
);
1030 priv
->find_last_direction
= TRUE
;
1032 /* Use the standard GTK+ method for case sensitive searches. It can't do
1033 * case insensitive searches (see bug #61852), so keep the custom method
1034 * around for case insensitive searches. */
1036 found
= gtk_text_iter_forward_search (&iter_at_mark
,
1043 found
= empathy_text_iter_forward_search (&iter_at_mark
,
1051 gboolean result
= FALSE
;
1057 /* Here we wrap around. */
1058 if (!new_search
&& !priv
->find_wrapped
) {
1059 priv
->find_wrapped
= TRUE
;
1060 result
= chat_text_view_find_next (view
,
1064 priv
->find_wrapped
= FALSE
;
1070 /* Set new mark and show on screen */
1071 if (!priv
->find_mark_next
) {
1072 priv
->find_mark_next
= gtk_text_buffer_create_mark (buffer
, NULL
,
1076 gtk_text_buffer_move_mark (buffer
,
1077 priv
->find_mark_next
,
1081 if (!priv
->find_mark_previous
) {
1082 priv
->find_mark_previous
= gtk_text_buffer_create_mark (buffer
, NULL
,
1086 gtk_text_buffer_move_mark (buffer
,
1087 priv
->find_mark_previous
,
1091 gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view
),
1092 priv
->find_mark_next
,
1098 gtk_text_buffer_move_mark_by_name (buffer
, "selection_bound", &iter_match_start
);
1099 gtk_text_buffer_move_mark_by_name (buffer
, "insert", &iter_match_end
);
1105 chat_text_view_find_abilities (EmpathyChatView
*view
,
1106 const gchar
*search_criteria
,
1107 gboolean match_case
,
1108 gboolean
*can_do_previous
,
1109 gboolean
*can_do_next
)
1111 EmpathyChatTextViewPriv
*priv
;
1112 GtkTextBuffer
*buffer
;
1113 GtkTextIter iter_at_mark
;
1114 GtkTextIter iter_match_start
;
1115 GtkTextIter iter_match_end
;
1117 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
1118 g_return_if_fail (search_criteria
!= NULL
);
1119 g_return_if_fail (can_do_previous
!= NULL
&& can_do_next
!= NULL
);
1121 priv
= GET_PRIV (view
);
1123 buffer
= priv
->buffer
;
1125 if (can_do_previous
) {
1126 if (priv
->find_mark_previous
) {
1127 gtk_text_buffer_get_iter_at_mark (buffer
,
1129 priv
->find_mark_previous
);
1131 gtk_text_buffer_get_start_iter (buffer
, &iter_at_mark
);
1135 *can_do_previous
= gtk_text_iter_backward_search (&iter_at_mark
,
1142 *can_do_previous
= empathy_text_iter_backward_search (&iter_at_mark
,
1151 if (priv
->find_mark_next
) {
1152 gtk_text_buffer_get_iter_at_mark (buffer
,
1154 priv
->find_mark_next
);
1156 gtk_text_buffer_get_start_iter (buffer
, &iter_at_mark
);
1160 *can_do_next
= gtk_text_iter_forward_search (&iter_at_mark
,
1167 *can_do_next
= empathy_text_iter_forward_search (&iter_at_mark
,
1177 chat_text_view_highlight (EmpathyChatView
*view
,
1179 gboolean match_case
)
1181 GtkTextBuffer
*buffer
;
1183 GtkTextIter iter_start
;
1184 GtkTextIter iter_end
;
1185 GtkTextIter iter_match_start
;
1186 GtkTextIter iter_match_end
;
1189 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
1191 buffer
= gtk_text_view_get_buffer (GTK_TEXT_VIEW (view
));
1193 gtk_text_buffer_get_start_iter (buffer
, &iter
);
1195 gtk_text_buffer_get_bounds (buffer
, &iter_start
, &iter_end
);
1196 gtk_text_buffer_remove_tag_by_name (buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_HIGHLIGHT
,
1200 if (EMP_STR_EMPTY (text
)) {
1206 found
= gtk_text_iter_forward_search (&iter
,
1213 found
= empathy_text_iter_forward_search (&iter
,
1223 gtk_text_buffer_apply_tag_by_name (buffer
, EMPATHY_CHAT_TEXT_VIEW_TAG_HIGHLIGHT
,
1227 iter
= iter_match_end
;
1232 chat_text_view_copy_clipboard (EmpathyChatView
*view
)
1234 GtkTextBuffer
*buffer
;
1235 GtkTextIter start
, iter
, end
;
1236 GtkClipboard
*clipboard
;
1239 GtkTextChildAnchor
*anchor
= NULL
;
1242 gboolean ignore_newlines
= FALSE
;
1244 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
1246 buffer
= gtk_text_view_get_buffer (GTK_TEXT_VIEW (view
));
1247 clipboard
= gtk_clipboard_get (GDK_SELECTION_CLIPBOARD
);
1249 if (!gtk_text_buffer_get_selection_bounds (buffer
, &start
, &end
))
1252 str
= g_string_new ("");
1254 for (iter
= start
; !gtk_text_iter_equal (&iter
, &end
); gtk_text_iter_forward_char (&iter
)) {
1255 c
= gtk_text_iter_get_char (&iter
);
1256 /* 0xFFFC is the 'object replacement' unicode character,
1257 * it indicates the presence of a pixbuf or a widget. */
1259 ignore_newlines
= FALSE
;
1260 if ((pixbuf
= gtk_text_iter_get_pixbuf (&iter
))) {
1262 text
= g_object_get_data (G_OBJECT(pixbuf
),
1265 str
= g_string_append (str
, text
);
1266 } else if ((anchor
= gtk_text_iter_get_child_anchor (&iter
))) {
1268 list
= gtk_text_child_anchor_get_widgets (anchor
);
1270 text
= g_object_get_data (G_OBJECT(list
->data
),
1273 str
= g_string_append (str
, text
);
1277 } else if (c
== '\n') {
1278 if (!ignore_newlines
) {
1279 ignore_newlines
= TRUE
;
1280 str
= g_string_append_unichar (str
, c
);
1283 ignore_newlines
= FALSE
;
1284 str
= g_string_append_unichar (str
, c
);
1288 gtk_clipboard_set_text (clipboard
, str
->str
, str
->len
);
1289 g_string_free (str
, TRUE
);
1293 chat_text_view_iface_init (EmpathyChatViewIface
*iface
)
1295 iface
->append_message
= chat_text_view_append_message
;
1296 iface
->append_event
= chat_text_view_append_event
;
1297 iface
->scroll
= chat_text_view_scroll
;
1298 iface
->scroll_down
= chat_text_view_scroll_down
;
1299 iface
->get_has_selection
= chat_text_view_get_has_selection
;
1300 iface
->clear
= chat_text_view_clear
;
1301 iface
->find_previous
= chat_text_view_find_previous
;
1302 iface
->find_next
= chat_text_view_find_next
;
1303 iface
->find_abilities
= chat_text_view_find_abilities
;
1304 iface
->highlight
= chat_text_view_highlight
;
1305 iface
->copy_clipboard
= chat_text_view_copy_clipboard
;
1309 empathy_chat_text_view_get_last_contact (EmpathyChatTextView
*view
)
1311 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
1313 g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
), NULL
);
1315 return priv
->last_contact
;
1319 empathy_chat_text_view_get_last_timestamp (EmpathyChatTextView
*view
)
1321 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
1323 g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
), 0);
1325 return priv
->last_timestamp
;
1329 empathy_chat_text_view_set_only_if_date (EmpathyChatTextView
*view
,
1330 gboolean only_if_date
)
1332 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
1334 g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
));
1336 if (only_if_date
!= priv
->only_if_date
) {
1337 priv
->only_if_date
= only_if_date
;
1338 g_object_notify (G_OBJECT (view
), "only-if-date");
1343 chat_text_view_replace_link (const gchar
*text
,
1345 gpointer match_data
,
1348 GtkTextBuffer
*buffer
= GTK_TEXT_BUFFER (user_data
);
1351 gtk_text_buffer_get_end_iter (buffer
, &iter
);
1352 gtk_text_buffer_insert_with_tags_by_name (buffer
, &iter
,
1354 EMPATHY_CHAT_TEXT_VIEW_TAG_LINK
,
1359 chat_text_view_replace_smiley (const gchar
*text
,
1361 gpointer match_data
,
1364 EmpathySmileyHit
*hit
= match_data
;
1365 GtkTextBuffer
*buffer
= GTK_TEXT_BUFFER (user_data
);
1368 gtk_text_buffer_get_end_iter (buffer
, &iter
);
1369 gtk_text_buffer_insert_pixbuf (buffer
, &iter
, hit
->pixbuf
);
1373 chat_text_view_replace_verbatim (const gchar
*text
,
1375 gpointer match_data
,
1378 GtkTextBuffer
*buffer
= GTK_TEXT_BUFFER (user_data
);
1381 gtk_text_buffer_get_end_iter (buffer
, &iter
);
1382 gtk_text_buffer_insert (buffer
, &iter
, text
, len
);
1385 static EmpathyStringParser string_parsers
[] = {
1386 {empathy_string_match_link
, chat_text_view_replace_link
},
1387 {empathy_string_match_all
, chat_text_view_replace_verbatim
},
1391 static EmpathyStringParser string_parsers_with_smiley
[] = {
1392 {empathy_string_match_link
, chat_text_view_replace_link
},
1393 {empathy_string_match_smiley
, chat_text_view_replace_smiley
},
1394 {empathy_string_match_all
, chat_text_view_replace_verbatim
},
1399 empathy_chat_text_view_append_body (EmpathyChatTextView
*view
,
1403 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
1404 EmpathyStringParser
*parsers
;
1405 gboolean use_smileys
;
1406 GtkTextIter start_iter
;
1410 /* Check if we have to parse smileys */
1411 use_smileys
= g_settings_get_boolean (priv
->gsettings_chat
,
1412 EMPATHY_PREFS_CHAT_SHOW_SMILEYS
);
1415 parsers
= string_parsers_with_smiley
;
1417 parsers
= string_parsers
;
1419 /* Create a mark at the place we'll start inserting */
1420 gtk_text_buffer_get_end_iter (priv
->buffer
, &start_iter
);
1421 mark
= gtk_text_buffer_create_mark (priv
->buffer
, NULL
, &start_iter
, TRUE
);
1423 /* Parse text for links/smileys and insert in the buffer */
1424 empathy_string_parser_substr (body
, -1, parsers
, priv
->buffer
);
1426 /* Insert a newline after the text inserted */
1427 gtk_text_buffer_get_end_iter (priv
->buffer
, &iter
);
1428 gtk_text_buffer_insert (priv
->buffer
, &iter
, "\n", 1);
1430 /* Apply the style to the inserted text. */
1431 gtk_text_buffer_get_iter_at_mark (priv
->buffer
, &start_iter
, mark
);
1432 gtk_text_buffer_get_end_iter (priv
->buffer
, &iter
);
1433 gtk_text_buffer_apply_tag_by_name (priv
->buffer
, tag
,
1437 gtk_text_buffer_delete_mark (priv
->buffer
, mark
);
1441 empathy_chat_text_view_append_spacing (EmpathyChatTextView
*view
)
1443 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
1446 gtk_text_buffer_get_end_iter (priv
->buffer
, &iter
);
1447 gtk_text_buffer_insert_with_tags_by_name (priv
->buffer
,
1451 EMPATHY_CHAT_TEXT_VIEW_TAG_CUT
,
1452 EMPATHY_CHAT_TEXT_VIEW_TAG_SPACING
,
1457 empathy_chat_text_view_tag_set (EmpathyChatTextView
*view
,
1458 const gchar
*tag_name
,
1459 const gchar
*first_property_name
,
1462 EmpathyChatTextViewPriv
*priv
= GET_PRIV (view
);
1464 GtkTextTagTable
*table
;
1467 g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view
), NULL
);
1468 g_return_val_if_fail (tag_name
!= NULL
, NULL
);
1470 table
= gtk_text_buffer_get_tag_table (priv
->buffer
);
1471 tag
= gtk_text_tag_table_lookup (table
, tag_name
);
1473 if (tag
&& first_property_name
) {
1474 va_start (list
, first_property_name
);
1475 g_object_set_valist (G_OBJECT (tag
), first_property_name
, list
);