diff options
Diffstat (limited to 'e-util/e-buffer-tagger.c')
-rw-r--r-- | e-util/e-buffer-tagger.c | 692 |
1 files changed, 692 insertions, 0 deletions
diff --git a/e-util/e-buffer-tagger.c b/e-util/e-buffer-tagger.c new file mode 100644 index 0000000000..c05a854020 --- /dev/null +++ b/e-util/e-buffer-tagger.c @@ -0,0 +1,692 @@ +/* + * 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) + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include <glib/gi18n.h> +#include <regex.h> +#include <string.h> +#include <ctype.h> + +#include "e-buffer-tagger.h" + +#include "e-misc-utils.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_IS_HOVERING_TOOLTIP = 1 << 3, /* mouse is over the link and the tooltip can be shown */ + E_BUFFER_TAGGER_STATE_CTRL_DOWN = 1 << 4 /* 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_$.+!*(),;:@%&=?/~#']*[^]'.}>\\) \n\r\t,?!;:\"]?)?", 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_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", NULL, "http://" }, + { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(([.])?/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]'.}>\\) \n\r\t,?!;:\"]?)?", 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; + + done = TRUE; + + 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; + gint i; + regmatch_t pmatch[2]; + gboolean any; + const gchar *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); + 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 void +get_pointer_position (GtkTextView *text_view, + gint *x, + gint *y) +{ + GdkWindow *window; + GdkDisplay *display; + GdkDeviceManager *device_manager; + GdkDevice *device; + + window = gtk_text_view_get_window (text_view, GTK_TEXT_WINDOW_WIDGET); + display = gdk_window_get_display (window); + device_manager = gdk_display_get_device_manager (display); + device = gdk_device_manager_get_client_pointer (device_manager); + + gdk_window_get_device_position (window, device, x, y, NULL); +} + +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 gchar * +get_url_at_iter (GtkTextBuffer *buffer, + GtkTextIter *iter) +{ + GtkTextTagTable *tag_table; + GtkTextTag *tag; + GtkTextIter start, end; + gchar *url = NULL; + + g_return_val_if_fail (buffer != NULL, 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_val_if_fail (tag != NULL, FALSE); + + if (get_tag_bounds (iter, tag, &start, &end)) + url = gtk_text_iter_get_text (&start, &end); + + return url; +} + +static gboolean +invoke_link_if_present (GtkTextBuffer *buffer, + GtkTextIter *iter) +{ + gboolean res; + gchar *url; + + g_return_val_if_fail (buffer != NULL, FALSE); + + url = get_url_at_iter (buffer, iter); + + res = url && *url; + if (res) + e_show_uri (NULL, url); + + g_free (url); + + 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, hovering_real; + guint32 state; + GtkTextBuffer *buffer = gtk_text_view_get_buffer (text_view); + GtkTextTagTable *tag_table; + GtkTextTag *tag; + GtkTextIter iter; + + 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); + + gtk_text_view_get_iter_at_location (text_view, &iter, x, y); + hovering_real = gtk_text_iter_has_tag (&iter, tag); + + hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING) != 0; + if ((state & E_BUFFER_TAGGER_STATE_CTRL_DOWN) == 0) { + hovering = FALSE; + } else { + hovering = hovering_real; + } + + if (hovering != hovering_over_link) { + update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING, hovering); + + if (hovering && gtk_widget_has_focus (GTK_WIDGET (text_view))) + 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); + + /* XXX Is this necessary? Appears to be a no-op. */ + get_pointer_position (text_view, NULL, NULL); + } + + hovering_over_link = (state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0; + + if (hovering_real != hovering_over_link) { + update_state (buffer, E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP, hovering_real); + + gtk_widget_trigger_tooltip_query (GTK_WIDGET (text_view)); + } +} + +static gboolean +textview_query_tooltip (GtkTextView *text_view, + gint x, + gint y, + gboolean keyboard_mode, + GtkTooltip *tooltip, + gpointer user_data) +{ + GtkTextBuffer *buffer; + guint32 state; + gboolean res = FALSE; + + if (keyboard_mode) + return FALSE; + + buffer = gtk_text_view_get_buffer (text_view); + g_return_val_if_fail (buffer != NULL, FALSE); + + state = get_state (buffer); + + if ((state & E_BUFFER_TAGGER_STATE_IS_HOVERING_TOOLTIP) != 0) { + gchar *url; + GtkTextIter iter; + + gtk_text_view_window_to_buffer_coords ( + text_view, + GTK_TEXT_WINDOW_WIDGET, + x, y, &x, &y); + gtk_text_view_get_iter_at_location (text_view, &iter, x, y); + + url = get_url_at_iter (buffer, &iter); + res = url && *url; + + if (res) { + gchar *str; + + /* To Translators: The text is concatenated to a form: "Ctrl-click to open a link http://www.example.com" */ + str = g_strconcat (_("Ctrl-click to open a link"), " ", url, NULL); + gtk_tooltip_set_text (tooltip, str); + g_free (str); + } + + g_free (url); + } + + return res; +} + +/* 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_KEY_Return: + case GDK_KEY_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); + } + + get_pointer_position (textview, &x, &y); + 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 *event) +{ + GtkTextIter start, end, iter; + GtkTextBuffer *buffer; + gint x, y; + GdkModifierType mt = 0; + guint event_button = 0; + gdouble event_x_win = 0; + gdouble event_y_win = 0; + + g_return_val_if_fail (GTK_IS_TEXT_VIEW (textview), FALSE); + + if (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) { + guint event_keyval = 0; + + gdk_event_get_keyval (event, &event_keyval); + + switch (event_keyval) { + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + update_ctrl_state ( + textview, + event->type == GDK_KEY_PRESS); + break; + } + + return FALSE; + } + + if (!gdk_event_get_state (event, &mt)) { + GdkWindow *window; + GdkDisplay *display; + GdkDeviceManager *device_manager; + GdkDevice *device; + + window = gtk_widget_get_parent_window (GTK_WIDGET (textview)); + display = gdk_window_get_display (window); + device_manager = gdk_display_get_device_manager (display); + device = gdk_device_manager_get_client_pointer (device_manager); + + gdk_window_get_device_position (window, device, NULL, NULL, &mt); + } + + update_ctrl_state (textview, (mt & GDK_CONTROL_MASK) != 0); + + if (event->type != GDK_BUTTON_RELEASE) + return FALSE; + + gdk_event_get_button (event, &event_button); + gdk_event_get_coords (event, &event_x_win, &event_y_win); + + if (event_button != 1 || (mt & 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_win, event_y_win, &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); + + get_pointer_position (textview, &wx, &wy); + + 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); + + gtk_widget_set_has_tooltip (GTK_WIDGET (textview), TRUE); + + g_signal_connect ( + textview, "query-tooltip", + G_CALLBACK (textview_query_tooltip), 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); + + gtk_widget_set_has_tooltip (GTK_WIDGET (textview), FALSE); + + g_signal_handlers_disconnect_by_func (textview, G_CALLBACK (textview_query_tooltip), 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); +} |