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