commit 674579ef1d393ca7adef50160fcf3dc1a6ee1cb3
Author: Milan Crha
Date:   Mon Aug 3 21:32:36 2009 +0200

    Bug #562512 - Make hyperlinks clickable in Memos, Tasks, and Calendar

diff --git a/calendar/gui/alarm-notify/alarm-notify-dialog.c b/calendar/gui/alarm-notify/alarm-notify-dialog.c
index e499e43..4efc863 100644
--- a/calendar/gui/alarm-notify/alarm-notify-dialog.c
+++ b/calendar/gui/alarm-notify/alarm-notify-dialog.c
@@ -33,6 +33,7 @@
 #include "config-data.h"
 #include "util.h"
 #include "e-util/e-util-private.h"
+#include "misc/e-buffer-tagger.h"
 
 enum {
 	ALARM_DISPLAY_COLUMN,
@@ -286,6 +287,8 @@ notified_alarms_dialog_new (void)
 		return NULL;
 	}
 
+	e_buffer_tagger_connect (GTK_TEXT_VIEW (an->description));
+
 	gtk_tree_view_set_model (GTK_TREE_VIEW(an->treeview), model);
 
 	gtk_window_set_keep_above (GTK_WINDOW (an->dialog), TRUE);
@@ -443,6 +446,7 @@ fill_in_labels (AlarmNotify *an, const gchar *summary, const gchar *description,
 	gtk_text_buffer_set_text (buffer, description, -1);
 	gtk_text_view_set_buffer (GTK_TEXT_VIEW (an->description), buffer);
 	gtk_label_set_text (GTK_LABEL (an->location), location);
+	e_buffer_tagger_update_tags (GTK_TEXT_VIEW (an->description));
 	g_object_unref (table);
 	g_object_unref (buffer);
 }
diff --git a/calendar/gui/dialogs/event-page.c b/calendar/gui/dialogs/event-page.c
index 7a59500..2812961 100644
--- a/calendar/gui/dialogs/event-page.c
+++ b/calendar/gui/dialogs/event-page.c
@@ -42,6 +42,7 @@
 #include "e-util/e-popup.h"
 #include "misc/e-dateedit.h"
 #include "misc/e-send-options.h"
+#include "misc/e-buffer-tagger.h"
 #include <libecal/e-cal-time-util.h>
 #include "../calendar-config.h"
 #include "../e-timezone-entry.h"
@@ -481,6 +482,7 @@ clear_widgets (EventPage *epage)
 	e_dialog_editable_set (priv->summary, NULL);
 	e_dialog_editable_set (priv->location, NULL);
 	gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->description)), "", 0);
+	e_buffer_tagger_update_tags (GTK_TEXT_VIEW (priv->description));
 
 	/* Start and end times */
 	g_signal_handlers_block_matched (priv->start_time, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, epage);
@@ -969,8 +971,12 @@ event_page_fill_widgets (CompEditorPage *page, ECalComponent *comp)
 		dtext = l->data;
 		gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->description)),
 					  dtext->value ? dtext->value : "", -1);
+	} else {
+		gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->description)),
+					  "", 0);
 	}
 	e_cal_component_free_text_list (l);
+	e_buffer_tagger_update_tags (GTK_TEXT_VIEW (priv->description));
 
 	e_cal_get_cal_address (client, &backend_addr, NULL);
 	set_subscriber_info_string (epage, backend_addr);
@@ -2860,6 +2866,8 @@ init_widgets (EventPage *epage)
 
 	gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (priv->description), GTK_WRAP_WORD);
 
+	e_buffer_tagger_connect (GTK_TEXT_VIEW (priv->description));
+
 	/* Start and end times */
 	g_signal_connect((priv->start_time), "changed",
 			    G_CALLBACK (start_date_changed_cb), epage);
diff --git a/calendar/gui/dialogs/memo-page.c b/calendar/gui/dialogs/memo-page.c
index 83cd00a..53c5c9b 100644
--- a/calendar/gui/dialogs/memo-page.c
+++ b/calendar/gui/dialogs/memo-page.c
@@ -40,6 +40,7 @@
 #include <libedataserverui/e-name-selector-entry.h>
 #include <libedataserverui/e-name-selector-list.h>
 #include <widgets/misc/e-dateedit.h>
+#include "misc/e-buffer-tagger.h"
 
 #include "common/authentication.h"
 #include "e-util/e-dialog-widgets.h"
