From 971a53bec2bf2ced267f94d1799fa288e08e8c28 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Thu, 15 Oct 2009 18:34:57 +0200 Subject: Bug #562512 - Make hyperlinks clickable in Memos, Tasks and Calendar --- widgets/misc/Makefile.am | 2 + widgets/misc/e-buffer-tagger.c | 534 +++++++++++++++++++++++++++++++++++++++++ widgets/misc/e-buffer-tagger.h | 35 +++ 3 files changed, 571 insertions(+) create mode 100644 widgets/misc/e-buffer-tagger.c create mode 100644 widgets/misc/e-buffer-tagger.h (limited to 'widgets') diff --git a/widgets/misc/Makefile.am b/widgets/misc/Makefile.am index da75d752bb..bf6291a0b2 100644 --- a/widgets/misc/Makefile.am +++ b/widgets/misc/Makefile.am @@ -36,6 +36,7 @@ widgetsinclude_HEADERS = \ e-attachment-store.h \ e-attachment-tree-view.h \ e-attachment-view.h \ + e-buffer-tagger.h \ e-calendar.h \ e-calendar-item.h \ e-canvas.h \ @@ -113,6 +114,7 @@ libemiscwidgets_la_SOURCES = \ e-attachment-store.c \ e-attachment-tree-view.c \ e-attachment-view.c \ + e-buffer-tagger.c \ e-calendar.c \ e-calendar-item.c \ e-canvas.c \ diff --git a/widgets/misc/e-buffer-tagger.c b/widgets/misc/e-buffer-tagger.c new file mode 100644 index 0000000000..ec5f3a1c45 --- /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 + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#include +#include +#include +#include +#include +#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 0000000000..86e6710d01 --- /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 + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifndef E_BUFFER_TAGGER_H +#define E_BUFFER_TAGGER_H + +#include + +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 */ -- cgit