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