- implementation of widget event callbacks is taken from gtk3-demo
Signed-off-by: Jakub Filak jfilak@redhat.com --- src/gtk-helpers/hyperlinks.c | 116 +++++++++++++---- src/gtk-helpers/internal_libreport_gtk.h | 10 ++ src/gui-wizard-gtk/wizard.c | 207 ++++++++++++++++++++++++++++++- 3 files changed, 308 insertions(+), 25 deletions(-)
diff --git a/src/gtk-helpers/hyperlinks.c b/src/gtk-helpers/hyperlinks.c index 3f8b570..044afe5 100644 --- a/src/gtk-helpers/hyperlinks.c +++ b/src/gtk-helpers/hyperlinks.c @@ -19,38 +19,110 @@ static char *find_end(const char* url_start) return url_end; }
-char *tag_url(const char* line, const char* prefix) +/* Returns a list of pointers to url token begining and it's lenght + * This implementation parses the following line: + * https://partner-bugzilla.redhat.com/ftp://ftp.kernel.org/http://bugzilla.red... + * to the following tokens: + * https://partner-bugzilla.redhat.com/ + * ftp://ftp.kernel.org/ + * http://bugzilla.redhat.com/ + * http://google.com/ + * https://gmail.com/ + */ +GList *find_url_tokens(const char *line) { static const char *const known_url_prefixes[] = {"http://", "https://", "ftp://", "file://", NULL}; - - char *result = xstrdup(line); - const char *const *pfx = known_url_prefixes; + GList *tokens = NULL; while (*pfx != NULL) { - char *cur_pos = result; - char *url_start; + const char *cur_pos = line; + const char *url_start; while ((url_start = strstr(cur_pos, *pfx)) != NULL) { char *url_end = find_end(url_start); int len = url_end - url_start; - char *hyperlink = xasprintf("%s<a href="%.*s">%.*s</a>", - prefix, - len, url_start, - len, url_start - ); - len = url_start - result; - char *old = result; - result = xasprintf("%.*s%s%s", - len, result, - hyperlink, - url_end - ); - cur_pos = result + len + strlen(hyperlink); - free(old); - free(hyperlink); + + GList *anc = tokens; + for (; anc; anc = g_list_next(anc)) + { + const struct url_token *const t = (struct url_token *)anc->data; + if (t->start >= url_start) + break; + } + + /* need it for overlap correction */ + GList *prev = NULL; + /* initialize it after overlap correction */ + struct url_token *tok = xmalloc(sizeof(*tok)); + if (anc) + { /* insert a new token before token following in the str*/ + prev = g_list_previous(anc); + + struct url_token *following = anc->data; + if (url_end > following->start) + /* correct ovrelaps with following token */ + len -= url_end - following->start; + + GList *new = g_list_prepend(anc, tok); + /* a new token is to become head of the list*/ + if (anc == tokens) + tokens = new; + } + else + { /* append a new token to the end of list */ + prev = g_list_last(tokens); + tokens = g_list_append(tokens, tok); + } + + if (prev) + { /* correct overlaps with previous token */ + struct url_token *previous = prev->data; + const char *prev_end = previous->start + previous->len; + + if (prev_end > url_start) + previous->len -= prev_end - url_start; + } + + tok->start = url_start; + tok->len = len; + + /* move right behind the current prefix */ + cur_pos = url_start + strlen(*pfx); } pfx++; } - return result; + + return tokens; +} + +char *tag_url(const char* line, const char* prefix) +{ + struct strbuf *result = strbuf_new(); + const char *last = line; + GList *urls = find_url_tokens(line); + for (GList *u = urls; u; u = g_list_next(u)) + { + const struct url_token *const t = (struct url_token *)u->data; + + /* add text between hyperlinks */ + if (last < t->start) + /* TODO : add strbuf_append_strn() */ + strbuf_append_strf(result, "%.*s", t->start - last, last); + + strbuf_append_strf(result, "%s<a href="%.*s">%.*s</a>", + prefix, + t->len, t->start, + t->len, t->start); + + last = t->start + t->len; + } + + g_list_free_full(urls, g_free); + + /* add a text following the last link */ + if (last[0] != '\0') + strbuf_append_str(result, last); + + return strbuf_free_nobuf(result); } diff --git a/src/gtk-helpers/internal_libreport_gtk.h b/src/gtk-helpers/internal_libreport_gtk.h index dd9ff87..aa28b16 100644 --- a/src/gtk-helpers/internal_libreport_gtk.h +++ b/src/gtk-helpers/internal_libreport_gtk.h @@ -49,6 +49,16 @@ int show_event_config_dialog(const char *event_name, GtkWindow *parent);
char * tag_url(const char* line, const char* prefix);
+#define url_token libreport_url_token +struct url_token +{ + const char *start; + int len; +}; + +#define find_url_tokens libreport_find_url_tokens +GList *find_url_tokens(const char *line); + #ifdef __cplusplus } #endif diff --git a/src/gui-wizard-gtk/wizard.c b/src/gui-wizard-gtk/wizard.c index a669736..ea2394f 100644 --- a/src/gui-wizard-gtk/wizard.c +++ b/src/gui-wizard-gtk/wizard.c @@ -475,7 +475,7 @@ static void append_to_textview(GtkTextView *tv, const char *str)
/* Ensure we insert text at the end */ GtkTextIter text_iter; - gtk_text_buffer_get_iter_at_offset(tb, &text_iter, -1); + gtk_text_buffer_get_end_iter(tb, &text_iter); gtk_text_buffer_place_cursor(tb, &text_iter);
/* Deal with possible broken Unicode */ @@ -488,14 +488,207 @@ static void append_to_textview(GtkTextView *tv, const char *str) gtk_text_buffer_insert_at_cursor(tb, buf, len); str = end + 1; } - gtk_text_buffer_insert_at_cursor(tb, str, strlen(str)); + + gtk_text_buffer_get_end_iter(tb, &text_iter); + + const char *last = str; + GList *urls = find_url_tokens(str); + for (GList *u = urls; u; u = g_list_next(u)) + { + const struct url_token *const t = (struct url_token *)u->data; + if (last < t->start) + gtk_text_buffer_insert(tb, &text_iter, last, t->start - last); + + GtkTextTag *tag; + tag = gtk_text_buffer_create_tag (tb, NULL, "foreground", "blue", + "underline", PANGO_UNDERLINE_SINGLE, NULL); + char *url = xstrndup(t->start, t->len); + g_object_set_data (G_OBJECT (tag), "url", url); + + gtk_text_buffer_insert_with_tags(tb, &text_iter, url, -1, tag, NULL); + + last = t->start + t->len; + } + + g_list_free_full(urls, g_free); + + if (last[0] != '\0') + gtk_text_buffer_insert(tb, &text_iter, last, strlen(last));
/* Scroll so that the end of the log is visible */ - gtk_text_buffer_get_iter_at_offset(tb, &text_iter, -1); gtk_text_view_scroll_to_iter(tv, &text_iter, /*within_margin:*/ 0.0, /*use_align:*/ FALSE, /*xalign:*/ 0, /*yalign:*/ 0); }
+/* Looks at all tags covering the position of iter in the text view, + * and if one of them is a link, follow it by showing the page identified + * by the data attached to it. + */ +static void open_browse_if_link(GtkWidget *text_view, GtkTextIter *iter) +{ + GSList *tags = NULL, *tagp = NULL; + + tags = gtk_text_iter_get_tags (iter); + for (tagp = tags; tagp != NULL; tagp = tagp->next) + { + GtkTextTag *tag = tagp->data; + const char *url = g_object_get_data (G_OBJECT (tag), "url"); + + if (url != 0) + { + if(fork() == 0) + { + execlp("gnome-open", "gnome-open", url, NULL); + execlp("xdg-open", "xdg-open", url, NULL); + exit(1); + } + break; + } + } + + if (tags) + g_slist_free (tags); +} + +/* Links can be activated by pressing Enter. + */ +static gboolean key_press_event(GtkWidget *text_view, GdkEventKey *event) +{ + GtkTextIter iter; + GtkTextBuffer *buffer; + + 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)); + open_browse_if_link(text_view, &iter); + break; + + default: + break; + } + + return FALSE; +} + +/* Links can also be activated by clicking. + */ +static gboolean event_after(GtkWidget *text_view, GdkEvent *ev) +{ + GtkTextIter start, end, iter; + GtkTextBuffer *buffer; + GdkEventButton *event; + gint x, y; + + if (ev->type != GDK_BUTTON_RELEASE) + return FALSE; + + event = (GdkEventButton *)ev; + + if (event->button != GDK_BUTTON_PRIMARY) + return FALSE; + + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view)); + + /* 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(GTK_TEXT_VIEW (text_view), + GTK_TEXT_WINDOW_WIDGET, + event->x, event->y, &x, &y); + + gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW (text_view), &iter, x, y); + + open_browse_if_link(text_view, &iter); + + return FALSE; +} + +static gboolean hovering_over_link = FALSE; +static GdkCursor *hand_cursor = NULL; +static GdkCursor *regular_cursor = NULL; + +/* Looks at all tags covering the position (x, y) in the text view, + * and if one of them is a link, change the cursor to the "hands" cursor + * typically used by web browsers. + */ +static void set_cursor_if_appropriate(GtkTextView *text_view, + gint x, + gint y) +{ + GSList *tags = NULL, *tagp = NULL; + GtkTextIter iter; + gboolean hovering = FALSE; + + gtk_text_view_get_iter_at_location(text_view, &iter, x, y); + + tags = gtk_text_iter_get_tags(&iter); + for (tagp = tags; tagp != NULL; tagp = tagp->next) + { + GtkTextTag *tag = tagp->data; + gpointer url = g_object_get_data(G_OBJECT (tag), "url"); + + if (url != 0) + { + hovering = TRUE; + break; + } + } + + if (hovering != hovering_over_link) + { + hovering_over_link = hovering; + + if (hovering_over_link) + 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); + } + + if (tags) + g_slist_free (tags); +} + + +/* Update the cursor image if the pointer moved. + */ +static gboolean motion_notify_event(GtkWidget *text_view, GdkEventMotion *event) +{ + gint x, y; + + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(text_view), + GTK_TEXT_WINDOW_WIDGET, + event->x, event->y, &x, &y); + + set_cursor_if_appropriate(GTK_TEXT_VIEW(text_view), x, y); + return FALSE; +} + +/* Also update the cursor image if the window becomes visible + * (e.g. when a window covering it got iconified). + */ +static gboolean visibility_notify_event(GtkWidget *text_view, GdkEventVisibility *event) +{ + gint wx, wy, bx, by; + + GdkWindow *win = gtk_text_view_get_window(GTK_TEXT_VIEW(text_view), GTK_TEXT_WINDOW_TEXT); + GdkDeviceManager *device_manager = gdk_display_get_device_manager(gdk_window_get_display (win)); + GdkDevice *pointer = gdk_device_manager_get_client_pointer(device_manager); + gdk_window_get_device_position(gtk_widget_get_window(text_view), pointer, &wx, &wy, NULL); + + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(text_view), + GTK_TEXT_WINDOW_WIDGET, + wx, wy, &bx, &by); + + set_cursor_if_appropriate(GTK_TEXT_VIEW (text_view), bx, by); + + return FALSE; +}
/* event_gui_data_t */
@@ -2680,6 +2873,14 @@ void create_assistant(void)
g_signal_connect(g_search_entry_bt, "changed", G_CALLBACK(search_timeout), NULL);
+ g_signal_connect (g_tv_event_log, "key-press-event", G_CALLBACK (key_press_event), NULL); + g_signal_connect (g_tv_event_log, "event-after", G_CALLBACK (event_after), NULL); + g_signal_connect (g_tv_event_log, "motion-notify-event", G_CALLBACK (motion_notify_event), NULL); + g_signal_connect (g_tv_event_log, "visibility-notify-event", G_CALLBACK (visibility_notify_event), NULL); + + hand_cursor = gdk_cursor_new (GDK_HAND2); + regular_cursor = gdk_cursor_new (GDK_XTERM); + /* switch to right starting page */ on_page_prepare(g_assistant, gtk_notebook_get_nth_page(g_assistant, 0), NULL); }