@@ -124,6 +125,7 @@ clear_widgets (MemoPage *mpage)
 	view = GTK_TEXT_VIEW (mpage->priv->memo_content);
 	buffer = gtk_text_view_get_buffer (view);
 	gtk_text_buffer_set_text (buffer, "", 0);
+	e_buffer_tagger_update_tags (view);
 
 	/* Classification */
 	editor = comp_editor_page_get_editor (COMP_EDITOR_PAGE (mpage));
@@ -228,6 +230,7 @@ memo_page_fill_widgets (CompEditorPage *page,
 					  "", 0);
 	}
 	e_cal_component_free_text_list (l);
+	e_buffer_tagger_update_tags (GTK_TEXT_VIEW (priv->memo_content));
 
 	/* Start Date. */
 	e_cal_component_get_dtstart (comp, &d);
@@ -964,6 +967,7 @@ init_widgets (MemoPage *mpage)
 	view = GTK_TEXT_VIEW (priv->memo_content);
 	buffer = gtk_text_view_get_buffer (view);
 	gtk_text_view_set_wrap_mode (view, GTK_WRAP_WORD);
+	e_buffer_tagger_connect (view);
 
 	/* Categories button */
 	g_signal_connect(
diff --git a/calendar/gui/dialogs/task-page.c b/calendar/gui/dialogs/task-page.c
index 8b5c028..ce3c6ee 100644
--- a/calendar/gui/dialogs/task-page.c
+++ b/calendar/gui/dialogs/task-page.c
@@ -37,6 +37,7 @@
 #include <libedataserverui/e-category-completion.h>
 #include <libedataserverui/e-source-combo-box.h>
 #include <misc/e-dateedit.h>
+#include "misc/e-buffer-tagger.h"
 #include <e-util/e-dialog-utils.h>
 #include "common/authentication.h"
 #include "e-util/e-popup.h"
@@ -265,6 +266,7 @@ clear_widgets (TaskPage *tpage)
 	/* Summary, description */
 	e_dialog_editable_set (priv->summary, NULL);
 	gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (priv->description)), "", 0);
+	e_buffer_tagger_update_tags (GTK_TEXT_VIEW (priv->description));
 
 	/* Start, due times */
 	e_date_edit_set_time (E_DATE_EDIT (priv->start_date), 0);
@@ -514,6 +516,7 @@ task_page_fill_widgets (CompEditorPage *page, ECalComponent *comp)
 					  "", 0);
 	}
 	e_cal_component_free_text_list (l);
+	e_buffer_tagger_update_tags (GTK_TEXT_VIEW (priv->description));
 
 	default_zone = calendar_config_get_icaltimezone ();
 
@@ -1892,6 +1895,8 @@ init_widgets (TaskPage *tpage)
 
 	gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (priv->description), GTK_WRAP_WORD);
 
+	e_buffer_tagger_connect (GTK_TEXT_VIEW (priv->description));
+
 	/* Dates */
 	g_signal_connect((priv->start_date), "changed",
 			    G_CALLBACK (date_changed_cb), tpage);
diff --git a/widgets/misc/Makefile.am b/widgets/misc/Makefile.am
index d6689ef..5ac1975 100644
--- a/widgets/misc/Makefile.am
+++ b/widgets/misc/Makefile.am
@@ -44,6 +44,7 @@ widgetsinclude_HEADERS =			\
 	e-attachment-store.h			\
 	e-attachment-tree-view.h		\
 	e-attachment-view.h			\
+	e-buffer-tagger.h			\
 	e-spinner.c				\
 	e-spinner.h				\
 	e-calendar.h				\
@@ -103,6 +104,7 @@ libemiscwidgets_la_SOURCES =			\
 	e-attachment-store.c			\
 	e-attachment-tree-view.c		\
 	e-attachment-view.c			\
+	e-buffer-tagger.c			\
 	e-calendar-item.c			\
 	e-cell-date-edit.c			\
 	e-cell-percent.c			\
