gtk2/gtk/gtklinkbutton.c
Carlos Garnacho 21062fab02 gtklinkbutton: Set cursor on button's event window
It is not necessary to (re)set the cursor on every crossing
event, and can probably yield the wrong results if there are
multiple master devices involved. Just set it on init(), and
let the inner machinery update the cursor whenever necessary.

This patch is an adaption of commit 0daf79676 in gtk-3-22, the
side effects are not as bad here because the cursor was already
being set on the widget specifically instead of the parent
widget's, but there's still some nonetheless (plus, it's simpler)

https://bugzilla.gnome.org/show_bug.cgi?id=785375
2017-07-26 13:11:43 +02:00

696 lines
20 KiB
C

/* GTK - The GIMP Toolkit
* gtklinkbutton.c - an hyperlink-enabled button
*
* Copyright (C) 2006 Emmanuele Bassi <ebassi@gmail.com>
* All rights reserved.
*
* Based on gnome-href code by:
* James Henstridge <james@daa.com.au>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* SECTION:gtklinkbutton
* @Title: GtkLinkButton
* @Short_description: Create buttons bound to a URL
* @See_also: #GtkButton
*
* A GtkLinkButton is a #GtkButton with a hyperlink, similar to the one
* used by web browsers, which triggers an action when clicked. It is useful
* to show quick links to resources.
*
* A link button is created by calling either gtk_link_button_new() or
* gtk_link_button_new_with_label(). If using the former, the URI you pass
* to the constructor is used as a label for the widget.
*
* The URI bound to a GtkLinkButton can be set specifically using
* gtk_link_button_set_uri(), and retrieved using gtk_link_button_get_uri().
*
* By default, GtkLinkButton calls gtk_show_uri_on_window() when the button is
* clicked. This behaviour can be overridden by connecting to the
* #GtkLinkButton::activate-link signal and returning %TRUE from the
* signal handler.
*
* # CSS nodes
*
* GtkLinkButton has a single CSS node with name button. To differentiate
* it from a plain #GtkButton, it gets the .link style class.
*/
#include "config.h"
#include "gtklinkbutton.h"
#include <string.h>
#include "gtkclipboard.h"
#include "gtkdnd.h"
#include "gtklabel.h"
#include "gtkmain.h"
#include "gtkmarshalers.h"
#include "gtkmenu.h"
#include "gtkmenuitem.h"
#include "gtksizerequest.h"
#include "gtkshow.h"
#include "gtktooltip.h"
#include "gtkprivate.h"
#include "gtkintl.h"
#include "gtkwidgetprivate.h"
#include "a11y/gtklinkbuttonaccessible.h"
struct _GtkLinkButtonPrivate
{
gchar *uri;
gboolean visited;
GtkWidget *popup_menu;
GtkGesture *click_gesture;
};
enum
{
PROP_0,
PROP_URI,
PROP_VISITED
};
enum
{
ACTIVATE_LINK,
LAST_SIGNAL
};
static void gtk_link_button_finalize (GObject *object);
static void gtk_link_button_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec);
static void gtk_link_button_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec);
static void gtk_link_button_clicked (GtkButton *button);
static gboolean gtk_link_button_popup_menu (GtkWidget *widget);
static void gtk_link_button_drag_data_get_cb (GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *selection,
guint _info,
guint _time,
gpointer user_data);
static gboolean gtk_link_button_query_tooltip_cb (GtkWidget *widget,
gint x,
gint y,
gboolean keyboard_tip,
GtkTooltip *tooltip,
gpointer data);
static void gtk_link_button_pressed_cb (GtkGestureMultiPress *gesture,
int n_press,
double x,
double y,
gpointer user_data);
static gboolean gtk_link_button_activate_link (GtkLinkButton *link_button);
static void set_hand_cursor (GtkWidget *widget,
gboolean show_hand);
static const GtkTargetEntry link_drop_types[] = {
{ (char *) "text/uri-list", 0, 0 },
{ (char *) "_NETSCAPE_URL", 0, 0 }
};
static guint link_signals[LAST_SIGNAL] = { 0, };
G_DEFINE_TYPE_WITH_PRIVATE (GtkLinkButton, gtk_link_button, GTK_TYPE_BUTTON)
static void
gtk_link_button_class_init (GtkLinkButtonClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass);
gobject_class->set_property = gtk_link_button_set_property;
gobject_class->get_property = gtk_link_button_get_property;
gobject_class->finalize = gtk_link_button_finalize;
widget_class->popup_menu = gtk_link_button_popup_menu;
button_class->clicked = gtk_link_button_clicked;
klass->activate_link = gtk_link_button_activate_link;
/**
* GtkLinkButton:uri:
*
* The URI bound to this button.
*
* Since: 2.10
*/
g_object_class_install_property (gobject_class,
PROP_URI,
g_param_spec_string ("uri",
P_("URI"),
P_("The URI bound to this button"),
NULL,
GTK_PARAM_READWRITE));
/**
* GtkLinkButton:visited:
*
* The 'visited' state of this button. A visited link is drawn in a
* different color.
*
* Since: 2.14
*/
g_object_class_install_property (gobject_class,
PROP_VISITED,
g_param_spec_boolean ("visited",
P_("Visited"),
P_("Whether this link has been visited."),
FALSE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY));
/**
* GtkLinkButton::activate-link:
* @button: the #GtkLinkButton that emitted the signal
*
* The ::activate-link signal is emitted each time the #GtkLinkButton
* has been clicked.
*
* The default handler will call gtk_show_uri_on_window() with the URI stored inside
* the #GtkLinkButton:uri property.
*
* To override the default behavior, you can connect to the ::activate-link
* signal and stop the propagation of the signal by returning %TRUE from
* your handler.
*/
link_signals[ACTIVATE_LINK] =
g_signal_new (I_("activate-link"),
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkLinkButtonClass, activate_link),
_gtk_boolean_handled_accumulator, NULL,
_gtk_marshal_BOOLEAN__VOID,
G_TYPE_BOOLEAN, 0);
gtk_widget_class_set_accessible_type (widget_class, GTK_TYPE_LINK_BUTTON_ACCESSIBLE);
gtk_widget_class_set_css_name (widget_class, "button");
}
static void
gtk_link_button_init (GtkLinkButton *link_button)
{
GtkLinkButtonPrivate *priv = gtk_link_button_get_instance_private (link_button);
GtkStyleContext *context;
link_button->priv = priv;
gtk_button_set_relief (GTK_BUTTON (link_button), GTK_RELIEF_NONE);
gtk_widget_set_state_flags (GTK_WIDGET (link_button), GTK_STATE_FLAG_LINK, FALSE);
g_signal_connect (link_button, "drag-data-get",
G_CALLBACK (gtk_link_button_drag_data_get_cb), NULL);
g_object_set (link_button, "has-tooltip", TRUE, NULL);
g_signal_connect (link_button, "query-tooltip",
G_CALLBACK (gtk_link_button_query_tooltip_cb), NULL);
/* enable drag source */
gtk_drag_source_set (GTK_WIDGET (link_button),
GDK_BUTTON1_MASK,
link_drop_types, G_N_ELEMENTS (link_drop_types),
GDK_ACTION_COPY);
priv->click_gesture = gtk_gesture_multi_press_new (GTK_WIDGET (link_button));
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (priv->click_gesture), FALSE);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (priv->click_gesture), 0);
gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->click_gesture), GTK_PHASE_BUBBLE);
g_signal_connect (priv->click_gesture, "pressed", G_CALLBACK (gtk_link_button_pressed_cb),
link_button);
context = gtk_widget_get_style_context (GTK_WIDGET (link_button));
gtk_style_context_add_class (context, "link");
set_hand_cursor (GTK_WIDGET (link_button), TRUE);
}
static void
gtk_link_button_finalize (GObject *object)
{
GtkLinkButton *link_button = GTK_LINK_BUTTON (object);
g_free (link_button->priv->uri);
g_object_unref (link_button->priv->click_gesture);
G_OBJECT_CLASS (gtk_link_button_parent_class)->finalize (object);
}
static void
gtk_link_button_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkLinkButton *link_button = GTK_LINK_BUTTON (object);
switch (prop_id)
{
case PROP_URI:
g_value_set_string (value, link_button->priv->uri);
break;
case PROP_VISITED:
g_value_set_boolean (value, link_button->priv->visited);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_link_button_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkLinkButton *link_button = GTK_LINK_BUTTON (object);
switch (prop_id)
{
case PROP_URI:
gtk_link_button_set_uri (link_button, g_value_get_string (value));
break;
case PROP_VISITED:
gtk_link_button_set_visited (link_button, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
set_hand_cursor (GtkWidget *widget,
gboolean show_hand)
{
GdkDisplay *display;
GdkCursor *cursor;
display = gtk_widget_get_display (widget);
cursor = NULL;
if (show_hand)
cursor = gdk_cursor_new_from_name (display, "pointer");
gtk_widget_set_cursor (widget, cursor);
if (cursor)
g_object_unref (cursor);
}
static void
popup_menu_detach (GtkWidget *attach_widget,
GtkMenu *menu)
{
GtkLinkButton *link_button = GTK_LINK_BUTTON (attach_widget);
link_button->priv->popup_menu = NULL;
}
static void
copy_activate_cb (GtkWidget *widget,
GtkLinkButton *link_button)
{
GtkLinkButtonPrivate *priv = link_button->priv;
gtk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (link_button),
GDK_SELECTION_CLIPBOARD),
priv->uri, -1);
}
static void
gtk_link_button_do_popup (GtkLinkButton *link_button,
const GdkEvent *event)
{
GtkLinkButtonPrivate *priv = link_button->priv;
if (gtk_widget_get_realized (GTK_WIDGET (link_button)))
{
GtkWidget *menu_item;
if (priv->popup_menu)
gtk_widget_destroy (priv->popup_menu);
priv->popup_menu = gtk_menu_new ();
gtk_style_context_add_class (gtk_widget_get_style_context (priv->popup_menu),
GTK_STYLE_CLASS_CONTEXT_MENU);
gtk_menu_attach_to_widget (GTK_MENU (priv->popup_menu),
GTK_WIDGET (link_button),
popup_menu_detach);
menu_item = gtk_menu_item_new_with_mnemonic (_("Copy URL"));
g_signal_connect (menu_item, "activate",
G_CALLBACK (copy_activate_cb), link_button);
gtk_widget_show (menu_item);
gtk_menu_shell_append (GTK_MENU_SHELL (priv->popup_menu), menu_item);
if (event && gdk_event_triggers_context_menu (event))
gtk_menu_popup_at_pointer (GTK_MENU (priv->popup_menu), event);
else
{
gtk_menu_popup_at_widget (GTK_MENU (priv->popup_menu),
GTK_WIDGET (link_button),
GDK_GRAVITY_SOUTH,
GDK_GRAVITY_NORTH_WEST,
event);
gtk_menu_shell_select_first (GTK_MENU_SHELL (priv->popup_menu), FALSE);
}
}
}
static void
gtk_link_button_pressed_cb (GtkGestureMultiPress *gesture,
int n_press,
double x,
double y,
gpointer user_data)
{
GtkLinkButton *link_button = user_data;
GtkLinkButtonPrivate *priv = gtk_link_button_get_instance_private (link_button);
GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
const GdkEvent *event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
if (!gtk_widget_has_focus (GTK_WIDGET (link_button)))
gtk_widget_grab_focus (GTK_WIDGET (link_button));
if (gdk_event_triggers_context_menu (event) &&
priv->uri != NULL)
{
gtk_link_button_do_popup (link_button, event);
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}
else
{
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
}
}
static gboolean
gtk_link_button_activate_link (GtkLinkButton *link_button)
{
GtkWidget *toplevel;
GError *error;
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (link_button));
error = NULL;
gtk_show_uri_on_window (GTK_WINDOW (toplevel), link_button->priv->uri, GDK_CURRENT_TIME, &error);
if (error)
{
g_warning ("Unable to show '%s': %s",
link_button->priv->uri,
error->message);
g_error_free (error);
return FALSE;
}
gtk_link_button_set_visited (link_button, TRUE);
return TRUE;
}
static void
gtk_link_button_clicked (GtkButton *button)
{
gboolean retval = FALSE;
g_signal_emit (button, link_signals[ACTIVATE_LINK], 0, &retval);
}
static gboolean
gtk_link_button_popup_menu (GtkWidget *widget)
{
gtk_link_button_do_popup (GTK_LINK_BUTTON (widget), NULL);
return TRUE;
}
static void
gtk_link_button_drag_data_get_cb (GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *selection,
guint _info,
guint _time,
gpointer user_data)
{
GtkLinkButton *link_button = GTK_LINK_BUTTON (widget);
gchar *uri;
uri = g_strdup_printf ("%s\r\n", link_button->priv->uri);
gtk_selection_data_set (selection,
gtk_selection_data_get_target (selection),
8,
(guchar *) uri,
strlen (uri));
g_free (uri);
}
/**
* gtk_link_button_new:
* @uri: a valid URI
*
* Creates a new #GtkLinkButton with the URI as its text.
*
* Returns: a new link button widget.
*
* Since: 2.10
*/
GtkWidget *
gtk_link_button_new (const gchar *uri)
{
gchar *utf8_uri = NULL;
GtkWidget *retval;
g_return_val_if_fail (uri != NULL, NULL);
if (g_utf8_validate (uri, -1, NULL))
{
utf8_uri = g_strdup (uri);
}
else
{
GError *conv_err = NULL;
utf8_uri = g_locale_to_utf8 (uri, -1, NULL, NULL, &conv_err);
if (conv_err)
{
g_warning ("Attempting to convert URI '%s' to UTF-8, but failed "
"with error: %s",
uri,
conv_err->message);
g_error_free (conv_err);
utf8_uri = g_strdup (_("Invalid URI"));
}
}
retval = g_object_new (GTK_TYPE_LINK_BUTTON,
"label", utf8_uri,
"uri", uri,
NULL);
g_free (utf8_uri);
return retval;
}
/**
* gtk_link_button_new_with_label:
* @uri: a valid URI
* @label: (allow-none): the text of the button
*
* Creates a new #GtkLinkButton containing a label.
*
* Returns: (transfer none): a new link button widget.
*
* Since: 2.10
*/
GtkWidget *
gtk_link_button_new_with_label (const gchar *uri,
const gchar *label)
{
GtkWidget *retval;
g_return_val_if_fail (uri != NULL, NULL);
if (!label)
return gtk_link_button_new (uri);
retval = g_object_new (GTK_TYPE_LINK_BUTTON,
"label", label,
"uri", uri,
NULL);
return retval;
}
static gboolean
gtk_link_button_query_tooltip_cb (GtkWidget *widget,
gint x,
gint y,
gboolean keyboard_tip,
GtkTooltip *tooltip,
gpointer data)
{
GtkLinkButton *link_button = GTK_LINK_BUTTON (widget);
const gchar *label, *uri;
gchar *text, *markup;
label = gtk_button_get_label (GTK_BUTTON (link_button));
uri = link_button->priv->uri;
text = gtk_widget_get_tooltip_text (widget);
markup = gtk_widget_get_tooltip_markup (widget);
if (text == NULL &&
markup == NULL &&
label && *label != '\0' && uri && strcmp (label, uri) != 0)
{
gtk_tooltip_set_text (tooltip, uri);
return TRUE;
}
g_free (text);
g_free (markup);
return FALSE;
}
/**
* gtk_link_button_set_uri:
* @link_button: a #GtkLinkButton
* @uri: a valid URI
*
* Sets @uri as the URI where the #GtkLinkButton points. As a side-effect
* this unsets the “visited” state of the button.
*
* Since: 2.10
*/
void
gtk_link_button_set_uri (GtkLinkButton *link_button,
const gchar *uri)
{
GtkLinkButtonPrivate *priv;
g_return_if_fail (GTK_IS_LINK_BUTTON (link_button));
g_return_if_fail (uri != NULL);
priv = link_button->priv;
g_free (priv->uri);
priv->uri = g_strdup (uri);
g_object_notify (G_OBJECT (link_button), "uri");
gtk_link_button_set_visited (link_button, FALSE);
}
/**
* gtk_link_button_get_uri:
* @link_button: a #GtkLinkButton
*
* Retrieves the URI set using gtk_link_button_set_uri().
*
* Returns: a valid URI. The returned string is owned by the link button
* and should not be modified or freed.
*
* Since: 2.10
*/
const gchar *
gtk_link_button_get_uri (GtkLinkButton *link_button)
{
g_return_val_if_fail (GTK_IS_LINK_BUTTON (link_button), NULL);
return link_button->priv->uri;
}
/**
* gtk_link_button_set_visited:
* @link_button: a #GtkLinkButton
* @visited: the new “visited” state
*
* Sets the “visited” state of the URI where the #GtkLinkButton
* points. See gtk_link_button_get_visited() for more details.
*
* Since: 2.14
*/
void
gtk_link_button_set_visited (GtkLinkButton *link_button,
gboolean visited)
{
g_return_if_fail (GTK_IS_LINK_BUTTON (link_button));
visited = visited != FALSE;
if (link_button->priv->visited != visited)
{
link_button->priv->visited = visited;
if (visited)
{
gtk_widget_unset_state_flags (GTK_WIDGET (link_button), GTK_STATE_FLAG_LINK);
gtk_widget_set_state_flags (GTK_WIDGET (link_button), GTK_STATE_FLAG_VISITED, FALSE);
}
else
{
gtk_widget_unset_state_flags (GTK_WIDGET (link_button), GTK_STATE_FLAG_VISITED);
gtk_widget_set_state_flags (GTK_WIDGET (link_button), GTK_STATE_FLAG_LINK, FALSE);
}
g_object_notify (G_OBJECT (link_button), "visited");
}
}
/**
* gtk_link_button_get_visited:
* @link_button: a #GtkLinkButton
*
* Retrieves the “visited” state of the URI where the #GtkLinkButton
* points. The button becomes visited when it is clicked. If the URI
* is changed on the button, the “visited” state is unset again.
*
* The state may also be changed using gtk_link_button_set_visited().
*
* Returns: %TRUE if the link has been visited, %FALSE otherwise
*
* Since: 2.14
*/
gboolean
gtk_link_button_get_visited (GtkLinkButton *link_button)
{
g_return_val_if_fail (GTK_IS_LINK_BUTTON (link_button), FALSE);
return link_button->priv->visited;
}