]> git.0d.be Git - empathy.git/blob - libempathy-gtk/empathy-chat-text-view.c
6dd8b3094c9a1efaf2d09506822f9858e9a52e75
[empathy.git] / libempathy-gtk / empathy-chat-text-view.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 2002-2007 Imendio AB
4  * Copyright (C) 2008 Collabora Ltd.
5  *
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.
10  *
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.
15  *
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
20  *
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>
25  */
26
27 #include "config.h"
28
29 #include <sys/types.h>
30 #include <string.h>
31 #include <time.h>
32
33 #include <glib/gi18n-lib.h>
34 #include <gtk/gtk.h>
35
36 #include <telepathy-glib/util.h>
37
38 #include <libempathy/empathy-utils.h>
39
40 #include "empathy-chat-text-view.h"
41 #include "empathy-chat.h"
42 #include "empathy-conf.h"
43 #include "empathy-ui-utils.h"
44 #include "empathy-smiley-manager.h"
45
46 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
47 #include <libempathy/empathy-debug.h>
48
49 /* Number of seconds between timestamps when using normal mode, 5 minutes. */
50 #define TIMESTAMP_INTERVAL 300
51
52 #define MAX_LINES 800
53 #define MAX_SCROLL_TIME 0.4 /* seconds */
54 #define SCROLL_DELAY 33     /* milliseconds */
55
56 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyChatTextView)
57
58 typedef struct {
59         GtkTextBuffer        *buffer;
60         guint                 scroll_timeout;
61         GTimer               *scroll_time;
62         GtkTextMark          *find_mark_previous;
63         GtkTextMark          *find_mark_next;
64         gboolean              find_wrapped;
65         gboolean              find_last_direction;
66         EmpathyContact       *last_contact;
67         time_t                last_timestamp;
68         gboolean              allow_scrolling;
69         guint                 notify_system_fonts_id;
70         EmpathySmileyManager *smiley_manager;
71         gboolean              only_if_date;
72 } EmpathyChatTextViewPriv;
73
74 static void chat_text_view_iface_init (EmpathyChatViewIface *iface);
75
76 G_DEFINE_TYPE_WITH_CODE (EmpathyChatTextView, empathy_chat_text_view,
77                          GTK_TYPE_TEXT_VIEW,
78                          G_IMPLEMENT_INTERFACE (EMPATHY_TYPE_CHAT_VIEW,
79                                                 chat_text_view_iface_init));
80
81 enum {
82         PROP_0,
83         PROP_LAST_CONTACT,
84         PROP_ONLY_IF_DATE
85 };
86
87 static gboolean
88 chat_text_view_url_event_cb (GtkTextTag          *tag,
89                              GObject             *object,
90                              GdkEvent            *event,
91                              GtkTextIter         *iter,
92                              EmpathyChatTextView *view)
93 {
94         EmpathyChatTextViewPriv *priv;
95         GtkTextIter              start, end;
96         gchar                   *str;
97
98         priv = GET_PRIV (view);
99
100         /* If the link is being selected, don't do anything. */
101         gtk_text_buffer_get_selection_bounds (priv->buffer, &start, &end);
102         if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end)) {
103                 return FALSE;
104         }
105
106         if (event->type == GDK_BUTTON_RELEASE && event->button.button == 1) {
107                 start = end = *iter;
108
109                 if (gtk_text_iter_backward_to_tag_toggle (&start, tag) &&
110                     gtk_text_iter_forward_to_tag_toggle (&end, tag)) {
111                             str = gtk_text_buffer_get_text (priv->buffer,
112                                                             &start,
113                                                             &end,
114                                                             FALSE);
115
116                             empathy_url_show (GTK_WIDGET (view), str);
117                             g_free (str);
118                     }
119         }
120
121         return FALSE;
122 }
123
124 static gboolean
125 chat_text_view_event_cb (EmpathyChatTextView *view,
126                          GdkEventMotion      *event,
127                          GtkTextTag          *tag)
128 {
129         static GdkCursor  *hand = NULL;
130         static GdkCursor  *beam = NULL;
131         GtkTextWindowType  type;
132         GtkTextIter        iter;
133         GdkWindow         *win;
134         gint               x, y, buf_x, buf_y;
135
136         type = gtk_text_view_get_window_type (GTK_TEXT_VIEW (view),
137                                               event->window);
138
139         if (type != GTK_TEXT_WINDOW_TEXT) {
140                 return FALSE;
141         }
142
143         /* Get where the pointer really is. */
144         win = gtk_text_view_get_window (GTK_TEXT_VIEW (view), type);
145         if (!win) {
146                 return FALSE;
147         }
148
149         gdk_window_get_pointer (win, &x, &y, NULL);
150
151         /* Get the iter where the cursor is at */
152         gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view), type,
153                                                x, y,
154                                                &buf_x, &buf_y);
155
156         gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view),
157                                             &iter,
158                                             buf_x, buf_y);
159
160         if (gtk_text_iter_has_tag (&iter, tag)) {
161                 if (!hand) {
162                         hand = gdk_cursor_new (GDK_HAND2);
163                         beam = gdk_cursor_new (GDK_XTERM);
164                 }
165                 gdk_window_set_cursor (win, hand);
166         } else {
167                 if (!beam) {
168                         beam = gdk_cursor_new (GDK_XTERM);
169                 }
170                 gdk_window_set_cursor (win, beam);
171         }
172
173         return FALSE;
174 }
175
176 static void
177 chat_text_view_create_tags (EmpathyChatTextView *view)
178 {
179         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
180         GtkTextTag              *tag;
181
182         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_CUT, NULL);
183         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_HIGHLIGHT, NULL);
184         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_SPACING, NULL);
185         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_TIME, NULL);
186         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_ACTION, NULL);
187         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_BODY, NULL);
188         gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_EVENT, NULL);
189
190         tag = gtk_text_buffer_create_tag (priv->buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_LINK, NULL);
191         g_signal_connect (tag, "event",
192                           G_CALLBACK (chat_text_view_url_event_cb),
193                           view);
194
195         g_signal_connect (view, "motion-notify-event",
196                           G_CALLBACK (chat_text_view_event_cb),
197                           tag);
198 }
199
200 static void
201 chat_text_view_system_font_update (EmpathyChatTextView *view)
202 {
203         PangoFontDescription *font_description = NULL;
204         gchar                *font_name;
205
206         if (empathy_conf_get_string (empathy_conf_get (),
207                                      "/desktop/gnome/interface/document_font_name",
208                                      &font_name) && font_name) {
209                                              font_description = pango_font_description_from_string (font_name);
210                                              g_free (font_name);
211                                      } else {
212                                              font_description = NULL;
213                                      }
214
215         gtk_widget_modify_font (GTK_WIDGET (view), font_description);
216
217         if (font_description) {
218                 pango_font_description_free (font_description);
219         }
220 }
221
222 static void
223 chat_text_view_notify_system_font_cb (EmpathyConf *conf,
224                                       const gchar *key,
225                                       gpointer     user_data)
226 {
227         EmpathyChatTextView *view = user_data;
228
229         chat_text_view_system_font_update (view);
230 }
231
232 static void
233 chat_text_view_open_address_cb (GtkMenuItem *menuitem, const gchar *url)
234 {
235         empathy_url_show (GTK_WIDGET (menuitem), url);
236 }
237
238 static void
239 chat_text_view_copy_address_cb (GtkMenuItem *menuitem, const gchar *url)
240 {
241         GtkClipboard *clipboard;
242
243         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
244         gtk_clipboard_set_text (clipboard, url, -1);
245
246         clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
247         gtk_clipboard_set_text (clipboard, url, -1);
248 }
249
250 static void
251 chat_text_view_populate_popup (EmpathyChatTextView *view,
252                                GtkMenu        *menu,
253                                gpointer        user_data)
254 {
255         EmpathyChatTextViewPriv *priv;
256         GtkTextTagTable    *table;
257         GtkTextTag         *tag;
258         gint                x, y;
259         GtkTextIter         iter, start, end;
260         GtkWidget          *item;
261         gchar              *str = NULL;
262
263         priv = GET_PRIV (view);
264
265         /* Clear menu item */
266         if (gtk_text_buffer_get_char_count (priv->buffer) > 0) {
267                 item = gtk_separator_menu_item_new ();
268                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
269                 gtk_widget_show (item);
270
271                 item = gtk_image_menu_item_new_from_stock (GTK_STOCK_CLEAR, NULL);
272                 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
273                 gtk_widget_show (item);
274
275                 g_signal_connect_swapped (item, "activate",
276                                           G_CALLBACK (empathy_chat_view_clear),
277                                           view);
278         }
279
280         /* Link context menu items */
281         table = gtk_text_buffer_get_tag_table (priv->buffer);
282         tag = gtk_text_tag_table_lookup (table, EMPATHY_CHAT_TEXT_VIEW_TAG_LINK);
283
284         gtk_widget_get_pointer (GTK_WIDGET (view), &x, &y);
285
286         gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
287                                                GTK_TEXT_WINDOW_WIDGET,
288                                                x, y,
289                                                &x, &y);
290
291         gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y);
292
293         start = end = iter;
294
295         if (gtk_text_iter_backward_to_tag_toggle (&start, tag) &&
296             gtk_text_iter_forward_to_tag_toggle (&end, tag)) {
297                     str = gtk_text_buffer_get_text (priv->buffer,
298                                                     &start, &end, FALSE);
299             }
300
301         if (EMP_STR_EMPTY (str)) {
302                 g_free (str);
303                 return;
304         }
305
306         /* NOTE: Set data just to get the string freed when not needed. */
307         g_object_set_data_full (G_OBJECT (menu),
308                                 "url", str,
309                                 (GDestroyNotify) g_free);
310
311         item = gtk_separator_menu_item_new ();
312         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
313         gtk_widget_show (item);
314
315         item = gtk_menu_item_new_with_mnemonic (_("_Copy Link Address"));
316         g_signal_connect (item, "activate",
317                           G_CALLBACK (chat_text_view_copy_address_cb),
318                           str);
319         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
320         gtk_widget_show (item);
321
322         item = gtk_menu_item_new_with_mnemonic (_("_Open Link"));
323         g_signal_connect (item, "activate",
324                           G_CALLBACK (chat_text_view_open_address_cb),
325                           str);
326         gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
327         gtk_widget_show (item);
328 }
329
330 static gboolean
331 chat_text_view_is_scrolled_down (EmpathyChatTextView *view)
332 {
333         GtkWidget *sw;
334
335         sw = gtk_widget_get_parent (GTK_WIDGET (view));
336         if (GTK_IS_SCROLLED_WINDOW (sw)) {
337                 GtkAdjustment *vadj;
338                 gdouble value;
339                 gdouble upper;
340                 gdouble page_size;
341
342                 vadj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (sw));
343                 value = gtk_adjustment_get_value (vadj);
344                 upper = gtk_adjustment_get_upper (vadj);
345                 page_size = gtk_adjustment_get_page_size (vadj);
346
347                 if (value < upper - page_size) {
348                         return FALSE;
349                 }
350         }
351
352         return TRUE;
353 }
354
355 static void
356 chat_text_view_maybe_trim_buffer (EmpathyChatTextView *view)
357 {
358         EmpathyChatTextViewPriv *priv;
359         GtkTextIter         top, bottom;
360         gint                line;
361         gint                remove_;
362         GtkTextTagTable    *table;
363         GtkTextTag         *tag;
364
365         priv = GET_PRIV (view);
366
367         gtk_text_buffer_get_end_iter (priv->buffer, &bottom);
368         line = gtk_text_iter_get_line (&bottom);
369         if (line < MAX_LINES) {
370                 return;
371         }
372
373         remove_ = line - MAX_LINES;
374         gtk_text_buffer_get_start_iter (priv->buffer, &top);
375
376         bottom = top;
377         if (!gtk_text_iter_forward_lines (&bottom, remove_)) {
378                 return;
379         }
380
381         /* Track backwords to a place where we can safely cut, we don't do it in
382           * the middle of a tag.
383           */
384         table = gtk_text_buffer_get_tag_table (priv->buffer);
385         tag = gtk_text_tag_table_lookup (table, EMPATHY_CHAT_TEXT_VIEW_TAG_CUT);
386         if (!tag) {
387                 return;
388         }
389
390         if (!gtk_text_iter_forward_to_tag_toggle (&bottom, tag)) {
391                 return;
392         }
393
394         if (!gtk_text_iter_equal (&top, &bottom)) {
395                 gtk_text_buffer_delete (priv->buffer, &top, &bottom);
396         }
397 }
398
399 static void
400 chat_text_view_append_timestamp (EmpathyChatTextView *view,
401                                  time_t               timestamp,
402                                  gboolean             show_date)
403 {
404         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
405         GtkTextIter              iter;
406         gchar                   *tmp;
407         GString                 *str;
408
409         str = g_string_new ("- ");
410
411         /* Append date if needed */
412         if (show_date) {
413                 GDate *date;
414                 gchar  buf[256];
415
416                 date = g_date_new ();
417                 g_date_set_time_t (date, timestamp);
418                 /* Translators: timestamp displayed between conversations in
419                  * chat windows (strftime format string) */
420                 g_date_strftime (buf, 256, _("%A %B %d %Y"), date);
421                 g_string_append (str, buf);
422                 g_string_append (str, ", ");
423                 g_date_free (date);
424         }
425
426         /* Append time */
427         tmp = empathy_time_to_string_local (timestamp, EMPATHY_TIME_FORMAT_DISPLAY_SHORT);
428         g_string_append (str, tmp);
429         g_free (tmp);
430
431         g_string_append (str, " -\n");
432
433         /* Insert the string in the buffer */
434         empathy_chat_text_view_append_spacing (view);
435         gtk_text_buffer_get_end_iter (priv->buffer, &iter);
436         gtk_text_buffer_insert_with_tags_by_name (priv->buffer,
437                                                   &iter,
438                                                   str->str, -1,
439                                                   EMPATHY_CHAT_TEXT_VIEW_TAG_TIME,
440                                                   NULL);
441
442         priv->last_timestamp = timestamp;
443
444         g_string_free (str, TRUE);
445 }
446
447 static void
448 chat_text_maybe_append_date_and_time (EmpathyChatTextView *view,
449                                       time_t               timestamp)
450 {
451         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
452         GDate                   *date, *last_date;
453         gboolean                 append_date = FALSE;
454         gboolean                 append_time = FALSE;
455
456         /* Get the date from last message */
457         last_date = g_date_new ();
458         g_date_set_time_t (last_date, priv->last_timestamp);
459
460         /* Get the date of the message we are appending */
461         date = g_date_new ();
462         g_date_set_time_t (date, timestamp);
463
464         /* If last message was from another day we append date and time */
465         if (g_date_compare (date, last_date) > 0) {
466                 append_date = TRUE;
467                 append_time = TRUE;
468         }
469
470         g_date_free (last_date);
471         g_date_free (date);
472
473         /* If last message is 'old' append the time */
474         if (timestamp - priv->last_timestamp >= TIMESTAMP_INTERVAL) {
475                 append_time = TRUE;
476         }
477
478         if (append_date || (!priv->only_if_date && append_time)) {
479                 chat_text_view_append_timestamp (view, timestamp, append_date);
480         }
481 }
482
483 static void
484 chat_text_view_size_allocate (GtkWidget     *widget,
485                               GtkAllocation *alloc)
486 {
487         gboolean down;
488
489         down = chat_text_view_is_scrolled_down (EMPATHY_CHAT_TEXT_VIEW (widget));
490
491         GTK_WIDGET_CLASS (empathy_chat_text_view_parent_class)->size_allocate (widget, alloc);
492
493         if (down) {
494                 GtkAdjustment *adj;
495
496                 adj = GTK_TEXT_VIEW (widget)->vadjustment;
497                 gtk_adjustment_set_value (adj,
498                                           gtk_adjustment_get_upper (adj) -
499                                           gtk_adjustment_get_page_size (adj));
500         }
501 }
502
503 static gboolean
504 chat_text_view_drag_motion (GtkWidget      *widget,
505                             GdkDragContext *context,
506                             gint            x,
507                             gint            y,
508                             guint           time_)
509 {
510         /* Don't handle drag motion, since we don't want the view to scroll as
511          * the result of dragging something across it. */
512
513         return FALSE;
514 }
515
516 static void
517 chat_text_view_get_property (GObject    *object,
518                              guint       param_id,
519                              GValue     *value,
520                              GParamSpec *pspec)
521 {
522         EmpathyChatTextViewPriv *priv = GET_PRIV (object);
523
524         switch (param_id) {
525         case PROP_LAST_CONTACT:
526                 g_value_set_object (value, priv->last_contact);
527                 break;
528         case PROP_ONLY_IF_DATE:
529                 g_value_set_boolean (value, priv->only_if_date);
530                 break;
531         default:
532                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
533                 break;
534         };
535 }
536
537 static void
538 chat_text_view_set_property (GObject      *object,
539                              guint         param_id,
540                              const GValue *value,
541                              GParamSpec   *pspec)
542 {
543         EmpathyChatTextViewPriv *priv = GET_PRIV (object);
544
545         switch (param_id) {
546         case PROP_ONLY_IF_DATE:
547                 priv->only_if_date = g_value_get_boolean (value);
548                 break;
549         default:
550                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
551                 break;
552         };
553 }
554
555 static void
556 chat_text_view_finalize (GObject *object)
557 {
558         EmpathyChatTextView     *view;
559         EmpathyChatTextViewPriv *priv;
560
561         view = EMPATHY_CHAT_TEXT_VIEW (object);
562         priv = GET_PRIV (view);
563
564         DEBUG ("%p", object);
565
566         empathy_conf_notify_remove (empathy_conf_get (), priv->notify_system_fonts_id);
567
568         if (priv->last_contact) {
569                 g_object_unref (priv->last_contact);
570         }
571         if (priv->scroll_time) {
572                 g_timer_destroy (priv->scroll_time);
573         }
574         if (priv->scroll_timeout) {
575                 g_source_remove (priv->scroll_timeout);
576         }
577         g_object_unref (priv->smiley_manager);
578
579         G_OBJECT_CLASS (empathy_chat_text_view_parent_class)->finalize (object);
580 }
581
582 static void
583 empathy_chat_text_view_class_init (EmpathyChatTextViewClass *klass)
584 {
585         GObjectClass   *object_class = G_OBJECT_CLASS (klass);
586         GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
587
588         object_class->finalize = chat_text_view_finalize;
589         object_class->get_property = chat_text_view_get_property;
590         object_class->set_property = chat_text_view_set_property;
591
592         widget_class->size_allocate = chat_text_view_size_allocate;
593         widget_class->drag_motion = chat_text_view_drag_motion;
594
595         g_object_class_install_property (object_class,
596                                          PROP_LAST_CONTACT,
597                                          g_param_spec_object ("last-contact",
598                                                               "Last contact",
599                                                               "The sender of the last received message",
600                                                               EMPATHY_TYPE_CONTACT,
601                                                               G_PARAM_READABLE));
602         g_object_class_install_property (object_class,
603                                          PROP_ONLY_IF_DATE,
604                                          g_param_spec_boolean ("only-if-date",
605                                                               "Only if date",
606                                                               "Display timestamp only if the date changes",
607                                                               FALSE,
608                                                               G_PARAM_READWRITE));
609
610
611         g_type_class_add_private (object_class, sizeof (EmpathyChatTextViewPriv));
612 }
613
614 static void
615 empathy_chat_text_view_init (EmpathyChatTextView *view)
616 {
617         EmpathyChatTextViewPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (view,
618                 EMPATHY_TYPE_CHAT_TEXT_VIEW, EmpathyChatTextViewPriv);
619
620         view->priv = priv;
621         priv->buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
622         priv->last_timestamp = 0;
623         priv->allow_scrolling = TRUE;
624         priv->smiley_manager = empathy_smiley_manager_dup_singleton ();
625
626         g_object_set (view,
627                       "wrap-mode", GTK_WRAP_WORD_CHAR,
628                       "editable", FALSE,
629                       "cursor-visible", FALSE,
630                       NULL);
631
632         priv->notify_system_fonts_id =
633                 empathy_conf_notify_add (empathy_conf_get (),
634                                          "/desktop/gnome/interface/document_font_name",
635                                          chat_text_view_notify_system_font_cb,
636                                          view);
637         chat_text_view_system_font_update (view);
638         chat_text_view_create_tags (view);
639
640         g_signal_connect (view,
641                           "populate-popup",
642                           G_CALLBACK (chat_text_view_populate_popup),
643                           NULL);
644 }
645
646 /* Code stolen from pidgin/gtkimhtml.c */
647 static gboolean
648 chat_text_view_scroll_cb (EmpathyChatTextView *view)
649 {
650         EmpathyChatTextViewPriv *priv;
651         GtkAdjustment      *adj;
652         gdouble             max_val;
653
654         priv = GET_PRIV (view);
655         adj = GTK_TEXT_VIEW (view)->vadjustment;
656         max_val = gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj);
657
658         g_return_val_if_fail (priv->scroll_time != NULL, FALSE);
659
660         if (g_timer_elapsed (priv->scroll_time, NULL) > MAX_SCROLL_TIME) {
661                 /* time's up. jump to the end and kill the timer */
662                 gtk_adjustment_set_value (adj, max_val);
663                 g_timer_destroy (priv->scroll_time);
664                 priv->scroll_time = NULL;
665                 priv->scroll_timeout = 0;
666                 return FALSE;
667         }
668
669         /* scroll by 1/3rd the remaining distance */
670         gtk_adjustment_set_value (adj, gtk_adjustment_get_value (adj) + ((max_val - gtk_adjustment_get_value (adj)) / 3));
671         return TRUE;
672 }
673
674 static void
675 chat_text_view_scroll_down (EmpathyChatView *view)
676 {
677         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
678
679         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
680
681         if (!priv->allow_scrolling) {
682                 return;
683         }
684
685         DEBUG ("Scrolling down");
686
687         if (priv->scroll_time) {
688                 g_timer_reset (priv->scroll_time);
689         } else {
690                 priv->scroll_time = g_timer_new ();
691         }
692         if (!priv->scroll_timeout) {
693                 priv->scroll_timeout = g_timeout_add (SCROLL_DELAY,
694                                                       (GSourceFunc) chat_text_view_scroll_cb,
695                                                       view);
696         }
697 }
698
699 static void
700 chat_text_view_append_message (EmpathyChatView *view,
701                                EmpathyMessage  *msg)
702 {
703         EmpathyChatTextView     *text_view = EMPATHY_CHAT_TEXT_VIEW (view);
704         EmpathyChatTextViewPriv *priv = GET_PRIV (text_view);
705         gboolean                 bottom;
706         time_t                   timestamp;
707
708         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
709         g_return_if_fail (EMPATHY_IS_MESSAGE (msg));
710
711         if (!empathy_message_get_body (msg)) {
712                 return;
713         }
714
715         bottom = chat_text_view_is_scrolled_down (text_view);
716
717         chat_text_view_maybe_trim_buffer (EMPATHY_CHAT_TEXT_VIEW (view));
718
719         timestamp = empathy_message_get_timestamp (msg);
720         chat_text_maybe_append_date_and_time (text_view, timestamp);
721         if (EMPATHY_CHAT_TEXT_VIEW_GET_CLASS (view)->append_message) {
722                 EMPATHY_CHAT_TEXT_VIEW_GET_CLASS (view)->append_message (text_view,
723                                                                          msg);
724         }
725
726         if (bottom) {
727                 chat_text_view_scroll_down (view);
728         }
729
730         if (priv->last_contact) {
731                 g_object_unref (priv->last_contact);
732         }
733         priv->last_contact = g_object_ref (empathy_message_get_sender (msg));
734         g_object_notify (G_OBJECT (view), "last-contact");
735 }
736
737 static void
738 chat_text_view_append_event (EmpathyChatView *view,
739                              const gchar     *str)
740 {
741         EmpathyChatTextView     *text_view = EMPATHY_CHAT_TEXT_VIEW (view);
742         EmpathyChatTextViewPriv *priv = GET_PRIV (text_view);
743         gboolean                 bottom;
744         GtkTextIter              iter;
745         gchar                   *msg;
746
747
748         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
749         g_return_if_fail (!EMP_STR_EMPTY (str));
750
751         bottom = chat_text_view_is_scrolled_down (text_view);
752         chat_text_view_maybe_trim_buffer (EMPATHY_CHAT_TEXT_VIEW (view));
753         chat_text_maybe_append_date_and_time (text_view,
754                                               empathy_time_get_current ());
755
756         gtk_text_buffer_get_end_iter (priv->buffer, &iter);
757         msg = g_strdup_printf (" - %s\n", str);
758         gtk_text_buffer_insert_with_tags_by_name (priv->buffer, &iter,
759                                                   msg, -1,
760                                                   EMPATHY_CHAT_TEXT_VIEW_TAG_EVENT,
761                                                   NULL);
762         g_free (msg);
763
764         if (bottom) {
765                 chat_text_view_scroll_down (view);
766         }
767
768         if (priv->last_contact) {
769                 g_object_unref (priv->last_contact);
770                 priv->last_contact = NULL;
771                 g_object_notify (G_OBJECT (view), "last-contact");
772         }
773 }
774
775 static void
776 chat_text_view_scroll (EmpathyChatView *view,
777                        gboolean         allow_scrolling)
778 {
779         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
780
781         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
782
783         DEBUG ("Scrolling %s", allow_scrolling ? "enabled" : "disabled");
784
785         priv->allow_scrolling = allow_scrolling;
786         if (allow_scrolling) {
787                 empathy_chat_view_scroll_down (view);
788         }
789 }
790
791 static gboolean
792 chat_text_view_get_has_selection (EmpathyChatView *view)
793 {
794         GtkTextBuffer *buffer;
795
796         g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view), FALSE);
797
798         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
799
800         return gtk_text_buffer_get_has_selection (buffer);
801 }
802
803 static void
804 chat_text_view_clear (EmpathyChatView *view)
805 {
806         GtkTextBuffer      *buffer;
807         EmpathyChatTextViewPriv *priv;
808
809         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
810
811         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
812         gtk_text_buffer_set_text (buffer, "", -1);
813
814         /* We set these back to the initial values so we get
815           * timestamps when clearing the window to know when
816           * conversations start.
817           */
818         priv = GET_PRIV (view);
819
820         priv->last_timestamp = 0;
821         if (priv->last_contact) {
822                 g_object_unref (priv->last_contact);
823                 priv->last_contact = NULL;
824         }
825 }
826
827 static gboolean
828 chat_text_view_find_previous (EmpathyChatView *view,
829                                 const gchar     *search_criteria,
830                                 gboolean         new_search)
831 {
832         EmpathyChatTextViewPriv *priv;
833         GtkTextBuffer      *buffer;
834         GtkTextIter         iter_at_mark;
835         GtkTextIter         iter_match_start;
836         GtkTextIter         iter_match_end;
837         gboolean            found;
838         gboolean            from_start = FALSE;
839
840         g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view), FALSE);
841         g_return_val_if_fail (search_criteria != NULL, FALSE);
842
843         priv = GET_PRIV (view);
844
845         buffer = priv->buffer;
846
847         if (EMP_STR_EMPTY (search_criteria)) {
848                 if (priv->find_mark_previous) {
849                         gtk_text_buffer_get_start_iter (buffer, &iter_at_mark);
850
851                         gtk_text_buffer_move_mark (buffer,
852                                                    priv->find_mark_previous,
853                                                    &iter_at_mark);
854                         gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view),
855                                                       priv->find_mark_previous,
856                                                       0.0,
857                                                       TRUE,
858                                                       0.0,
859                                                       0.0);
860                         gtk_text_buffer_select_range (buffer,
861                                                       &iter_at_mark,
862                                                       &iter_at_mark);
863                 }
864
865                 return FALSE;
866         }
867
868         if (new_search) {
869                 from_start = TRUE;
870         }
871
872         if (priv->find_mark_previous) {
873                 gtk_text_buffer_get_iter_at_mark (buffer,
874                                                   &iter_at_mark,
875                                                   priv->find_mark_previous);
876         } else {
877                 gtk_text_buffer_get_end_iter (buffer, &iter_at_mark);
878                 from_start = TRUE;
879         }
880
881         priv->find_last_direction = FALSE;
882
883         found = empathy_text_iter_backward_search (&iter_at_mark,
884                                                    search_criteria,
885                                                    &iter_match_start,
886                                                    &iter_match_end,
887                                                    NULL);
888
889         if (!found) {
890                 gboolean result = FALSE;
891
892                 if (from_start) {
893                         return result;
894                 }
895
896                 /* Here we wrap around. */
897                 if (!new_search && !priv->find_wrapped) {
898                         priv->find_wrapped = TRUE;
899                         result = chat_text_view_find_previous (view,
900                                                                  search_criteria,
901                                                                  FALSE);
902                         priv->find_wrapped = FALSE;
903                 }
904
905                 return result;
906         }
907
908         /* Set new mark and show on screen */
909         if (!priv->find_mark_previous) {
910                 priv->find_mark_previous = gtk_text_buffer_create_mark (buffer, NULL,
911                                                                         &iter_match_start,
912                                                                         TRUE);
913         } else {
914                 gtk_text_buffer_move_mark (buffer,
915                                            priv->find_mark_previous,
916                                            &iter_match_start);
917         }
918
919         if (!priv->find_mark_next) {
920                 priv->find_mark_next = gtk_text_buffer_create_mark (buffer, NULL,
921                                                                     &iter_match_end,
922                                                                     TRUE);
923         } else {
924                 gtk_text_buffer_move_mark (buffer,
925                                            priv->find_mark_next,
926                                            &iter_match_end);
927         }
928
929         gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view),
930                                       priv->find_mark_previous,
931                                       0.0,
932                                       TRUE,
933                                       0.5,
934                                       0.5);
935
936         gtk_text_buffer_move_mark_by_name (buffer, "selection_bound", &iter_match_start);
937         gtk_text_buffer_move_mark_by_name (buffer, "insert", &iter_match_end);
938
939         return TRUE;
940 }
941
942 static gboolean
943 chat_text_view_find_next (EmpathyChatView *view,
944                             const gchar     *search_criteria,
945                             gboolean         new_search)
946 {
947         EmpathyChatTextViewPriv *priv;
948         GtkTextBuffer      *buffer;
949         GtkTextIter         iter_at_mark;
950         GtkTextIter         iter_match_start;
951         GtkTextIter         iter_match_end;
952         gboolean            found;
953         gboolean            from_start = FALSE;
954
955         g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view), FALSE);
956         g_return_val_if_fail (search_criteria != NULL, FALSE);
957
958         priv = GET_PRIV (view);
959
960         buffer = priv->buffer;
961
962         if (EMP_STR_EMPTY (search_criteria)) {
963                 if (priv->find_mark_next) {
964                         gtk_text_buffer_get_start_iter (buffer, &iter_at_mark);
965
966                         gtk_text_buffer_move_mark (buffer,
967                                                    priv->find_mark_next,
968                                                    &iter_at_mark);
969                         gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view),
970                                                       priv->find_mark_next,
971                                                       0.0,
972                                                       TRUE,
973                                                       0.0,
974                                                       0.0);
975                         gtk_text_buffer_select_range (buffer,
976                                                       &iter_at_mark,
977                                                       &iter_at_mark);
978                 }
979
980                 return FALSE;
981         }
982
983         if (new_search) {
984                 from_start = TRUE;
985         }
986
987         if (priv->find_mark_next) {
988                 gtk_text_buffer_get_iter_at_mark (buffer,
989                                                   &iter_at_mark,
990                                                   priv->find_mark_next);
991         } else {
992                 gtk_text_buffer_get_start_iter (buffer, &iter_at_mark);
993                 from_start = TRUE;
994         }
995
996         priv->find_last_direction = TRUE;
997
998         found = empathy_text_iter_forward_search (&iter_at_mark,
999                                                   search_criteria,
1000                                                   &iter_match_start,
1001                                                   &iter_match_end,
1002                                                   NULL);
1003
1004         if (!found) {
1005                 gboolean result = FALSE;
1006
1007                 if (from_start) {
1008                         return result;
1009                 }
1010
1011                 /* Here we wrap around. */
1012                 if (!new_search && !priv->find_wrapped) {
1013                         priv->find_wrapped = TRUE;
1014                         result = chat_text_view_find_next (view,
1015                                                              search_criteria,
1016                                                              FALSE);
1017                         priv->find_wrapped = FALSE;
1018                 }
1019
1020                 return result;
1021         }
1022
1023         /* Set new mark and show on screen */
1024         if (!priv->find_mark_next) {
1025                 priv->find_mark_next = gtk_text_buffer_create_mark (buffer, NULL,
1026                                                                     &iter_match_end,
1027                                                                     TRUE);
1028         } else {
1029                 gtk_text_buffer_move_mark (buffer,
1030                                            priv->find_mark_next,
1031                                            &iter_match_end);
1032         }
1033
1034         if (!priv->find_mark_previous) {
1035                 priv->find_mark_previous = gtk_text_buffer_create_mark (buffer, NULL,
1036                                                                         &iter_match_start,
1037                                                                         TRUE);
1038         } else {
1039                 gtk_text_buffer_move_mark (buffer,
1040                                            priv->find_mark_previous,
1041                                            &iter_match_start);
1042         }
1043
1044         gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (view),
1045                                       priv->find_mark_next,
1046                                       0.0,
1047                                       TRUE,
1048                                       0.5,
1049                                       0.5);
1050
1051         gtk_text_buffer_move_mark_by_name (buffer, "selection_bound", &iter_match_start);
1052         gtk_text_buffer_move_mark_by_name (buffer, "insert", &iter_match_end);
1053
1054         return TRUE;
1055 }
1056
1057 static void
1058 chat_text_view_find_abilities (EmpathyChatView *view,
1059                                  const gchar    *search_criteria,
1060                                  gboolean       *can_do_previous,
1061                                  gboolean       *can_do_next)
1062 {
1063         EmpathyChatTextViewPriv *priv;
1064         GtkTextBuffer           *buffer;
1065         GtkTextIter              iter_at_mark;
1066         GtkTextIter              iter_match_start;
1067         GtkTextIter              iter_match_end;
1068
1069         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
1070         g_return_if_fail (search_criteria != NULL);
1071         g_return_if_fail (can_do_previous != NULL && can_do_next != NULL);
1072
1073         priv = GET_PRIV (view);
1074
1075         buffer = priv->buffer;
1076
1077         if (can_do_previous) {
1078                 if (priv->find_mark_previous) {
1079                         gtk_text_buffer_get_iter_at_mark (buffer,
1080                                                           &iter_at_mark,
1081                                                           priv->find_mark_previous);
1082                 } else {
1083                         gtk_text_buffer_get_start_iter (buffer, &iter_at_mark);
1084                 }
1085
1086                 *can_do_previous = empathy_text_iter_backward_search (&iter_at_mark,
1087                                                                       search_criteria,
1088                                                                       &iter_match_start,
1089                                                                       &iter_match_end,
1090                                                                       NULL);
1091         }
1092
1093         if (can_do_next) {
1094                 if (priv->find_mark_next) {
1095                         gtk_text_buffer_get_iter_at_mark (buffer,
1096                                                           &iter_at_mark,
1097                                                           priv->find_mark_next);
1098                 } else {
1099                         gtk_text_buffer_get_start_iter (buffer, &iter_at_mark);
1100                 }
1101
1102                 *can_do_next = empathy_text_iter_forward_search (&iter_at_mark,
1103                                                                  search_criteria,
1104                                                                  &iter_match_start,
1105                                                                  &iter_match_end,
1106                                                                  NULL);
1107         }
1108 }
1109
1110 static void
1111 chat_text_view_highlight (EmpathyChatView *view,
1112                             const gchar     *text)
1113 {
1114         GtkTextBuffer *buffer;
1115         GtkTextIter    iter;
1116         GtkTextIter    iter_start;
1117         GtkTextIter    iter_end;
1118         GtkTextIter    iter_match_start;
1119         GtkTextIter    iter_match_end;
1120         gboolean       found;
1121
1122         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
1123
1124         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
1125
1126         gtk_text_buffer_get_start_iter (buffer, &iter);
1127
1128         gtk_text_buffer_get_bounds (buffer, &iter_start, &iter_end);
1129         gtk_text_buffer_remove_tag_by_name (buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_HIGHLIGHT,
1130                                             &iter_start,
1131                                             &iter_end);
1132
1133         if (EMP_STR_EMPTY (text)) {
1134                 return;
1135         }
1136
1137         while (1) {
1138                 found = empathy_text_iter_forward_search (&iter,
1139                                                           text,
1140                                                           &iter_match_start,
1141                                                           &iter_match_end,
1142                                                           NULL);
1143
1144                 if (!found) {
1145                         break;
1146                 }
1147
1148                 gtk_text_buffer_apply_tag_by_name (buffer, EMPATHY_CHAT_TEXT_VIEW_TAG_HIGHLIGHT,
1149                                                    &iter_match_start,
1150                                                    &iter_match_end);
1151
1152                 iter = iter_match_end;
1153                 gtk_text_iter_forward_char (&iter);
1154         }
1155 }
1156
1157 static void
1158 chat_text_view_copy_clipboard (EmpathyChatView *view)
1159 {
1160         GtkTextBuffer *buffer;
1161         GtkTextIter start, iter, end;
1162         GtkClipboard  *clipboard;
1163         GdkPixbuf *pixbuf;
1164         gunichar c;
1165         GtkTextChildAnchor *anchor = NULL;
1166         GString *str;
1167         GList *list;
1168         gboolean ignore_newlines = FALSE;
1169
1170         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
1171
1172         buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
1173         clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
1174
1175         if (!gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
1176                 return;
1177
1178         str = g_string_new ("");
1179
1180         for (iter = start; !gtk_text_iter_equal (&iter, &end); gtk_text_iter_forward_char (&iter)) {
1181                 c = gtk_text_iter_get_char (&iter);
1182                 /* 0xFFFC is the 'object replacement' unicode character,
1183                  * it indicates the presence of a pixbuf or a widget. */
1184                 if (c == 0xFFFC) {
1185                         ignore_newlines = FALSE;
1186                         if ((pixbuf = gtk_text_iter_get_pixbuf (&iter))) {
1187                                 gchar *text;
1188                                 text = g_object_get_data (G_OBJECT(pixbuf),
1189                                                           "smiley_str");
1190                                 if (text)
1191                                         str = g_string_append (str, text);
1192                         } else if ((anchor = gtk_text_iter_get_child_anchor (&iter))) {
1193                                 gchar *text;
1194                                 list = gtk_text_child_anchor_get_widgets (anchor);
1195                                 if (list) {
1196                                         text = g_object_get_data (G_OBJECT(list->data),
1197                                                                   "str_obj");
1198                                         if (text)
1199                                                 str = g_string_append (str, text);
1200                                 }
1201                                 g_list_free (list);
1202                         }
1203                 } else if (c == '\n') {
1204                         if (!ignore_newlines) {
1205                                 ignore_newlines = TRUE;
1206                                 str = g_string_append_unichar (str, c);
1207                         }
1208                 } else {
1209                         ignore_newlines = FALSE;
1210                         str = g_string_append_unichar (str, c);
1211                 }
1212         }
1213
1214         gtk_clipboard_set_text (clipboard, str->str, str->len);
1215         g_string_free (str, TRUE);
1216 }
1217
1218 static void
1219 chat_text_view_iface_init (EmpathyChatViewIface *iface)
1220 {
1221         iface->append_message = chat_text_view_append_message;
1222         iface->append_event = chat_text_view_append_event;
1223         iface->scroll = chat_text_view_scroll;
1224         iface->scroll_down = chat_text_view_scroll_down;
1225         iface->get_has_selection = chat_text_view_get_has_selection;
1226         iface->clear = chat_text_view_clear;
1227         iface->find_previous = chat_text_view_find_previous;
1228         iface->find_next = chat_text_view_find_next;
1229         iface->find_abilities = chat_text_view_find_abilities;
1230         iface->highlight = chat_text_view_highlight;
1231         iface->copy_clipboard = chat_text_view_copy_clipboard;
1232 }
1233
1234 EmpathyContact *
1235 empathy_chat_text_view_get_last_contact (EmpathyChatTextView *view)
1236 {
1237         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
1238
1239         g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view), NULL);
1240
1241         return priv->last_contact;
1242 }
1243
1244 void
1245 empathy_chat_text_view_set_only_if_date (EmpathyChatTextView *view,
1246                                          gboolean             only_if_date)
1247 {
1248         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
1249
1250         g_return_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view));
1251
1252         if (only_if_date != priv->only_if_date) {
1253                 priv->only_if_date = only_if_date;
1254                 g_object_notify (G_OBJECT (view), "only-if-date");
1255         }
1256 }
1257
1258 static void
1259 chat_text_view_insert_text_with_emoticons (EmpathyChatTextView *view,
1260                                            GtkTextIter         *iter,
1261                                            const gchar         *str)
1262 {
1263         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
1264         gboolean                 use_smileys = FALSE;
1265         GSList                  *hits, *l;
1266         guint                    last = 0;
1267
1268         empathy_conf_get_bool (empathy_conf_get (),
1269                                EMPATHY_PREFS_CHAT_SHOW_SMILEYS,
1270                                &use_smileys);
1271
1272         if (!use_smileys) {
1273                 gtk_text_buffer_insert (priv->buffer, iter, str, -1);
1274                 return;
1275         }
1276
1277         hits = empathy_smiley_manager_parse_len (priv->smiley_manager, str, -1);
1278         for (l = hits; l; l = l->next) {
1279                 EmpathySmileyHit *hit = l->data;
1280
1281                 if (hit->start > last) {
1282                         /* Append the text between last smiley (or the
1283                          * start of the message) and this smiley */
1284                         gtk_text_buffer_insert (priv->buffer, iter, str + last,
1285                                                 hit->start - last);
1286                 }
1287                 gtk_text_buffer_insert_pixbuf (priv->buffer, iter, hit->pixbuf);
1288
1289                 last = hit->end;
1290
1291                 empathy_smiley_hit_free (hit);
1292         }
1293         g_slist_free (hits);
1294
1295         gtk_text_buffer_insert (priv->buffer, iter, str + last, -1);
1296 }
1297
1298 void
1299 empathy_chat_text_view_append_body (EmpathyChatTextView *view,
1300                                     const gchar         *body,
1301                                     const gchar         *tag)
1302 {
1303         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
1304         GtkTextIter              start_iter, end_iter;
1305         GtkTextMark             *mark;
1306         GtkTextIter              iter;
1307         GRegex                  *uri_regex;
1308         GMatchInfo              *match_info;
1309         gboolean                 match;
1310         gint                     last = 0;
1311         gint                     s = 0, e = 0;
1312         gchar                   *tmp;
1313
1314         priv = GET_PRIV (view);
1315
1316         gtk_text_buffer_get_end_iter (priv->buffer, &start_iter);
1317         mark = gtk_text_buffer_create_mark (priv->buffer, NULL, &start_iter, TRUE);
1318
1319         uri_regex = empathy_uri_regex_dup_singleton ();
1320         for (match = g_regex_match (uri_regex, body, 0, &match_info); match;
1321              match = g_match_info_next (match_info, NULL)) {
1322                 if (!g_match_info_fetch_pos (match_info, 0, &s, &e))
1323                         continue;
1324
1325                 if (s > last) {
1326                         tmp = empathy_substring (body, last, s);
1327
1328                         gtk_text_buffer_get_end_iter (priv->buffer, &iter);
1329                         chat_text_view_insert_text_with_emoticons (view,
1330                                                                    &iter,
1331                                                                    tmp);
1332                         g_free (tmp);
1333                 }
1334
1335                 tmp = empathy_substring (body, s, e);
1336
1337                 gtk_text_buffer_get_end_iter (priv->buffer, &iter);
1338                 gtk_text_buffer_insert_with_tags_by_name (priv->buffer,
1339                                                           &iter,
1340                                                           tmp,
1341                                                           -1,
1342                                                           EMPATHY_CHAT_TEXT_VIEW_TAG_LINK,
1343                                                           NULL);
1344
1345                 g_free (tmp);
1346                 last = e;
1347         }
1348         g_match_info_free (match_info);
1349         g_regex_unref (uri_regex);
1350
1351         if (last < (gint) strlen (body)) {
1352                 gtk_text_buffer_get_end_iter (priv->buffer, &iter);
1353                 chat_text_view_insert_text_with_emoticons (view,
1354                                                            &iter,
1355                                                            body + last);
1356         }
1357
1358         gtk_text_buffer_get_end_iter (priv->buffer, &iter);
1359         gtk_text_buffer_insert (priv->buffer, &iter, "\n", 1);
1360
1361         /* Apply the style to the inserted text. */
1362         gtk_text_buffer_get_iter_at_mark (priv->buffer, &start_iter, mark);
1363         gtk_text_buffer_get_end_iter (priv->buffer, &end_iter);
1364
1365         gtk_text_buffer_apply_tag_by_name (priv->buffer,
1366                                            tag,
1367                                            &start_iter,
1368                                            &end_iter);
1369
1370         gtk_text_buffer_delete_mark (priv->buffer, mark);
1371 }
1372
1373 void
1374 empathy_chat_text_view_append_spacing (EmpathyChatTextView *view)
1375 {
1376         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
1377         GtkTextIter              iter;
1378
1379         gtk_text_buffer_get_end_iter (priv->buffer, &iter);
1380         gtk_text_buffer_insert_with_tags_by_name (priv->buffer,
1381                                                   &iter,
1382                                                   "\n",
1383                                                   -1,
1384                                                   EMPATHY_CHAT_TEXT_VIEW_TAG_CUT,
1385                                                   EMPATHY_CHAT_TEXT_VIEW_TAG_SPACING,
1386                                                   NULL);
1387 }
1388
1389 GtkTextTag *
1390 empathy_chat_text_view_tag_set (EmpathyChatTextView *view,
1391                                 const gchar         *tag_name,
1392                                 const gchar         *first_property_name,
1393                                 ...)
1394 {
1395         EmpathyChatTextViewPriv *priv = GET_PRIV (view);
1396         GtkTextTag              *tag;
1397         GtkTextTagTable         *table;
1398         va_list                  list;
1399
1400         g_return_val_if_fail (EMPATHY_IS_CHAT_TEXT_VIEW (view), NULL);
1401         g_return_val_if_fail (tag_name != NULL, NULL);
1402
1403         table = gtk_text_buffer_get_tag_table (priv->buffer);
1404         tag = gtk_text_tag_table_lookup (table, tag_name);
1405
1406         if (tag && first_property_name) {
1407                 va_start (list, first_property_name);
1408                 g_object_set_valist (G_OBJECT (tag), first_property_name, list);
1409                 va_end (list);
1410         }
1411
1412         return tag;
1413 }
1414