Implement touch text selection in GtkEntry

GtkTextHandle is used to indicate both the cursor position
and the selection bound, dragging the handles will modify
the selection and scroll if necessary.

Backwards text selection is also blocked for touch devices,
so the handles don't get inverted positions (This is more
important though on GtkTextView, as inverted handles may
obscure portions of the selected text, good for consistence
though)
This commit is contained in:
Carlos Garnacho 2012-07-11 15:55:00 +02:00 committed by Matthias Clasen
parent 1f7e375c33
commit d38efb1713

View File

@ -65,6 +65,7 @@
#include "gtkicontheme.h"
#include "gtkwidgetprivate.h"
#include "gtkstylecontextprivate.h"
#include "gtktexthandleprivate.h"
#include "a11y/gtkentryaccessible.h"
@ -157,6 +158,8 @@ struct _GtkEntryPrivate
gchar *placeholder_text;
GtkTextHandle *text_handle;
gfloat xalign;
gint ascent; /* font ascent in pango units */
@ -213,6 +216,8 @@ struct _GtkEntryPrivate
guint select_words : 1;
guint select_lines : 1;
guint truncate_multiline : 1;
guint cursor_handle_dragged : 1;
guint selection_handle_dragged : 1;
};
struct _EntryIconInfo
@ -575,6 +580,12 @@ static GdkPixbuf * gtk_entry_ensure_pixbuf (GtkEntry *en
static void gtk_entry_update_cached_style_values(GtkEntry *entry);
static gboolean get_middle_click_paste (GtkEntry *entry);
/* GtkTextHandle handlers */
static void gtk_entry_handle_dragged (GtkTextHandle *handle,
GtkTextHandlePosition pos,
gint x,
gint y,
GtkEntry *entry);
static void begin_change (GtkEntry *entry);
static void end_change (GtkEntry *entry);
@ -2552,6 +2563,10 @@ gtk_entry_init (GtkEntry *entry)
gtk_style_context_add_class (context, GTK_STYLE_CLASS_ENTRY);
gtk_entry_update_cached_style_values (entry);
priv->text_handle = _gtk_text_handle_new (GTK_WIDGET (entry));
g_signal_connect (priv->text_handle, "handle-dragged",
G_CALLBACK (gtk_entry_handle_dragged), entry);
}
static void
@ -2788,6 +2803,7 @@ gtk_entry_finalize (GObject *object)
if (priv->recompute_idle)
g_source_remove (priv->recompute_idle);
g_object_unref (priv->text_handle);
g_free (priv->placeholder_text);
g_free (priv->im_module);
@ -3009,6 +3025,9 @@ gtk_entry_unmap (GtkWidget *widget)
EntryIconInfo *icon_info = NULL;
gint i;
_gtk_text_handle_set_mode (priv->text_handle,
GTK_TEXT_HANDLE_MODE_NONE);
for (i = 0; i < MAX_ICONS; i++)
{
if ((icon_info = priv->icons[i]) != NULL)
@ -3082,7 +3101,7 @@ gtk_entry_realize (GtkWidget *widget)
gtk_entry_adjust_scroll (entry);
gtk_entry_update_primary_selection (entry);
_gtk_text_handle_set_relative_to (priv->text_handle, priv->text_area);
/* If the icon positions are already setup, create their windows.
* Otherwise if they don't exist yet, then construct_icon_info()
@ -3110,6 +3129,7 @@ gtk_entry_unrealize (GtkWidget *widget)
gtk_entry_reset_layout (entry);
gtk_im_context_set_client_window (priv->im_context, NULL);
_gtk_text_handle_set_relative_to (priv->text_handle, NULL);
clipboard = gtk_widget_get_clipboard (widget, GDK_SELECTION_PRIMARY);
if (gtk_clipboard_get_owner (clipboard) == G_OBJECT (entry))
@ -3851,7 +3871,108 @@ in_selection (GtkEntry *entry,
g_free (ranges);
return retval;
}
static void
gtk_entry_move_handle (GtkEntry *entry,
GtkTextHandlePosition pos,
gint x,
gint y,
gint height)
{
GtkEntryPrivate *priv = entry->priv;
if (!_gtk_text_handle_get_is_dragged (priv->text_handle, pos) &&
(x < 0 || x > gdk_window_get_width (priv->text_area)))
{
/* Hide the handle if it's not being manipulated
* and fell outside of the visible text area.
*/
_gtk_text_handle_set_visible (priv->text_handle, pos, FALSE);
}
else
{
GdkRectangle rect;
rect.x = CLAMP (x, 0, gdk_window_get_width (priv->text_area));
rect.y = y;
rect.width = 1;
rect.height = height;
_gtk_text_handle_set_visible (priv->text_handle, pos, TRUE);
_gtk_text_handle_set_position (priv->text_handle, pos, &rect);
}
}
static gint
gtk_entry_get_selection_bound_location (GtkEntry *entry)
{
GtkEntryPrivate *priv = entry->priv;
PangoLayout *layout;
PangoRectangle pos;
gint x;
const gchar *text;
gint index;
layout = gtk_entry_ensure_layout (entry, FALSE);
text = pango_layout_get_text (layout);
index = g_utf8_offset_to_pointer (text, priv->selection_bound) - text;
pango_layout_index_to_pos (layout, index, &pos);
if (gtk_widget_get_direction (GTK_WIDGET (entry)) == GTK_TEXT_DIR_RTL)
x = (pos.x + pos.width) / PANGO_SCALE;
else
x = pos.x / PANGO_SCALE;
return x;
}
static void
gtk_entry_update_handles (GtkEntry *entry,
GtkTextHandleMode mode)
{
GtkEntryPrivate *priv = entry->priv;
gint strong_x, height;
gint cursor, bound;
_gtk_text_handle_set_mode (priv->text_handle, mode);
/* Wait for recomputation before repositioning */
if (priv->recompute_idle != 0)
return;
height = gdk_window_get_height (priv->text_area);
gtk_entry_get_cursor_locations (entry, CURSOR_STANDARD, &strong_x, NULL);
cursor = strong_x - priv->scroll_offset;
if (mode == GTK_TEXT_HANDLE_MODE_SELECTION)
{
gint start, end;
bound = gtk_entry_get_selection_bound_location (entry) - priv->scroll_offset;
if (priv->selection_bound > priv->current_pos)
{
start = cursor;
end = bound;
}
else
{
start = bound;
end = cursor;
}
/* Update start selection bound */
gtk_entry_move_handle (entry, GTK_TEXT_HANDLE_POSITION_SELECTION_START,
start, 0, height);
gtk_entry_move_handle (entry, GTK_TEXT_HANDLE_POSITION_SELECTION_END,
end, 0, height);
}
else
gtk_entry_move_handle (entry, GTK_TEXT_HANDLE_POSITION_CURSOR,
cursor, 0, height);
}
static gint
gtk_entry_button_press (GtkWidget *widget,
GdkEventButton *event)
@ -3919,6 +4040,11 @@ gtk_entry_button_press (GtkWidget *widget,
else if (event->button == GDK_BUTTON_PRIMARY)
{
gboolean have_selection = gtk_editable_get_selection_bounds (editable, &sel_start, &sel_end);
gboolean is_touchscreen;
GdkDevice *source;
source = gdk_event_get_source_device ((GdkEvent *) event);
is_touchscreen = gdk_device_get_source (source) == GDK_SOURCE_TOUCHSCREEN;
priv->select_words = FALSE;
priv->select_lines = FALSE;
@ -3997,7 +4123,12 @@ gtk_entry_button_press (GtkWidget *widget,
priv->drag_start_y = event->y;
}
else
gtk_editable_set_position (editable, tmp_pos);
{
gtk_editable_set_position (editable, tmp_pos);
if (is_touchscreen)
gtk_entry_update_handles (entry, GTK_TEXT_HANDLE_MODE_CURSOR);
}
break;
case GDK_2BUTTON_PRESS:
@ -4008,6 +4139,9 @@ gtk_entry_button_press (GtkWidget *widget,
priv->in_drag = FALSE;
priv->select_words = TRUE;
gtk_entry_select_word (entry);
if (is_touchscreen)
gtk_entry_update_handles (entry, GTK_TEXT_HANDLE_MODE_SELECTION);
break;
case GDK_3BUTTON_PRESS:
@ -4018,6 +4152,8 @@ gtk_entry_button_press (GtkWidget *widget,
priv->in_drag = FALSE;
priv->select_lines = TRUE;
gtk_entry_select_line (entry);
if (is_touchscreen)
gtk_entry_update_handles (entry, GTK_TEXT_HANDLE_MODE_SELECTION);
break;
default:
@ -4087,9 +4223,15 @@ gtk_entry_button_release (GtkWidget *widget,
if (priv->in_drag)
{
gint tmp_pos = gtk_entry_find_position (entry, priv->drag_start_x);
GdkDevice *source;
gtk_editable_set_position (GTK_EDITABLE (entry), tmp_pos);
source = gdk_event_get_source_device ((GdkEvent *) event);
if (gdk_device_get_source (source) == GDK_SOURCE_TOUCHSCREEN)
gtk_entry_update_handles (entry, GTK_TEXT_HANDLE_MODE_CURSOR);
priv->in_drag = 0;
}
@ -4211,13 +4353,22 @@ gtk_entry_motion_notify (GtkWidget *widget,
}
else
{
GdkInputSource input_source;
GdkDevice *source;
guint length;
length = gtk_entry_buffer_get_length (get_buffer (entry));
if (event->y < 0)
tmp_pos = 0;
else if (event->y >= gdk_window_get_height (priv->text_area))
tmp_pos = gtk_entry_buffer_get_length (get_buffer (entry));
tmp_pos = length;
else
tmp_pos = gtk_entry_find_position (entry, event->x + priv->scroll_offset);
source = gdk_event_get_source_device ((GdkEvent *) event);
input_source = gdk_device_get_source (source);
if (priv->select_words)
{
gint min, max;
@ -4253,13 +4404,20 @@ gtk_entry_motion_notify (GtkWidget *widget,
if (priv->current_pos != max)
pos = min;
}
gtk_entry_set_positions (entry, pos, bound);
}
else
gtk_entry_set_positions (entry, tmp_pos, -1);
/* Update touch handles' position */
if (input_source == GDK_SOURCE_TOUCHSCREEN)
gtk_entry_update_handles (entry,
(priv->current_pos == priv->selection_bound) ?
GTK_TEXT_HANDLE_MODE_CURSOR :
GTK_TEXT_HANDLE_MODE_SELECTION);
}
return TRUE;
}
@ -4299,6 +4457,8 @@ gtk_entry_key_press (GtkWidget *widget,
gtk_entry_reset_blink_time (entry);
gtk_entry_pend_cursor_blink (entry);
_gtk_text_handle_set_mode (priv->text_handle,
GTK_TEXT_HANDLE_MODE_NONE);
if (priv->editable)
{
@ -4393,6 +4553,9 @@ gtk_entry_focus_out (GtkWidget *widget,
GtkEntryCompletion *completion;
GdkKeymap *keymap;
_gtk_text_handle_set_mode (priv->text_handle,
GTK_TEXT_HANDLE_MODE_NONE);
gtk_widget_queue_draw (widget);
keymap = gdk_keymap_get_for_display (gtk_widget_get_display (widget));
@ -5573,10 +5736,17 @@ recompute_idle_func (gpointer data)
if (gtk_widget_has_screen (GTK_WIDGET (entry)))
{
GtkTextHandleMode handle_mode;
gtk_entry_adjust_scroll (entry);
gtk_widget_queue_draw (GTK_WIDGET (entry));
update_im_cursor_location (entry);
handle_mode = _gtk_text_handle_get_mode (priv->text_handle);
if (handle_mode != GTK_TEXT_HANDLE_MODE_NONE)
gtk_entry_update_handles (entry, handle_mode);
}
return FALSE;
@ -6010,6 +6180,70 @@ gtk_entry_draw_cursor (GtkEntry *entry,
}
}
static void
gtk_entry_handle_dragged (GtkTextHandle *handle,
GtkTextHandlePosition pos,
gint x,
gint y,
GtkEntry *entry)
{
gint cursor_pos, selection_bound_pos, tmp_pos;
GtkEntryPrivate *priv = entry->priv;
GtkTextHandleMode mode;
gint *min, *max;
cursor_pos = priv->current_pos;
selection_bound_pos = priv->selection_bound;
mode = _gtk_text_handle_get_mode (handle);
tmp_pos = gtk_entry_find_position (entry, x + priv->scroll_offset);
if (mode == GTK_TEXT_HANDLE_MODE_CURSOR ||
cursor_pos >= selection_bound_pos)
{
max = &cursor_pos;
min = &selection_bound_pos;
}
else
{
max = &selection_bound_pos;
min = &cursor_pos;
}
if (pos == GTK_TEXT_HANDLE_POSITION_SELECTION_END)
{
if (mode == GTK_TEXT_HANDLE_MODE_SELECTION)
{
gint min_pos;
min_pos = MAX (*min + 1, 0);
tmp_pos = MAX (tmp_pos, min_pos);
}
*max = tmp_pos;
}
else
{
if (mode == GTK_TEXT_HANDLE_MODE_SELECTION)
{
gint max_pos;
max_pos = *max - 1;
*min = MIN (tmp_pos, max_pos);
}
}
if (cursor_pos != priv->current_pos ||
selection_bound_pos != priv->selection_bound)
{
if (mode == GTK_TEXT_HANDLE_MODE_CURSOR)
gtk_entry_set_positions (entry, cursor_pos, cursor_pos);
else
gtk_entry_set_positions (entry, cursor_pos, selection_bound_pos);
gtk_entry_update_handles (entry, mode);
}
}
/**
* gtk_entry_reset_im_context:
* @entry: a #GtkEntry
@ -6166,6 +6400,23 @@ gtk_entry_get_cursor_locations (GtkEntry *entry,
}
}
static gboolean
gtk_entry_get_is_selection_handle_dragged (GtkEntry *entry)
{
GtkEntryPrivate *priv = entry->priv;
GtkTextHandlePosition pos;
if (_gtk_text_handle_get_mode (priv->text_handle) != GTK_TEXT_HANDLE_MODE_SELECTION)
return FALSE;
if (priv->current_pos >= priv->selection_bound)
pos = GTK_TEXT_HANDLE_POSITION_SELECTION_START;
else
pos = GTK_TEXT_HANDLE_POSITION_SELECTION_END;
return _gtk_text_handle_get_is_dragged (priv->text_handle, pos);
}
static void
gtk_entry_adjust_scroll (GtkEntry *entry)
{
@ -6178,6 +6429,7 @@ gtk_entry_adjust_scroll (GtkEntry *entry)
PangoLayout *layout;
PangoLayoutLine *line;
PangoRectangle logical_rect;
GtkTextHandleMode handle_mode;
if (!gtk_widget_get_realized (GTK_WIDGET (entry)))
return;
@ -6214,22 +6466,33 @@ gtk_entry_adjust_scroll (GtkEntry *entry)
priv->scroll_offset = CLAMP (priv->scroll_offset, min_offset, max_offset);
/* And make sure cursors are on screen. Note that the cursor is
* actually drawn one pixel into the INNER_BORDER space on
* the right, when the scroll is at the utmost right. This
* looks better to to me than confining the cursor inside the
* border entirely, though it means that the cursor gets one
* pixel closer to the edge of the widget on the right than
* on the left. This might need changing if one changed
* INNER_BORDER from 2 to 1, as one would do on a
* small-screen-real-estate display.
*
* We always make sure that the strong cursor is on screen, and
* put the weak cursor on screen if possible.
*/
if (gtk_entry_get_is_selection_handle_dragged (entry))
{
/* The text handle corresponding to the selection bound is
* being dragged, ensure it stays onscreen even if we scroll
* cursors away, this is so both handles can cause content
* to scroll.
*/
strong_x = weak_x = gtk_entry_get_selection_bound_location (entry);
}
else
{
/* And make sure cursors are on screen. Note that the cursor is
* actually drawn one pixel into the INNER_BORDER space on
* the right, when the scroll is at the utmost right. This
* looks better to to me than confining the cursor inside the
* border entirely, though it means that the cursor gets one
* pixel closer to the edge of the widget on the right than
* on the left. This might need changing if one changed
* INNER_BORDER from 2 to 1, as one would do on a
* small-screen-real-estate display.
*
* We always make sure that the strong cursor is on screen, and
* put the weak cursor on screen if possible.
*/
gtk_entry_get_cursor_locations (entry, CURSOR_STANDARD, &strong_x, &weak_x);
}
gtk_entry_get_cursor_locations (entry, CURSOR_STANDARD, &strong_x, &weak_x);
strong_xoffset = strong_x - priv->scroll_offset;
if (strong_xoffset < 0)
@ -6256,6 +6519,11 @@ gtk_entry_adjust_scroll (GtkEntry *entry)
}
g_object_notify (G_OBJECT (entry), "scroll-offset");
handle_mode = _gtk_text_handle_get_mode (priv->text_handle);
if (handle_mode != GTK_TEXT_HANDLE_MODE_NONE)
gtk_entry_update_handles (entry, handle_mode);
}
static void
@ -6278,7 +6546,7 @@ gtk_entry_move_adjustments (GtkEntry *entry)
gtk_widget_get_allocation (widget, &allocation);
/* Cursor position, layout offset, border width, and widget allocation */
/* Cursor/char position, layout offset, border width, and widget allocation */
gtk_entry_get_cursor_locations (entry, CURSOR_STANDARD, &x, NULL);
get_layout_position (entry, &layout_x, NULL);
_gtk_entry_get_borders (entry, &borders);