/*
* 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)
*
*/
#ifdef HAVE_CONFIG_H
#include
#endif
#include
#include
#include
#include
#include
#include
#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);
}