1 /*
2  * e-buffer-tagger.c
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11  * for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program; if not, see <http://www.gnu.org/licenses/>.
15  *
16  *
17  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
18  *
19  */
20 
21 #include "evolution-data-server-config.h"
22 
23 #include <gtk/gtk.h>
24 #include <gdk/gdkkeysyms.h>
25 #include <glib/gi18n-lib.h>
26 #include <regex.h>
27 #include <string.h>
28 #include <ctype.h>
29 
30 #include "e-buffer-tagger.h"
31 
32 static void
e_show_uri(GtkWindow * parent,const gchar * uri)33 e_show_uri (GtkWindow *parent,
34 	    const gchar *uri)
35 {
36 	GtkWidget *dialog;
37 	GdkScreen *screen = NULL;
38 	gchar *scheme;
39 	GError *error = NULL;
40 	guint32 timestamp;
41 	gboolean success;
42 
43 	g_return_if_fail (uri != NULL);
44 
45 	timestamp = gtk_get_current_event_time ();
46 
47 	if (parent != NULL)
48 		screen = gtk_widget_get_screen (GTK_WIDGET (parent));
49 
50 	scheme = g_uri_parse_scheme (uri);
51 
52 	if (!scheme || !*scheme) {
53 		gchar *schemed_uri;
54 
55 		schemed_uri = g_strconcat ("http://", uri, NULL);
56 		success = gtk_show_uri (screen, schemed_uri, timestamp, &error);
57 		g_free (schemed_uri);
58 	} else {
59 		success = gtk_show_uri (screen, uri, timestamp, &error);
60 	}
61 
62 	g_free (scheme);
63 
64 	if (success)
65 		return;
66 
67 	dialog = gtk_message_dialog_new_with_markup (
68 		parent, GTK_DIALOG_DESTROY_WITH_PARENT,
69 		GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
70 		"<big><b>%s</b></big>",
71 		_("Could not open the link."));
72 
73 	gtk_message_dialog_format_secondary_text (
74 		GTK_MESSAGE_DIALOG (dialog), "%s", error->message);
75 
76 	gtk_dialog_run (GTK_DIALOG (dialog));
77 
78 	gtk_widget_destroy (dialog);
79 	g_error_free (error);
80 }
81 
82 enum EBufferTaggerState {
83 	E_BUFFER_TAGGER_STATE_NONE = 0,
84 	E_BUFFER_TAGGER_STATE_INSDEL = 1 << 0, /* set when was called insert or delete of a text */
85 	E_BUFFER_TAGGER_STATE_CHANGED = 1 << 1, /* remark of the buffer is scheduled */
86 	E_BUFFER_TAGGER_STATE_IS_HOVERING = 1 << 2, /* mouse is over the link */
87 	E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP = 1 << 3, /* mouse is over the link and the tooltip can be shown */
88 	E_BUFFER_TAGGER_STATE_CTRL_DOWN = 1 << 4  /* Ctrl key is down */
89 };
90 
91 #define E_BUFFER_TAGGER_DATA_STATE "EBufferTagger::state"
92 #define E_BUFFER_TAGGER_LINK_TAG   "EBufferTagger::link"
93 
94 struct _MagicInsertMatch {
95 	const gchar *regex;
96 	regex_t *preg;
97 	const gchar *prefix;
98 };
99 
100 typedef struct _MagicInsertMatch MagicInsertMatch;
101 
102 static MagicInsertMatch mim[] = {
103 	/* prefixed expressions */
104 	{ "(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 },
105 	{ "(sip|h323|callto|tel):([-_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 },
106 	{ "mailto:[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, NULL },
107 	/* not prefixed expression */
108 	{ "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "http://" },
109 	{ "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "ftp://" },
110 	{ "[-_a-z0-9.'\\+]+@[-_a-z0-9.%=?]+", NULL, "mailto:" }
111 };
112 
113 static void
init_magic_links(void)114 init_magic_links (void)
115 {
116 	static gboolean done = FALSE;
117 	gint i;
118 
119 	if (done)
120 		return;
121 
122 	done = TRUE;
123 
124 	for (i = 0; i < G_N_ELEMENTS (mim); i++) {
125 		mim[i].preg = g_new0 (regex_t, 1);
126 		if (regcomp (mim[i].preg, mim[i].regex, REG_EXTENDED | REG_ICASE)) {
127 			/* error */
128 			g_free (mim[i].preg);
129 			mim[i].preg = 0;
130 		}
131 	}
132 }
133 
134 static void
markup_text(GtkTextBuffer * buffer)135 markup_text (GtkTextBuffer *buffer)
136 {
137 	GtkTextIter start, end;
138 	gchar *text;
139 	gint i;
140 	regmatch_t pmatch[2];
141 	gboolean any;
142 	const gchar *str;
143 	gint offset = 0;
144 
145 	g_return_if_fail (buffer != NULL);
146 
147 	gtk_text_buffer_get_start_iter (buffer, &start);
148 	gtk_text_buffer_get_end_iter (buffer, &end);
149 	gtk_text_buffer_remove_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end);
150 	text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
151 
152 	str = text;
153 	any = TRUE;
154 	while (any) {
155 		any = FALSE;
156 		for (i = 0; i < G_N_ELEMENTS (mim); i++) {
157 			if (mim[i].preg && !regexec (mim[i].preg, str, 2, pmatch, 0)) {
158 				gint char_so, char_eo, rm_eo;
159 
160 				/* Stop on the angle brackets, which cannot be part of the URL (see RFC 3986 Appendix C) */
161 				for (rm_eo = pmatch[0].rm_eo - 1; rm_eo > pmatch[0].rm_so; rm_eo--) {
162 					if (str[rm_eo] == '<' || str[rm_eo] == '>') {
163 						pmatch[0].rm_eo = rm_eo;
164 						break;
165 					}
166 				}
167 
168 				rm_eo = pmatch[0].rm_eo;
169 
170 				/* URLs are extremely unlikely to end with any
171 				 * punctuation, so strip any trailing
172 				 * punctuation off. Also strip off any closing
173 				 * double-quotes. */
174 				while (rm_eo > pmatch[0].rm_so && strchr (",.:;?!-|}])\">", str[rm_eo - 1])) {
175 					gchar open_bracket = 0, close_bracket = str[rm_eo - 1];
176 
177 					if (close_bracket == ')')
178 						open_bracket = '(';
179 					else if (close_bracket == '}')
180 						open_bracket = '{';
181 					else if (close_bracket == ']')
182 						open_bracket = '[';
183 					else if (close_bracket == '>')
184 						open_bracket = '<';
185 
186 					if (open_bracket != 0) {
187 						const gchar *ptr, *endptr;
188 						gint n_opened = 0, n_closed = 0;
189 
190 						endptr = str + rm_eo;
191 
192 						for (ptr = str + pmatch[0].rm_so; ptr < endptr; ptr++) {
193 							if (*ptr == open_bracket)
194 								n_opened++;
195 							else if (*ptr == close_bracket)
196 								n_closed++;
197 						}
198 
199 						/* The closing bracket can match one inside the URL,
200 						   thus keep it there. */
201 						if (n_opened > 0 && n_opened - n_closed >= 0)
202 							break;
203 					}
204 
205 					rm_eo--;
206 					pmatch[0].rm_eo--;
207 				}
208 
209 				char_so = g_utf8_pointer_to_offset (str, str + pmatch[0].rm_so);
210 				char_eo = g_utf8_pointer_to_offset (str, str + pmatch[0].rm_eo);
211 
212 				gtk_text_buffer_get_iter_at_offset (buffer, &start, offset + char_so);
213 				gtk_text_buffer_get_iter_at_offset (buffer, &end, offset + char_eo);
214 				gtk_text_buffer_apply_tag_by_name (buffer, E_BUFFER_TAGGER_LINK_TAG, &start, &end);
215 
216 				any = TRUE;
217 				str += pmatch[0].rm_eo;
218 				offset += char_eo;
219 				break;
220 			}
221 		}
222 	}
223 
224 	g_free (text);
225 }
226 
227 static void
get_pointer_position(GtkTextView * text_view,gint * x,gint * y)228 get_pointer_position (GtkTextView *text_view,
229                       gint *x,
230                       gint *y)
231 {
232 	GdkWindow *window;
233 	GdkDisplay *display;
234 	GdkDeviceManager *device_manager;
235 	GdkDevice *device;
236 
237 	window = gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_WIDGET);
238 	display = gdk_window_get_display (window);
239 	device_manager = gdk_display_get_device_manager (display);
240 	device = gdk_device_manager_get_client_pointer (device_manager);
241 
242 	gdk_window_get_device_position (window, device, x, y, NULL);
243 }
244 
245 static guint32
get_state(GtkTextBuffer * buffer)246 get_state (GtkTextBuffer *buffer)
247 {
248 	g_return_val_if_fail (buffer != NULL, E_BUFFER_TAGGER_STATE_NONE);
249 	g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), E_BUFFER_TAGGER_STATE_NONE);
250 
251 	return GPOINTER_TO_INT (g_object_get_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE));
252 }
253 
254 static void
set_state(GtkTextBuffer * buffer,guint32 state)255 set_state (GtkTextBuffer *buffer,
256            guint32 state)
257 {
258 	g_object_set_data (G_OBJECT (buffer), E_BUFFER_TAGGER_DATA_STATE, GINT_TO_POINTER (state));
259 }
260 
261 static void
update_state(GtkTextBuffer * buffer,guint32 value,gboolean do_set)262 update_state (GtkTextBuffer *buffer,
263               guint32 value,
264               gboolean do_set)
265 {
266 	guint32 state;
267 
268 	g_return_if_fail (buffer != NULL);
269 	g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
270 
271 	state = get_state (buffer);
272 
273 	if (do_set)
274 		state = state | value;
275 	else
276 		state = state & (~value);
277 
278 	set_state (buffer, state);
279 }
280 
281 static gboolean
get_tag_bounds(GtkTextIter * iter,GtkTextTag * tag,GtkTextIter * start,GtkTextIter * end)282 get_tag_bounds (GtkTextIter *iter,
283                 GtkTextTag *tag,
284                 GtkTextIter *start,
285                 GtkTextIter *end)
286 {
287 	gboolean res = FALSE;
288 
289 	g_return_val_if_fail (iter != NULL, FALSE);
290 	g_return_val_if_fail (tag != NULL, FALSE);
291 	g_return_val_if_fail (start != NULL, FALSE);
292 	g_return_val_if_fail (end != NULL, FALSE);
293 
294 	if (gtk_text_iter_has_tag (iter, tag)) {
295 		*start = *iter;
296 		*end = *iter;
297 
298 		if (!gtk_text_iter_begins_tag (start, tag))
299 			gtk_text_iter_backward_to_tag_toggle (start, tag);
300 
301 		if (!gtk_text_iter_ends_tag (end, tag))
302 			gtk_text_iter_forward_to_tag_toggle (end, tag);
303 
304 		res = TRUE;
305 	}
306 
307 	return res;
308 }
309 
310 static gchar *
get_url_at_iter(GtkTextBuffer * buffer,GtkTextIter * iter)311 get_url_at_iter (GtkTextBuffer *buffer,
312                  GtkTextIter *iter)
313 {
314 	GtkTextTagTable *tag_table;
315 	GtkTextTag *tag;
316 	GtkTextIter start, end;
317 	gchar *url = NULL;
318 
319 	g_return_val_if_fail (buffer != NULL, NULL);
320 
321 	tag_table = gtk_text_buffer_get_tag_table (buffer);
322 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
323 	g_return_val_if_fail (tag != NULL, NULL);
324 
325 	if (get_tag_bounds (iter, tag, &start, &end))
326 		url = gtk_text_iter_get_text (&start, &end);
327 
328 	return url;
329 }
330 
331 static gboolean
invoke_link_if_present(GtkTextBuffer * buffer,GtkTextIter * iter)332 invoke_link_if_present (GtkTextBuffer *buffer,
333                         GtkTextIter *iter)
334 {
335 	gboolean res;
336 	gchar *url;
337 
338 	g_return_val_if_fail (buffer != NULL, FALSE);
339 
340 	url = get_url_at_iter (buffer, iter);
341 
342 	res = url && *url;
343 	if (res)
344 		e_show_uri (NULL, url);
345 
346 	g_free (url);
347 
348 	return res;
349 }
350 
351 static void
remove_tag_if_present(GtkTextBuffer * buffer,GtkTextIter * where)352 remove_tag_if_present (GtkTextBuffer *buffer,
353                        GtkTextIter *where)
354 {
355 	GtkTextTagTable *tag_table;
356 	GtkTextTag *tag;
357 	GtkTextIter start, end;
358 
359 	g_return_if_fail (buffer != NULL);
360 	g_return_if_fail (where != NULL);
361 
362 	tag_table = gtk_text_buffer_get_tag_table (buffer);
363 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
364 	g_return_if_fail (tag != NULL);
365 
366 	if (get_tag_bounds (where, tag, &start, &end))
367 		gtk_text_buffer_remove_tag (buffer, tag, &start, &end);
368 }
369 
370 static void
buffer_insert_text(GtkTextBuffer * buffer,GtkTextIter * location,gchar * text,gint len,gpointer user_data)371 buffer_insert_text (GtkTextBuffer *buffer,
372                     GtkTextIter *location,
373                     gchar *text,
374                     gint len,
375                     gpointer user_data)
376 {
377 	update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE);
378 	remove_tag_if_present (buffer, location);
379 }
380 
381 static void
buffer_delete_range(GtkTextBuffer * buffer,GtkTextIter * start,GtkTextIter * end,gpointer user_data)382 buffer_delete_range (GtkTextBuffer *buffer,
383                      GtkTextIter *start,
384                      GtkTextIter *end,
385                      gpointer user_data)
386 {
387 	update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL, TRUE);
388 	remove_tag_if_present (buffer, start);
389 	remove_tag_if_present (buffer, end);
390 }
391 
392 static void
buffer_cursor_position(GtkTextBuffer * buffer,gpointer user_data)393 buffer_cursor_position (GtkTextBuffer *buffer,
394                         gpointer user_data)
395 {
396 	guint32 state;
397 
398 	state = get_state (buffer);
399 	if (state & E_BUFFER_TAGGER_STATE_INSDEL) {
400 		state = (state & (~E_BUFFER_TAGGER_STATE_INSDEL)) | E_BUFFER_TAGGER_STATE_CHANGED;
401 	} else {
402 		if (state & E_BUFFER_TAGGER_STATE_CHANGED) {
403 			markup_text (buffer);
404 		}
405 
406 		state = state & (~ (E_BUFFER_TAGGER_STATE_CHANGED | E_BUFFER_TAGGER_STATE_INSDEL));
407 	}
408 
409 	set_state (buffer, state);
410 }
411 
412 static void
update_mouse_cursor(GtkTextView * text_view,gint x,gint y)413 update_mouse_cursor (GtkTextView *text_view,
414                      gint x,
415                      gint y)
416 {
417 	static GdkCursor *hand_cursor = NULL;
418 	static GdkCursor *regular_cursor = NULL;
419 	gboolean hovering = FALSE, hovering_over_link = FALSE, hovering_real;
420 	guint32 state;
421 	GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view);
422 	GtkTextTagTable *tag_table;
423 	GtkTextTag *tag;
424 	GtkTextIter iter;
425 
426 	if (!hand_cursor) {
427 		hand_cursor = gdk_cursor_new (GDK_HAND2);
428 		regular_cursor = gdk_cursor_new (GDK_XTERM);
429 	}
430 
431 	g_return_if_fail (buffer != NULL);
432 
433 	tag_table = gtk_text_buffer_get_tag_table (buffer);
434 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
435 	g_return_if_fail (tag != NULL);
436 
437 	state = get_state (buffer);
438 
439 	gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
440 	hovering_real = gtk_text_iter_has_tag (&iter, tag);
441 
442 	hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING) != 0;
443 	if ((state & E_BUFFER_TAGGER_STATE_CTRL_DOWN) == 0) {
444 		hovering = FALSE;
445 	} else {
446 		hovering = hovering_real;
447 	}
448 
449 	if (hovering != hovering_over_link) {
450 		update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING, hovering);
451 
452 		if (hovering && gtk_widget_has_focus (GTK_WIDGET (text_view)))
453 			gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), hand_cursor);
454 		else
455 			gdk_window_set_cursor (gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_TEXT), regular_cursor);
456 
457 		/* XXX Is this necessary?  Appears to be a no-op. */
458 		get_pointer_position (text_view, NULL, NULL);
459 	}
460 
461 	hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0;
462 
463 	if (hovering_real != hovering_over_link) {
464 		update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP, hovering_real);
465 
466 		gtk_widget_trigger_tooltip_query (GTK_WIDGET (text_view));
467 	}
468 }
469 
470 static void
textview_style_updated_cb(GtkWidget * textview,gpointer user_data)471 textview_style_updated_cb (GtkWidget *textview,
472 			   gpointer user_data)
473 {
474 	GtkStyleContext *context;
475 	GtkStateFlags state;
476 	GtkTextBuffer *buffer;
477 	GtkTextTagTable *tag_table;
478 	GtkTextTag *tag;
479 	GdkRGBA rgba;
480 
481 	g_return_if_fail (GTK_IS_WIDGET (textview));
482 
483 	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (textview));
484 	tag_table = gtk_text_buffer_get_tag_table (buffer);
485 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
486 
487 	if (!tag)
488 		return;
489 
490 	context = gtk_widget_get_style_context (textview);
491 
492 	rgba.red = 0;
493 	rgba.green = 0;
494 	rgba.blue = 1;
495 	rgba.alpha = 1;
496 
497 	state = gtk_style_context_get_state (context);
498 	state = state & (~(GTK_STATE_FLAG_VISITED | GTK_STATE_FLAG_LINK));
499 	state = state | GTK_STATE_FLAG_LINK;
500 
501 	gtk_style_context_save (context);
502 	/* Remove the 'view' style, because it can "confuse" some themes */
503 	gtk_style_context_remove_class (context, GTK_STYLE_CLASS_VIEW);
504 	gtk_style_context_set_state (context, state);
505 	gtk_style_context_get_color (context, state, &rgba);
506 	gtk_style_context_restore (context);
507 
508 	g_object_set (G_OBJECT (tag), "foreground-rgba", &rgba, NULL);
509 }
510 
511 static gboolean
textview_query_tooltip(GtkTextView * text_view,gint x,gint y,gboolean keyboard_mode,GtkTooltip * tooltip,gpointer user_data)512 textview_query_tooltip (GtkTextView *text_view,
513                         gint x,
514                         gint y,
515                         gboolean keyboard_mode,
516                         GtkTooltip *tooltip,
517                         gpointer user_data)
518 {
519 	GtkTextBuffer *buffer;
520 	guint32 state;
521 	gboolean res = FALSE;
522 
523 	if (keyboard_mode)
524 		return FALSE;
525 
526 	buffer = gtk_text_view_get_buffer (text_view);
527 	g_return_val_if_fail (buffer != NULL, FALSE);
528 
529 	state = get_state (buffer);
530 
531 	if ((state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0) {
532 		gchar *url;
533 		GtkTextIter iter;
534 
535 		gtk_text_view_window_to_buffer_coords (
536 			text_view,
537 			GTK_TEXT_WINDOW_WIDGET,
538 			x, y, &x, &y);
539 		gtk_text_view_get_iter_at_location (text_view, &iter, x, y);
540 
541 		url = get_url_at_iter (buffer, &iter);
542 		res = url && *url;
543 
544 		if (res) {
545 			gchar *str;
546 
547 			/* To Translators: The text is concatenated to a form: "Ctrl-click to open a link http://www.example.com" */
548 			str = g_strconcat (_("Ctrl-click to open a link"), " ", url, NULL);
549 			gtk_tooltip_set_text (tooltip, str);
550 			g_free (str);
551 		}
552 
553 		g_free (url);
554 	}
555 
556 	return res;
557 }
558 
559 /* Links can be activated by pressing Enter. */
560 static gboolean
textview_key_press_event(GtkWidget * text_view,GdkEventKey * event)561 textview_key_press_event (GtkWidget *text_view,
562                           GdkEventKey *event)
563 {
564 	GtkTextIter iter;
565 	GtkTextBuffer *buffer;
566 
567 	if ((event->state & GDK_CONTROL_MASK) == 0)
568 		return FALSE;
569 
570 	switch (event->keyval) {
571 	case GDK_KEY_Return:
572 	case GDK_KEY_KP_Enter:
573 		buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
574 		gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer));
575 		if (invoke_link_if_present (buffer, &iter))
576 			return TRUE;
577 		break;
578 
579 	default:
580 		break;
581 	}
582 
583 	return FALSE;
584 }
585 
586 static void
update_ctrl_state(GtkTextView * textview,gboolean ctrl_is_down)587 update_ctrl_state (GtkTextView *textview,
588                    gboolean ctrl_is_down)
589 {
590 	GtkTextBuffer *buffer;
591 	gint x, y;
592 
593 	buffer = gtk_text_view_get_buffer (textview);
594 	if (buffer) {
595 		if (((get_state (buffer) & E_BUFFER_TAGGER_STATE_CTRL_DOWN) != 0) != (ctrl_is_down != FALSE)) {
596 			update_state (buffer, E_BUFFER_TAGGER_STATE_CTRL_DOWN, ctrl_is_down != FALSE);
597 		}
598 
599 		get_pointer_position (textview, &x, &y);
600 		gtk_text_view_window_to_buffer_coords (textview, GTK_TEXT_WINDOW_WIDGET, x, y, &x, &y);
601 		update_mouse_cursor (textview, x, y);
602 	}
603 }
604 
605 /* Links can also be activated by clicking. */
606 static gboolean
textview_event_after(GtkTextView * textview,GdkEvent * event)607 textview_event_after (GtkTextView *textview,
608                       GdkEvent *event)
609 {
610 	GtkTextIter start, end, iter;
611 	GtkTextBuffer *buffer;
612 	gint x, y;
613 	GdkModifierType mt = 0;
614 	guint event_button = 0;
615 	gdouble event_x_win = 0;
616 	gdouble event_y_win = 0;
617 
618 	g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
619 
620 	if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) {
621 		guint event_keyval = 0;
622 
623 		gdk_event_get_keyval (event, &event_keyval);
624 
625 		switch (event_keyval) {
626 			case GDK_KEY_Control_L:
627 			case GDK_KEY_Control_R:
628 				update_ctrl_state (
629 					textview,
630 					event->type == GDK_KEY_PRESS);
631 				break;
632 		}
633 
634 		return FALSE;
635 	}
636 
637 	if (!gdk_event_get_state (event, &mt)) {
638 		GdkWindow *window;
639 		GdkDisplay *display;
640 		GdkDeviceManager *device_manager;
641 		GdkDevice *device;
642 
643 		window = gtk_widget_get_parent_window (GTK_WIDGET (textview));
644 		display = gdk_window_get_display (window);
645 		device_manager = gdk_display_get_device_manager (display);
646 		device = gdk_device_manager_get_client_pointer (device_manager);
647 
648 		gdk_window_get_device_position (window, device, NULL, NULL, &mt);
649 	}
650 
651 	update_ctrl_state (textview, (mt & GDK_CONTROL_MASK) != 0);
652 
653 	if (event->type != GDK_BUTTON_RELEASE)
654 		return FALSE;
655 
656 	gdk_event_get_button (event, &event_button);
657 	gdk_event_get_coords (event, &event_x_win, &event_y_win);
658 
659 	if (event_button != 1 || (mt & GDK_CONTROL_MASK) == 0)
660 		return FALSE;
661 
662 	buffer = gtk_text_view_get_buffer (textview);
663 
664 	/* we shouldn't follow a link if the user has selected something */
665 	gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
666 	if (gtk_text_iter_get_offset (&start) != gtk_text_iter_get_offset (&end))
667 		return FALSE;
668 
669 	gtk_text_view_window_to_buffer_coords (
670 		textview,
671 		GTK_TEXT_WINDOW_WIDGET,
672 		event_x_win, event_y_win, &x, &y);
673 
674 	gtk_text_view_get_iter_at_location (textview, &iter, x, y);
675 
676 	invoke_link_if_present (buffer, &iter);
677 	update_mouse_cursor (textview, x, y);
678 
679 	return FALSE;
680 }
681 
682 static gboolean
textview_motion_notify_event(GtkTextView * textview,GdkEventMotion * event)683 textview_motion_notify_event (GtkTextView *textview,
684                               GdkEventMotion *event)
685 {
686 	gint x, y;
687 
688 	g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
689 
690 	gtk_text_view_window_to_buffer_coords (
691 		textview,
692 		GTK_TEXT_WINDOW_WIDGET,
693 		event->x, event->y, &x, &y);
694 
695 	update_mouse_cursor (textview, x, y);
696 
697 	return FALSE;
698 }
699 
700 static gboolean
textview_visibility_notify_event(GtkTextView * textview,GdkEventVisibility * event)701 textview_visibility_notify_event (GtkTextView *textview,
702                                   GdkEventVisibility *event)
703 {
704 	gint wx, wy, bx, by;
705 
706 	g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE);
707 
708 	get_pointer_position (textview, &wx, &wy);
709 
710 	gtk_text_view_window_to_buffer_coords (
711 		textview,
712 		GTK_TEXT_WINDOW_WIDGET,
713 		wx, wy, &bx, &by);
714 
715 	update_mouse_cursor (textview, bx, by);
716 
717 	return FALSE;
718 }
719 
720 static void
textview_open_uri_cb(GtkWidget * widget,gpointer user_data)721 textview_open_uri_cb (GtkWidget *widget,
722 		      gpointer user_data)
723 {
724 	const gchar *uri = user_data;
725 
726 	g_return_if_fail (uri != NULL);
727 
728 	e_show_uri (NULL, uri);
729 }
730 
731 static void
textview_copy_uri_cb(GtkWidget * widget,gpointer user_data)732 textview_copy_uri_cb (GtkWidget *widget,
733 		      gpointer user_data)
734 {
735 	GtkClipboard *clipboard;
736 	const gchar *uri = user_data;
737 
738 	g_return_if_fail (uri != NULL);
739 
740 	clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
741 	gtk_clipboard_set_text (clipboard, uri, -1);
742 	gtk_clipboard_store (clipboard);
743 
744 	clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
745 	gtk_clipboard_set_text (clipboard, uri, -1);
746 	gtk_clipboard_store (clipboard);
747 }
748 
749 static void
textview_populate_popup_cb(GtkTextView * textview,GtkWidget * widget,gpointer user_data)750 textview_populate_popup_cb (GtkTextView *textview,
751 			    GtkWidget *widget,
752 			    gpointer user_data)
753 {
754 	GtkTextIter iter;
755 	GtkTextBuffer *buffer;
756 	GdkDisplay *display;
757 	gboolean iter_set = FALSE;
758 	gchar *uri;
759 
760 	if (!GTK_IS_MENU (widget))
761 		return;
762 
763 	buffer = gtk_text_view_get_buffer (textview);
764 
765 	display = gtk_widget_get_display (GTK_WIDGET (textview));
766 
767 	if (display && gtk_widget_get_window (GTK_WIDGET (textview))) {
768 		GdkDeviceManager *device_manager;
769 		GdkDevice *pointer;
770 		gint px = 0, py = 0, xx = 0, yy = 0;
771 
772 		device_manager = gdk_display_get_device_manager (display);
773 		pointer = gdk_device_manager_get_client_pointer (device_manager);
774 
775 		gdk_device_get_position (pointer, NULL, &px, &py);
776 		gdk_window_get_origin (gtk_widget_get_window (GTK_WIDGET (textview)), &xx, &yy);
777 
778 		px -= xx;
779 		py -= yy;
780 
781 		gtk_text_view_window_to_buffer_coords (
782 			textview,
783 			GTK_TEXT_WINDOW_WIDGET,
784 			px, py, &xx, &yy);
785 
786 		iter_set = gtk_text_view_get_iter_at_location (textview, &iter, xx, yy);
787 	}
788 
789 	if (!iter_set) {
790 		GtkTextMark *mark;
791 
792 		mark = gtk_text_buffer_get_selection_bound (buffer);
793 
794 		if (!mark)
795 			return;
796 
797 		gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
798 	}
799 
800 	uri = get_url_at_iter (buffer, &iter);
801 
802 	if (uri && *uri) {
803 		GtkMenuShell *menu = GTK_MENU_SHELL (widget);
804 		GtkWidget *item;
805 
806 		item = gtk_separator_menu_item_new ();
807 		gtk_widget_show (item);
808 		gtk_menu_shell_prepend (menu, item);
809 
810 		item = gtk_menu_item_new_with_mnemonic (_("Copy _Link Location"));
811 		gtk_widget_show (item);
812 		gtk_menu_shell_prepend (menu, item);
813 
814 		g_signal_connect_data (item, "activate",
815 			G_CALLBACK (textview_copy_uri_cb), g_strdup (uri), (GClosureNotify) g_free, 0);
816 
817 		item = gtk_menu_item_new_with_mnemonic (_("O_pen Link in Browser"));
818 		gtk_widget_show (item);
819 		gtk_menu_shell_prepend (menu, item);
820 
821 		g_signal_connect_data (item, "activate",
822 			G_CALLBACK (textview_open_uri_cb), uri, (GClosureNotify) g_free, 0);
823 	} else {
824 		g_free (uri);
825 	}
826 }
827 
828 void
e_buffer_tagger_connect(GtkTextView * textview)829 e_buffer_tagger_connect (GtkTextView *textview)
830 {
831 	GtkTextBuffer *buffer;
832 	GtkTextTagTable *tag_table;
833 	GtkTextTag *tag;
834 
835 	init_magic_links ();
836 
837 	g_return_if_fail (textview != NULL);
838 	g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
839 
840 	buffer = gtk_text_view_get_buffer (textview);
841 	tag_table = gtk_text_buffer_get_tag_table (buffer);
842 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
843 
844 	/* if tag is there already, then it is connected, thus claim */
845 	g_return_if_fail (tag == NULL);
846 
847 	gtk_text_buffer_create_tag (
848 		buffer, E_BUFFER_TAGGER_LINK_TAG,
849 		"foreground", "blue",
850 		"underline", PANGO_UNDERLINE_SINGLE,
851 		NULL);
852 
853 	set_state (buffer, E_BUFFER_TAGGER_STATE_NONE);
854 
855 	g_signal_connect (
856 		buffer, "insert-text",
857 		G_CALLBACK (buffer_insert_text), NULL);
858 	g_signal_connect (
859 		buffer, "delete-range",
860 		G_CALLBACK (buffer_delete_range), NULL);
861 	g_signal_connect (
862 		buffer, "notify::cursor-position",
863 		G_CALLBACK (buffer_cursor_position), NULL);
864 
865 	gtk_widget_set_has_tooltip (GTK_WIDGET (textview), TRUE);
866 
867 	g_signal_connect (
868 		textview, "style-updated",
869 		G_CALLBACK (textview_style_updated_cb), NULL);
870 	g_signal_connect (
871 		textview, "query-tooltip",
872 		G_CALLBACK (textview_query_tooltip), NULL);
873 	g_signal_connect (
874 		textview, "key-press-event",
875 		G_CALLBACK (textview_key_press_event), NULL);
876 	g_signal_connect (
877 		textview, "event-after",
878 		G_CALLBACK (textview_event_after), NULL);
879 	g_signal_connect (
880 		textview, "motion-notify-event",
881 		G_CALLBACK (textview_motion_notify_event), NULL);
882 	g_signal_connect (
883 		textview, "visibility-notify-event",
884 		G_CALLBACK (textview_visibility_notify_event), NULL);
885 	g_signal_connect (
886 		textview, "populate-popup",
887 		G_CALLBACK (textview_populate_popup_cb), NULL);
888 }
889 
890 void
e_buffer_tagger_disconnect(GtkTextView * textview)891 e_buffer_tagger_disconnect (GtkTextView *textview)
892 {
893 	GtkTextBuffer *buffer;
894 	GtkTextTagTable *tag_table;
895 	GtkTextTag *tag;
896 
897 	g_return_if_fail (textview != NULL);
898 	g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
899 
900 	buffer = gtk_text_view_get_buffer (textview);
901 	tag_table = gtk_text_buffer_get_tag_table (buffer);
902 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
903 
904 	/* if tag is not there, then it is not connected, thus claim */
905 	g_return_if_fail (tag != NULL);
906 
907 	gtk_text_tag_table_remove (tag_table, tag);
908 
909 	set_state (buffer, E_BUFFER_TAGGER_STATE_NONE);
910 
911 	g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_insert_text), NULL);
912 	g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_delete_range), NULL);
913 	g_signal_handlers_disconnect_by_func (buffer, G_CALLBACK (buffer_cursor_position), NULL);
914 
915 	gtk_widget_set_has_tooltip (GTK_WIDGET (textview), FALSE);
916 
917 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_style_updated_cb), NULL);
918 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_query_tooltip), NULL);
919 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_key_press_event), NULL);
920 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_event_after), NULL);
921 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_motion_notify_event), NULL);
922 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_visibility_notify_event), NULL);
923 	g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_populate_popup_cb), NULL);
924 }
925 
926 void
e_buffer_tagger_update_tags(GtkTextView * textview)927 e_buffer_tagger_update_tags (GtkTextView *textview)
928 {
929 	GtkTextBuffer *buffer;
930 	GtkTextTagTable *tag_table;
931 	GtkTextTag *tag;
932 
933 	g_return_if_fail (textview != NULL);
934 	g_return_if_fail (GTK_IS_TEXT_VIEW (textview));
935 
936 	buffer = gtk_text_view_get_buffer (textview);
937 	tag_table = gtk_text_buffer_get_tag_table (buffer);
938 	tag = gtk_text_tag_table_lookup (tag_table, E_BUFFER_TAGGER_LINK_TAG);
939 
940 	/* if tag is not there, then it is not connected, thus claim */
941 	g_return_if_fail (tag != NULL);
942 
943 	update_state (buffer, E_BUFFER_TAGGER_STATE_INSDEL | E_BUFFER_TAGGER_STATE_CHANGED, FALSE);
944 
945 	markup_text (buffer);
946 }
947