gtk2/gtk/gtktexthandle.c
Carlos Garnacho a97178af65 texthandle: Set a bigger input shape, covering the line height
Now, even if the handles being rendered are small, the handle touch
input shape will be as wide as the visible part of the rendered asset, and
high enough to cover both the handle and the height of the line where
the selection bound is.

Also, make handles have the same virtual distance to the line top/bottom
when a drag starts, so the handle doesn't jump to another line after a
too short threshold.
2013-03-05 16:47:58 -05:00

747 lines
22 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 "gtkprivatetypebuiltins.h"
#include "gtktexthandleprivate.h"
#include "gtkmarshalers.h"
#include "gtkprivate.h"
#include "gtkintl.h"
#include <gtk/gtk.h>
typedef struct _GtkTextHandlePrivate GtkTextHandlePrivate;
typedef struct _HandleWindow HandleWindow;
enum {
HANDLE_DRAGGED,
DRAG_FINISHED,
LAST_SIGNAL
};
enum {
PROP_0,
PROP_PARENT,
PROP_RELATIVE_TO
};
struct _HandleWindow
{
GdkWindow *window;
GdkRectangle pointing_to;
gint dx;
gint dy;
guint dragged : 1;
guint mode_visible : 1;
guint user_visible : 1;
guint has_point : 1;
};
struct _GtkTextHandlePrivate
{
HandleWindow windows[2];
GtkWidget *parent;
GdkWindow *relative_to;
GtkStyleContext *style_context;
gulong draw_signal_id;
gulong event_signal_id;
gulong style_updated_id;
gulong composited_changed_id;
guint realized : 1;
guint mode : 2;
};
G_DEFINE_TYPE (GtkTextHandle, _gtk_text_handle, G_TYPE_OBJECT)
static guint signals[LAST_SIGNAL] = { 0 };
static void
_gtk_text_handle_get_size (GtkTextHandle *handle,
gint *width,
gint *height)
{
GtkTextHandlePrivate *priv;
gint w, h;
priv = handle->priv;
gtk_widget_style_get (priv->parent,
"text-handle-width", &w,
"text-handle-height", &h,
NULL);
if (width)
*width = w;
if (height)
*height = h;
}
static void
_gtk_text_handle_draw (GtkTextHandle *handle,
cairo_t *cr,
GtkTextHandlePosition pos)
{
GtkTextHandlePrivate *priv;
gint width, height;
priv = handle->priv;
cairo_save (cr);
cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
cairo_set_source_rgba (cr, 0, 0, 0, 0);
cairo_paint (cr);
if (pos == GTK_TEXT_HANDLE_POSITION_SELECTION_END)
cairo_translate (cr, 0, priv->windows[pos].pointing_to.height);
gtk_style_context_save (priv->style_context);
gtk_style_context_add_class (priv->style_context,
GTK_STYLE_CLASS_CURSOR_HANDLE);
if (pos == GTK_TEXT_HANDLE_POSITION_SELECTION_END)
{
gtk_style_context_add_class (priv->style_context,
GTK_STYLE_CLASS_BOTTOM);
if (priv->mode == GTK_TEXT_HANDLE_MODE_CURSOR)
gtk_style_context_add_class (priv->style_context,
GTK_STYLE_CLASS_INSERTION_CURSOR);
}
else
gtk_style_context_add_class (priv->style_context,
GTK_STYLE_CLASS_TOP);
_gtk_text_handle_get_size (handle, &width, &height);
gtk_render_background (priv->style_context, cr, 0, 0, width, height);
gtk_style_context_restore (priv->style_context);
cairo_restore (cr);
}
static void
_gtk_text_handle_update_shape (GtkTextHandle *handle,
GdkWindow *window,
GtkTextHandlePosition pos)
{
GtkTextHandlePrivate *priv;
cairo_rectangle_int_t rect;
cairo_surface_t *surface;
cairo_region_t *region;
cairo_t *cr;
priv = handle->priv;
surface =
gdk_window_create_similar_surface (window,
CAIRO_CONTENT_COLOR_ALPHA,
gdk_window_get_width (window),
gdk_window_get_height (window));
cr = cairo_create (surface);
_gtk_text_handle_draw (handle, cr, pos);
cairo_destroy (cr);
region = gdk_cairo_region_create_from_surface (surface);
if (gtk_widget_is_composited (priv->parent))
gdk_window_shape_combine_region (window, NULL, 0, 0);
else
gdk_window_shape_combine_region (window, region, 0, 0);
cairo_region_get_extents (region, &rect);
cairo_region_destroy (region);
/* Preserve x/width, but extend input shape
* vertically to all window height */
rect.y = 0;
rect.height = gdk_window_get_height (window);
region = cairo_region_create_rectangle (&rect);
gdk_window_input_shape_combine_region (window, region, 0, 0);
cairo_surface_destroy (surface);
cairo_region_destroy (region);
}
static GdkWindow *
_gtk_text_handle_create_window (GtkTextHandle *handle,
GtkTextHandlePosition pos)
{
GtkTextHandlePrivate *priv;
GdkRGBA bg = { 0, 0, 0, 0 };
GdkWindowAttr attributes;
GdkWindow *window;
GdkVisual *visual;
gint mask;
priv = handle->priv;
attributes.x = 0;
attributes.y = 0;
_gtk_text_handle_get_size (handle, &attributes.width, &attributes.height);
attributes.window_type = GDK_WINDOW_TEMP;
attributes.wclass = GDK_INPUT_OUTPUT;
attributes.event_mask = (GDK_EXPOSURE_MASK |
GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK |
GDK_BUTTON1_MOTION_MASK);
mask = GDK_WA_X | GDK_WA_Y;
visual = gdk_screen_get_rgba_visual (gtk_widget_get_screen (priv->parent));
if (visual)
{
attributes.visual = visual;
mask |= GDK_WA_VISUAL;
}
window = gdk_window_new (gtk_widget_get_root_window (priv->parent),
&attributes, mask);
gtk_widget_register_window (priv->parent, window);
gdk_window_set_background_rgba (window, &bg);
_gtk_text_handle_update_shape (handle, window, pos);
return window;
}
static gboolean
gtk_text_handle_widget_draw (GtkWidget *widget,
cairo_t *cr,
GtkTextHandle *handle)
{
GtkTextHandlePrivate *priv;
GtkTextHandlePosition pos;
HandleWindow *handle_window;
priv = handle->priv;
if (!priv->realized)
return FALSE;
if (gtk_cairo_should_draw_window (cr, priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window))
pos = GTK_TEXT_HANDLE_POSITION_SELECTION_START;
else if (gtk_cairo_should_draw_window (cr, priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window))
pos = GTK_TEXT_HANDLE_POSITION_SELECTION_END;
else
return FALSE;
handle_window = &priv->windows[pos];
if (gdk_window_is_visible (handle_window->window))
_gtk_text_handle_draw (handle, cr, pos);
return FALSE;
}
static gboolean
gtk_text_handle_widget_event (GtkWidget *widget,
GdkEvent *event,
GtkTextHandle *handle)
{
GtkTextHandlePrivate *priv;
GtkTextHandlePosition pos;
priv = handle->priv;
if (event->any.window == priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window)
pos = GTK_TEXT_HANDLE_POSITION_SELECTION_START;
else if (event->any.window == priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window)
pos = GTK_TEXT_HANDLE_POSITION_SELECTION_END;
else
return FALSE;
if (event->type == GDK_BUTTON_PRESS)
{
priv->windows[pos].dx = event->button.x;
priv->windows[pos].dy = event->button.y;
priv->windows[pos].dragged = TRUE;
}
else if (event->type == GDK_BUTTON_RELEASE)
{
g_signal_emit (handle, signals[DRAG_FINISHED], 0, pos);
priv->windows[pos].dx = priv->windows[pos].dy = 0;
priv->windows[pos].dragged = FALSE;
}
else if (event->type == GDK_MOTION_NOTIFY && priv->windows[pos].dragged)
{
gint x, y, width, height;
_gtk_text_handle_get_size (handle, &width, &height);
gdk_window_get_origin (priv->relative_to, &x, &y);
x = event->motion.x_root - priv->windows[pos].dx + (width / 2) - x;
y = event->motion.y_root - priv->windows[pos].dy - y;
if (pos == GTK_TEXT_HANDLE_POSITION_SELECTION_START)
y += height;
y += priv->windows[pos].pointing_to.height / 2;
g_signal_emit (handle, signals[HANDLE_DRAGGED], 0, pos, x, y);
}
return TRUE;
}
static void
_gtk_text_handle_update_window_state (GtkTextHandle *handle,
GtkTextHandlePosition pos)
{
GtkTextHandlePrivate *priv;
HandleWindow *handle_window;
priv = handle->priv;
handle_window = &priv->windows[pos];
if (!handle_window->window)
return;
if (handle_window->has_point &&
handle_window->mode_visible && handle_window->user_visible)
{
gint x, y, width, height;
x = handle_window->pointing_to.x;
y = handle_window->pointing_to.y;
_gtk_text_handle_get_size (handle, &width, &height);
if (pos != GTK_TEXT_HANDLE_POSITION_CURSOR)
y -= height;
height += handle_window->pointing_to.height;
x -= width / 2;
gdk_window_move_resize (handle_window->window, x, y, width, height);
gdk_window_show (handle_window->window);
}
else
gdk_window_hide (handle_window->window);
}
static void
_gtk_text_handle_update_window (GtkTextHandle *handle,
GtkTextHandlePosition pos,
gboolean recreate)
{
GtkTextHandlePrivate *priv;
HandleWindow *handle_window;
priv = handle->priv;
handle_window = &priv->windows[pos];
if (!handle_window->window)
return;
if (recreate)
{
gtk_widget_unregister_window (priv->parent, handle_window->window);
gdk_window_destroy (handle_window->window);
handle_window->window = _gtk_text_handle_create_window (handle, pos);
}
_gtk_text_handle_update_window_state (handle, pos);
}
static void
_gtk_text_handle_update_windows (GtkTextHandle *handle)
{
GtkTextHandlePrivate *priv = handle->priv;
gtk_style_context_invalidate (priv->style_context);
_gtk_text_handle_update_window (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_START, FALSE);
_gtk_text_handle_update_window (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_END, FALSE);
}
static void
_gtk_text_handle_composited_changed (GtkTextHandle *handle)
{
_gtk_text_handle_update_window (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_START, TRUE);
_gtk_text_handle_update_window (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_END, TRUE);
}
static void
gtk_text_handle_constructed (GObject *object)
{
GtkTextHandlePrivate *priv;
priv = GTK_TEXT_HANDLE (object)->priv;
g_assert (priv->parent != NULL);
priv->draw_signal_id =
g_signal_connect (priv->parent, "draw",
G_CALLBACK (gtk_text_handle_widget_draw),
object);
priv->event_signal_id =
g_signal_connect (priv->parent, "event",
G_CALLBACK (gtk_text_handle_widget_event),
object);
priv->composited_changed_id =
g_signal_connect_swapped (priv->parent, "composited-changed",
G_CALLBACK (_gtk_text_handle_composited_changed),
object);
priv->style_updated_id =
g_signal_connect_swapped (priv->parent, "style-updated",
G_CALLBACK (_gtk_text_handle_update_windows),
object);
}
static void
gtk_text_handle_finalize (GObject *object)
{
GtkTextHandlePrivate *priv;
priv = GTK_TEXT_HANDLE (object)->priv;
if (priv->relative_to)
g_object_unref (priv->relative_to);
if (priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window)
{
gtk_widget_unregister_window (priv->parent,
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window);
gdk_window_destroy (priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window);
}
if (priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window)
{
gtk_widget_unregister_window (priv->parent,
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window);
gdk_window_destroy (priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window);
}
if (g_signal_handler_is_connected (priv->parent, priv->draw_signal_id))
g_signal_handler_disconnect (priv->parent, priv->draw_signal_id);
if (g_signal_handler_is_connected (priv->parent, priv->event_signal_id))
g_signal_handler_disconnect (priv->parent, priv->event_signal_id);
if (g_signal_handler_is_connected (priv->parent, priv->composited_changed_id))
g_signal_handler_disconnect (priv->parent, priv->composited_changed_id);
if (g_signal_handler_is_connected (priv->parent, priv->style_updated_id))
g_signal_handler_disconnect (priv->parent, priv->style_updated_id);
g_object_unref (priv->style_context);
G_OBJECT_CLASS (_gtk_text_handle_parent_class)->finalize (object);
}
static void
gtk_text_handle_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkTextHandlePrivate *priv;
GtkTextHandle *handle;
handle = GTK_TEXT_HANDLE (object);
priv = handle->priv;
switch (prop_id)
{
case PROP_PARENT:
priv->parent = g_value_get_object (value);
break;
case PROP_RELATIVE_TO:
_gtk_text_handle_set_relative_to (handle,
g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gtk_text_handle_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkTextHandlePrivate *priv;
priv = GTK_TEXT_HANDLE (object)->priv;
switch (prop_id)
{
case PROP_PARENT:
g_value_set_object (value, priv->parent);
break;
case PROP_RELATIVE_TO:
g_value_set_object (value, priv->relative_to);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
_gtk_text_handle_class_init (GtkTextHandleClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = gtk_text_handle_constructed;
object_class->finalize = gtk_text_handle_finalize;
object_class->set_property = gtk_text_handle_set_property;
object_class->get_property = gtk_text_handle_get_property;
signals[HANDLE_DRAGGED] =
g_signal_new (I_("handle-dragged"),
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GtkTextHandleClass, handle_dragged),
NULL, NULL,
_gtk_marshal_VOID__ENUM_INT_INT,
G_TYPE_NONE, 3,
GTK_TYPE_TEXT_HANDLE_POSITION,
G_TYPE_INT, G_TYPE_INT);
signals[DRAG_FINISHED] =
g_signal_new (I_("drag-finished"),
G_OBJECT_CLASS_TYPE (object_class),
G_SIGNAL_RUN_LAST, 0,
NULL, NULL,
g_cclosure_marshal_VOID__ENUM,
G_TYPE_NONE, 1,
GTK_TYPE_TEXT_HANDLE_POSITION);
g_object_class_install_property (object_class,
PROP_PARENT,
g_param_spec_object ("parent",
P_("Parent widget"),
P_("Parent widget"),
GTK_TYPE_WIDGET,
GTK_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (object_class,
PROP_RELATIVE_TO,
g_param_spec_object ("relative-to",
P_("Window"),
P_("Window the coordinates are based upon"),
GDK_TYPE_WINDOW,
GTK_PARAM_READWRITE));
g_type_class_add_private (object_class, sizeof (GtkTextHandlePrivate));
}
static void
_gtk_text_handle_init (GtkTextHandle *handle)
{
GtkTextHandlePrivate *priv;
GtkWidgetPath *path;
handle->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE (handle,
GTK_TYPE_TEXT_HANDLE,
GtkTextHandlePrivate);
path = gtk_widget_path_new ();
gtk_widget_path_append_type (path, GTK_TYPE_TEXT_HANDLE);
priv->style_context = gtk_style_context_new ();
gtk_style_context_set_path (priv->style_context, path);
gtk_widget_path_free (path);
}
GtkTextHandle *
_gtk_text_handle_new (GtkWidget *parent)
{
return g_object_new (GTK_TYPE_TEXT_HANDLE,
"parent", parent,
NULL);
}
void
_gtk_text_handle_set_relative_to (GtkTextHandle *handle,
GdkWindow *window)
{
GtkTextHandlePrivate *priv;
g_return_if_fail (GTK_IS_TEXT_HANDLE (handle));
g_return_if_fail (!window || GDK_IS_WINDOW (window));
priv = handle->priv;
if (priv->relative_to)
{
gtk_widget_unregister_window (priv->parent,
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window);
gdk_window_destroy (priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window);
gtk_widget_unregister_window (priv->parent,
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window);
gdk_window_destroy (priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window);
g_object_unref (priv->relative_to);
}
if (window)
{
priv->relative_to = g_object_ref (window);
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window =
_gtk_text_handle_create_window (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_START);
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window =
_gtk_text_handle_create_window (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_END);
priv->realized = TRUE;
}
else
{
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window = NULL;
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].window = NULL;
priv->relative_to = NULL;
priv->realized = FALSE;
}
g_object_notify (G_OBJECT (handle), "relative-to");
}
void
_gtk_text_handle_set_mode (GtkTextHandle *handle,
GtkTextHandleMode mode)
{
GtkTextHandlePrivate *priv;
g_return_if_fail (GTK_IS_TEXT_HANDLE (handle));
priv = handle->priv;
if (priv->mode == mode)
return;
priv->mode = mode;
switch (mode)
{
case GTK_TEXT_HANDLE_MODE_CURSOR:
priv->windows[GTK_TEXT_HANDLE_POSITION_CURSOR].mode_visible = TRUE;
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].mode_visible = FALSE;
break;
case GTK_TEXT_HANDLE_MODE_SELECTION:
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].mode_visible = TRUE;
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].mode_visible = TRUE;
break;
case GTK_TEXT_HANDLE_MODE_NONE:
default:
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].mode_visible = FALSE;
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_END].mode_visible = FALSE;
break;
}
_gtk_text_handle_update_shape (handle,
priv->windows[GTK_TEXT_HANDLE_POSITION_CURSOR].window,
GTK_TEXT_HANDLE_POSITION_CURSOR);
_gtk_text_handle_update_shape (handle,
priv->windows[GTK_TEXT_HANDLE_POSITION_SELECTION_START].window,
GTK_TEXT_HANDLE_POSITION_SELECTION_START);
_gtk_text_handle_update_window_state (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_START);
_gtk_text_handle_update_window_state (handle, GTK_TEXT_HANDLE_POSITION_SELECTION_END);
}
GtkTextHandleMode
_gtk_text_handle_get_mode (GtkTextHandle *handle)
{
GtkTextHandlePrivate *priv;
g_return_val_if_fail (GTK_IS_TEXT_HANDLE (handle), GTK_TEXT_HANDLE_MODE_NONE);
priv = handle->priv;
return priv->mode;
}
void
_gtk_text_handle_set_position (GtkTextHandle *handle,
GtkTextHandlePosition pos,
GdkRectangle *rect)
{
GtkTextHandlePrivate *priv;
HandleWindow *handle_window;
gboolean size_changed;
g_return_if_fail (GTK_IS_TEXT_HANDLE (handle));
priv = handle->priv;
pos = CLAMP (pos, GTK_TEXT_HANDLE_POSITION_CURSOR,
GTK_TEXT_HANDLE_POSITION_SELECTION_START);
handle_window = &priv->windows[pos];
if (!priv->realized)
return;
if (priv->mode == GTK_TEXT_HANDLE_MODE_NONE ||
(priv->mode == GTK_TEXT_HANDLE_MODE_CURSOR &&
pos != GTK_TEXT_HANDLE_POSITION_CURSOR))
return;
size_changed = (rect->width != handle_window->pointing_to.width ||
rect->height != handle_window->pointing_to.height);
handle_window->pointing_to = *rect;
handle_window->has_point = TRUE;
gdk_window_get_root_coords (priv->relative_to,
rect->x, rect->y,
&handle_window->pointing_to.x,
&handle_window->pointing_to.y);
_gtk_text_handle_update_window_state (handle, pos);
if (size_changed)
_gtk_text_handle_update_shape (handle, handle_window->window, pos);
}
void
_gtk_text_handle_set_visible (GtkTextHandle *handle,
GtkTextHandlePosition pos,
gboolean visible)
{
GtkTextHandlePrivate *priv;
GdkWindow *window;
g_return_if_fail (GTK_IS_TEXT_HANDLE (handle));
priv = handle->priv;
pos = CLAMP (pos, GTK_TEXT_HANDLE_POSITION_CURSOR,
GTK_TEXT_HANDLE_POSITION_SELECTION_START);
if (!priv->realized)
return;
window = priv->windows[pos].window;
if (!window)
return;
if (!gdk_window_is_visible (window))
_gtk_text_handle_update_shape (handle, window, pos);
priv->windows[pos].user_visible = visible;
_gtk_text_handle_update_window_state (handle, pos);
}
gboolean
_gtk_text_handle_get_is_dragged (GtkTextHandle *handle,
GtkTextHandlePosition pos)
{
GtkTextHandlePrivate *priv;
g_return_val_if_fail (GTK_IS_TEXT_HANDLE (handle), FALSE);
priv = handle->priv;
pos = CLAMP (pos, GTK_TEXT_HANDLE_POSITION_CURSOR,
GTK_TEXT_HANDLE_POSITION_SELECTION_START);
return priv->windows[pos].dragged;
}