gtk2/gtk/gtktexthandle.c
Carlos Garnacho 9b9cc2f947 gtk/texthandle: Handle events on parent surface's native
In wayland, popup positioning and event handling are doubly async.
This makes it unreliable to figure out parent surface coordinates
out of the popup position and the events received. This results in
jumpy text handles there.

The best way to deal with parent surface coordinates is to handle
the events there. Make the handles transparent to input, and make
the drag gesture be set up on the parent widget's native.

The gesture is set up in the capture phase, setting it on the native
(as opposed to the parent widget) achieves a feeling similar to it
being a distinct surface, as it should take precedence over other
gestures in the emission chain (e.g. scrolledwindows).

As everything is in parent widget's native's coordinates, the drag
handles become smooth again.
2020-11-05 21:29:20 +01:00

569 lines
17 KiB
C

/* GTK - The GIMP Toolkit
* Copyright © 2012 Carlos Garnacho <carlosg@gnome.org>
*
* This library 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) 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "gtkcssnumbervalueprivate.h"
#include "gtkprivatetypebuiltins.h"
#include "gtktexthandleprivate.h"
#include "gtkmarshalers.h"
#include "gtkprivate.h"
#include "gtkwindowprivate.h"
#include "gtkwidgetprivate.h"
#include "gtkrendericonprivate.h"
#include "gtkcssboxesimplprivate.h"
#include "gtkcssnumbervalueprivate.h"
#include "gtkstylecontextprivate.h"
#include "gtknativeprivate.h"
#include "gtkintl.h"
#include <gtk/gtk.h>
enum {
DRAG_STARTED,
HANDLE_DRAGGED,
DRAG_FINISHED,
LAST_SIGNAL
};
struct _GtkTextHandle
{
GtkWidget parent_instance;
GdkSurface *surface;
GskRenderer *renderer;
GtkEventController *controller;
GtkWidget *controller_widget;
GdkRectangle pointing_to;
GtkBorder border;
int dx;
int dy;
guint role : 2;
guint dragged : 1;
guint mode_visible : 1;
guint user_visible : 1;
guint has_point : 1;
};
static void gtk_text_handle_native_interface_init (GtkNativeInterface *iface);
static void handle_drag_begin (GtkGestureDrag *gesture,
double x,
double y,
GtkTextHandle *handle);
static void handle_drag_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkWidget *widget);
static void handle_drag_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkTextHandle *handle);
G_DEFINE_TYPE_WITH_CODE (GtkTextHandle, gtk_text_handle, GTK_TYPE_WIDGET,
G_IMPLEMENT_INTERFACE (GTK_TYPE_NATIVE,
gtk_text_handle_native_interface_init))
static guint signals[LAST_SIGNAL] = { 0 };
static GdkSurface *
gtk_text_handle_native_get_surface (GtkNative *native)
{
return GTK_TEXT_HANDLE (native)->surface;
}
static GskRenderer *
gtk_text_handle_native_get_renderer (GtkNative *native)
{
return GTK_TEXT_HANDLE (native)->renderer;
}
static void
gtk_text_handle_native_get_surface_transform (GtkNative *native,
double *x,
double *y)
{
GtkCssBoxes css_boxes;
const graphene_rect_t *margin_rect;
gtk_css_boxes_init (&css_boxes, GTK_WIDGET (native));
margin_rect = gtk_css_boxes_get_margin_rect (&css_boxes);
*x = - margin_rect->origin.x;
*y = - margin_rect->origin.y;
}
static void
gtk_text_handle_get_padding (GtkTextHandle *handle,
GtkBorder *border)
{
GtkWidget *widget = GTK_WIDGET (handle);
GtkStyleContext *context;
context = gtk_widget_get_style_context (widget);
border->left = _gtk_css_number_value_get (_gtk_style_context_peek_property (context, GTK_CSS_PROPERTY_PADDING_LEFT), 100);
border->right = _gtk_css_number_value_get (_gtk_style_context_peek_property (context, GTK_CSS_PROPERTY_PADDING_RIGHT), 100);
border->top = _gtk_css_number_value_get (_gtk_style_context_peek_property (context, GTK_CSS_PROPERTY_PADDING_TOP), 100);
border->bottom = _gtk_css_number_value_get (_gtk_style_context_peek_property (context, GTK_CSS_PROPERTY_PADDING_BOTTOM), 100);
}
static void
gtk_text_handle_present_surface (GtkTextHandle *handle)
{
GtkWidget *widget = GTK_WIDGET (handle);
GdkPopupLayout *layout;
GdkRectangle rect;
GtkRequisition req;
GtkWidget *parent;
gtk_widget_get_preferred_size (widget, NULL, &req);
gtk_text_handle_get_padding (handle, &handle->border);
parent = gtk_widget_get_parent (widget);
gtk_widget_get_surface_allocation (parent, &rect);
rect.x += handle->pointing_to.x;
rect.y += handle->pointing_to.y + handle->pointing_to.height - handle->border.top;
rect.width = req.width - handle->border.left - handle->border.right;
rect.height = 1;
if (handle->role == GTK_TEXT_HANDLE_ROLE_CURSOR)
rect.x -= rect.width / 2;
else if ((handle->role == GTK_TEXT_HANDLE_ROLE_SELECTION_END &&
gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) ||
(handle->role == GTK_TEXT_HANDLE_ROLE_SELECTION_START &&
gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL))
rect.x -= rect.width;
layout = gdk_popup_layout_new (&rect,
GDK_GRAVITY_SOUTH,
GDK_GRAVITY_NORTH);
gdk_popup_layout_set_anchor_hints (layout,
GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE_X);
gdk_popup_present (GDK_POPUP (handle->surface),
MAX (req.width, 1),
MAX (req.height, 1),
layout);
gdk_popup_layout_unref (layout);
gtk_widget_allocate (widget,
gdk_surface_get_width (handle->surface),
gdk_surface_get_height (handle->surface),
-1, NULL);
}
static void
gtk_text_handle_native_check_resize (GtkNative *native)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (native);
GtkWidget *widget = GTK_WIDGET (native);
if (!_gtk_widget_get_alloc_needed (widget))
gtk_widget_ensure_allocate (widget);
else if (gtk_widget_get_visible (widget))
gtk_text_handle_present_surface (handle);
}
static void
gtk_text_handle_native_interface_init (GtkNativeInterface *iface)
{
iface->get_surface = gtk_text_handle_native_get_surface;
iface->get_renderer = gtk_text_handle_native_get_renderer;
iface->get_surface_transform = gtk_text_handle_native_get_surface_transform;
iface->check_resize = gtk_text_handle_native_check_resize;
}
static gboolean
surface_render (GdkSurface *surface,
cairo_region_t *region,
GtkTextHandle *handle)
{
gtk_widget_render (GTK_WIDGET (handle), surface, region);
return TRUE;
}
static void
surface_mapped_changed (GtkWidget *widget)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (widget);
gtk_widget_set_visible (widget, gdk_surface_get_mapped (handle->surface));
}
static void
gtk_text_handle_snapshot (GtkWidget *widget,
GtkSnapshot *snapshot)
{
GtkCssStyle *style = gtk_css_node_get_style (gtk_widget_get_css_node (widget));
gtk_css_style_snapshot_icon (style,
snapshot,
gtk_widget_get_width (widget),
gtk_widget_get_height (widget));
}
static void
gtk_text_handle_realize (GtkWidget *widget)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (widget);
GdkSurface *parent_surface;
GtkWidget *parent;
parent = gtk_widget_get_parent (widget);
parent_surface = gtk_native_get_surface (gtk_widget_get_native (parent));
handle->surface = gdk_surface_new_popup (parent_surface, FALSE);
gdk_surface_set_widget (handle->surface, widget);
gdk_surface_set_input_region (handle->surface, cairo_region_create ());
g_signal_connect_swapped (handle->surface, "notify::mapped",
G_CALLBACK (surface_mapped_changed), widget);
g_signal_connect (handle->surface, "render", G_CALLBACK (surface_render), widget);
GTK_WIDGET_CLASS (gtk_text_handle_parent_class)->realize (widget);
handle->renderer = gsk_renderer_new_for_surface (handle->surface);
}
static void
gtk_text_handle_unrealize (GtkWidget *widget)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (widget);
GTK_WIDGET_CLASS (gtk_text_handle_parent_class)->unrealize (widget);
gsk_renderer_unrealize (handle->renderer);
g_clear_object (&handle->renderer);
g_signal_handlers_disconnect_by_func (handle->surface, surface_render, widget);
g_signal_handlers_disconnect_by_func (handle->surface, surface_mapped_changed, widget);
gdk_surface_set_widget (handle->surface, NULL);
gdk_surface_destroy (handle->surface);
g_clear_object (&handle->surface);
}
static void
text_handle_set_up_gesture (GtkTextHandle *handle)
{
GtkNative *native;
/* The drag gesture is hooked on the parent native */
native = gtk_widget_get_native (gtk_widget_get_parent (GTK_WIDGET (handle)));
handle->controller_widget = GTK_WIDGET (native);
handle->controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ());
gtk_event_controller_set_propagation_phase (handle->controller,
GTK_PHASE_CAPTURE);
g_signal_connect (handle->controller, "drag-begin",
G_CALLBACK (handle_drag_begin), handle);
g_signal_connect (handle->controller, "drag-update",
G_CALLBACK (handle_drag_update), handle);
g_signal_connect (handle->controller, "drag-end",
G_CALLBACK (handle_drag_end), handle);
gtk_widget_add_controller (handle->controller_widget, handle->controller);
}
static void
gtk_text_handle_map (GtkWidget *widget)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (widget);
GTK_WIDGET_CLASS (gtk_text_handle_parent_class)->map (widget);
if (handle->has_point)
{
gtk_text_handle_present_surface (handle);
text_handle_set_up_gesture (handle);
}
}
static void
gtk_text_handle_unmap (GtkWidget *widget)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (widget);
GTK_WIDGET_CLASS (gtk_text_handle_parent_class)->unmap (widget);
gdk_surface_hide (handle->surface);
if (handle->controller_widget)
{
gtk_widget_remove_controller (handle->controller_widget,
handle->controller);
handle->controller_widget = NULL;
handle->controller = NULL;
}
}
static void
gtk_text_handle_measure (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
*natural = 0;
*minimum = 0;
*minimum_baseline = -1;
*natural_baseline = -1;
}
static void
gtk_text_handle_class_init (GtkTextHandleClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
widget_class->snapshot = gtk_text_handle_snapshot;
widget_class->realize = gtk_text_handle_realize;
widget_class->unrealize = gtk_text_handle_unrealize;
widget_class->map = gtk_text_handle_map;
widget_class->unmap = gtk_text_handle_unmap;
widget_class->measure = gtk_text_handle_measure;
signals[HANDLE_DRAGGED] =
g_signal_new (I_("handle-dragged"),
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST, 0,
NULL, NULL,
_gtk_marshal_VOID__INT_INT,
G_TYPE_NONE, 2,
G_TYPE_INT, G_TYPE_INT);
signals[DRAG_STARTED] =
g_signal_new (I_("drag-started"),
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST, 0,
NULL, NULL,
NULL,
G_TYPE_NONE, 0, G_TYPE_NONE);
signals[DRAG_FINISHED] =
g_signal_new (I_("drag-finished"),
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST, 0,
NULL, NULL,
NULL,
G_TYPE_NONE, 0, G_TYPE_NONE);
gtk_widget_class_set_css_name (widget_class, I_("cursor-handle"));
}
/* Relative to pointing_to x/y */
static void
handle_get_input_extents (GtkTextHandle *handle,
GtkBorder *border)
{
GtkWidget *widget = GTK_WIDGET (handle);
if (handle->role == GTK_TEXT_HANDLE_ROLE_CURSOR)
{
border->left = (-gtk_widget_get_width (widget) / 2) - handle->border.left;
border->right = (gtk_widget_get_width (widget) / 2) + handle->border.right;
}
else if ((handle->role == GTK_TEXT_HANDLE_ROLE_SELECTION_END &&
gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) ||
(handle->role == GTK_TEXT_HANDLE_ROLE_SELECTION_START &&
gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL))
{
border->left = -gtk_widget_get_width (widget) - handle->border.left;
border->right = handle->border.right;
}
else
{
border->left = -handle->border.left;
border->right = gtk_widget_get_width (widget) + handle->border.right;
}
border->top = - handle->border.top;
border->bottom = gtk_widget_get_height (widget) + handle->border.bottom;
}
static void
handle_drag_begin (GtkGestureDrag *gesture,
double x,
double y,
GtkTextHandle *handle)
{
GtkBorder input_extents;
double widget_x, widget_y;
x -= handle->pointing_to.x;
y -= handle->pointing_to.y;
/* Figure out if the coordinates fall into the handle input area, coordinates
* are relative to the parent widget.
*/
handle_get_input_extents (handle, &input_extents);
gtk_widget_translate_coordinates (handle->controller_widget,
gtk_widget_get_parent (GTK_WIDGET (handle)),
x, y, &widget_x, &widget_y);
if (widget_x < input_extents.left || widget_x >= input_extents.right ||
widget_y < input_extents.top || widget_y >= input_extents.bottom)
{
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
return;
}
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
/* Store untranslated coordinates here, so ::update does not need
* an extra translation
*/
handle->dx = x;
handle->dy = y;
handle->dragged = TRUE;
g_signal_emit (handle, signals[DRAG_STARTED], 0);
}
static void
handle_drag_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkWidget *widget)
{
GtkTextHandle *handle = GTK_TEXT_HANDLE (widget);
double start_x, start_y;
int x, y;
gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
x = start_x + offset_x - handle->dx;
y = start_y + offset_y - handle->dy;
g_signal_emit (widget, signals[HANDLE_DRAGGED], 0, x, y);
}
static void
handle_drag_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkTextHandle *handle)
{
GdkEventSequence *sequence;
GtkEventSequenceState state;
sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
state = gtk_gesture_get_sequence_state (GTK_GESTURE (gesture), sequence);
if (state == GTK_EVENT_SEQUENCE_CLAIMED)
g_signal_emit (handle, signals[DRAG_FINISHED], 0);
handle->dragged = FALSE;
}
static void
gtk_text_handle_update_for_role (GtkTextHandle *handle)
{
GtkWidget *widget = GTK_WIDGET (handle);
if (handle->role == GTK_TEXT_HANDLE_ROLE_CURSOR)
{
gtk_widget_remove_css_class (widget, "top");
gtk_widget_add_css_class (widget, "bottom");
gtk_widget_add_css_class (widget, "insertion-cursor");
}
else if (handle->role == GTK_TEXT_HANDLE_ROLE_SELECTION_END)
{
gtk_widget_remove_css_class (widget, "top");
gtk_widget_add_css_class (widget, "bottom");
gtk_widget_remove_css_class (widget, "insertion-cursor");
}
else if (handle->role == GTK_TEXT_HANDLE_ROLE_SELECTION_START)
{
gtk_widget_add_css_class (widget, "top");
gtk_widget_remove_css_class (widget, "bottom");
gtk_widget_remove_css_class (widget, "insertion-cursor");
}
gtk_widget_queue_draw (widget);
}
static void
gtk_text_handle_init (GtkTextHandle *handle)
{
gtk_text_handle_update_for_role (handle);
}
GtkTextHandle *
gtk_text_handle_new (GtkWidget *parent)
{
GtkTextHandle *handle;
handle = g_object_new (GTK_TYPE_TEXT_HANDLE, NULL);
gtk_widget_set_parent (GTK_WIDGET (handle), parent);
return handle;
}
void
gtk_text_handle_set_role (GtkTextHandle *handle,
GtkTextHandleRole role)
{
g_return_if_fail (GTK_IS_TEXT_HANDLE (handle));
if (handle->role == role)
return;
handle->role = role;
gtk_text_handle_update_for_role (handle);
if (gtk_widget_get_visible (GTK_WIDGET (handle)))
{
if (handle->has_point)
gtk_text_handle_present_surface (handle);
}
}
GtkTextHandleRole
gtk_text_handle_get_role (GtkTextHandle *handle)
{
g_return_val_if_fail (GTK_IS_TEXT_HANDLE (handle), GTK_TEXT_HANDLE_ROLE_CURSOR);
return handle->role;
}
void
gtk_text_handle_set_position (GtkTextHandle *handle,
const GdkRectangle *rect)
{
g_return_if_fail (GTK_IS_TEXT_HANDLE (handle));
if (handle->pointing_to.x == rect->x &&
handle->pointing_to.y == rect->y &&
handle->pointing_to.width == rect->width &&
handle->pointing_to.height == rect->height)
return;
handle->pointing_to = *rect;
handle->has_point = TRUE;
if (gtk_widget_is_visible (GTK_WIDGET (handle)))
gtk_text_handle_present_surface (handle);
}
gboolean
gtk_text_handle_get_is_dragged (GtkTextHandle *handle)
{
g_return_val_if_fail (GTK_IS_TEXT_HANDLE (handle), FALSE);
return handle->dragged;
}