No issues found
1 /*
2 * e-buffer-tagger.c
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) version 3.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with the program; if not, see <http://www.gnu.org/licenses/>
16 *
17 *
18 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
19 *
20 */
21
22 #ifdef HAVE_CONFIG_H
23 #include <config.h>
24 #endif
25
26 #include <gtk/gtk.h>
27 #include <gdk/gdkkeysyms.h>
28 #include <glib/gi18n.h>
29 #include <regex.h>
30 #include <string.h>
31 #include <ctype.h>
32
33 #include "e-util/e-util.h"
34 #include "e-buffer-tagger.h"
35
36 enum EBufferTaggerState
37 {
38 E_BUFFER_TAGGER_STATE_NONE = 0,
39 E_BUFFER_TAGGER_STATE_INSDEL = 1 << 0, /* set when was called insert or delete of a text */
40 E_BUFFER_TAGGER_STATE_CHANGED = 1 << 1, /* remark of the buffer is scheduled */
41 E_BUFFER_TAGGER_STATE_IS_HOVERING = 1 << 2, /* mouse is over the link */
42 E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP = 1 << 3, /* mouse is over the link and the tooltip can be shown */
43 E_BUFFER_TAGGER_STATE_CTRL_DOWN = 1 << 4 /* Ctrl key is down */
44 };
45
46 #define E_BUFFER_TAGGER_DATA_STATE "EBufferTagger::state"
47 #define E_BUFFER_TAGGER_LINK_TAG "EBufferTagger::link"
48
49 struct _MagicInsertMatch
50 {
51 const gchar *regex;
52 regex_t *preg;
53 const gchar *prefix;
54 };
55
56 typedef struct _MagicInsertMatch MagicInsertMatch;
57
58 static MagicInsertMatch mim[] = {
59 /* prefixed expressions */
60 { "(news|telnet|nntp|file|http|ftp|sftp|https|webcal)://([-a-z0-9]+(:[-a-z0-9]+)?@)?[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-a-z0-9_$.+!*(),;:@%&=?/~#']*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, NULL },
61 { "(sip|h323|callto):([-_a-z0-9.'\\+]+(:[0-9]{1,5})?(/[-_a-z0-9.']+)?)(@([-_a-z0-9.%=?]+|([0-9]{1,3}.){3}[0-9]{1,3})?)?(:[0-9]{1,5})?", NULL, NULL },
62 { "mailto:[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, NULL },
63 /* not prefixed expression */
64 { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "http://" },
65 { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "ftp://" },
66 { "[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, "mailto:" }
67 };
68
69 static void
70 init_magic_links (void)
71 {
72 static gboolean done = FALSE;
73 gint i;
74
75 if (done)
76 return;
77
78 done = TRUE;
79
80 for (i = 0; i < G_N_ELEMENTS (mim); i++) {
81 mim[i].preg = g_new0 (regex_t, 1);
82 if (regcomp (mim[i].preg, mim[i].regex, REG_EXTENDED | REG_ICASE)) {
83 /* error */
84 g_free (mim[i].preg);
85 mim[i].preg = 0;
86 }
87 }
88 }
89
90 static void
91 markup_text (GtkTextBuffer *buffer)
92 {
93 GtkTextIter start, end;
94 gchar *text;
95 gint i;
96 regmatch_t pmatch[2];
97 gboolean any;
98 const gchar *str;
99 gint offset = 0;
100
101 g_return_if_fail (buffer != NULL);
102
103 gtk_text_buffer_get_start_iter (buffer, &start);
104 gtk_text_buffer_get_end_iter (buffer, &end);
105 gtk_text_buffer_remove_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end);
106 text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
107
108 str = text;
109 any = TRUE;
110 while (any) {
111 any = FALSE;
112 for (i = 0; i < G_N_ELEMENTS (mim); i++) {
113 if (mim[i].preg && !regexec (mim[i].preg, str, 2, pmatch, 0)) {
114 gtk_text_buffer_get_iter_at_offset (buffer, &start, offset + pmatch[0].rm_so);
115 gtk_text_buffer_get_iter_at_offset (buffer, &end, offset + pmatch[0].rm_eo);
116 gtk_text_buffer_apply_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end);
117
118 any = TRUE;
119 str += pmatch[0].rm_eo;
120 offset += pmatch[0].rm_eo;
121 break;
122 }
123 }
124 }
125
126 g_free (text);
127 }
128
129 static guint32
130 get_state (GtkTextBuffer *buffer)
131 {
132 g_return_val_if_fail (buffer != NULL, E_BUFFER_TAGGER_STATE_NONE);
133 g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), E_BUFFER_TAGGER_STATE_NONE);
134
135 return GPOINTER_TO_INT (g_object_get_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE));
136 }
137
138 static void
139 set_state (GtkTextBuffer *buffer,
140 guint32 state)
141 {
142 g_object_set_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE, GINT_TO_POINTER (state));
143 }
144
145 static void
146 update_state (GtkTextBuffer *buffer,
147 guint32 value,
148 gboolean do_set)
149 {
150 guint32 state;
151
152 g_return_if_fail (buffer != NULL);
153 g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
154
155 state = get_state (buffer);
156
157 if (do_set)
158 state = state | value;
159 else
160 state = state & (~value);
161
162 set_state (buffer, state);
163 }
164
165 static gboolean
166 get_tag_bounds (GtkTextIter *iter,
167 GtkTextTag *tag,
168 GtkTextIter *start,
169 GtkTextIter *end)
170 {
171 gboolean res = FALSE;
172
173 g_return_val_if_fail (iter != NULL, FALSE);
174 g_return_val_if_fail (tag != NULL, FALSE);
175 g_return_val_if_fail (start != NULL, FALSE);
176 g_return_val_if_fail (end != NULL, FALSE);
177
178 if (gtk_text_iter_has_tag (iter, tag)) {
179 *start = *iter;
180 *end = *iter;
181
182 if (!gtk_text_iter_begins_tag (start, tag))
183 gtk_text_iter_backward_to_tag_toggle (start, tag);
184
185 if (!gtk_text_iter_ends_tag (end, tag))
186 gtk_text_iter_forward_to_tag_toggle (end, tag);
187
188 res = TRUE;
189 }
190
191 return res;
192 }
193
194 static gchar *
195 get_url_at_iter (GtkTextBuffer *buffer,
196 GtkTextIter *iter)
197 {
198 GtkTextTagTable *tag_table;
199 GtkTextTag *tag;
200 GtkTextIter start, end;
201 gchar *url = NULL;
202
203 g_return_val_if_fail (buffer != NULL, NULL);
204
205 tag_table = gtk_text_buffer_get_tag_table (buffer);
206 tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
207 g_return_val_if_fail (tag != NULL, FALSE);
208
209 if (get_tag_bounds (iter, tag, &start, &end))
210 url = gtk_text_iter_get_text (&start, &end);
211
212 return url;
213 }
214
215 static gboolean
216 invoke_link_if_present (GtkTextBuffer *buffer,
217 GtkTextIter *iter)
218 {
219 gboolean res;
220 gchar *url;
221
222 g_return_val_if_fail (buffer != NULL, FALSE);
223
224 url = get_url_at_iter (buffer, iter);
225
226 res = url && *url;
227 if (res)
228 e_show_uri (NULL, url);
229
230 g_free (url);
231
232 return res;
233 }
234
235 static void
236 remove_tag_if_present (GtkTextBuffer *buffer,
237 GtkTextIter *where)
238 {
239 GtkTextTagTable *tag_table;
240 GtkTextTag *tag;
241 GtkTextIter start, end;
242
243 g_return_if_fail (buffer != NULL);
244 g_return_if_fail (where != NULL);
245
246 tag_table = gtk_text_buffer_get_tag_table (buffer);
247 tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
248 g_return_if_fail (tag != NULL);
249
250 if (get_tag_bounds (where, tag, &start, &end))
251 gtk_text_buffer_remove_tag (buffer, tag, &start, &end);
252 }
253
254 static void
255 buffer_insert_text (GtkTextBuffer *buffer,
256 GtkTextIter *location,
257 gchar *text,
258 gint len,
259 gpointer user_data)
260 {
261 update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE);
262 remove_tag_if_present (buffer, location);
263 }
264
265 static void
266 buffer_delete_range (GtkTextBuffer *buffer,
267 GtkTextIter *start,
268 GtkTextIter *end,
269 gpointer user_data)
270 {
271 update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE);
272 remove_tag_if_present (buffer, start);
273 remove_tag_if_present (buffer, end);
274 }
275
276 static void
277 buffer_cursor_position (GtkTextBuffer *buffer,
278 gpointer user_data)
279 {
280 guint32 state;
281
282 state = get_state (buffer);
283 if (state & E_BUFFER_TAGGER_STATE_INSDEL) {
284 state = (state & (~E_BUFFER_TAGGER_STATE_INSDEL)) | E_BUFFER_TAGGER_STATE_CHANGED;
285 } else {
286 if (state & E_BUFFER_TAGGER_STATE_CHANGED) {
287 markup_text (buffer);
288 }
289
290 state = state & (~ (E_BUFFER_TAGGER_STATE_CHANGED | E_BUFFER_TAGGER_STATE_INSDEL));
291 }
292
293 set_state (buffer, state);
294 }
295
296 static void
297 update_mouse_cursor (GtkTextView *text_view,
298 gint x,
299 gint y)
300 {
301 static GdkCursor *hand_cursor = NULL;
302 static GdkCursor *regular_cursor = NULL;
303 gboolean hovering = FALSE, hovering_over_link = FALSE, hovering_real;
304 guint32 state;
305 GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view);
306 GtkTextTagTable *tag_table;
307 GtkTextTag *tag;
308 GtkTextIter iter;
309
310 if (!hand_cursor) {
311 hand_cursor = gdk_cursor_new (GDK_HAND2);
312 regular_cursor = gdk_cursor_new (GDK_XTERM);
313 }
314
315 g_return_if_fail (buffer != NULL);
316
317 tag_table = gtk_text_buffer_get_tag_table (buffer);
318 tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
319 g_return_if_fail (tag != NULL);
320
321 state = get_state (buffer);
322
323 gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
324 hovering_real = gtk_text_iter_has_tag (&iter, tag);
325
326 hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING) != 0;
327 if ((state & E_BUFFER_TAGGER_STATE_CTRL_DOWN) == 0) {
328 hovering = FALSE;
329 } else {
330 hovering = hovering_real;
331 }
332
333 if (hovering != hovering_over_link) {
334 update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING, hovering);
335
336 if (hovering && gtk_widget_has_focus (GTK_WIDGET (text_view)))
337 gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), hand_cursor);
338 else
339 gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), regular_cursor);
340
341 gdk_window_get_pointer (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_WIDGET), NULL, NULL, NULL);
342 }
343
344 hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0;
345
346 if (hovering_real != hovering_over_link) {
347 update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP, hovering_real);
348
349 gtk_widget_trigger_tooltip_query (GTK_WIDGET (text_view));
350 }
351 }
352
353 static gboolean
354 textview_query_tooltip (GtkTextView *text_view,
355 gint x,
356 gint y,
357 gboolean keyboard_mode,
358 GtkTooltip *tooltip,
359 gpointer user_data)
360 {
361 GtkTextBuffer *buffer;
362 guint32 state;
363 gboolean res = FALSE;
364
365 if (keyboard_mode)
366 return FALSE;
367
368 buffer = gtk_text_view_get_buffer (text_view);
369 g_return_val_if_fail (buffer != NULL, FALSE);
370
371 state = get_state (buffer);
372
373 if ((state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0) {
374 gchar *url;
375 GtkTextIter iter;
376
377 gtk_text_view_window_to_buffer_coords (
378 text_view,
379 GTK_TEXT_WINDOW_WIDGET,
380 x, y, &x, &y);
381 gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
382
383 url = get_url_at_iter (buffer, &iter);
384 res = url && *url;
385
386 if (res) {
387 gchar *str;
388
389 /* To Translators: The text is concatenated to a form: "Ctrl-click to open a link http://www.example.com" */
390 str = g_strconcat (_("Ctrl-click to open a link"), " ", url, NULL);
391 gtk_tooltip_set_text (tooltip, str);
392 g_free (str);
393 }
394
395 g_free (url);
396 }
397
398 return res;
399 }
400
401 /* Links can be activated by pressing Enter. */
402 static gboolean
403 textview_key_press_event (GtkWidget *text_view,
404 GdkEventKey *event)
405 {
406 GtkTextIter iter;
407 GtkTextBuffer *buffer;
408
409 if ((event->state & GDK_CONTROL_MASK) == 0)
410 return FALSE;
411
412 switch (event->keyval) {
413 case GDK_KEY_Return:
414 case GDK_KEY_KP_Enter:
415 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
416 gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer));
417 if (invoke_link_if_present (buffer, &iter))
418 return TRUE;
419 break;
420
421 default:
422 break;
423 }
424
425 return FALSE;
426 }
427
428 static void
429 update_ctrl_state (GtkTextView *textview,
430 gboolean ctrl_is_down)
431 {
432 GtkTextBuffer *buffer;
433 gint x, y;
434
435 buffer = gtk_text_view_get_buffer (textview);
436 if (buffer) {
437 if (((get_state (buffer) & E_BUFFER_TAGGER_STATE_CTRL_DOWN) != 0) != (ctrl_is_down != FALSE)) {
438 update_state (buffer, E_BUFFER_TAGGER_STATE_CTRL_DOWN, ctrl_is_down != FALSE);
439 }
440
441 gdk_window_get_pointer (gtk_text_view_get_window (textview, GTK_TEXT_WINDOW_WIDGET), &x, &y, NULL);
442 gtk_text_view_window_to_buffer_coords (textview, GTK_TEXT_WINDOW_WIDGET, x, y, &x, &y);
443 update_mouse_cursor (textview, x, y);
444 }
445 }
446
447 /* Links can also be activated by clicking. */
448 static gboolean
449 textview_event_after (GtkTextView *textview,
450 GdkEvent *ev)
451 {
452 GtkTextIter start, end, iter;
453 GtkTextBuffer *buffer;
454 GdkEventButton *event;
455 gint x, y;
456 GdkModifierType mt = 0;
457
458 g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
459
460 if (ev->type == GDK_KEY_PRESS || ev->type == GDK_KEY_RELEASE) {
461 GdkEventKey *event_key = (GdkEventKey *) ev;
462
463 switch (event_key->keyval) {
464 case GDK_KEY_Control_L:
465 case GDK_KEY_Control_R:
466 update_ctrl_state (textview, ev->type == GDK_KEY_PRESS);
467 break;
468 }
469
470 return FALSE;
471 }
472
473 if (!gdk_event_get_state (ev, &mt)) {
474 GdkWindow *w = gtk_widget_get_parent_window (GTK_WIDGET (textview));
475
476 if (w)
477 gdk_window_get_pointer (w, NULL, NULL, &mt);
478 }
479
480 update_ctrl_state (textview, (mt & GDK_CONTROL_MASK) != 0);
481
482 if (ev->type != GDK_BUTTON_RELEASE)
483 return FALSE;
484
485 event = (GdkEventButton *) ev;
486
487 if (event->button != 1 || (event->state & GDK_CONTROL_MASK) == 0)
488 return FALSE;
489
490 buffer = gtk_text_view_get_buffer (textview);
491
492 /* we shouldn't follow a link if the user has selected something */
493 gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
494 if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end))
495 return FALSE;
496
497 gtk_text_view_window_to_buffer_coords (
498 textview,
499 GTK_TEXT_WINDOW_WIDGET,
500 event->x, event->y, &x, &y);
501
502 gtk_text_view_get_iter_at_location (textview, &iter, x, y);
503
504 invoke_link_if_present (buffer, &iter);
505 update_mouse_cursor (textview, x, y);
506
507 return FALSE;
508 }
509
510 static gboolean
511 textview_motion_notify_event (GtkTextView *textview,
512 GdkEventMotion *event)
513 {
514 gint x, y;
515
516 g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
517
518 gtk_text_view_window_to_buffer_coords (
519 textview,
520 GTK_TEXT_WINDOW_WIDGET,
521 event->x, event->y, &x, &y);
522
523 update_mouse_cursor (textview, x, y);
524
525 return FALSE;
526 }
527
528 static gboolean
529 textview_visibility_notify_event (GtkTextView *textview,
530 GdkEventVisibility *event)
531 {
532 gint wx, wy, bx, by;
533
534 g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
535
536 gdk_window_get_pointer (gtk_text_view_get_window (textview, GTK_TEXT_WINDOW_WIDGET), &wx, &wy, NULL);
537
538 gtk_text_view_window_to_buffer_coords (
539 textview,
540 GTK_TEXT_WINDOW_WIDGET,
541 wx, wy, &bx, &by);
542
543 update_mouse_cursor (textview, bx, by);
544
545 return FALSE;
546 }
547
548 void
549 e_buffer_tagger_connect (GtkTextView *textview)
550 {
551 GtkTextBuffer *buffer;
552 GtkTextTagTable *tag_table;
553 GtkTextTag *tag;
554
555 init_magic_links ();
556
557 g_return_if_fail (textview != NULL);
558 g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
559
560 buffer = gtk_text_view_get_buffer (textview);
561 tag_table = gtk_text_buffer_get_tag_table (buffer);
562 tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
563
564 /* if tag is there already, then it is connected, thus claim */
565 g_return_if_fail (tag == NULL);
566
567 gtk_text_buffer_create_tag (
568 buffer, E_BUFFER_TAGGER_LINK_TAG,
569 "foreground", "blue",
570 "underline", PANGO_UNDERLINE_SINGLE,
571 NULL);
572
573 set_state (buffer, E_BUFFER_TAGGER_STATE_NONE);
574
575 g_signal_connect (
576 buffer, "insert-text",
577 G_CALLBACK (buffer_insert_text), NULL);
578 g_signal_connect (
579 buffer, "delete-range",
580 G_CALLBACK (buffer_delete_range), NULL);
581 g_signal_connect (
582 buffer, "notify::cursor-position",
583 G_CALLBACK (buffer_cursor_position), NULL);
584
585 gtk_widget_set_has_tooltip (GTK_WIDGET (textview), TRUE);
586
587 g_signal_connect (
588 textview, "query-tooltip",
589 G_CALLBACK (textview_query_tooltip), NULL);
590 g_signal_connect (
591 textview, "key-press-event",
592 G_CALLBACK (textview_key_press_event), NULL);
593 g_signal_connect (
594 textview, "event-after",
595 G_CALLBACK (textview_event_after), NULL);
596 g_signal_connect (
597 textview, "motion-notify-event",
598 G_CALLBACK (textview_motion_notify_event), NULL);
599 g_signal_connect (
600 textview, "visibility-notify-event",
601 G_CALLBACK (textview_visibility_notify_event), NULL);
602 }
603
604 void
605 e_buffer_tagger_disconnect (GtkTextView *textview)
606 {
607 GtkTextBuffer *buffer;
608 GtkTextTagTable *tag_table;
609 GtkTextTag *tag;
610
611 g_return_if_fail (textview != NULL);
612 g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
613
614 buffer = gtk_text_view_get_buffer (textview);
615 tag_table = gtk_text_buffer_get_tag_table (buffer);
616 tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
617
618 /* if tag is not there, then it is not connected, thus claim */
619 g_return_if_fail (tag != NULL);
620
621 gtk_text_tag_table_remove (tag_table, tag);
622
623 set_state (buffer, E_BUFFER_TAGGER_STATE_NONE);
624
625 g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_insert_text), NULL);
626 g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_delete_range), NULL);
627 g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_cursor_position), NULL);
628
629 gtk_widget_set_has_tooltip (GTK_WIDGET (textview), FALSE);
630
631 g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_query_tooltip), NULL);
632 g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_key_press_event), NULL);
633 g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_event_after), NULL);
634 g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_motion_notify_event), NULL);
635 g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_visibility_notify_event), NULL);
636 }
637
638 void
639 e_buffer_tagger_update_tags (GtkTextView *textview)
640 {
641 GtkTextBuffer *buffer;
642 GtkTextTagTable *tag_table;
643 GtkTextTag *tag;
644
645 g_return_if_fail (textview != NULL);
646 g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
647
648 buffer = gtk_text_view_get_buffer (textview);
649 tag_table = gtk_text_buffer_get_tag_table (buffer);
650 tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
651
652 /* if tag is not there, then it is not connected, thus claim */
653 g_return_if_fail (tag != NULL);
654
655 update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL | E_BUFFER_TAGGER_STATE_CHANGED, FALSE);
656
657 markup_text (buffer);
658 }