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