Commit 9dbb3048 authored by Matthias Clasen's avatar Matthias Clasen

Add link support to GtkLabel

This patch is based on SexyUrlLabel, but with significantly enhanced
functionality: keynav, tooltips, context menu, theming.
parent 141ddd99
......@@ -3,7 +3,7 @@ include $(top_srcdir)/Makefile.decl
democodedir=$(datadir)/gtk-2.0/demo
## These should be in the order you want them to appear in the
## These should be in the order you want them to appear in the
## demo app, which means alphabetized by demo title, not filename
demos = \
appwindow.c \
......@@ -23,6 +23,7 @@ demos = \
iconview.c \
iconview_edit.c \
images.c \
links.c \
list_store.c \
menus.c \
panes.c \
......
/* Links
*
* GtkLabel can show hyperlinks. The default action is to call
* gtk_show_uri() on their URI, but it is possible to override
* this with a custom handler.
*/
#include <gtk/gtk.h>
static void
response_cb (GtkWidget *dialog,
gint response_id,
gpointer data)
{
gtk_widget_destroy (dialog);
}
static gboolean
activate_link (GtkWidget *label,
gpointer data)
{
const gchar *uri;
uri = gtk_label_get_current_uri (GTK_LABEL (label));
if (g_strcmp0 (uri, "keynav") == 0)
{
GtkWidget *dialog;
GtkWidget *parent;
parent = gtk_widget_get_toplevel (label);
dialog = gtk_message_dialog_new_with_markup (GTK_WINDOW (parent),
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_INFO,
GTK_BUTTONS_OK,
"The term <i>keynav</i> is a shorthand for "
"keyboard navigation and refers to the process of using "
"a program (exclusively) via keyboard input.");
gtk_window_present (GTK_WINDOW (dialog));
g_signal_connect (dialog, "response", G_CALLBACK (response_cb), NULL);
return TRUE;
}
return FALSE;
}
GtkWidget *
do_links (GtkWidget *do_widget)
{
static GtkWidget *window = NULL;
GtkWidget *box;
GtkWidget *label;
if (!window)
{
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_screen (GTK_WINDOW (window),
gtk_widget_get_screen (do_widget));
gtk_container_set_border_width (GTK_CONTAINER (window), 12);
g_signal_connect (window, "destroy",
G_CALLBACK(gtk_widget_destroyed), &window);
g_signal_connect (window, "delete-event",
G_CALLBACK (gtk_true), NULL);
label = gtk_label_new ("Some <a href=\"http://en.wikipedia.org/wiki/Text\""
"title=\"plain text\">text</a> may be marked up\n"
"as hyperlinks, which can be clicked\n"
"or activated via <a href=\"keynav\">keynav</a>");
gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
g_signal_connect (label, "activate-link", G_CALLBACK (activate_link), NULL);
gtk_container_add (GTK_CONTAINER (window), label);
gtk_widget_show (label);
}
if (!GTK_WIDGET_VISIBLE (window))
{
gtk_widget_show (window);
}
else
{
gtk_widget_destroy (window);
window = NULL;
}
return window;
}
......@@ -162,6 +162,24 @@ aligns in its available space, see gtk_misc_set_alignment().
</refsect2>
<refsect2>
<title>Links</title>
<para>
Since 2.18, GTK+ supports markup for clickable hyperlinks in addition
to regular Pango markup. The markup for links is borrowed from HTML, using the
<tag>a</tag> with href and title attributes. GTK+ renders links similar to the
way they appear in web browsers, with colored, underlined text. The title
attribute is displayed as a tooltip on the link. An example looks like this:
<informalexample><programlisting>
gtk_label_set_markup (label, "Go to the &lt;a href=\"http://www.gtk.org\" title=\"&amp;lt;i&amp;gt;Our&amp;/i&amp;gt; website\"&gt;GTK+ website</a> for more...");
</programlisting></informalexample>
It is possible to implement custom handling for links with the
#GtkLabel::activate-link signal and the gtk_label_get_current_uri() function.
</para>
</refsect2>
<!-- ##### SECTION See_Also ##### -->
<para>
......
......@@ -2210,6 +2210,7 @@ gtk_label_set_text_with_mnemonic
gtk_label_set_use_markup
gtk_label_set_use_underline
gtk_label_set_width_chars
gtk_label_get_current_uri
#endif
#endif
......
......@@ -45,6 +45,9 @@
#include "gtkstock.h"
#include "gtkbindings.h"
#include "gtkbuildable.h"
#include "gtkimage.h"
#include "gtkshow.h"
#include "gtktooltip.h"
#include "gtkprivate.h"
#include "gtkalias.h"
......@@ -55,8 +58,47 @@ typedef struct
gint wrap_width;
gint width_chars;
gint max_width_chars;
}
GtkLabelPrivate;
} GtkLabelPrivate;
/* Notes about the handling of links:
*
* Links share the GtkLabelSelectionInfo struct with selectable labels.
* There are some new fields for links. The links field contains the list
* of GtkLabelLink structs that describe the links which are embedded in
* the label. The active_link field points to the link under the mouse
* pointer. For keyboard navigation, the 'focus' link is determined by
* finding the link which contains the selection_anchor position.
* The link_clicked field is used with button press and release events
* to ensure that pressing inside a link and releasing outside of it
* does not activate the link.
*
* Links are rendered with the link-color/visited-link-color colors
* that are determined by the style and with an underline. When the mouse
* pointer is over a link, the pointer is changed to indicate the link,
* and the background behind the link is rendered with the base[PRELIGHT]
* color. While a button is pressed over a link, the background is rendered
* with the base[ACTIVE] color.
*
* Labels with links accept keyboard focus, and it is possible to move
* the focus between the embedded links using Tab/Shift-Tab. The focus
* is indicated by a focus rectangle that is drawn around the link text.
* Pressing Enter activates the focussed link, and there is a suitable
* context menu for links that can be opened with the Menu key. Pressing
* Control-C copies the link URI to the clipboard.
*
* In selectable labels with links, link functionality is only available
* when the selection is empty.
*/
typedef struct
{
gchar *uri;
gchar *title; /* the title attribute, used as tooltip */
gboolean visited; /* get set when the link is activated; this flag
* gets preserved over later set_markup() calls
*/
gint start; /* position of the link in the PangoLayout */
gint end;
} GtkLabelLink;
struct _GtkLabelSelectionInfo
{
......@@ -64,18 +106,24 @@ struct _GtkLabelSelectionInfo
gint selection_anchor;
gint selection_end;
GtkWidget *popup_menu;
GList *links;
GtkLabelLink *active_link;
gint drag_start_x;
gint drag_start_y;
guint in_drag : 1;
guint in_drag : 1;
guint select_words : 1;
guint selectable : 1;
guint link_clicked : 1;
};
enum {
MOVE_CURSOR,
COPY_CLIPBOARD,
POPULATE_POPUP,
ACTIVATE_LINK,
LAST_SIGNAL
};
......@@ -103,6 +151,9 @@ enum {
static guint signals[LAST_SIGNAL] = { 0 };
static const GdkColor default_link_color = { 0, 0, 0, 0xeeee };
static const GdkColor default_visited_link_color = { 0, 0x5555, 0x1a1a, 0x8b8b };
static void gtk_label_set_property (GObject *object,
guint prop_id,
const GValue *value,
......@@ -125,6 +176,8 @@ static void gtk_label_direction_changed (GtkWidget *widget,
GtkTextDirection previous_dir);
static gint gtk_label_expose (GtkWidget *widget,
GdkEventExpose *event);
static gboolean gtk_label_focus (GtkWidget *widget,
GtkDirectionType direction);
static void gtk_label_realize (GtkWidget *widget);
static void gtk_label_unrealize (GtkWidget *widget);
......@@ -137,8 +190,16 @@ static gboolean gtk_label_button_release (GtkWidget *widget,
GdkEventButton *event);
static gboolean gtk_label_motion (GtkWidget *widget,
GdkEventMotion *event);
static gboolean gtk_label_leave_notify (GtkWidget *widget,
GdkEventCrossing *event);
static void gtk_label_grab_focus (GtkWidget *widget);
static gboolean gtk_label_query_tooltip (GtkWidget *widget,
gint x,
gint y,
gboolean keyboard_tip,
GtkTooltip *tooltip);
static void gtk_label_set_text_internal (GtkLabel *label,
gchar *str);
......@@ -154,7 +215,7 @@ static void gtk_label_set_uline_text_internal (GtkLabel *label,
const gchar *str);
static void gtk_label_set_pattern_internal (GtkLabel *label,
const gchar *pattern);
static void set_markup (GtkLabel *label,
static void gtk_label_set_markup_internal (GtkLabel *label,
const gchar *str,
gboolean with_uline);
static void gtk_label_recalculate (GtkLabel *label);
......@@ -162,9 +223,13 @@ static void gtk_label_hierarchy_changed (GtkWidget *widget,
GtkWidget *old_toplevel);
static void gtk_label_screen_changed (GtkWidget *widget,
GdkScreen *old_screen);
static gboolean gtk_label_popup_menu (GtkWidget *widget);
static void gtk_label_create_window (GtkLabel *label);
static void gtk_label_destroy_window (GtkLabel *label);
static void gtk_label_ensure_select_info (GtkLabel *label);
static void gtk_label_clear_select_info (GtkLabel *label);
static void gtk_label_update_cursor (GtkLabel *label);
static void gtk_label_clear_layout (GtkLabel *label);
static void gtk_label_ensure_layout (GtkLabel *label);
static void gtk_label_invalidate_wrap_width (GtkLabel *label);
......@@ -197,7 +262,7 @@ static void gtk_label_buildable_custom_finished (GtkBuildable *builda
gpointer user_data);
/* For selectable lables: */
/* For selectable labels: */
static void gtk_label_move_cursor (GtkLabel *label,
GtkMovementStep step,
gint count,
......@@ -206,12 +271,20 @@ static void gtk_label_copy_clipboard (GtkLabel *label);
static void gtk_label_select_all (GtkLabel *label);
static void gtk_label_do_popup (GtkLabel *label,
GdkEventButton *event);
static gint gtk_label_move_forward_word (GtkLabel *label,
gint start);
static gint gtk_label_move_backward_word (GtkLabel *label,
gint start);
/* For links: */
static void gtk_label_rescan_links (GtkLabel *label);
static void gtk_label_clear_links (GtkLabel *label);
static gboolean gtk_label_activate_link (GtkLabel *label);
static GtkLabelLink *gtk_label_get_current_link (GtkLabel *label);
static void gtk_label_get_link_colors (GtkWidget *widget,
GdkColor **link_color,
GdkColor **visited_link_color);
static GQuark quark_angle = 0;
static GtkBuildableIface *buildable_parent_iface = NULL;
......@@ -258,11 +331,12 @@ gtk_label_class_init (GtkLabelClass *class)
gobject_class->finalize = gtk_label_finalize;
object_class->destroy = gtk_label_destroy;
widget_class->size_request = gtk_label_size_request;
widget_class->size_allocate = gtk_label_size_allocate;
widget_class->state_changed = gtk_label_state_changed;
widget_class->style_set = gtk_label_style_set;
widget_class->query_tooltip = gtk_label_query_tooltip;
widget_class->direction_changed = gtk_label_direction_changed;
widget_class->expose_event = gtk_label_expose;
widget_class->realize = gtk_label_realize;
......@@ -272,15 +346,19 @@ gtk_label_class_init (GtkLabelClass *class)
widget_class->button_press_event = gtk_label_button_press;
widget_class->button_release_event = gtk_label_button_release;
widget_class->motion_notify_event = gtk_label_motion;
widget_class->leave_notify_event = gtk_label_leave_notify;
widget_class->hierarchy_changed = gtk_label_hierarchy_changed;
widget_class->screen_changed = gtk_label_screen_changed;
widget_class->mnemonic_activate = gtk_label_mnemonic_activate;
widget_class->drag_data_get = gtk_label_drag_data_get;
widget_class->grab_focus = gtk_label_grab_focus;
widget_class->popup_menu = gtk_label_popup_menu;
widget_class->focus = gtk_label_focus;
class->move_cursor = gtk_label_move_cursor;
class->copy_clipboard = gtk_label_copy_clipboard;
class->activate_link = gtk_label_activate_link;
/**
* GtkLabel::move-cursor:
* @entry: the object which received the signal
......@@ -361,6 +439,35 @@ gtk_label_class_init (GtkLabelClass *class)
G_TYPE_NONE, 1,
GTK_TYPE_MENU);
/**
* GtkLabel::activate-link:
* @label: The label on which the signal was emitted.
*
* A <link linkend="keybinding-signals">keybinding signal</link>
* which gets emitted when the user activates a link in the label.
*
* Applications may connect to it to override the default behaviour,
* which is to call gtk_show_uri(). To obtain the URI that is being
* activated, use gtk_label_get_current_uri().
*
* Applications may also emit the signal with g_signal_emit_by_name()
* if they need to control activation of URIs programmatically.
*
* The default bindings for this signal are all forms of the Enter key.
*
* Returns: %TRUE if the link has been activated
*
* Since: 2.18
*/
signals[ACTIVATE_LINK] =
g_signal_new ("activate-link",
G_TYPE_FROM_CLASS (object_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkLabelClass, activate_link),
_gtk_boolean_handled_accumulator, NULL,
_gtk_marshal_BOOLEAN__VOID,
G_TYPE_BOOLEAN, 0);
g_object_class_install_property (gobject_class,
PROP_LABEL,
g_param_spec_string ("label",
......@@ -691,6 +798,13 @@ gtk_label_class_init (GtkLabelClass *class)
gtk_binding_entry_add_signal (binding_set, GDK_c, GDK_CONTROL_MASK,
"copy-clipboard", 0);
gtk_binding_entry_add_signal (binding_set, GDK_Return, 0,
"activate-link", 0);
gtk_binding_entry_add_signal (binding_set, GDK_ISO_Enter, 0,
"activate-link", 0);
gtk_binding_entry_add_signal (binding_set, GDK_KP_Enter, 0,
"activate-link", 0);
gtk_settings_install_property (g_param_spec_boolean ("gtk-label-select-on-focus",
P_("Select on focus"),
P_("Whether to select the contents of a selectable label when it is focused"),
......@@ -807,7 +921,7 @@ gtk_label_get_property (GObject *object,
g_value_set_object (value, (GObject*) label->mnemonic_widget);
break;
case PROP_CURSOR_POSITION:
if (label->select_info)
if (label->select_info && label->select_info->selectable)
{
gint offset = g_utf8_pointer_to_offset (label->text,
label->text + label->select_info->selection_end);
......@@ -817,7 +931,7 @@ gtk_label_get_property (GObject *object,
g_value_set_int (value, 0);
break;
case PROP_SELECTION_BOUND:
if (label->select_info)
if (label->select_info && label->select_info->selectable)
{
gint offset = g_utf8_pointer_to_offset (label->text,
label->text + label->select_info->selection_anchor);
......@@ -1634,7 +1748,7 @@ gtk_label_recalculate (GtkLabel *label)
if (label->use_markup)
{
set_markup (label, label->label, label->use_underline);
gtk_label_set_markup_internal (label, label->label, label->use_underline);
gtk_label_compose_effective_attrs (label);
}
else
......@@ -1664,7 +1778,8 @@ gtk_label_recalculate (GtkLabel *label)
g_object_notify (G_OBJECT (label), "mnemonic-keyval");
}
gtk_label_clear_layout (label);
gtk_label_clear_layout (label);
gtk_label_clear_select_info (label);
gtk_widget_queue_resize (GTK_WIDGET (label));
}
......@@ -1788,17 +1903,317 @@ gtk_label_get_label (GtkLabel *label)
return label->label;
}
typedef struct
{
GtkLabel *label;
GList *links;
GString *new_str;
GdkColor *link_color;
GdkColor *visited_link_color;
} UriParserData;
static void
start_element_handler (GMarkupParseContext *context,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
gpointer user_data,
GError **error)
{
UriParserData *pdata = user_data;
if (strcmp (element_name, "a") == 0)
{
GtkLabelLink *link;
const gchar *uri = NULL;
const gchar *title = NULL;
gboolean visited = FALSE;
gint line_number;
gint char_number;
gint i;
GdkColor *color = NULL;
g_markup_parse_context_get_position (context, &line_number, &char_number);
for (i = 0; attribute_names[i] != NULL; i++)
{
const gchar *attr = attribute_names[i];
if (strcmp (attr, "href") == 0)
uri = attribute_values[i];
else if (strcmp (attr, "title") == 0)
title = attribute_values[i];
else
{
g_set_error (error,
G_MARKUP_ERROR,
G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
"Attribute '%s' is not allowed on the <a> tag "
"on line %d char %d",
attr, line_number, char_number);
return;
}
}
if (uri == NULL)
{
g_set_error (error,
G_MARKUP_ERROR,
G_MARKUP_ERROR_INVALID_CONTENT,
"Attribute 'href' was missing on the <a> tag "
"on line %d char %d",
line_number, char_number);
return;
}
if (pdata->label->select_info)
{
GList *l;
for (l = pdata->label->select_info->links; l; l = l->next)
{
link = l->data;
if (strcmp (uri, link->uri) == 0)
{
visited = link->visited;
break;
}
}
}
if (visited)
color = pdata->visited_link_color;
else
color = pdata->link_color;
g_string_append_printf (pdata->new_str,
"<span color=\"#%04x%04x%04x\" underline=\"single\">",
color->red,
color->green,
color->blue);
link = g_new0 (GtkLabelLink, 1);
link->uri = g_strdup (uri);
link->title = g_strdup (title);
link->visited = visited;
pdata->links = g_list_append (pdata->links, link);
}
else
{
gint i;
g_string_append_c (pdata->new_str, '<');
g_string_append (pdata->new_str, element_name);
for (i = 0; attribute_names[i] != NULL; i++)
{
const gchar *attr = attribute_names[i];
const gchar *value = attribute_values[i];
gchar *newvalue;
newvalue = g_markup_escape_text (value, -1);
g_string_append_c (pdata->new_str, ' ');
g_string_append (pdata->new_str, attr);
g_string_append (pdata->new_str, "=\"");
g_string_append (pdata->new_str, newvalue);
g_string_append_c (pdata->new_str, '\"');
g_free (newvalue);
}
g_string_append_c (pdata->new_str, '>');
}
}
static void
end_element_handler (GMarkupParseContext *context,
const gchar *element_name,
gpointer user_data,
GError **error)
{
UriParserData *pdata = user_data;
if (!strcmp (element_name, "a"))
g_string_append (pdata->new_str, "</span>");
else
{
g_string_append (pdata->new_str, "</");
g_string_append (pdata->new_str, element_name);
g_string_append_c (pdata->new_str, '>');
}
}
static void
text_handler (GMarkupParseContext *context,
const gchar *text,
gsize text_len,
gpointer user_data,
GError **error)
{
UriParserData *pdata = user_data;
gchar *newtext;
newtext = g_markup_escape_text (text, text_len);
g_string_append (pdata->new_str, newtext);
g_free (newtext);
}
static const GMarkupParser markup_parser =
{
start_element_handler,
end_element_handler,
text_handler,
NULL,
NULL
};
static gboolean
xml_isspace (gchar c)
{
return (c == ' ' || c == '\t' || c == '\n' || c == '\r');
}
static void
link_free (GtkLabelLink *link)
{
g_free (link->uri);
g_free (link->title);
g_free (link);
}
static void
gtk_label_get_link_colors (GtkWidget *widget,
GdkColor **link_color,
GdkColor **visited_link_color)
{
gtk_widget_ensure_style (widget);
gtk_widget_style_get (widget,
"link-color", link_color,
"visited-link-color", visited_link_color,
NULL);
if (!*link_color)
*link_color = gdk_color_copy (&default_link_color);
if (!*visited_link_color)
*visited_link_color = gdk_color_copy (&default_visited_link_color);
}
static gboolean
parse_uri_markup (GtkLabel *label,
const gchar *str,
gchar **new_str,
GList **links,
GError **error)
{
GMarkupParseContext *context = NULL;
const gchar *p, *end;
gboolean needs_root = TRUE;
gsize length;
UriParserData pdata;
length = strlen (str);
p = str;
end = str + length;
pdata.label = label;
pdata.links = NULL;
pdata.new_str = g_string_sized_new (length);
gtk_label_get_link_colors (GTK_WIDGET (label), &pdata.link_color, &pdata.visited_link_color);
while (p != end && xml_isspace (*p))
p++;
if (end - p >= 8 && strncmp (p, "<markup>", 8) == 0)
needs_root = FALSE;
context = g_markup_parse_context_new (&markup_parser, 0, &pdata, NULL);
if (needs_root)
{
if (!g_markup_parse_context_parse (context, "<markup>", -1, error))
goto failed;
}
if (!g_markup_parse_context_parse (context, str, length, error))
goto failed;
if (needs_root)
{
if (!g_markup_parse_context_parse (context, "</markup>", -1, error))
goto failed;
}
if (!g_markup_parse_context_end_parse (context, error))
goto failed;
g_markup_parse_context_free (context);
*new_str = g_string_free (pdata.new_str, FALSE);
*links = pdata.links;
gdk_color_free (pdata.link_color);
gdk_color_free (pdata.visited_link_color);
return TRUE;
failed:
g_markup_parse_context_free (context);
g_string_free (pdata.new_str, TRUE);
g_list_foreach (pdata.links, (GFunc)link_free, NULL);
g_list_free (pdata.links);
gdk_color_free (pdata.link_color);
gdk_color_free (pdata.visited_link_color);
return FALSE;
}
static void
gtk_label_ensure_has_tooltip (GtkLabel *label)
{
GList *l;
gboolean has_tooltip = FALSE;