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