diff --git a/widgets/misc/e-buffer-tagger.c b/widgets/misc/e-buffer-tagger.c
new file mode 100644
index 0000000..ec5f3a1
--- /dev/null
+++ b/widgets/misc/e-buffer-tagger.c
@@ -0,0 +1,534 @@
+/*
+ * e-buffer-tagger.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ */
+
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#include <regex.h>
+#include <string.h>
+#include <ctype.h>
+#include "e-util/e-util.h"
+#include "e-buffer-tagger.h"
+
+enum EBufferTaggerState
+{
+	E_BUFFER_TAGGER_STATE_NONE        = 0,
+	E_BUFFER_TAGGER_STATE_INSDEL      = 1 << 0, /* set when was called insert or delete of a text */
+	E_BUFFER_TAGGER_STATE_CHANGED     = 1 << 1, /* remark of the buffer is scheduled */
+	E_BUFFER_TAGGER_STATE_IS_HOVERING = 1 << 2, /* mouse is over the link */
+	E_BUFFER_TAGGER_STATE_CTRL_DOWN   = 1 << 3  /* Ctrl key is down */
+};
+
+#define E_BUFFER_TAGGER_DATA_STATE "EBufferTagger::state"
+#define E_BUFFER_TAGGER_LINK_TAG   "EBufferTagger::link"
+
+struct _MagicInsertMatch
+{
+	const gchar *regex;
+	regex_t *preg;
+	const gchar *prefix;
+};
+
+typedef struct _MagicInsertMatch MagicInsertMatch;
+
+static MagicInsertMatch mim [] = {
+	/* prefixed expressions */
+	{ "(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_$.+!*(),;:@%&=?/~#']*[^]'.}>\\) ,?!;:\"]?)?", NULL, NULL },
+	{ "(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 },
+	{ "mailto:[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, NULL },
+	/* not prefixed expression */
+	{ "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) ,?!;:\"]?)?", NULL, "http://" },
+	{ "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) ,?!;:\"]?)?", NULL, "ftp://" },
+	{ "[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, "mailto:" }
+};
+
+static void
+init_magic_links (void)
+{
+	static gboolean done = FALSE;
+	gint i;
+
+	if (done)
+		return;
+
+	for (i = 0; i < G_N_ELEMENTS (mim); i++) {
+		mim [i].preg = g_new0 (regex_t, 1);
+		if (regcomp (mim [i].preg, mim [i].regex, REG_EXTENDED | REG_ICASE)) {
+			/* error */
+			g_free (mim [i].preg);
+			mim [i].preg = 0;
+		}
+	}
+}
+
+static void
+markup_text (GtkTextBuffer *buffer)
+{
+	GtkTextIter start, end;
+	gchar *text;
+	int i;
+	regmatch_t pmatch [2];
+	gboolean any;
+	const char *str;
+	gint offset = 0;
+
+	g_return_if_fail (buffer != NULL);
+
+	gtk_text_buffer_get_start_iter (buffer, &start);
+	gtk_text_buffer_get_end_iter (buffer, &end);
+	gtk_text_buffer_remove_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end);
+	text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+
+	str = text;
+	any = TRUE;
+	while (any) {
+		any = FALSE;
+		for (i = 0; i < G_N_ELEMENTS (mim); i++) {
+			if (mim [i].preg && !regexec (mim [i].preg, str, 2, pmatch, 0)) {
+				gtk_text_buffer_get_iter_at_offset (buffer, &start, offset + pmatch [0].rm_so);
+				gtk_text_buffer_get_iter_at_offset (buffer, &end, offset + pmatch [0].rm_eo - 1);
+				gtk_text_buffer_apply_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end);
+
+				any = TRUE;
+				str += pmatch [0].rm_eo;
+				offset += pmatch [0].rm_eo;
+				break;
+			}
+		}
+	}
+
+	g_free (text);
+}
+
+static guint32
+get_state (GtkTextBuffer *buffer)
+{
+	g_return_val_if_fail (buffer != NULL, E_BUFFER_TAGGER_STATE_NONE);
+	g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), E_BUFFER_TAGGER_STATE_NONE);
+
+	return GPOINTER_TO_INT (g_object_get_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE));
+}
+
+static void
+set_state (GtkTextBuffer *buffer, guint32 state)
+{
+	g_object_set_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE, GINT_TO_POINTER (state));
+}
+
+static void
+update_state (GtkTextBuffer *buffer, guint32 value, gboolean do_set)
+{
+	guint32 state;
+
+	g_return_if_fail (buffer != NULL);
+	g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+
+	state = get_state (buffer);
+
+	if (do_set)
+		state = state | value;
+	else
+		state = state & (~value);
+
+	set_state (buffer, state);
+}
+
+static gboolean
+get_tag_bounds (GtkTextIter *iter, GtkTextTag *tag, GtkTextIter *start, GtkTextIter *end)
+{
+	gboolean res = FALSE;
+
+	g_return_val_if_fail (iter != NULL, FALSE);
+	g_return_val_if_fail (tag != NULL, FALSE);
+	g_return_val_if_fail (start != NULL, FALSE);
+	g_return_val_if_fail (end != NULL, FALSE);
+
+	if (gtk_text_iter_has_tag (iter, tag)) {
+		*start = *iter;
+		*end = *iter;
+
+		if (!gtk_text_iter_begins_tag (start, tag))
+			gtk_text_iter_backward_to_tag_toggle (start, tag);
+
+		if (!gtk_text_iter_ends_tag (end, tag))
+			gtk_text_iter_forward_to_tag_toggle (end, tag);
+
+		res = TRUE;
+	}
+
+	return res;
+}
+
+static gboolean
+invoke_link_if_present (GtkTextBuffer *buffer, GtkTextIter *iter)
+{
+	GtkTextTagTable *tag_table;
+	GtkTextTag *tag;
+	GtkTextIter start, end;
+	gboolean res = FALSE;
+
+	g_return_val_if_fail (buffer != NULL, FALSE);
+
+	tag_table = gtk_text_buffer_get_tag_table (buffer);
+	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
+	g_return_val_if_fail (tag != NULL, FALSE);
+
+	if (get_tag_bounds (iter, tag, &start, &end)) {
+		gchar *text;
+
+		text = gtk_text_iter_get_text (&start, &end);
+
+		res = text && *text;
+		if (res)
+			e_show_uri (NULL, text);
+
+		g_free (text);
+	}
+
+	return res;
+}
+
+static void
+remove_tag_if_present (GtkTextBuffer *buffer, GtkTextIter *where)
+{
+	GtkTextTagTable *tag_table;
+	GtkTextTag *tag;
+	GtkTextIter start, end;
+
+	g_return_if_fail (buffer != NULL);
+	g_return_if_fail (where != NULL);
+
+	tag_table = gtk_text_buffer_get_tag_table (buffer);
+	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
+	g_return_if_fail (tag != NULL);
+
+	if (get_tag_bounds (where, tag, &start, &end))
+		gtk_text_buffer_remove_tag (buffer, tag, &start, &end);
+}
+
+static void
+buffer_insert_text (GtkTextBuffer *buffer, GtkTextIter *location, gchar *text, gint len, gpointer user_data)
+{
+	update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE);
+	remove_tag_if_present (buffer, location);
+}
+
+static void
+buffer_delete_range (GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end, gpointer user_data)
+{
+	update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE);
+	remove_tag_if_present (buffer, start);
+	remove_tag_if_present (buffer, end);
+}
+
+static void
+buffer_cursor_position (GtkTextBuffer *buffer, gpointer user_data)
+{
+	guint32 state;
+
+	state = get_state (buffer);
+	if (state & E_BUFFER_TAGGER_STATE_INSDEL) {
+		state = (state & (~E_BUFFER_TAGGER_STATE_INSDEL)) | E_BUFFER_TAGGER_STATE_CHANGED;
+	} else {
+		if (state & E_BUFFER_TAGGER_STATE_CHANGED) {
+			markup_text (buffer);
+		}
+
+		state = state & (~ (E_BUFFER_TAGGER_STATE_CHANGED | E_BUFFER_TAGGER_STATE_INSDEL));
+	}
+
+	set_state (buffer, state);
+}
+
+static void
+update_mouse_cursor (GtkTextView *text_view, gint x, gint y)
+{
+	static GdkCursor *hand_cursor = NULL;
+	static GdkCursor *regular_cursor = NULL;
+	gboolean hovering = FALSE, hovering_over_link = FALSE;
+	guint32 state;
+	GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view);
+	GtkTextTagTable *tag_table;
+	GtkTextTag *tag;
+
+	if (!hand_cursor) {
+		hand_cursor = gdk_cursor_new (GDK_HAND2);
+		regular_cursor = gdk_cursor_new (GDK_XTERM);
+	}
+
+	g_return_if_fail (buffer != NULL);
+
+	tag_table = gtk_text_buffer_get_tag_table (buffer);
+	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
+	g_return_if_fail (tag != NULL);
+
+	state = get_state (buffer);
+
+	hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING) != 0;
+
+	if ((state & E_BUFFER_TAGGER_STATE_CTRL_DOWN) == 0) {
+		hovering = FALSE;
+	} else {
+		GtkTextIter iter;
+
+		gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
+  
+		hovering = gtk_text_iter_has_tag (&iter, tag);
+	}
+
+	if (hovering != hovering_over_link) {
+		update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING, hovering);
+
+		if (hovering)
+			gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), hand_cursor);
+		else
+			gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), regular_cursor);
+
+		gdk_window_get_pointer (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_WIDGET), NULL, NULL, NULL);
+	}
+}
+
+/* Links can be activated by pressing Enter. */
+static gboolean
+textview_key_press_event (GtkWidget *text_view, GdkEventKey *event)
+{
+	GtkTextIter iter;
+	GtkTextBuffer *buffer;
+
+	if ((event->state & GDK_CONTROL_MASK) == 0)
+		return FALSE;
+
+	switch (event->keyval) {
+	case GDK_Return: 
+	case GDK_KP_Enter:
+		buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
+		gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer));
+		if (invoke_link_if_present (buffer, &iter))
+			return TRUE;
+		break;
+
+	default:
+		break;
+	}
+
+	return FALSE;
+}
+
+static void
+update_ctrl_state (GtkTextView *textview, gboolean ctrl_is_down)
+{
+	GtkTextBuffer *buffer;
+	gint x, y;
+
+	buffer = gtk_text_view_get_buffer (textview);
+	if (buffer) {
+		if (((get_state (buffer) & E_BUFFER_TAGGER_STATE_CTRL_DOWN) != 0) != (ctrl_is_down != FALSE)) {
+			update_state (buffer, E_BUFFER_TAGGER_STATE_CTRL_DOWN, ctrl_is_down != FALSE);
+		}
+
+		gdk_window_get_pointer (gtk_text_view_get_window (textview, GTK_TEXT_WINDOW_WIDGET), &x, &y, NULL);
+		gtk_text_view_window_to_buffer_coords (textview, GTK_TEXT_WINDOW_WIDGET, x, y, &x, &y);
+		update_mouse_cursor (textview, x, y);
+	}
+}
+
+/* Links can also be activated by clicking. */
+static gboolean
+textview_event_after (GtkTextView *textview, GdkEvent  *ev)
+{
+	GtkTextIter start, end, iter;
+	GtkTextBuffer *buffer;
+	GdkEventButton *event;
+	gint x, y;
+	GdkModifierType mt = 0;
+
+	g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
+
+	if (ev->type == GDK_KEY_PRESS || ev->type == GDK_KEY_RELEASE) {
+		GdkEventKey *event_key = (GdkEventKey *)ev;
+
+		switch (event_key->keyval) {
+		case GDK_Control_L:
+		case GDK_Control_R:
+			update_ctrl_state (textview, ev->type == GDK_KEY_PRESS);
+			break;
+		}
+
+		return FALSE;
+	}
+
+	if (!gdk_event_get_state (ev, &mt)) {
+		GdkWindow *w = gtk_widget_get_parent_window (GTK_WIDGET (textview));
+
+		if (w)
+			gdk_window_get_pointer (w, NULL, NULL, &mt);
+	}
+
+	update_ctrl_state (textview, (mt & GDK_CONTROL_MASK) != 0);
+
+	if (ev->type != GDK_BUTTON_RELEASE)
+		return FALSE;
+
+	event = (GdkEventButton *)ev;
+
+	if (event->button != 1 || (event->state & GDK_CONTROL_MASK) == 0)
+		return FALSE;
+
+	buffer = gtk_text_view_get_buffer (textview);
+
+	/* we shouldn't follow a link if the user has selected something */
+	gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
+	if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end))
+		return FALSE;
+
+	gtk_text_view_window_to_buffer_coords (textview,
+						GTK_TEXT_WINDOW_WIDGET,
+						event->x, event->y, &x, &y);
+
+	gtk_text_view_get_iter_at_location (textview, &iter, x, y);
+
+	invoke_link_if_present (buffer, &iter);
+	update_mouse_cursor (textview, x, y);
+
+	return FALSE;
+}
+
+static gboolean
+textview_motion_notify_event (GtkTextView *textview, GdkEventMotion *event)
+{
+	gint x, y;
+
+	g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
+
+	gtk_text_view_window_to_buffer_coords (textview,
+                GTK_TEXT_WINDOW_WIDGET,
+                event->x, event->y, &x, &y);
+
+	update_mouse_cursor (textview, x, y);
+
+	return FALSE;
+}
+
+static gboolean
+textview_visibility_notify_event (GtkTextView *textview, GdkEventVisibility *event)
+{
+	gint wx, wy, bx, by;
+
+	g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
+
+	gdk_window_get_pointer (gtk_text_view_get_window (textview, GTK_TEXT_WINDOW_WIDGET), &wx, &wy, NULL);
+
+	gtk_text_view_window_to_buffer_coords (textview, 
+		GTK_TEXT_WINDOW_WIDGET,
+		wx, wy, &bx, &by);
+
+	update_mouse_cursor (textview, bx, by);
+
+	return FALSE;
+}
+
+void
+e_buffer_tagger_connect (GtkTextView *textview)
+{
+	GtkTextBuffer *buffer;
+	GtkTextTagTable *tag_table;
+	GtkTextTag *tag;
+
+	init_magic_links ();
+
+	g_return_if_fail (textview != NULL);
+	g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
+
+	buffer = gtk_text_view_get_buffer (textview);
+	tag_table = gtk_text_buffer_get_tag_table (buffer);
+	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
+
+	/* if tag is there already, then it is connected, thus claim */
+	g_return_if_fail (tag == NULL);
+      
+	gtk_text_buffer_create_tag (buffer, E_BUFFER_TAGGER_LINK_TAG,
+		"foreground", "blue",
+		"underline", PANGO_UNDERLINE_SINGLE,
+		NULL);
+
+	set_state (buffer, E_BUFFER_TAGGER_STATE_NONE);
+
+	g_signal_connect (buffer, "insert-text", G_CALLBACK (buffer_insert_text), NULL);
+	g_signal_connect (buffer, "delete-range", G_CALLBACK (buffer_delete_range), NULL);
+	g_signal_connect (buffer, "notify::cursor-position", G_CALLBACK (buffer_cursor_position), NULL);
+
+	g_signal_connect (textview, "key-press-event", G_CALLBACK (textview_key_press_event), NULL);
+	g_signal_connect (textview, "event-after", G_CALLBACK (textview_event_after), NULL);
+	g_signal_connect (textview, "motion-notify-event", G_CALLBACK (textview_motion_notify_event), NULL);
+	g_signal_connect (textview, "visibility-notify-event", G_CALLBACK (textview_visibility_notify_event), NULL);
+}
+
+void
+e_buffer_tagger_disconnect (GtkTextView *textview)
+{
+	GtkTextBuffer *buffer;
+	GtkTextTagTable *tag_table;
+	GtkTextTag *tag;
+
+	g_return_if_fail (textview != NULL);
+	g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
+
+	buffer = gtk_text_view_get_buffer (textview);
+	tag_table = gtk_text_buffer_get_tag_table (buffer);
+	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
+
+	/* if tag is not there, then it is not connected, thus claim */
+	g_return_if_fail (tag != NULL);
+
+	gtk_text_tag_table_remove (tag_table, tag);
+
+	set_state (buffer, E_BUFFER_TAGGER_STATE_NONE);
+
+	g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_insert_text), NULL);
+	g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_delete_range), NULL);
+	g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_cursor_position), NULL);
+
+	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_key_press_event), NULL);
+	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_event_after), NULL);
+	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_motion_notify_event), NULL);
+	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_visibility_notify_event), NULL);
+}
+
+void
+e_buffer_tagger_update_tags (GtkTextView *textview)
+{
+	GtkTextBuffer *buffer;
+	GtkTextTagTable *tag_table;
+	GtkTextTag *tag;
+
+	g_return_if_fail (textview != NULL);
+	g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
+
+	buffer = gtk_text_view_get_buffer (textview);
+	tag_table = gtk_text_buffer_get_tag_table (buffer);
+	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
+
+	/* if tag is not there, then it is not connected, thus claim */
+	g_return_if_fail (tag != NULL);
+
+	update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL | E_BUFFER_TAGGER_STATE_CHANGED, FALSE);
+
+	markup_text (buffer);
+}
diff --git a/widgets/misc/e-buffer-tagger.h b/widgets/misc/e-buffer-tagger.h
new file mode 100644
index 0000000..86e6710
--- /dev/null
+++ b/widgets/misc/e-buffer-tagger.h
@@ -0,0 +1,35 @@
+/*
+ * e-buffer-tagger.h
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ */
+
+#ifndef E_BUFFER_TAGGER_H
+#define E_BUFFER_TAGGER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void e_buffer_tagger_connect     (GtkTextView *textview);
+void e_buffer_tagger_disconnect  (GtkTextView *textview);
+void e_buffer_tagger_update_tags (GtkTextView *textview);
+
+G_END_DECLS
+
+#endif /* E_BUFFER_TAGGER_H */
