gtk2/gtk/gtktext.c
Alexander Mikhaylenko f63e6394ac dragsource: Use double coordinates for checking drag threshold
If multiple nested widgets have drag sources on them, both using bubble
phase, we need to reliably pick the inner one. Both of them will try to
start dragging, and we need to make sure there are no situations where the
outer widget starts drag earlier and cancels the inner one.

Currently, this can easily happen via integer rounding: start and current
coordinates passed into gtk_drag_check_threshold() are initially doubles
(other than in GtkNotebook and GtkIconView), and are casted to ints. Then
those rounded values are used to calculate deltas to compare to the drag
threshold, losing quite a lot of precision along the way, and often
resulting in the outer widget getting larger deltas.

To avoid it, just don't round it. Introduce a variant of the function that
operates on doubles: gtk_drag_check_threshold_double() and use it instead
of the original everywhere.
2021-01-29 12:01:34 +05:00

7082 lines
217 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* GTK - The GIMP Toolkit
* Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
* Copyright (C) 2004-2006 Christian Hammond
* Copyright (C) 2008 Cody Russell
* Copyright (C) 2008 Red Hat, Inc.
*
* 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 "gtktextprivate.h"
#include "gtkactionable.h"
#include "gtkadjustment.h"
#include "gtkbox.h"
#include "gtkbutton.h"
#include "gtkdebug.h"
#include "gtkdragicon.h"
#include "gtkdragsourceprivate.h"
#include "gtkdroptarget.h"
#include "gtkeditable.h"
#include "gtkemojichooser.h"
#include "gtkemojicompletion.h"
#include "gtkentrybuffer.h"
#include "gtkeventcontrollerfocus.h"
#include "gtkeventcontrollerkey.h"
#include "gtkeventcontrollermotion.h"
#include "gtkgesturedrag.h"
#include "gtkgestureclick.h"
#include "gtkgesturesingle.h"
#include "gtkimageprivate.h"
#include "gtkimcontextsimple.h"
#include "gtkimmulticontext.h"
#include "gtkintl.h"
#include "gtklabel.h"
#include "gtkmagnifierprivate.h"
#include "gtkmain.h"
#include "gtkmarshalers.h"
#include "gtkpango.h"
#include "gtkpopovermenu.h"
#include "gtkprivate.h"
#include "gtksettings.h"
#include "gtksnapshot.h"
#include "gtkstylecontextprivate.h"
#include "gtktexthandleprivate.h"
#include "gtktexthistoryprivate.h"
#include "gtktextutil.h"
#include "gtktooltip.h"
#include "gtktypebuiltins.h"
#include "gtkwidgetprivate.h"
#include "gtkwindow.h"
#include "gtknative.h"
#include "gtkactionmuxerprivate.h"
#include <cairo-gobject.h>
#include <string.h>
/**
* SECTION:gtktext
* @Short_description: A simple single-line text entry field
* @Title: GtkText
* @See_also: #GtkEntry, #GtkTextView
*
* The #GtkText widget is a single line text entry widget.
*
* A fairly large set of key bindings are supported by default. If the
* entered text is longer than the allocation of the widget, the widget
* will scroll so that the cursor position is visible.
*
* When using an entry for passwords and other sensitive information,
* it can be put into “password mode” using gtk_text_set_visibility().
* In this mode, entered text is displayed using a “invisible” character.
* By default, GTK picks the best invisible character that is available
* in the current font, but it can be changed with gtk_text_set_invisible_char().
*
* If you are looking to add icons or progress display in an entry, look
* at #GtkEntry. There other alternatives for more specialized use cases,
* such as #GtkSearchEntry.
*
* If you need multi-line editable text, look at #GtkTextView.
*
* # CSS nodes
*
* |[<!-- language="plain" -->
* text[.read-only]
* ├── placeholder
* ├── undershoot.left
* ├── undershoot.right
* ├── [selection]
* ├── [block-cursor]
* ╰── [window.popup]
* ]|
*
* GtkText has a main node with the name text. Depending on the properties
* of the widget, the .read-only style class may appear.
*
* When the entry has a selection, it adds a subnode with the name selection.
*
* When the entry is in overwrite mode, it adds a subnode with the name block-cursor
* that determines how the block cursor is drawn.
*
* The CSS node for a context menu is added as a subnode below text as well.
*
* The undershoot nodes are used to draw the underflow indication when content
* is scrolled out of view. These nodes get the .left and .right style classes
* added depending on where the indication is drawn.
*
* When touch is used and touch selection handles are shown, they are using
* CSS nodes with name cursor-handle. They get the .top or .bottom style class
* depending on where they are shown in relation to the selection. If there is
* just a single handle for the text cursor, it gets the style class .insertion-cursor.
*
* # Accessibility
*
* GtkText uses the #GTK_ACCESSIBLE_ROLE_NONE role, which causes it to be skipped
* for accessibility. This is because GtkText is expected to be used as a delegate
* for a #GtkEditable implementation that will be represented to accessibility.
*/
#define NAT_ENTRY_WIDTH 150
#define UNDERSHOOT_SIZE 20
#define DEFAULT_MAX_UNDO 200
static GQuark quark_password_hint = 0;
enum
{
TEXT_HANDLE_CURSOR,
TEXT_HANDLE_SELECTION_BOUND,
TEXT_HANDLE_N_HANDLES
};
typedef struct _GtkTextPasswordHint GtkTextPasswordHint;
typedef struct _GtkTextPrivate GtkTextPrivate;
struct _GtkTextPrivate
{
GtkEntryBuffer *buffer;
GtkIMContext *im_context;
int text_baseline;
PangoLayout *cached_layout;
PangoAttrList *attrs;
PangoTabArray *tabs;
GdkContentProvider *selection_content;
char *im_module;
GtkWidget *emoji_completion;
GtkTextHandle *text_handles[TEXT_HANDLE_N_HANDLES];
GtkWidget *selection_bubble;
guint selection_bubble_timeout_id;
GtkWidget *magnifier_popover;
GtkWidget *magnifier;
GtkWidget *placeholder;
GtkGesture *drag_gesture;
GtkEventController *key_controller;
GtkCssNode *selection_node;
GtkCssNode *block_cursor_node;
GtkCssNode *undershoot_node[2];
GtkWidget *popup_menu;
GMenuModel *extra_menu;
GtkTextHistory *history;
GdkDrag *drag;
float xalign;
int ascent; /* font ascent in pango units */
int current_pos;
int descent; /* font descent in pango units */
int dnd_position; /* In chars, -1 == no DND cursor */
int drag_start_x;
int drag_start_y;
int insert_pos;
int selection_bound;
int scroll_offset;
int width_chars;
int max_width_chars;
gunichar invisible_char;
guint64 blink_start_time;
guint blink_tick;
float cursor_alpha;
guint16 preedit_length; /* length of preedit string, in bytes */
guint16 preedit_cursor; /* offset of cursor within preedit string, in chars */
gint64 handle_place_time;
guint editable : 1;
guint enable_emoji_completion : 1;
guint in_drag : 1;
guint overwrite_mode : 1;
guint visible : 1;
guint activates_default : 1;
guint cache_includes_preedit : 1;
guint change_count : 8;
guint in_click : 1; /* Flag so we don't select all when clicking in entry to focus in */
guint invisible_char_set : 1;
guint mouse_cursor_obscured : 1;
guint need_im_reset : 1;
guint real_changed : 1;
guint resolved_dir : 4; /* PangoDirection */
guint select_words : 1;
guint select_lines : 1;
guint truncate_multiline : 1;
guint cursor_handle_dragged : 1;
guint selection_handle_dragged : 1;
guint populate_all : 1;
guint propagate_text_width : 1;
guint text_handles_enabled : 1;
};
struct _GtkTextPasswordHint
{
int position; /* Position (in text) of the last password hint */
guint source_id; /* Timeout source id */
};
enum {
ACTIVATE,
MOVE_CURSOR,
INSERT_AT_CURSOR,
DELETE_FROM_CURSOR,
BACKSPACE,
CUT_CLIPBOARD,
COPY_CLIPBOARD,
PASTE_CLIPBOARD,
TOGGLE_OVERWRITE,
PREEDIT_CHANGED,
INSERT_EMOJI,
LAST_SIGNAL
};
enum {
PROP_0,
PROP_BUFFER,
PROP_MAX_LENGTH,
PROP_VISIBILITY,
PROP_INVISIBLE_CHAR,
PROP_INVISIBLE_CHAR_SET,
PROP_ACTIVATES_DEFAULT,
PROP_SCROLL_OFFSET,
PROP_TRUNCATE_MULTILINE,
PROP_OVERWRITE_MODE,
PROP_IM_MODULE,
PROP_PLACEHOLDER_TEXT,
PROP_INPUT_PURPOSE,
PROP_INPUT_HINTS,
PROP_ATTRIBUTES,
PROP_TABS,
PROP_ENABLE_EMOJI_COMPLETION,
PROP_PROPAGATE_TEXT_WIDTH,
PROP_EXTRA_MENU,
NUM_PROPERTIES
};
static GParamSpec *text_props[NUM_PROPERTIES] = { NULL, };
static guint signals[LAST_SIGNAL] = { 0 };
typedef enum {
CURSOR_STANDARD,
CURSOR_DND
} CursorType;
typedef enum
{
DISPLAY_NORMAL, /* The text is being shown */
DISPLAY_INVISIBLE, /* In invisible mode, text replaced by (eg) bullets */
DISPLAY_BLANK /* In invisible mode, nothing shown at all */
} DisplayMode;
/* GObject methods
*/
static void gtk_text_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec);
static void gtk_text_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec);
static void gtk_text_finalize (GObject *object);
static void gtk_text_dispose (GObject *object);
/* GtkWidget methods
*/
static void gtk_text_realize (GtkWidget *widget);
static void gtk_text_unrealize (GtkWidget *widget);
static void gtk_text_map (GtkWidget *widget);
static void gtk_text_unmap (GtkWidget *widget);
static void gtk_text_measure (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline);
static void gtk_text_size_allocate (GtkWidget *widget,
int width,
int height,
int baseline);
static void gtk_text_snapshot (GtkWidget *widget,
GtkSnapshot *snapshot);
static void gtk_text_focus_changed (GtkEventControllerFocus *focus,
GParamSpec *pspec,
GtkWidget *widget);
static gboolean gtk_text_grab_focus (GtkWidget *widget);
static void gtk_text_css_changed (GtkWidget *widget,
GtkCssStyleChange *change);
static void gtk_text_direction_changed (GtkWidget *widget,
GtkTextDirection previous_dir);
static void gtk_text_state_flags_changed (GtkWidget *widget,
GtkStateFlags previous_state);
static gboolean gtk_text_drag_drop (GtkDropTarget *dest,
const GValue *value,
double x,
double y,
GtkText *text);
static gboolean gtk_text_drag_accept (GtkDropTarget *dest,
GdkDrop *drop,
GtkText *self);
static GdkDragAction gtk_text_drag_motion (GtkDropTarget *dest,
double x,
double y,
GtkText *text);
static void gtk_text_drag_leave (GtkDropTarget *dest,
GtkText *text);
/* GtkEditable method implementations
*/
static void gtk_text_editable_init (GtkEditableInterface *iface);
static void gtk_text_insert_text (GtkText *self,
const char *text,
int length,
int *position);
static void gtk_text_delete_text (GtkText *self,
int start_pos,
int end_pos);
static void gtk_text_delete_selection (GtkText *self);
static void gtk_text_set_selection_bounds (GtkText *self,
int start,
int end);
static gboolean gtk_text_get_selection_bounds (GtkText *self,
int *start,
int *end);
static void gtk_text_set_editable (GtkText *self,
gboolean is_editable);
static void gtk_text_set_text (GtkText *self,
const char *text);
static void gtk_text_set_width_chars (GtkText *self,
int n_chars);
static void gtk_text_set_max_width_chars (GtkText *self,
int n_chars);
static void gtk_text_set_alignment (GtkText *self,
float xalign);
/* Default signal handlers
*/
static GMenuModel *gtk_text_get_menu_model (GtkText *self);
static void gtk_text_popup_menu (GtkWidget *widget,
const char *action_name,
GVariant *parameters);
static void gtk_text_move_cursor (GtkText *self,
GtkMovementStep step,
int count,
gboolean extend);
static void gtk_text_insert_at_cursor (GtkText *self,
const char *str);
static void gtk_text_delete_from_cursor (GtkText *self,
GtkDeleteType type,
int count);
static void gtk_text_backspace (GtkText *self);
static void gtk_text_cut_clipboard (GtkText *self);
static void gtk_text_copy_clipboard (GtkText *self);
static void gtk_text_paste_clipboard (GtkText *self);
static void gtk_text_toggle_overwrite (GtkText *self);
static void gtk_text_insert_emoji (GtkText *self);
static void gtk_text_select_all (GtkText *self);
static void gtk_text_real_activate (GtkText *self);
static void direction_changed (GdkDevice *keyboard,
GParamSpec *pspec,
GtkText *self);
/* IM Context Callbacks
*/
static void gtk_text_commit_cb (GtkIMContext *context,
const char *str,
GtkText *self);
static void gtk_text_preedit_changed_cb (GtkIMContext *context,
GtkText *self);
static gboolean gtk_text_retrieve_surrounding_cb (GtkIMContext *context,
GtkText *self);
static gboolean gtk_text_delete_surrounding_cb (GtkIMContext *context,
int offset,
int n_chars,
GtkText *self);
/* Entry buffer signal handlers
*/
static void buffer_inserted_text (GtkEntryBuffer *buffer,
guint position,
const char *chars,
guint n_chars,
GtkText *self);
static void buffer_deleted_text (GtkEntryBuffer *buffer,
guint position,
guint n_chars,
GtkText *self);
static void buffer_notify_text (GtkEntryBuffer *buffer,
GParamSpec *spec,
GtkText *self);
static void buffer_notify_max_length (GtkEntryBuffer *buffer,
GParamSpec *spec,
GtkText *self);
/* Event controller callbacks
*/
static void gtk_text_motion_controller_motion (GtkEventControllerMotion *controller,
double x,
double y,
GtkText *self);
static void gtk_text_click_gesture_pressed (GtkGestureClick *gesture,
int n_press,
double x,
double y,
GtkText *self);
static void gtk_text_drag_gesture_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkText *self);
static void gtk_text_drag_gesture_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkText *self);
static gboolean gtk_text_key_controller_key_pressed (GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
GtkText *self);
/* GtkTextHandle handlers */
static void gtk_text_handle_drag_started (GtkTextHandle *handle,
GtkText *self);
static void gtk_text_handle_dragged (GtkTextHandle *handle,
int x,
int y,
GtkText *self);
static void gtk_text_handle_drag_finished (GtkTextHandle *handle,
GtkText *self);
/* Internal routines
*/
static void gtk_text_draw_text (GtkText *self,
GtkSnapshot *snapshot);
static void gtk_text_draw_cursor (GtkText *self,
GtkSnapshot *snapshot,
CursorType type);
static PangoLayout *gtk_text_ensure_layout (GtkText *self,
gboolean include_preedit);
static void gtk_text_reset_layout (GtkText *self);
static void gtk_text_recompute (GtkText *self);
static int gtk_text_find_position (GtkText *self,
int x);
static void gtk_text_get_cursor_locations (GtkText *self,
int *strong_x,
int *weak_x);
static void gtk_text_adjust_scroll (GtkText *self);
static int gtk_text_move_visually (GtkText *editable,
int start,
int count);
static int gtk_text_move_logically (GtkText *self,
int start,
int count);
static int gtk_text_move_forward_word (GtkText *self,
int start,
gboolean allow_whitespace);
static int gtk_text_move_backward_word (GtkText *self,
int start,
gboolean allow_whitespace);
static void gtk_text_delete_whitespace (GtkText *self);
static void gtk_text_select_word (GtkText *self);
static void gtk_text_select_line (GtkText *self);
static void gtk_text_paste (GtkText *self,
GdkClipboard *clipboard);
static void gtk_text_update_primary_selection (GtkText *self);
static void gtk_text_schedule_im_reset (GtkText *self);
static gboolean gtk_text_mnemonic_activate (GtkWidget *widget,
gboolean group_cycling);
static void gtk_text_check_cursor_blink (GtkText *self);
static void gtk_text_pend_cursor_blink (GtkText *self);
static void gtk_text_reset_blink_time (GtkText *self);
static void gtk_text_update_cached_style_values(GtkText *self);
static gboolean get_middle_click_paste (GtkText *self);
static void gtk_text_get_scroll_limits (GtkText *self,
int *min_offset,
int *max_offset);
static GtkEntryBuffer *get_buffer (GtkText *self);
static void set_text_cursor (GtkWidget *widget);
static void update_placeholder_visibility (GtkText *self);
static void buffer_connect_signals (GtkText *self);
static void buffer_disconnect_signals (GtkText *self);
static void gtk_text_selection_bubble_popup_set (GtkText *self);
static void gtk_text_selection_bubble_popup_unset (GtkText *self);
static void begin_change (GtkText *self);
static void end_change (GtkText *self);
static void emit_changed (GtkText *self);
static void gtk_text_update_clipboard_actions (GtkText *self);
static void gtk_text_update_emoji_action (GtkText *self);
static void gtk_text_update_handles (GtkText *self);
static void gtk_text_activate_clipboard_cut (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
static void gtk_text_activate_clipboard_copy (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
static void gtk_text_activate_clipboard_paste (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
static void gtk_text_activate_selection_delete (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
static void gtk_text_activate_selection_select_all (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
static void gtk_text_activate_misc_insert_emoji (GtkWidget *widget,
const char *action_name,
GVariant *parameter);
static void gtk_text_real_undo (GtkWidget *widget,
const char *action_name,
GVariant *parameters);
static void gtk_text_real_redo (GtkWidget *widget,
const char *action_name,
GVariant *parameters);
static void gtk_text_history_change_state_cb (gpointer funcs_data,
gboolean is_modified,
gboolean can_undo,
gboolean can_redo);
static void gtk_text_history_insert_cb (gpointer funcs_data,
guint begin,
guint end,
const char *text,
guint len);
static void gtk_text_history_delete_cb (gpointer funcs_data,
guint begin,
guint end,
const char *expected_text,
guint len);
static void gtk_text_history_select_cb (gpointer funcs_data,
int selection_insert,
int selection_bound);
/* GtkTextContent implementation
*/
#define GTK_TYPE_TEXT_CONTENT (gtk_text_content_get_type ())
#define GTK_TEXT_CONTENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_TEXT_CONTENT, GtkTextContent))
#define GTK_IS_TEXT_CONTENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_TEXT_CONTENT))
#define GTK_TEXT_CONTENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_TEXT_CONTENT, GtkTextContentClass))
#define GTK_IS_TEXT_CONTENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_TEXT_CONTENT))
#define GTK_TEXT_CONTENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_TEXT_CONTENT, GtkTextContentClass))
typedef struct _GtkTextContent GtkTextContent;
typedef struct _GtkTextContentClass GtkTextContentClass;
struct _GtkTextContent
{
GdkContentProvider parent;
GtkText *self;
};
struct _GtkTextContentClass
{
GdkContentProviderClass parent_class;
};
GType gtk_text_content_get_type (void) G_GNUC_CONST;
G_DEFINE_TYPE (GtkTextContent, gtk_text_content, GDK_TYPE_CONTENT_PROVIDER)
static GdkContentFormats *
gtk_text_content_ref_formats (GdkContentProvider *provider)
{
return gdk_content_formats_new_for_gtype (G_TYPE_STRING);
}
static gboolean
gtk_text_content_get_value (GdkContentProvider *provider,
GValue *value,
GError **error)
{
GtkTextContent *content = GTK_TEXT_CONTENT (provider);
if (G_VALUE_HOLDS (value, G_TYPE_STRING))
{
int start, end;
if (gtk_text_get_selection_bounds (content->self, &start, &end))
{
char *str = gtk_text_get_display_text (content->self, start, end);
g_value_take_string (value, str);
}
return TRUE;
}
return GDK_CONTENT_PROVIDER_CLASS (gtk_text_content_parent_class)->get_value (provider, value, error);
}
static void
gtk_text_content_detach (GdkContentProvider *provider,
GdkClipboard *clipboard)
{
GtkTextContent *content = GTK_TEXT_CONTENT (provider);
int current_pos, selection_bound;
gtk_text_get_selection_bounds (content->self, &current_pos, &selection_bound);
gtk_text_set_selection_bounds (content->self, current_pos, current_pos);
}
static void
gtk_text_content_class_init (GtkTextContentClass *class)
{
GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (class);
provider_class->ref_formats = gtk_text_content_ref_formats;
provider_class->get_value = gtk_text_content_get_value;
provider_class->detach_clipboard = gtk_text_content_detach;
}
static void
gtk_text_content_init (GtkTextContent *content)
{
}
/* GtkText
*/
static const GtkTextHistoryFuncs history_funcs = {
gtk_text_history_change_state_cb,
gtk_text_history_insert_cb,
gtk_text_history_delete_cb,
gtk_text_history_select_cb,
};
G_DEFINE_TYPE_WITH_CODE (GtkText, gtk_text, GTK_TYPE_WIDGET,
G_ADD_PRIVATE (GtkText)
G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, gtk_text_editable_init))
static void
add_move_binding (GtkWidgetClass *widget_class,
guint keyval,
guint modmask,
GtkMovementStep step,
int count)
{
g_return_if_fail ((modmask & GDK_SHIFT_MASK) == 0);
gtk_widget_class_add_binding_signal (widget_class,
keyval, modmask,
"move-cursor",
"(iib)", step, count, FALSE);
/* Selection-extending version */
gtk_widget_class_add_binding_signal (widget_class,
keyval, modmask | GDK_SHIFT_MASK,
"move-cursor",
"(iib)", step, count, TRUE);
}
static void
gtk_text_class_init (GtkTextClass *class)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (class);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
gobject_class->dispose = gtk_text_dispose;
gobject_class->finalize = gtk_text_finalize;
gobject_class->set_property = gtk_text_set_property;
gobject_class->get_property = gtk_text_get_property;
widget_class->map = gtk_text_map;
widget_class->unmap = gtk_text_unmap;
widget_class->realize = gtk_text_realize;
widget_class->unrealize = gtk_text_unrealize;
widget_class->measure = gtk_text_measure;
widget_class->size_allocate = gtk_text_size_allocate;
widget_class->snapshot = gtk_text_snapshot;
widget_class->grab_focus = gtk_text_grab_focus;
widget_class->css_changed = gtk_text_css_changed;
widget_class->direction_changed = gtk_text_direction_changed;
widget_class->state_flags_changed = gtk_text_state_flags_changed;
widget_class->mnemonic_activate = gtk_text_mnemonic_activate;
class->move_cursor = gtk_text_move_cursor;
class->insert_at_cursor = gtk_text_insert_at_cursor;
class->delete_from_cursor = gtk_text_delete_from_cursor;
class->backspace = gtk_text_backspace;
class->cut_clipboard = gtk_text_cut_clipboard;
class->copy_clipboard = gtk_text_copy_clipboard;
class->paste_clipboard = gtk_text_paste_clipboard;
class->toggle_overwrite = gtk_text_toggle_overwrite;
class->insert_emoji = gtk_text_insert_emoji;
class->activate = gtk_text_real_activate;
quark_password_hint = g_quark_from_static_string ("gtk-entry-password-hint");
text_props[PROP_BUFFER] =
g_param_spec_object ("buffer",
P_("Text Buffer"),
P_("Text buffer object which actually stores self text"),
GTK_TYPE_ENTRY_BUFFER,
GTK_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_MAX_LENGTH] =
g_param_spec_int ("max-length",
P_("Maximum length"),
P_("Maximum number of characters for this self. Zero if no maximum"),
0, GTK_ENTRY_BUFFER_MAX_SIZE,
0,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_INVISIBLE_CHAR] =
g_param_spec_unichar ("invisible-char",
P_("Invisible character"),
P_("The character to use when masking self contents (in “password mode”)"),
'*',
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_ACTIVATES_DEFAULT] =
g_param_spec_boolean ("activates-default",
P_("Activates default"),
P_("Whether to activate the default widget (such as the default button in a dialog) when Enter is pressed"),
FALSE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_SCROLL_OFFSET] =
g_param_spec_int ("scroll-offset",
P_("Scroll offset"),
P_("Number of pixels of the self scrolled off the screen to the left"),
0, G_MAXINT,
0,
GTK_PARAM_READABLE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:truncate-multiline:
*
* When %TRUE, pasted multi-line text is truncated to the first line.
*/
text_props[PROP_TRUNCATE_MULTILINE] =
g_param_spec_boolean ("truncate-multiline",
P_("Truncate multiline"),
P_("Whether to truncate multiline pastes to one line."),
FALSE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:overwrite-mode:
*
* If text is overwritten when typing in the #GtkText.
*/
text_props[PROP_OVERWRITE_MODE] =
g_param_spec_boolean ("overwrite-mode",
P_("Overwrite mode"),
P_("Whether new text overwrites existing text"),
FALSE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:invisible-char-set:
*
* Whether the invisible char has been set for the #GtkText.
*/
text_props[PROP_INVISIBLE_CHAR_SET] =
g_param_spec_boolean ("invisible-char-set",
P_("Invisible character set"),
P_("Whether the invisible character has been set"),
FALSE,
GTK_PARAM_READWRITE);
/**
* GtkText:placeholder-text:
*
* The text that will be displayed in the #GtkText when it is empty
* and unfocused.
*/
text_props[PROP_PLACEHOLDER_TEXT] =
g_param_spec_string ("placeholder-text",
P_("Placeholder text"),
P_("Show text in the self when its empty and unfocused"),
NULL,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:im-module:
*
* Which IM (input method) module should be used for this self.
* See #GtkIMContext.
*
* Setting this to a non-%NULL value overrides the
* system-wide IM module setting. See the GtkSettings
* #GtkSettings:gtk-im-module property.
*/
text_props[PROP_IM_MODULE] =
g_param_spec_string ("im-module",
P_("IM module"),
P_("Which IM module should be used"),
NULL,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:input-purpose:
*
* The purpose of this text field.
*
* This property can be used by on-screen keyboards and other input
* methods to adjust their behaviour.
*
* Note that setting the purpose to %GTK_INPUT_PURPOSE_PASSWORD or
* %GTK_INPUT_PURPOSE_PIN is independent from setting
* #GtkText:visibility.
*/
text_props[PROP_INPUT_PURPOSE] =
g_param_spec_enum ("input-purpose",
P_("Purpose"),
P_("Purpose of the text field"),
GTK_TYPE_INPUT_PURPOSE,
GTK_INPUT_PURPOSE_FREE_FORM,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:input-hints:
*
* Additional hints (beyond #GtkText:input-purpose) that
* allow input methods to fine-tune their behaviour.
*/
text_props[PROP_INPUT_HINTS] =
g_param_spec_flags ("input-hints",
P_("hints"),
P_("Hints for the text field behaviour"),
GTK_TYPE_INPUT_HINTS,
GTK_INPUT_HINT_NONE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:attributes:
*
* A list of Pango attributes to apply to the text of the self.
*
* This is mainly useful to change the size or weight of the text.
*
* The #PangoAttribute's @start_index and @end_index must refer to the
* #GtkEntryBuffer text, i.e. without the preedit string.
*/
text_props[PROP_ATTRIBUTES] =
g_param_spec_boxed ("attributes",
P_("Attributes"),
P_("A list of style attributes to apply to the text of the self"),
PANGO_TYPE_ATTR_LIST,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:tabs:
*
* A list of tabstops to apply to the text of the self.
*/
text_props[PROP_TABS] =
g_param_spec_boxed ("tabs",
P_("Tabs"),
P_("A list of tabstop locations to apply to the text of the self"),
PANGO_TYPE_TAB_ARRAY,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_ENABLE_EMOJI_COMPLETION] =
g_param_spec_boolean ("enable-emoji-completion",
P_("Enable Emoji completion"),
P_("Whether to suggest Emoji replacements"),
FALSE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_VISIBILITY] =
g_param_spec_boolean ("visibility",
P_("Visibility"),
P_("FALSE displays the “invisible char” instead of the actual text (password mode)"),
TRUE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
text_props[PROP_PROPAGATE_TEXT_WIDTH] =
g_param_spec_boolean ("propagate-text-width",
P_("Propagate text width"),
P_("Whether the entry should grow and shrink with the content"),
FALSE,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
/**
* GtkText:extra-menu:
*
* A menu model whose contents will be appended to
* the context menu.
*/
text_props[PROP_EXTRA_MENU] =
g_param_spec_object ("extra-menu",
P_("Extra menu"),
P_("Menu model to append to the context menu"),
G_TYPE_MENU_MODEL,
GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (gobject_class, NUM_PROPERTIES, text_props);
gtk_editable_install_properties (gobject_class, NUM_PROPERTIES);
/* Action signals */
/**
* GtkText::activate:
* @self: The widget on which the signal is emitted
*
* The ::activate signal is emitted when the user hits
* the Enter key.
*
* The default bindings for this signal are all forms of the Enter key.
*/
signals[ACTIVATE] =
g_signal_new (I_("activate"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, activate),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkText::move-cursor:
* @self: the object which received the signal
* @step: the granularity of the move, as a #GtkMovementStep
* @count: the number of @step units to move
* @extend: %TRUE if the move should extend the selection
*
* The ::move-cursor signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted when the user initiates a cursor movement.
* If the cursor is not visible in @self, this signal causes
* the viewport to be moved instead.
*
* Applications should not connect to it, but may emit it with
* g_signal_emit_by_name() if they need to control the cursor
* programmatically.
*
* The default bindings for this signal come in two variants,
* the variant with the Shift modifier extends the selection,
* the variant without the Shift modifier does not.
* There are too many key combinations to list them all here.
* - Arrow keys move by individual characters/lines
* - Ctrl-arrow key combinations move by words/paragraphs
* - Home/End keys move to the ends of the buffer
*/
signals[MOVE_CURSOR] =
g_signal_new (I_("move-cursor"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, move_cursor),
NULL, NULL,
_gtk_marshal_VOID__ENUM_INT_BOOLEAN,
G_TYPE_NONE, 3,
GTK_TYPE_MOVEMENT_STEP,
G_TYPE_INT,
G_TYPE_BOOLEAN);
/**
* GtkText::insert-at-cursor:
* @self: the object which received the signal
* @string: the string to insert
*
* The ::insert-at-cursor signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted when the user initiates the insertion of a
* fixed string at the cursor.
*
* This signal has no default bindings.
*/
signals[INSERT_AT_CURSOR] =
g_signal_new (I_("insert-at-cursor"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, insert_at_cursor),
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
G_TYPE_STRING);
/**
* GtkText::delete-from-cursor:
* @self: the object which received the signal
* @type: the granularity of the deletion, as a #GtkDeleteType
* @count: the number of @type units to delete
*
* The ::delete-from-cursor signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted when the user initiates a text deletion.
*
* If the @type is %GTK_DELETE_CHARS, GTK deletes the selection
* if there is one, otherwise it deletes the requested number
* of characters.
*
* The default bindings for this signal are
* Delete for deleting a character and Ctrl-Delete for
* deleting a word.
*/
signals[DELETE_FROM_CURSOR] =
g_signal_new (I_("delete-from-cursor"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, delete_from_cursor),
NULL, NULL,
_gtk_marshal_VOID__ENUM_INT,
G_TYPE_NONE, 2,
GTK_TYPE_DELETE_TYPE,
G_TYPE_INT);
/**
* GtkText::backspace:
* @self: the object which received the signal
*
* The ::backspace signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted when the user asks for it.
*
* The default bindings for this signal are
* Backspace and Shift-Backspace.
*/
signals[BACKSPACE] =
g_signal_new (I_("backspace"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, backspace),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkText::cut-clipboard:
* @self: the object which received the signal
*
* The ::cut-clipboard signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted to cut the selection to the clipboard.
*
* The default bindings for this signal are
* Ctrl-x and Shift-Delete.
*/
signals[CUT_CLIPBOARD] =
g_signal_new (I_("cut-clipboard"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, cut_clipboard),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkText::copy-clipboard:
* @self: the object which received the signal
*
* The ::copy-clipboard signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted to copy the selection to the clipboard.
*
* The default bindings for this signal are
* Ctrl-c and Ctrl-Insert.
*/
signals[COPY_CLIPBOARD] =
g_signal_new (I_("copy-clipboard"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, copy_clipboard),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkText::paste-clipboard:
* @self: the object which received the signal
*
* The ::paste-clipboard signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted to paste the contents of the clipboard
* into the text view.
*
* The default bindings for this signal are
* Ctrl-v and Shift-Insert.
*/
signals[PASTE_CLIPBOARD] =
g_signal_new (I_("paste-clipboard"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, paste_clipboard),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkText::toggle-overwrite:
* @self: the object which received the signal
*
* The ::toggle-overwrite signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted to toggle the overwrite mode of the self.
*
* The default bindings for this signal is Insert.
*/
signals[TOGGLE_OVERWRITE] =
g_signal_new (I_("toggle-overwrite"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, toggle_overwrite),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/**
* GtkText::preedit-changed:
* @self: the object which received the signal
* @preedit: the current preedit string
*
* If an input method is used, the typed text will not immediately
* be committed to the buffer. So if you are interested in the text,
* connect to this signal.
*/
signals[PREEDIT_CHANGED] =
g_signal_new_class_handler (I_("preedit-changed"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
NULL,
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
G_TYPE_STRING);
/**
* GtkText::insert-emoji:
* @self: the object which received the signal
*
* The ::insert-emoji signal is a
* [keybinding signal][GtkSignalAction]
* which gets emitted to present the Emoji chooser for the @self.
*
* The default bindings for this signal are Ctrl-. and Ctrl-;
*/
signals[INSERT_EMOJI] =
g_signal_new (I_("insert-emoji"),
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
G_STRUCT_OFFSET (GtkTextClass, insert_emoji),
NULL, NULL,
NULL,
G_TYPE_NONE, 0);
/*
* Actions
*/
/**
* GtkText|clipboard.cut:
*
* Copies the contents to the clipboard and deletes it from the widget.
*/
gtk_widget_class_install_action (widget_class, "clipboard.cut", NULL,
gtk_text_activate_clipboard_cut);
/**
* GtkText|clipboard.copy:
*
* Copies the contents to the clipboard.
*/
gtk_widget_class_install_action (widget_class, "clipboard.copy", NULL,
gtk_text_activate_clipboard_copy);
/**
* GtkText|clipboard.paste:
*
* Inserts the contents of the clipboard into the widget.
*/
gtk_widget_class_install_action (widget_class, "clipboard.paste", NULL,
gtk_text_activate_clipboard_paste);
/**
* GtkText|selection.delete:
*
* Deletes the current selection.
*/
gtk_widget_class_install_action (widget_class, "selection.delete", NULL,
gtk_text_activate_selection_delete);
/**
* GtkText|selection.select-all:
*
* Selects all of the widgets content.
*/
gtk_widget_class_install_action (widget_class, "selection.select-all", NULL,
gtk_text_activate_selection_select_all);
/**
* GtkText|misc.insert-emoji:
*
* Opens the Emoji chooser.
*/
gtk_widget_class_install_action (widget_class, "misc.insert-emoji", NULL,
gtk_text_activate_misc_insert_emoji);
/**
* GtkText|misc.toggle-visibility:
*
* Toggles the #GtkText:visibility property.
*/
gtk_widget_class_install_property_action (widget_class,
"misc.toggle-visibility",
"visibility");
/**
* GtkText|text.undo:
*
* Undoes the last change to the contents.
*/
gtk_widget_class_install_action (widget_class, "text.undo", NULL, gtk_text_real_undo);
/**
* GtkText|text.redo:
*
* Redoes the last change to the contents.
*/
gtk_widget_class_install_action (widget_class, "text.redo", NULL, gtk_text_real_redo);
/**
* GtkText|menu.popup:
*
* Opens the context menu.
*/
gtk_widget_class_install_action (widget_class, "menu.popup", NULL, gtk_text_popup_menu);
/*
* Key bindings
*/
gtk_widget_class_add_binding_action (widget_class,
GDK_KEY_F10, GDK_SHIFT_MASK,
"menu.popup",
NULL);
gtk_widget_class_add_binding_action (widget_class,
GDK_KEY_Menu, 0,
"menu.popup",
NULL);
/* Moving the insertion point */
add_move_binding (widget_class, GDK_KEY_Right, 0,
GTK_MOVEMENT_VISUAL_POSITIONS, 1);
add_move_binding (widget_class, GDK_KEY_Left, 0,
GTK_MOVEMENT_VISUAL_POSITIONS, -1);
add_move_binding (widget_class, GDK_KEY_KP_Right, 0,
GTK_MOVEMENT_VISUAL_POSITIONS, 1);
add_move_binding (widget_class, GDK_KEY_KP_Left, 0,
GTK_MOVEMENT_VISUAL_POSITIONS, -1);
add_move_binding (widget_class, GDK_KEY_Right, GDK_CONTROL_MASK,
GTK_MOVEMENT_WORDS, 1);
add_move_binding (widget_class, GDK_KEY_Left, GDK_CONTROL_MASK,
GTK_MOVEMENT_WORDS, -1);
add_move_binding (widget_class, GDK_KEY_KP_Right, GDK_CONTROL_MASK,
GTK_MOVEMENT_WORDS, 1);
add_move_binding (widget_class, GDK_KEY_KP_Left, GDK_CONTROL_MASK,
GTK_MOVEMENT_WORDS, -1);
add_move_binding (widget_class, GDK_KEY_Home, 0,
GTK_MOVEMENT_DISPLAY_LINE_ENDS, -1);
add_move_binding (widget_class, GDK_KEY_End, 0,
GTK_MOVEMENT_DISPLAY_LINE_ENDS, 1);
add_move_binding (widget_class, GDK_KEY_KP_Home, 0,
GTK_MOVEMENT_DISPLAY_LINE_ENDS, -1);
add_move_binding (widget_class, GDK_KEY_KP_End, 0,
GTK_MOVEMENT_DISPLAY_LINE_ENDS, 1);
add_move_binding (widget_class, GDK_KEY_Home, GDK_CONTROL_MASK,
GTK_MOVEMENT_BUFFER_ENDS, -1);
add_move_binding (widget_class, GDK_KEY_End, GDK_CONTROL_MASK,
GTK_MOVEMENT_BUFFER_ENDS, 1);
add_move_binding (widget_class, GDK_KEY_KP_Home, GDK_CONTROL_MASK,
GTK_MOVEMENT_BUFFER_ENDS, -1);
add_move_binding (widget_class, GDK_KEY_KP_End, GDK_CONTROL_MASK,
GTK_MOVEMENT_BUFFER_ENDS, 1);
/* Select all
*/
gtk_widget_class_add_binding (widget_class,
GDK_KEY_a, GDK_CONTROL_MASK,
(GtkShortcutFunc) gtk_text_select_all,
NULL);
gtk_widget_class_add_binding (widget_class,
GDK_KEY_slash, GDK_CONTROL_MASK,
(GtkShortcutFunc) gtk_text_select_all,
NULL);
/* Unselect all
*/
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_backslash, GDK_CONTROL_MASK,
"move-cursor",
"(iib)", GTK_MOVEMENT_VISUAL_POSITIONS, 0, FALSE);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_a, GDK_SHIFT_MASK | GDK_CONTROL_MASK,
"move-cursor",
"(iib)", GTK_MOVEMENT_VISUAL_POSITIONS, 0, FALSE);
/* Activate
*/
gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Return, 0,
"activate",
NULL);
gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_ISO_Enter, 0,
"activate",
NULL);
gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Enter, 0,
"activate",
NULL);
/* Deleting text */
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_Delete, 0,
"delete-from-cursor",
"(ii)", GTK_DELETE_CHARS, 1);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_KP_Delete, 0,
"delete-from-cursor",
"(ii)", GTK_DELETE_CHARS, 1);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_BackSpace, 0,
"backspace",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_u, GDK_CONTROL_MASK,
"delete-from-cursor",
"(ii)", GTK_DELETE_PARAGRAPH_ENDS, -1);
/* Make this do the same as Backspace, to help with mis-typing */
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_BackSpace, GDK_SHIFT_MASK,
"backspace",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_Delete, GDK_CONTROL_MASK,
"delete-from-cursor",
"(ii)", GTK_DELETE_WORD_ENDS, 1);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_KP_Delete, GDK_CONTROL_MASK,
"delete-from-cursor",
"(ii)", GTK_DELETE_WORD_ENDS, 1);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_BackSpace, GDK_CONTROL_MASK,
"delete-from-cursor",
"(ii)", GTK_DELETE_WORD_ENDS, -1);
/* Cut/copy/paste */
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_x, GDK_CONTROL_MASK,
"cut-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_c, GDK_CONTROL_MASK,
"copy-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_v, GDK_CONTROL_MASK,
"paste-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_Delete, GDK_SHIFT_MASK,
"cut-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_Insert, GDK_CONTROL_MASK,
"copy-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_Insert, GDK_SHIFT_MASK,
"paste-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_KP_Delete, GDK_SHIFT_MASK,
"cut-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_KP_Insert, GDK_CONTROL_MASK,
"copy-clipboard",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_KP_Insert, GDK_SHIFT_MASK,
"paste-clipboard",
NULL);
/* Overwrite */
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_Insert, 0,
"toggle-overwrite",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_KP_Insert, 0,
"toggle-overwrite",
NULL);
/* Emoji */
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_period, GDK_CONTROL_MASK,
"insert-emoji",
NULL);
gtk_widget_class_add_binding_signal (widget_class,
GDK_KEY_semicolon, GDK_CONTROL_MASK,
"insert-emoji",
NULL);
/* Undo/Redo */
gtk_widget_class_add_binding_action (widget_class,
GDK_KEY_z, GDK_CONTROL_MASK,
"text.undo", NULL);
gtk_widget_class_add_binding_action (widget_class,
GDK_KEY_y, GDK_CONTROL_MASK,
"text.redo", NULL);
gtk_widget_class_add_binding_action (widget_class,
GDK_KEY_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK,
"text.redo", NULL);
gtk_widget_class_set_css_name (widget_class, I_("text"));
gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_NONE);
}
static void
editable_insert_text (GtkEditable *editable,
const char *text,
int length,
int *position)
{
gtk_text_insert_text (GTK_TEXT (editable), text, length, position);
}
static void
editable_delete_text (GtkEditable *editable,
int start_pos,
int end_pos)
{
gtk_text_delete_text (GTK_TEXT (editable), start_pos, end_pos);
}
static const char *
editable_get_text (GtkEditable *editable)
{
return gtk_entry_buffer_get_text (get_buffer (GTK_TEXT (editable)));
}
static void
editable_set_selection_bounds (GtkEditable *editable,
int start_pos,
int end_pos)
{
gtk_text_set_selection_bounds (GTK_TEXT (editable), start_pos, end_pos);
}
static gboolean
editable_get_selection_bounds (GtkEditable *editable,
int *start_pos,
int *end_pos)
{
return gtk_text_get_selection_bounds (GTK_TEXT (editable), start_pos, end_pos);
}
static void
gtk_text_editable_init (GtkEditableInterface *iface)
{
iface->insert_text = editable_insert_text;
iface->delete_text = editable_delete_text;
iface->get_text = editable_get_text;
iface->set_selection_bounds = editable_set_selection_bounds;
iface->get_selection_bounds = editable_get_selection_bounds;
}
static void
gtk_text_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkText *self = GTK_TEXT (object);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
switch (prop_id)
{
/* GtkEditable properties */
case NUM_PROPERTIES + GTK_EDITABLE_PROP_EDITABLE:
gtk_text_set_editable (self, g_value_get_boolean (value));
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_WIDTH_CHARS:
gtk_text_set_width_chars (self, g_value_get_int (value));
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_MAX_WIDTH_CHARS:
gtk_text_set_max_width_chars (self, g_value_get_int (value));
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_TEXT:
gtk_text_set_text (self, g_value_get_string (value));
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_XALIGN:
gtk_text_set_alignment (self, g_value_get_float (value));
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_ENABLE_UNDO:
if (g_value_get_boolean (value) != gtk_text_history_get_enabled (priv->history))
{
gtk_text_history_set_enabled (priv->history, g_value_get_boolean (value));
g_object_notify_by_pspec (object, pspec);
}
break;
/* GtkText properties */
case PROP_BUFFER:
gtk_text_set_buffer (self, g_value_get_object (value));
break;
case PROP_MAX_LENGTH:
gtk_text_set_max_length (self, g_value_get_int (value));
break;
case PROP_VISIBILITY:
gtk_text_set_visibility (self, g_value_get_boolean (value));
break;
case PROP_INVISIBLE_CHAR:
gtk_text_set_invisible_char (self, g_value_get_uint (value));
break;
case PROP_ACTIVATES_DEFAULT:
gtk_text_set_activates_default (self, g_value_get_boolean (value));
break;
case PROP_TRUNCATE_MULTILINE:
gtk_text_set_truncate_multiline (self, g_value_get_boolean (value));
break;
case PROP_OVERWRITE_MODE:
gtk_text_set_overwrite_mode (self, g_value_get_boolean (value));
break;
case PROP_INVISIBLE_CHAR_SET:
if (g_value_get_boolean (value))
priv->invisible_char_set = TRUE;
else
gtk_text_unset_invisible_char (self);
break;
case PROP_PLACEHOLDER_TEXT:
gtk_text_set_placeholder_text (self, g_value_get_string (value));
break;
case PROP_IM_MODULE:
g_free (priv->im_module);
priv->im_module = g_value_dup_string (value);
if (GTK_IS_IM_MULTICONTEXT (priv->im_context))
gtk_im_multicontext_set_context_id (GTK_IM_MULTICONTEXT (priv->im_context), priv->im_module);
g_object_notify_by_pspec (object, pspec);
break;
case PROP_INPUT_PURPOSE:
gtk_text_set_input_purpose (self, g_value_get_enum (value));
break;
case PROP_INPUT_HINTS:
gtk_text_set_input_hints (self, g_value_get_flags (value));
break;
case PROP_ATTRIBUTES:
gtk_text_set_attributes (self, g_value_get_boxed (value));
break;
case PROP_TABS:
gtk_text_set_tabs (self, g_value_get_boxed (value));
break;
case PROP_ENABLE_EMOJI_COMPLETION:
gtk_text_set_enable_emoji_completion (self, g_value_get_boolean (value));
break;
case PROP_PROPAGATE_TEXT_WIDTH:
gtk_text_set_propagate_text_width (self, g_value_get_boolean (value));
break;
case PROP_EXTRA_MENU:
gtk_text_set_extra_menu (self, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_text_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkText *self = GTK_TEXT (object);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
switch (prop_id)
{
/* GtkEditable properties */
case NUM_PROPERTIES + GTK_EDITABLE_PROP_CURSOR_POSITION:
g_value_set_int (value, priv->current_pos);
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_SELECTION_BOUND:
g_value_set_int (value, priv->selection_bound);
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_EDITABLE:
g_value_set_boolean (value, priv->editable);
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_WIDTH_CHARS:
g_value_set_int (value, priv->width_chars);
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_MAX_WIDTH_CHARS:
g_value_set_int (value, priv->max_width_chars);
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_TEXT:
g_value_set_string (value, gtk_entry_buffer_get_text (get_buffer (self)));
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_XALIGN:
g_value_set_float (value, priv->xalign);
break;
case NUM_PROPERTIES + GTK_EDITABLE_PROP_ENABLE_UNDO:
g_value_set_boolean (value, gtk_text_history_get_enabled (priv->history));
break;
/* GtkText properties */
case PROP_BUFFER:
g_value_set_object (value, get_buffer (self));
break;
case PROP_MAX_LENGTH:
g_value_set_int (value, gtk_entry_buffer_get_max_length (get_buffer (self)));
break;
case PROP_VISIBILITY:
g_value_set_boolean (value, priv->visible);
break;
case PROP_INVISIBLE_CHAR:
g_value_set_uint (value, priv->invisible_char);
break;
case PROP_ACTIVATES_DEFAULT:
g_value_set_boolean (value, priv->activates_default);
break;
case PROP_SCROLL_OFFSET:
g_value_set_int (value, priv->scroll_offset);
break;
case PROP_TRUNCATE_MULTILINE:
g_value_set_boolean (value, priv->truncate_multiline);
break;
case PROP_OVERWRITE_MODE:
g_value_set_boolean (value, priv->overwrite_mode);
break;
case PROP_INVISIBLE_CHAR_SET:
g_value_set_boolean (value, priv->invisible_char_set);
break;
case PROP_IM_MODULE:
g_value_set_string (value, priv->im_module);
break;
case PROP_PLACEHOLDER_TEXT:
g_value_set_string (value, gtk_text_get_placeholder_text (self));
break;
case PROP_INPUT_PURPOSE:
g_value_set_enum (value, gtk_text_get_input_purpose (self));
break;
case PROP_INPUT_HINTS:
g_value_set_flags (value, gtk_text_get_input_hints (self));
break;
case PROP_ATTRIBUTES:
g_value_set_boxed (value, priv->attrs);
break;
case PROP_TABS:
g_value_set_boxed (value, priv->tabs);
break;
case PROP_ENABLE_EMOJI_COMPLETION:
g_value_set_boolean (value, priv->enable_emoji_completion);
break;
case PROP_PROPAGATE_TEXT_WIDTH:
g_value_set_boolean (value, priv->propagate_text_width);
break;
case PROP_EXTRA_MENU:
g_value_set_object (value, priv->extra_menu);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gtk_text_ensure_text_handles (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int i;
for (i = 0; i < TEXT_HANDLE_N_HANDLES; i++)
{
if (priv->text_handles[i])
continue;
priv->text_handles[i] = gtk_text_handle_new (GTK_WIDGET (self));
g_signal_connect (priv->text_handles[i], "drag-started",
G_CALLBACK (gtk_text_handle_drag_started), self);
g_signal_connect (priv->text_handles[i], "handle-dragged",
G_CALLBACK (gtk_text_handle_dragged), self);
g_signal_connect (priv->text_handles[i], "drag-finished",
G_CALLBACK (gtk_text_handle_drag_finished), self);
}
}
static void
gtk_text_init (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkCssNode *widget_node;
GtkGesture *gesture;
GtkEventController *controller;
int i;
GtkDropTarget *target;
gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
priv->editable = TRUE;
priv->visible = TRUE;
priv->dnd_position = -1;
priv->width_chars = -1;
priv->max_width_chars = -1;
priv->truncate_multiline = FALSE;
priv->xalign = 0.0;
priv->insert_pos = -1;
priv->cursor_alpha = 1.0;
priv->invisible_char = 0;
priv->history = gtk_text_history_new (&history_funcs, self);
gtk_text_history_set_max_undo_levels (priv->history, DEFAULT_MAX_UNDO);
priv->selection_content = g_object_new (GTK_TYPE_TEXT_CONTENT, NULL);
GTK_TEXT_CONTENT (priv->selection_content)->self = self;
target = gtk_drop_target_new (G_TYPE_STRING, GDK_ACTION_COPY | GDK_ACTION_MOVE);
g_signal_connect (target, "accept", G_CALLBACK (gtk_text_drag_accept), self);
g_signal_connect (target, "enter", G_CALLBACK (gtk_text_drag_motion), self);
g_signal_connect (target, "motion", G_CALLBACK (gtk_text_drag_motion), self);
g_signal_connect (target, "leave", G_CALLBACK (gtk_text_drag_leave), self);
g_signal_connect (target, "drop", G_CALLBACK (gtk_text_drag_drop), self);
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (target));
/* This object is completely private. No external entity can gain a reference
* to it; so we create it here and destroy it in finalize().
*/
priv->im_context = gtk_im_multicontext_new ();
g_signal_connect (priv->im_context, "commit",
G_CALLBACK (gtk_text_commit_cb), self);
g_signal_connect (priv->im_context, "preedit-changed",
G_CALLBACK (gtk_text_preedit_changed_cb), self);
g_signal_connect (priv->im_context, "retrieve-surrounding",
G_CALLBACK (gtk_text_retrieve_surrounding_cb), self);
g_signal_connect (priv->im_context, "delete-surrounding",
G_CALLBACK (gtk_text_delete_surrounding_cb), self);
priv->drag_gesture = gtk_gesture_drag_new ();
g_signal_connect (priv->drag_gesture, "drag-update",
G_CALLBACK (gtk_text_drag_gesture_update), self);
g_signal_connect (priv->drag_gesture, "drag-end",
G_CALLBACK (gtk_text_drag_gesture_end), self);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (priv->drag_gesture), 0);
gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (priv->drag_gesture), TRUE);
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture));
gesture = gtk_gesture_click_new ();
gtk_event_controller_set_name (GTK_EVENT_CONTROLLER (gesture), "gtk-text-click-gesture");
g_signal_connect (gesture, "pressed",
G_CALLBACK (gtk_text_click_gesture_pressed), self);
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 0);
gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (gesture), TRUE);
gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
controller = gtk_event_controller_motion_new ();
gtk_event_controller_set_name (controller, "gtk-text-motion-controller");
g_signal_connect (controller, "motion",
G_CALLBACK (gtk_text_motion_controller_motion), self);
gtk_widget_add_controller (GTK_WIDGET (self), controller);
priv->key_controller = gtk_event_controller_key_new ();
gtk_event_controller_set_propagation_phase (priv->key_controller, GTK_PHASE_TARGET);
gtk_event_controller_set_name (priv->key_controller, "gtk-text-key-controller");
g_signal_connect (priv->key_controller, "key-pressed",
G_CALLBACK (gtk_text_key_controller_key_pressed), self);
g_signal_connect_swapped (priv->key_controller, "im-update",
G_CALLBACK (gtk_text_schedule_im_reset), self);
gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY (priv->key_controller),
priv->im_context);
gtk_widget_add_controller (GTK_WIDGET (self), priv->key_controller);
controller = gtk_event_controller_focus_new ();
gtk_event_controller_set_name (controller, "gtk-text-focus-controller");
g_signal_connect (controller, "notify::is-focus",
G_CALLBACK (gtk_text_focus_changed), self);
gtk_widget_add_controller (GTK_WIDGET (self), controller);
widget_node = gtk_widget_get_css_node (GTK_WIDGET (self));
for (i = 0; i < 2; i++)
{
priv->undershoot_node[i] = gtk_css_node_new ();
gtk_css_node_set_name (priv->undershoot_node[i], g_quark_from_static_string ("undershoot"));
gtk_css_node_add_class (priv->undershoot_node[i], g_quark_from_static_string (i == 0 ? "left" : "right"));
gtk_css_node_set_parent (priv->undershoot_node[i], widget_node);
gtk_css_node_set_state (priv->undershoot_node[i], gtk_css_node_get_state (widget_node) & ~GTK_STATE_FLAG_DROP_ACTIVE);
g_object_unref (priv->undershoot_node[i]);
}
set_text_cursor (GTK_WIDGET (self));
}
static void
gtk_text_dispose (GObject *object)
{
GtkText *self = GTK_TEXT (object);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkSeat *seat;
GdkDevice *keyboard = NULL;
GtkWidget *chooser;
priv->current_pos = priv->selection_bound = 0;
gtk_text_reset_im_context (self);
gtk_text_reset_layout (self);
if (priv->blink_tick)
{
gtk_widget_remove_tick_callback (GTK_WIDGET (object), priv->blink_tick);
priv->blink_tick = 0;
}
if (priv->magnifier)
_gtk_magnifier_set_inspected (GTK_MAGNIFIER (priv->magnifier), NULL);
if (priv->buffer)
{
buffer_disconnect_signals (self);
g_object_unref (priv->buffer);
priv->buffer = NULL;
}
g_clear_pointer (&priv->emoji_completion, gtk_widget_unparent);
chooser = g_object_get_data (object, "gtk-emoji-chooser");
if (chooser)
gtk_widget_unparent (chooser);
seat = gdk_display_get_default_seat (gtk_widget_get_display (GTK_WIDGET (object)));
if (seat)
keyboard = gdk_seat_get_keyboard (seat);
if (keyboard)
g_signal_handlers_disconnect_by_func (keyboard, direction_changed, self);
g_clear_pointer (&priv->selection_bubble, gtk_widget_unparent);
g_clear_pointer (&priv->popup_menu, gtk_widget_unparent);
g_clear_pointer ((GtkWidget **) &priv->text_handles[TEXT_HANDLE_CURSOR], gtk_widget_unparent);
g_clear_pointer ((GtkWidget **) &priv->text_handles[TEXT_HANDLE_SELECTION_BOUND], gtk_widget_unparent);
g_clear_object (&priv->extra_menu);
g_clear_pointer (&priv->magnifier_popover, gtk_widget_unparent);
g_clear_pointer (&priv->placeholder, gtk_widget_unparent);
G_OBJECT_CLASS (gtk_text_parent_class)->dispose (object);
}
static void
gtk_text_finalize (GObject *object)
{
GtkText *self = GTK_TEXT (object);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_clear_object (&priv->selection_content);
g_clear_object (&priv->history);
g_clear_object (&priv->cached_layout);
g_clear_object (&priv->im_context);
g_free (priv->im_module);
if (priv->tabs)
pango_tab_array_free (priv->tabs);
if (priv->attrs)
pango_attr_list_unref (priv->attrs);
G_OBJECT_CLASS (gtk_text_parent_class)->finalize (object);
}
static void
gtk_text_ensure_magnifier (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->magnifier_popover)
return;
priv->magnifier = _gtk_magnifier_new (GTK_WIDGET (self));
gtk_widget_set_size_request (priv->magnifier, 100, 60);
_gtk_magnifier_set_magnification (GTK_MAGNIFIER (priv->magnifier), 2.0);
priv->magnifier_popover = gtk_popover_new ();
gtk_popover_set_position (GTK_POPOVER (priv->magnifier_popover), GTK_POS_TOP);
gtk_widget_set_parent (priv->magnifier_popover, GTK_WIDGET (self));
gtk_widget_add_css_class (priv->magnifier_popover, "magnifier");
gtk_popover_set_autohide (GTK_POPOVER (priv->magnifier_popover), FALSE);
gtk_popover_set_child (GTK_POPOVER (priv->magnifier_popover), priv->magnifier);
gtk_widget_show (priv->magnifier);
}
static void
begin_change (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
priv->change_count++;
g_object_freeze_notify (G_OBJECT (self));
}
static void
end_change (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (priv->change_count > 0);
g_object_thaw_notify (G_OBJECT (self));
priv->change_count--;
if (priv->change_count == 0)
{
if (priv->real_changed)
{
g_signal_emit_by_name (self, "changed");
priv->real_changed = FALSE;
}
}
}
static void
emit_changed (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->change_count == 0)
g_signal_emit_by_name (self, "changed");
else
priv->real_changed = TRUE;
}
static DisplayMode
gtk_text_get_display_mode (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->visible)
return DISPLAY_NORMAL;
if (priv->invisible_char == 0 && priv->invisible_char_set)
return DISPLAY_BLANK;
return DISPLAY_INVISIBLE;
}
char *
gtk_text_get_display_text (GtkText *self,
int start_pos,
int end_pos)
{
GtkTextPasswordHint *password_hint;
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gunichar invisible_char;
const char *start;
const char *end;
const char *text;
char char_str[7];
int char_len;
GString *str;
guint length;
int i;
text = gtk_entry_buffer_get_text (get_buffer (self));
length = gtk_entry_buffer_get_length (get_buffer (self));
if (end_pos < 0 || end_pos > length)
end_pos = length;
if (start_pos > length)
start_pos = length;
if (end_pos <= start_pos)
return g_strdup ("");
else if (priv->visible)
{
start = g_utf8_offset_to_pointer (text, start_pos);
end = g_utf8_offset_to_pointer (start, end_pos - start_pos);
return g_strndup (start, end - start);
}
else
{
str = g_string_sized_new (length * 2);
/* Figure out what our invisible char is and encode it */
if (!priv->invisible_char)
invisible_char = priv->invisible_char_set ? ' ' : '*';
else
invisible_char = priv->invisible_char;
char_len = g_unichar_to_utf8 (invisible_char, char_str);
/*
* Add hidden characters for each character in the text
* buffer. If there is a password hint, then keep that
* character visible.
*/
password_hint = g_object_get_qdata (G_OBJECT (self), quark_password_hint);
for (i = start_pos; i < end_pos; ++i)
{
if (password_hint && i == password_hint->position)
{
start = g_utf8_offset_to_pointer (text, i);
g_string_append_len (str, start, g_utf8_next_char (start) - start);
}
else
{
g_string_append_len (str, char_str, char_len);
}
}
return g_string_free (str, FALSE);
}
}
static void
gtk_text_map (GtkWidget *widget)
{
GtkText *self = GTK_TEXT (widget);
GTK_WIDGET_CLASS (gtk_text_parent_class)->map (widget);
gtk_text_recompute (self);
}
static void
gtk_text_unmap (GtkWidget *widget)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
priv->text_handles_enabled = FALSE;
gtk_text_update_handles (self);
priv->cursor_alpha = 1.0;
GTK_WIDGET_CLASS (gtk_text_parent_class)->unmap (widget);
}
static void
gtk_text_realize (GtkWidget *widget)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GTK_WIDGET_CLASS (gtk_text_parent_class)->realize (widget);
gtk_im_context_set_client_widget (priv->im_context, widget);
gtk_text_adjust_scroll (self);
gtk_text_update_primary_selection (self);
}
static void
gtk_text_unrealize (GtkWidget *widget)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkClipboard *clipboard;
gtk_text_reset_layout (self);
gtk_im_context_set_client_widget (priv->im_context, NULL);
clipboard = gtk_widget_get_primary_clipboard (widget);
if (gdk_clipboard_get_content (clipboard) == priv->selection_content)
gdk_clipboard_set_content (clipboard, NULL);
GTK_WIDGET_CLASS (gtk_text_parent_class)->unrealize (widget);
}
static void
update_im_cursor_location (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_width = gtk_widget_get_width (GTK_WIDGET (self));
GdkRectangle area;
int strong_x;
int strong_xoffset;
gtk_text_get_cursor_locations (self, &strong_x, NULL);
strong_xoffset = strong_x - priv->scroll_offset;
if (strong_xoffset < 0)
strong_xoffset = 0;
else if (strong_xoffset > text_width)
strong_xoffset = text_width;
area.x = strong_xoffset;
area.y = 0;
area.width = 0;
area.height = gtk_widget_get_height (GTK_WIDGET (self));
gtk_im_context_set_cursor_location (priv->im_context, &area);
}
static void
gtk_text_move_handle (GtkText *self,
GtkTextHandle *handle,
int x,
int y,
int height)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (!gtk_text_handle_get_is_dragged (handle) &&
(x < 0 || x > gtk_widget_get_width (GTK_WIDGET (self))))
{
/* Hide the handle if it's not being manipulated
* and fell outside of the visible text area.
*/
gtk_widget_hide (GTK_WIDGET (handle));
}
else
{
GdkRectangle rect;
rect.x = x;
rect.y = y;
rect.width = 1;
rect.height = height;
gtk_text_handle_set_position (handle, &rect);
gtk_widget_set_direction (GTK_WIDGET (handle), priv->resolved_dir);
gtk_widget_show (GTK_WIDGET (handle));
}
}
static int
gtk_text_get_selection_bound_location (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
PangoLayout *layout;
PangoRectangle pos;
int x;
const char *text;
int index;
layout = gtk_text_ensure_layout (self, 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 (self)) == GTK_TEXT_DIR_RTL)
x = (pos.x + pos.width) / PANGO_SCALE;
else
x = pos.x / PANGO_SCALE;
return x;
}
static void
gtk_text_update_handles (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_height = gtk_widget_get_height (GTK_WIDGET (self));
int strong_x;
int cursor, bound;
if (!priv->text_handles_enabled)
{
if (priv->text_handles[TEXT_HANDLE_CURSOR])
gtk_widget_hide (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_CURSOR]));
if (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])
gtk_widget_hide (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]));
}
else
{
gtk_text_ensure_text_handles (self);
gtk_text_get_cursor_locations (self, &strong_x, NULL);
cursor = strong_x - priv->scroll_offset;
if (priv->selection_bound != priv->current_pos)
{
int start, end;
bound = gtk_text_get_selection_bound_location (self) - priv->scroll_offset;
if (priv->selection_bound > priv->current_pos)
{
start = cursor;
end = bound;
}
else
{
start = bound;
end = cursor;
}
/* Update start selection bound */
gtk_text_handle_set_role (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND],
GTK_TEXT_HANDLE_ROLE_SELECTION_END);
gtk_text_move_handle (self,
priv->text_handles[TEXT_HANDLE_SELECTION_BOUND],
end, 0, text_height);
gtk_text_handle_set_role (priv->text_handles[TEXT_HANDLE_CURSOR],
GTK_TEXT_HANDLE_ROLE_SELECTION_START);
gtk_text_move_handle (self,
priv->text_handles[TEXT_HANDLE_CURSOR],
start, 0, text_height);
}
else
{
gtk_widget_hide (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]));
gtk_text_handle_set_role (priv->text_handles[TEXT_HANDLE_CURSOR],
GTK_TEXT_HANDLE_ROLE_CURSOR);
gtk_text_move_handle (self,
priv->text_handles[TEXT_HANDLE_CURSOR],
cursor, 0, text_height);
}
}
}
static void
gtk_text_measure (GtkWidget *widget,
GtkOrientation orientation,
int for_size,
int *minimum,
int *natural,
int *minimum_baseline,
int *natural_baseline)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
PangoContext *context;
PangoFontMetrics *metrics;
context = gtk_widget_get_pango_context (widget);
metrics = pango_context_get_metrics (context, NULL, NULL);
if (orientation == GTK_ORIENTATION_HORIZONTAL)
{
int min, nat;
int char_width;
int digit_width;
int char_pixels;
char_width = pango_font_metrics_get_approximate_char_width (metrics);
digit_width = pango_font_metrics_get_approximate_digit_width (metrics);
char_pixels = (MAX (char_width, digit_width) + PANGO_SCALE - 1) / PANGO_SCALE;
if (priv->width_chars >= 0)
min = char_pixels * priv->width_chars;
else
min = 0;
if (priv->max_width_chars < 0)
nat = NAT_ENTRY_WIDTH;
else
nat = char_pixels * priv->max_width_chars;
if (priv->propagate_text_width)
{
PangoLayout *layout;
int act;
layout = gtk_text_ensure_layout (self, TRUE);
pango_layout_get_pixel_size (layout, &act, NULL);
nat = MIN (act, nat);
}
nat = MAX (min, nat);
if (priv->placeholder)
{
int pmin, pnat;
gtk_widget_measure (priv->placeholder, GTK_ORIENTATION_HORIZONTAL, -1,
&pmin, &pnat, NULL, NULL);
min = MAX (min, pmin);
nat = MAX (nat, pnat);
}
*minimum = min;
*natural = nat;
}
else
{
int height, baseline;
PangoLayout *layout;
layout = gtk_text_ensure_layout (self, TRUE);
priv->ascent = pango_font_metrics_get_ascent (metrics);
priv->descent = pango_font_metrics_get_descent (metrics);
pango_layout_get_pixel_size (layout, NULL, &height);
height = MAX (height, PANGO_PIXELS (priv->ascent + priv->descent));
baseline = pango_layout_get_baseline (layout) / PANGO_SCALE;
*minimum = *natural = height;
if (priv->placeholder)
{
int min, nat;
gtk_widget_measure (priv->placeholder, GTK_ORIENTATION_VERTICAL, -1,
&min, &nat, NULL, NULL);
*minimum = MAX (*minimum, min);
*natural = MAX (*natural, nat);
}
if (minimum_baseline)
*minimum_baseline = baseline;
if (natural_baseline)
*natural_baseline = baseline;
}
pango_font_metrics_unref (metrics);
}
static void
gtk_text_size_allocate (GtkWidget *widget,
int width,
int height,
int baseline)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkEmojiChooser *chooser;
priv->text_baseline = baseline;
if (priv->placeholder)
{
gtk_widget_size_allocate (priv->placeholder,
&(GtkAllocation) { 0, 0, width, height },
-1);
}
gtk_text_adjust_scroll (self);
gtk_text_check_cursor_blink (self);
update_im_cursor_location (self);
chooser = g_object_get_data (G_OBJECT (self), "gtk-emoji-chooser");
if (chooser)
gtk_popover_present (GTK_POPOVER (chooser));
gtk_text_update_handles (self);
if (priv->emoji_completion)
gtk_popover_present (GTK_POPOVER (priv->emoji_completion));
if (priv->magnifier_popover)
gtk_popover_present (GTK_POPOVER (priv->magnifier_popover));
if (priv->popup_menu)
gtk_popover_present (GTK_POPOVER (priv->popup_menu));
if (priv->selection_bubble)
gtk_popover_present (GTK_POPOVER (priv->selection_bubble));
if (priv->text_handles[TEXT_HANDLE_CURSOR])
gtk_text_handle_present (priv->text_handles[TEXT_HANDLE_CURSOR]);
if (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])
gtk_text_handle_present (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]);
}
static void
gtk_text_draw_undershoot (GtkText *self,
GtkSnapshot *snapshot)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_width = gtk_widget_get_width (GTK_WIDGET (self));
const int text_height = gtk_widget_get_height (GTK_WIDGET (self));
GtkStyleContext *context;
int min_offset, max_offset;
context = gtk_widget_get_style_context (GTK_WIDGET (self));
gtk_text_get_scroll_limits (self, &min_offset, &max_offset);
if (priv->scroll_offset > min_offset)
{
gtk_style_context_save_to_node (context, priv->undershoot_node[0]);
gtk_snapshot_render_background (snapshot, context, 0, 0, UNDERSHOOT_SIZE, text_height);
gtk_snapshot_render_frame (snapshot, context, 0, 0, UNDERSHOOT_SIZE, text_height);
gtk_style_context_restore (context);
}
if (priv->scroll_offset < max_offset)
{
gtk_style_context_save_to_node (context, priv->undershoot_node[1]);
gtk_snapshot_render_background (snapshot, context, text_width - UNDERSHOOT_SIZE, 0, UNDERSHOOT_SIZE, text_height);
gtk_snapshot_render_frame (snapshot, context, text_width - UNDERSHOOT_SIZE, 0, UNDERSHOOT_SIZE, text_height);
gtk_style_context_restore (context);
}
}
static void
gtk_text_snapshot (GtkWidget *widget,
GtkSnapshot *snapshot)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
/* Draw text and cursor */
if (priv->dnd_position != -1)
gtk_text_draw_cursor (self, snapshot, CURSOR_DND);
if (priv->placeholder)
gtk_widget_snapshot_child (widget, priv->placeholder, snapshot);
gtk_text_draw_text (self, snapshot);
/* When no text is being displayed at all, don't show the cursor */
if (gtk_text_get_display_mode (self) != DISPLAY_BLANK &&
gtk_widget_has_focus (widget) &&
priv->selection_bound == priv->current_pos)
{
gtk_snapshot_push_opacity (snapshot, priv->cursor_alpha);
gtk_text_draw_cursor (self, snapshot, CURSOR_STANDARD);
gtk_snapshot_pop (snapshot);
}
gtk_text_draw_undershoot (self, snapshot);
}
static void
gtk_text_get_pixel_ranges (GtkText *self,
int **ranges,
int *n_ranges)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->selection_bound != priv->current_pos)
{
PangoLayout *layout = gtk_text_ensure_layout (self, TRUE);
PangoLayoutLine *line = pango_layout_get_lines_readonly (layout)->data;
const char *text = pango_layout_get_text (layout);
int start_index = g_utf8_offset_to_pointer (text, priv->selection_bound) - text;
int end_index = g_utf8_offset_to_pointer (text, priv->current_pos) - text;
int real_n_ranges, i;
pango_layout_line_get_x_ranges (line,
MIN (start_index, end_index),
MAX (start_index, end_index),
ranges,
&real_n_ranges);
if (ranges)
{
int *r = *ranges;
for (i = 0; i < real_n_ranges; ++i)
{
r[2 * i + 1] = (r[2 * i + 1] - r[2 * i]) / PANGO_SCALE;
r[2 * i] = r[2 * i] / PANGO_SCALE;
}
}
if (n_ranges)
*n_ranges = real_n_ranges;
}
else
{
if (n_ranges)
*n_ranges = 0;
if (ranges)
*ranges = NULL;
}
}
static gboolean
in_selection (GtkText *self,
int x)
{
int *ranges;
int n_ranges, i;
int retval = FALSE;
gtk_text_get_pixel_ranges (self, &ranges, &n_ranges);
for (i = 0; i < n_ranges; ++i)
{
if (x >= ranges[2 * i] && x < ranges[2 * i] + ranges[2 * i + 1])
{
retval = TRUE;
break;
}
}
g_free (ranges);
return retval;
}
static void
gesture_get_current_point_in_layout (GtkGestureSingle *gesture,
GtkText *self,
int *x,
int *y)
{
int tx, ty;
GdkEventSequence *sequence;
double px, py;
sequence = gtk_gesture_single_get_current_sequence (gesture);
gtk_gesture_get_point (GTK_GESTURE (gesture), sequence, &px, &py);
gtk_text_get_layout_offsets (self, &tx, &ty);
if (x)
*x = px - tx;
if (y)
*y = py - ty;
}
static void
gtk_text_do_popup (GtkText *self,
double x,
double y)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gtk_text_update_clipboard_actions (self);
gtk_text_update_emoji_action (self);
if (!priv->popup_menu)
{
GMenuModel *model;
model = gtk_text_get_menu_model (self);
priv->popup_menu = gtk_popover_menu_new_from_model (model);
gtk_widget_set_parent (priv->popup_menu, GTK_WIDGET (self));
gtk_popover_set_position (GTK_POPOVER (priv->popup_menu), GTK_POS_BOTTOM);
gtk_popover_set_has_arrow (GTK_POPOVER (priv->popup_menu), FALSE);
gtk_widget_set_halign (priv->popup_menu, GTK_ALIGN_START);
g_object_unref (model);
}
if (x != -1 && y != -1)
{
GdkRectangle rect = { x, y, 1, 1 };
gtk_popover_set_pointing_to (GTK_POPOVER (priv->popup_menu), &rect);
}
else
gtk_popover_set_pointing_to (GTK_POPOVER (priv->popup_menu), NULL);
gtk_popover_popup (GTK_POPOVER (priv->popup_menu));
}
static void
gtk_text_click_gesture_pressed (GtkGestureClick *gesture,
int n_press,
double widget_x,
double widget_y,
GtkText *self)
{
GtkWidget *widget = GTK_WIDGET (self);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkEventSequence *current;
GdkEvent *event;
int x, y, sel_start, sel_end;
guint button;
int tmp_pos;
button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), current);
gesture_get_current_point_in_layout (GTK_GESTURE_SINGLE (gesture), self, &x, &y);
gtk_text_reset_blink_time (self);
if (!gtk_widget_has_focus (widget))
{
priv->in_click = TRUE;
gtk_widget_grab_focus (widget);
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
priv->in_click = FALSE;
}
tmp_pos = gtk_text_find_position (self, x);
if (gdk_event_triggers_context_menu (event))
{
gtk_text_do_popup (self, widget_x, widget_y);
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}
else if (n_press == 1 && button == GDK_BUTTON_MIDDLE &&
get_middle_click_paste (self))
{
if (priv->editable)
{
priv->insert_pos = tmp_pos;
gtk_text_paste (self, gtk_widget_get_primary_clipboard (widget));
}
else
{
gtk_widget_error_bell (widget);
}
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
}
else if (button == GDK_BUTTON_PRIMARY)
{
gboolean have_selection;
gboolean is_touchscreen, extend_selection;
GdkDevice *source;
guint state;
sel_start = priv->selection_bound;
sel_end = priv->current_pos;
have_selection = sel_start != sel_end;
source = gdk_event_get_device (event);
is_touchscreen = gtk_simulate_touchscreen () ||
gdk_device_get_source (source) == GDK_SOURCE_TOUCHSCREEN;
priv->text_handles_enabled = is_touchscreen;
priv->in_drag = FALSE;
priv->select_words = FALSE;
priv->select_lines = FALSE;
state = gdk_event_get_modifier_state (event);
extend_selection = (state & GDK_SHIFT_MASK) != 0;
/* Always emit reset when preedit is shown */
priv->need_im_reset = TRUE;
gtk_text_reset_im_context (self);
switch (n_press)
{
case 1:
if (in_selection (self, x))
{
if (is_touchscreen)
{
if (priv->selection_bubble &&
gtk_widget_get_visible (priv->selection_bubble))
gtk_text_selection_bubble_popup_unset (self);
else
gtk_text_selection_bubble_popup_set (self);
}
else if (extend_selection)
{
/* Truncate current selection, but keep it as big as possible */
if (tmp_pos - sel_start > sel_end - tmp_pos)
gtk_text_set_positions (self, sel_start, tmp_pos);
else
gtk_text_set_positions (self, tmp_pos, sel_end);
/* all done, so skip the extend_to_left stuff later */
extend_selection = FALSE;
}
else
{
/* We'll either start a drag, or clear the selection */
priv->in_drag = TRUE;
priv->drag_start_x = x;
priv->drag_start_y = y;
}
}
else
{
gtk_text_selection_bubble_popup_unset (self);
if (!extend_selection)
{
gtk_text_set_selection_bounds (self, tmp_pos, tmp_pos);
priv->handle_place_time = g_get_monotonic_time ();
}
else
{
/* select from the current position to the clicked position */
if (!have_selection)
sel_start = sel_end = priv->current_pos;
gtk_text_set_positions (self, tmp_pos, tmp_pos);
}
}
break;
case 2:
priv->select_words = TRUE;
gtk_text_select_word (self);
break;
case 3:
priv->select_lines = TRUE;
gtk_text_select_line (self);
break;
default:
break;
}
if (extend_selection)
{
gboolean extend_to_left;
int start, end;
start = MIN (priv->current_pos, priv->selection_bound);
start = MIN (sel_start, start);
end = MAX (priv->current_pos, priv->selection_bound);
end = MAX (sel_end, end);
if (tmp_pos == sel_start || tmp_pos == sel_end)
extend_to_left = (tmp_pos == start);
else
extend_to_left = (end == sel_end);
if (extend_to_left)
gtk_text_set_positions (self, start, end);
else
gtk_text_set_positions (self, end, start);
}
gtk_text_update_handles (self);
}
if (n_press >= 3)
gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
}
static char *
_gtk_text_get_selected_text (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->selection_bound != priv->current_pos)
{
const int start = MIN (priv->selection_bound, priv->current_pos);
const int end = MAX (priv->selection_bound, priv->current_pos);
const char *text = gtk_entry_buffer_get_text (get_buffer (self));
const int start_index = g_utf8_offset_to_pointer (text, start) - text;
const int end_index = g_utf8_offset_to_pointer (text, end) - text;
return g_strndup (text + start_index, end_index - start_index);
}
return NULL;
}
static void
gtk_text_show_magnifier (GtkText *self,
int x,
int y)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_height = gtk_widget_get_height (GTK_WIDGET (self));
cairo_rectangle_int_t rect;
gtk_text_ensure_magnifier (self);
rect.x = x;
rect.width = 1;
rect.y = 0;
rect.height = text_height;
_gtk_magnifier_set_coords (GTK_MAGNIFIER (priv->magnifier), rect.x,
rect.y + rect.height / 2);
gtk_popover_set_pointing_to (GTK_POPOVER (priv->magnifier_popover),
&rect);
gtk_popover_popup (GTK_POPOVER (priv->magnifier_popover));
}
static void
gtk_text_motion_controller_motion (GtkEventControllerMotion *controller,
double x,
double y,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->mouse_cursor_obscured)
{
set_text_cursor (GTK_WIDGET (self));
priv->mouse_cursor_obscured = FALSE;
}
}
static void
dnd_finished_cb (GdkDrag *drag,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (gdk_drag_get_selected_action (drag) == GDK_ACTION_MOVE)
gtk_text_delete_selection (self);
priv->drag = NULL;
}
static void
dnd_cancel_cb (GdkDrag *drag,
GdkDragCancelReason reason,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
priv->drag = NULL;
}
static void
gtk_text_drag_gesture_update (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkText *self)
{
GtkWidget *widget = GTK_WIDGET (self);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkEventSequence *sequence;
GdkEvent *event;
int x, y;
gtk_text_selection_bubble_popup_unset (self);
gesture_get_current_point_in_layout (GTK_GESTURE_SINGLE (gesture), self, &x, &y);
sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
if (priv->mouse_cursor_obscured)
{
set_text_cursor (widget);
priv->mouse_cursor_obscured = FALSE;
}
if (priv->select_lines)
return;
if (priv->in_drag)
{
if (gtk_text_get_display_mode (self) == DISPLAY_NORMAL &&
gtk_drag_check_threshold_double (widget, 0, 0, offset_x, offset_y))
{
int *ranges;
int n_ranges;
char *text;
GdkDragAction actions;
GdkDrag *drag;
GdkPaintable *paintable;
GdkContentProvider *content;
text = _gtk_text_get_selected_text (self);
gtk_text_get_pixel_ranges (self, &ranges, &n_ranges);
g_assert (n_ranges > 0);
if (priv->editable)
actions = GDK_ACTION_COPY|GDK_ACTION_MOVE;
else
actions = GDK_ACTION_COPY;
content = gdk_content_provider_new_typed (G_TYPE_STRING, text);
drag = gdk_drag_begin (gdk_event_get_surface ((GdkEvent*) event),
gdk_event_get_device ((GdkEvent*) event),
content,
actions,
priv->drag_start_x,
priv->drag_start_y);
g_object_unref (content);
g_signal_connect (drag, "dnd-finished", G_CALLBACK (dnd_finished_cb), self);
g_signal_connect (drag, "cancel", G_CALLBACK (dnd_cancel_cb), self);
paintable = gtk_text_util_create_drag_icon (widget, text, -1);
gtk_drag_icon_set_from_paintable (drag, paintable,
(priv->drag_start_x - ranges[0]),
priv->drag_start_y);
g_clear_object (&paintable);
priv->drag = drag;
g_object_unref (drag);
g_free (ranges);
g_free (text);
priv->in_drag = FALSE;
/* Deny the gesture so we don't get further updates */
gtk_gesture_set_state (priv->drag_gesture, GTK_EVENT_SEQUENCE_DENIED);
}
}
else
{
GdkInputSource input_source;
GdkDevice *source;
guint length;
int tmp_pos;
gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
length = gtk_entry_buffer_get_length (get_buffer (self));
if (y < 0)
tmp_pos = 0;
else if (y >= gtk_widget_get_height (GTK_WIDGET (self)))
tmp_pos = length;
else
tmp_pos = gtk_text_find_position (self, x);
source = gdk_event_get_device (event);
input_source = gdk_device_get_source (source);
if (priv->select_words)
{
int min, max;
int old_min, old_max;
int pos, bound;
min = gtk_text_move_backward_word (self, tmp_pos, TRUE);
max = gtk_text_move_forward_word (self, tmp_pos, TRUE);
pos = priv->current_pos;
bound = priv->selection_bound;
old_min = MIN (priv->current_pos, priv->selection_bound);
old_max = MAX (priv->current_pos, priv->selection_bound);
if (min < old_min)
{
pos = min;
bound = old_max;
}
else if (old_max < max)
{
pos = max;
bound = old_min;
}
else if (pos == old_min)
{
if (priv->current_pos != min)
pos = max;
}
else
{
if (priv->current_pos != max)
pos = min;
}
gtk_text_set_positions (self, pos, bound);
}
else
gtk_text_set_positions (self, tmp_pos, -1);
/* Update touch handles' position */
if (gtk_simulate_touchscreen () ||
input_source == GDK_SOURCE_TOUCHSCREEN)
{
priv->text_handles_enabled = TRUE;
gtk_text_update_handles (self);
gtk_text_show_magnifier (self, x - priv->scroll_offset, y);
}
}
}
static void
gtk_text_drag_gesture_end (GtkGestureDrag *gesture,
double offset_x,
double offset_y,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gboolean in_drag;
GdkEventSequence *sequence;
sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
in_drag = priv->in_drag;
priv->in_drag = FALSE;
if (priv->magnifier_popover)
gtk_popover_popdown (GTK_POPOVER (priv->magnifier_popover));
/* Check whether the drag was cancelled rather than finished */
if (!gtk_gesture_handles_sequence (GTK_GESTURE (gesture), sequence))
return;
if (in_drag)
{
int tmp_pos = gtk_text_find_position (self, priv->drag_start_x);
gtk_text_set_selection_bounds (self, tmp_pos, tmp_pos);
}
gtk_text_update_handles (self);
gtk_text_update_primary_selection (self);
}
static void
gtk_text_obscure_mouse_cursor (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->mouse_cursor_obscured)
return;
gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "none");
priv->mouse_cursor_obscured = TRUE;
}
static gboolean
gtk_text_key_controller_key_pressed (GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gunichar unichar;
gtk_text_reset_blink_time (self);
gtk_text_pend_cursor_blink (self);
gtk_text_selection_bubble_popup_unset (self);
priv->text_handles_enabled = FALSE;
gtk_text_update_handles (self);
if (keyval == GDK_KEY_Return ||
keyval == GDK_KEY_KP_Enter ||
keyval == GDK_KEY_ISO_Enter ||
keyval == GDK_KEY_Escape)
gtk_text_reset_im_context (self);
unichar = gdk_keyval_to_unicode (keyval);
if (!priv->editable && unichar != 0)
gtk_widget_error_bell (GTK_WIDGET (self));
gtk_text_obscure_mouse_cursor (self);
return FALSE;
}
static void
gtk_text_focus_changed (GtkEventControllerFocus *controller,
GParamSpec *pspec,
GtkWidget *widget)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkSeat *seat = NULL;
GdkDevice *keyboard = NULL;
seat = gdk_display_get_default_seat (gtk_widget_get_display (widget));
if (seat)
keyboard = gdk_seat_get_keyboard (seat);
gtk_widget_queue_draw (widget);
if (gtk_event_controller_focus_is_focus (controller))
{
if (keyboard)
g_signal_connect (keyboard, "notify::direction",
G_CALLBACK (direction_changed), self);
if (priv->editable)
{
gtk_text_schedule_im_reset (self);
gtk_im_context_focus_in (priv->im_context);
}
gtk_text_reset_blink_time (self);
}
else /* Focus out */
{
gtk_text_selection_bubble_popup_unset (self);
priv->text_handles_enabled = FALSE;
gtk_text_update_handles (self);
if (keyboard)
g_signal_handlers_disconnect_by_func (keyboard, direction_changed, self);
if (priv->editable)
{
gtk_text_schedule_im_reset (self);
gtk_im_context_focus_out (priv->im_context);
}
}
gtk_text_check_cursor_blink (self);
}
static gboolean
gtk_text_grab_focus (GtkWidget *widget)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gboolean select_on_focus;
GtkWidget *prev_focus;
gboolean prev_focus_was_child;
prev_focus = gtk_root_get_focus (gtk_widget_get_root (widget));
prev_focus_was_child = prev_focus && gtk_widget_is_ancestor (prev_focus, widget);
if (!GTK_WIDGET_CLASS (gtk_text_parent_class)->grab_focus (GTK_WIDGET (self)))
return FALSE;
if (priv->editable && !priv->in_click && !prev_focus_was_child)
{
g_object_get (gtk_widget_get_settings (widget),
"gtk-entry-select-on-focus",
&select_on_focus,
NULL);
if (select_on_focus)
gtk_text_set_selection_bounds (self, 0, -1);
}
return TRUE;
}
/**
* gtk_text_grab_focus_without_selecting:
* @self: a #GtkText
*
* Causes @self to have keyboard focus.
*
* It behaves like gtk_widget_grab_focus(),
* except that it doesn't select the contents of @self.
* You only want to call this on some special entries
* which the user usually doesn't want to replace all text in,
* such as search-as-you-type entries.
*
* Returns: %TRUE if focus is now inside @self
*/
gboolean
gtk_text_grab_focus_without_selecting (GtkText *self)
{
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return GTK_WIDGET_CLASS (gtk_text_parent_class)->grab_focus (GTK_WIDGET (self));
}
static void
gtk_text_direction_changed (GtkWidget *widget,
GtkTextDirection previous_dir)
{
GtkText *self = GTK_TEXT (widget);
gtk_text_recompute (self);
GTK_WIDGET_CLASS (gtk_text_parent_class)->direction_changed (widget, previous_dir);
}
static void
gtk_text_state_flags_changed (GtkWidget *widget,
GtkStateFlags previous_state)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkStateFlags state;
state = gtk_widget_get_state_flags (GTK_WIDGET (self));
if (gtk_widget_get_realized (widget))
{
set_text_cursor (widget);
priv->mouse_cursor_obscured = FALSE;
}
if (!gtk_widget_is_sensitive (widget))
{
/* Clear any selection */
gtk_text_set_selection_bounds (self, priv->current_pos, priv->current_pos);
}
state &= ~GTK_STATE_FLAG_DROP_ACTIVE;
if (priv->selection_node)
gtk_css_node_set_state (priv->selection_node, state);
if (priv->block_cursor_node)
gtk_css_node_set_state (priv->block_cursor_node, state);
gtk_css_node_set_state (priv->undershoot_node[0], state);
gtk_css_node_set_state (priv->undershoot_node[1], state);
gtk_text_update_cached_style_values (self);
}
/* GtkEditable method implementations
*/
static void
gtk_text_insert_text (GtkText *self,
const char *text,
int length,
int *position)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int n_inserted;
int n_chars;
n_chars = g_utf8_strlen (text, length);
/*
* The incoming text may a password or other secret. We make sure
* not to copy it into temporary buffers.
*/
begin_change (self);
n_inserted = gtk_entry_buffer_insert_text (get_buffer (self), *position, text, n_chars);
end_change (self);
if (n_inserted != n_chars)
gtk_widget_error_bell (GTK_WIDGET (self));
*position += n_inserted;
update_placeholder_visibility (self);
if (priv->propagate_text_width)
gtk_widget_queue_resize (GTK_WIDGET (self));
}
static void
gtk_text_delete_text (GtkText *self,
int start_pos,
int end_pos)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
begin_change (self);
gtk_entry_buffer_delete_text (get_buffer (self), start_pos, end_pos - start_pos);
end_change (self);
update_placeholder_visibility (self);
if (priv->propagate_text_width)
gtk_widget_queue_resize (GTK_WIDGET (self));
}
static void
gtk_text_delete_selection (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int start_pos = MIN (priv->selection_bound, priv->current_pos);
int end_pos = MAX (priv->selection_bound, priv->current_pos);
gtk_editable_delete_text (GTK_EDITABLE (self), start_pos, end_pos);
}
static void
gtk_text_set_selection_bounds (GtkText *self,
int start,
int end)
{
guint length;
length = gtk_entry_buffer_get_length (get_buffer (self));
if (start < 0)
start = length;
if (end < 0)
end = length;
gtk_text_reset_im_context (self);
gtk_text_set_positions (self, MIN (end, length), MIN (start, length));
gtk_text_update_primary_selection (self);
}
static gboolean
gtk_text_get_selection_bounds (GtkText *self,
int *start,
int *end)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
*start = priv->selection_bound;
*end = priv->current_pos;
return (priv->selection_bound != priv->current_pos);
}
static gunichar
find_invisible_char (GtkWidget *widget)
{
PangoLayout *layout;
PangoAttrList *attr_list;
int i;
gunichar invisible_chars [] = {
0x25cf, /* BLACK CIRCLE */
0x2022, /* BULLET */
0x2731, /* HEAVY ASTERISK */
0x273a /* SIXTEEN POINTED ASTERISK */
};
layout = gtk_widget_create_pango_layout (widget, NULL);
attr_list = pango_attr_list_new ();
pango_attr_list_insert (attr_list, pango_attr_fallback_new (FALSE));
pango_layout_set_attributes (layout, attr_list);
pango_attr_list_unref (attr_list);
for (i = 0; i < G_N_ELEMENTS (invisible_chars); i++)
{
char text[7] = { 0, };
int len, count;
len = g_unichar_to_utf8 (invisible_chars[i], text);
pango_layout_set_text (layout, text, len);
count = pango_layout_get_unknown_glyphs_count (layout);
if (count == 0)
{
g_object_unref (layout);
return invisible_chars[i];
}
}
g_object_unref (layout);
return '*';
}
static void
gtk_text_update_cached_style_values (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (!priv->visible && !priv->invisible_char_set)
{
gunichar ch = find_invisible_char (GTK_WIDGET (self));
if (priv->invisible_char != ch)
{
priv->invisible_char = ch;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INVISIBLE_CHAR]);
}
}
}
static void
gtk_text_css_changed (GtkWidget *widget,
GtkCssStyleChange *change)
{
GtkText *self = GTK_TEXT (widget);
GTK_WIDGET_CLASS (gtk_text_parent_class)->css_changed (widget, change);
gtk_text_update_cached_style_values (self);
if (gtk_css_style_change_affects (change, GTK_CSS_AFFECTS_TEXT |
GTK_CSS_AFFECTS_BACKGROUND |
GTK_CSS_AFFECTS_CONTENT))
gtk_widget_queue_draw (widget);
if (gtk_css_style_change_affects (change, GTK_CSS_AFFECTS_TEXT_ATTRS))
gtk_text_recompute (self);
}
static void
gtk_text_password_hint_free (GtkTextPasswordHint *password_hint)
{
if (password_hint->source_id)
g_source_remove (password_hint->source_id);
g_slice_free (GtkTextPasswordHint, password_hint);
}
static gboolean
gtk_text_remove_password_hint (gpointer data)
{
GtkTextPasswordHint *password_hint = g_object_get_qdata (data, quark_password_hint);
password_hint->position = -1;
password_hint->source_id = 0;
/* Force the string to be redrawn, but now without a visible character */
gtk_text_recompute (GTK_TEXT (data));
return G_SOURCE_REMOVE;
}
static void
update_placeholder_visibility (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->placeholder)
gtk_widget_set_child_visible (priv->placeholder,
priv->preedit_length == 0 &&
gtk_entry_buffer_get_length (priv->buffer) == 0);
}
/* GtkEntryBuffer signal handlers
*/
static void
buffer_inserted_text (GtkEntryBuffer *buffer,
guint position,
const char *chars,
guint n_chars,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
guint password_hint_timeout;
guint current_pos;
int selection_bound;
current_pos = priv->current_pos;
if (current_pos > position)
current_pos += n_chars;
selection_bound = priv->selection_bound;
if (selection_bound > position)
selection_bound += n_chars;
gtk_text_set_positions (self, current_pos, selection_bound);
gtk_text_recompute (self);
gtk_text_history_text_inserted (priv->history, position, chars, -1);
/* Calculate the password hint if it needs to be displayed. */
if (n_chars == 1 && !priv->visible)
{
g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
"gtk-entry-password-hint-timeout", &password_hint_timeout,
NULL);
if (password_hint_timeout > 0)
{
GtkTextPasswordHint *password_hint = g_object_get_qdata (G_OBJECT (self),
quark_password_hint);
if (!password_hint)
{
password_hint = g_slice_new0 (GtkTextPasswordHint);
g_object_set_qdata_full (G_OBJECT (self), quark_password_hint, password_hint,
(GDestroyNotify)gtk_text_password_hint_free);
}
password_hint->position = position;
if (password_hint->source_id)
g_source_remove (password_hint->source_id);
password_hint->source_id = g_timeout_add (password_hint_timeout,
(GSourceFunc)gtk_text_remove_password_hint,
self);
g_source_set_name_by_id (password_hint->source_id, "[gtk] gtk_text_remove_password_hint");
}
}
}
static void
buffer_deleted_text (GtkEntryBuffer *buffer,
guint position,
guint n_chars,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
guint end_pos = position + n_chars;
if (gtk_text_history_get_enabled (priv->history))
{
char *deleted_text;
deleted_text = gtk_editable_get_chars (GTK_EDITABLE (self),
position,
end_pos);
gtk_text_history_selection_changed (priv->history,
priv->current_pos,
priv->selection_bound);
gtk_text_history_text_deleted (priv->history,
position,
end_pos,
deleted_text,
-1);
g_free (deleted_text);
}
}
static void
buffer_deleted_text_after (GtkEntryBuffer *buffer,
guint position,
guint n_chars,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
guint end_pos = position + n_chars;
int selection_bound;
guint current_pos;
current_pos = priv->current_pos;
if (current_pos > position)
current_pos -= MIN (current_pos, end_pos) - position;
selection_bound = priv->selection_bound;
if (selection_bound > position)
selection_bound -= MIN (selection_bound, end_pos) - position;
gtk_text_set_positions (self, current_pos, selection_bound);
gtk_text_recompute (self);
/* We might have deleted the selection */
gtk_text_update_primary_selection (self);
/* Disable the password hint if one exists. */
if (!priv->visible)
{
GtkTextPasswordHint *password_hint = g_object_get_qdata (G_OBJECT (self),
quark_password_hint);
if (password_hint)
{
if (password_hint->source_id)
g_source_remove (password_hint->source_id);
password_hint->source_id = 0;
password_hint->position = -1;
}
}
}
static void
buffer_notify_text (GtkEntryBuffer *buffer,
GParamSpec *spec,
GtkText *self)
{
emit_changed (self);
g_object_notify (G_OBJECT (self), "text");
}
static void
buffer_notify_max_length (GtkEntryBuffer *buffer,
GParamSpec *spec,
GtkText *self)
{
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_MAX_LENGTH]);
}
static void
buffer_connect_signals (GtkText *self)
{
g_signal_connect (get_buffer (self), "inserted-text", G_CALLBACK (buffer_inserted_text), self);
g_signal_connect (get_buffer (self), "deleted-text", G_CALLBACK (buffer_deleted_text), self);
g_signal_connect_after (get_buffer (self), "deleted-text", G_CALLBACK (buffer_deleted_text_after), self);
g_signal_connect (get_buffer (self), "notify::text", G_CALLBACK (buffer_notify_text), self);
g_signal_connect (get_buffer (self), "notify::max-length", G_CALLBACK (buffer_notify_max_length), self);
}
static void
buffer_disconnect_signals (GtkText *self)
{
g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_inserted_text, self);
g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_deleted_text, self);
g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_deleted_text_after, self);
g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_notify_text, self);
g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_notify_max_length, self);
}
/* Compute the X position for an offset that corresponds to the "more important
* cursor position for that offset. We use this when trying to guess to which
* end of the selection we should go to when the user hits the left or
* right arrow key.
*/
static int
get_better_cursor_x (GtkText *self,
int offset)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkSeat *seat;
GdkDevice *keyboard = NULL;
PangoDirection direction = PANGO_DIRECTION_LTR;
gboolean split_cursor;
PangoLayout *layout = gtk_text_ensure_layout (self, TRUE);
const char *text = pango_layout_get_text (layout);
int index = g_utf8_offset_to_pointer (text, offset) - text;
PangoRectangle strong_pos, weak_pos;
seat = gdk_display_get_default_seat (gtk_widget_get_display (GTK_WIDGET (self)));
if (seat)
keyboard = gdk_seat_get_keyboard (seat);
if (keyboard)
direction = gdk_device_get_direction (keyboard);
g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
"gtk-split-cursor", &split_cursor,
NULL);
pango_layout_get_cursor_pos (layout, index, &strong_pos, &weak_pos);
if (split_cursor)
return strong_pos.x / PANGO_SCALE;
else
return (direction == priv->resolved_dir) ? strong_pos.x / PANGO_SCALE : weak_pos.x / PANGO_SCALE;
}
static void
gtk_text_move_cursor (GtkText *self,
GtkMovementStep step,
int count,
gboolean extend_selection)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int new_pos = priv->current_pos;
gtk_text_reset_im_context (self);
if (priv->current_pos != priv->selection_bound && !extend_selection)
{
/* If we have a current selection and aren't extending it, move to the
* start/or end of the selection as appropriate
*/
switch (step)
{
case GTK_MOVEMENT_VISUAL_POSITIONS:
{
int current_x = get_better_cursor_x (self, priv->current_pos);
int bound_x = get_better_cursor_x (self, priv->selection_bound);
if (count <= 0)
new_pos = current_x < bound_x ? priv->current_pos : priv->selection_bound;
else
new_pos = current_x > bound_x ? priv->current_pos : priv->selection_bound;
}
break;
case GTK_MOVEMENT_WORDS:
if (priv->resolved_dir == PANGO_DIRECTION_RTL)
count *= -1;
G_GNUC_FALLTHROUGH;
case GTK_MOVEMENT_LOGICAL_POSITIONS:
if (count < 0)
new_pos = MIN (priv->current_pos, priv->selection_bound);
else
new_pos = MAX (priv->current_pos, priv->selection_bound);
break;
case GTK_MOVEMENT_DISPLAY_LINE_ENDS:
case GTK_MOVEMENT_PARAGRAPH_ENDS:
case GTK_MOVEMENT_BUFFER_ENDS:
new_pos = count < 0 ? 0 : gtk_entry_buffer_get_length (get_buffer (self));
break;
case GTK_MOVEMENT_DISPLAY_LINES:
case GTK_MOVEMENT_PARAGRAPHS:
case GTK_MOVEMENT_PAGES:
case GTK_MOVEMENT_HORIZONTAL_PAGES:
default:
break;
}
}
else
{
switch (step)
{
case GTK_MOVEMENT_LOGICAL_POSITIONS:
new_pos = gtk_text_move_logically (self, new_pos, count);
break;
case GTK_MOVEMENT_VISUAL_POSITIONS:
new_pos = gtk_text_move_visually (self, new_pos, count);
if (priv->current_pos == new_pos)
{
if (!extend_selection)
{
if (!gtk_widget_keynav_failed (GTK_WIDGET (self),
count > 0 ?
GTK_DIR_RIGHT : GTK_DIR_LEFT))
{
GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
if (toplevel)
gtk_widget_child_focus (toplevel,
count > 0 ?
GTK_DIR_RIGHT : GTK_DIR_LEFT);
}
}
else
{
gtk_widget_error_bell (GTK_WIDGET (self));
}
}
break;
case GTK_MOVEMENT_WORDS:
if (priv->resolved_dir == PANGO_DIRECTION_RTL)
count *= -1;
while (count > 0)
{
new_pos = gtk_text_move_forward_word (self, new_pos, FALSE);
count--;
}
while (count < 0)
{
new_pos = gtk_text_move_backward_word (self, new_pos, FALSE);
count++;
}
if (priv->current_pos == new_pos)
gtk_widget_error_bell (GTK_WIDGET (self));
break;
case GTK_MOVEMENT_DISPLAY_LINE_ENDS:
case GTK_MOVEMENT_PARAGRAPH_ENDS:
case GTK_MOVEMENT_BUFFER_ENDS:
new_pos = count < 0 ? 0 : gtk_entry_buffer_get_length (get_buffer (self));
if (priv->current_pos == new_pos)
gtk_widget_error_bell (GTK_WIDGET (self));
break;
case GTK_MOVEMENT_DISPLAY_LINES:
case GTK_MOVEMENT_PARAGRAPHS:
case GTK_MOVEMENT_PAGES:
case GTK_MOVEMENT_HORIZONTAL_PAGES:
default:
break;
}
}
if (extend_selection)
gtk_text_set_selection_bounds (self, priv->selection_bound, new_pos);
else
gtk_text_set_selection_bounds (self, new_pos, new_pos);
gtk_text_pend_cursor_blink (self);
}
static void
gtk_text_insert_at_cursor (GtkText *self,
const char *str)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int pos = priv->current_pos;
if (priv->editable)
{
gtk_text_reset_im_context (self);
gtk_editable_insert_text (GTK_EDITABLE (self), str, -1, &pos);
gtk_text_set_selection_bounds (self, pos, pos);
}
}
static void
gtk_text_delete_from_cursor (GtkText *self,
GtkDeleteType type,
int count)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int start_pos = priv->current_pos;
int end_pos = priv->current_pos;
int old_n_bytes = gtk_entry_buffer_get_bytes (get_buffer (self));
gtk_text_reset_im_context (self);
if (!priv->editable)
{
gtk_widget_error_bell (GTK_WIDGET (self));
return;
}
if (priv->selection_bound != priv->current_pos)
{
gtk_text_delete_selection (self);
return;
}
switch (type)
{
case GTK_DELETE_CHARS:
end_pos = gtk_text_move_logically (self, priv->current_pos, count);
gtk_editable_delete_text (GTK_EDITABLE (self), MIN (start_pos, end_pos), MAX (start_pos, end_pos));
break;
case GTK_DELETE_WORDS:
if (count < 0)
{
/* Move to end of current word, or if not on a word, end of previous word */
end_pos = gtk_text_move_backward_word (self, end_pos, FALSE);
end_pos = gtk_text_move_forward_word (self, end_pos, FALSE);
}
else if (count > 0)
{
/* Move to beginning of current word, or if not on a word, beginning of next word */
start_pos = gtk_text_move_forward_word (self, start_pos, FALSE);
start_pos = gtk_text_move_backward_word (self, start_pos, FALSE);
}
G_GNUC_FALLTHROUGH;
case GTK_DELETE_WORD_ENDS:
while (count < 0)
{
start_pos = gtk_text_move_backward_word (self, start_pos, FALSE);
count++;
}
while (count > 0)
{
end_pos = gtk_text_move_forward_word (self, end_pos, FALSE);
count--;
}
gtk_editable_delete_text (GTK_EDITABLE (self), start_pos, end_pos);
break;
case GTK_DELETE_DISPLAY_LINE_ENDS:
case GTK_DELETE_PARAGRAPH_ENDS:
if (count < 0)
gtk_editable_delete_text (GTK_EDITABLE (self), 0, priv->current_pos);
else
gtk_editable_delete_text (GTK_EDITABLE (self), priv->current_pos, -1);
break;
case GTK_DELETE_DISPLAY_LINES:
case GTK_DELETE_PARAGRAPHS:
gtk_editable_delete_text (GTK_EDITABLE (self), 0, -1);
break;
case GTK_DELETE_WHITESPACE:
gtk_text_delete_whitespace (self);
break;
default:
break;
}
if (gtk_entry_buffer_get_bytes (get_buffer (self)) == old_n_bytes)
gtk_widget_error_bell (GTK_WIDGET (self));
gtk_text_pend_cursor_blink (self);
}
static void
gtk_text_backspace (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int prev_pos;
gtk_text_reset_im_context (self);
if (!priv->editable)
{
gtk_widget_error_bell (GTK_WIDGET (self));
return;
}
if (priv->selection_bound != priv->current_pos)
{
gtk_text_delete_selection (self);
return;
}
prev_pos = gtk_text_move_logically (self, priv->current_pos, -1);
if (prev_pos < priv->current_pos)
{
PangoLayout *layout = gtk_text_ensure_layout (self, FALSE);
const PangoLogAttr *log_attrs;
int n_attrs;
log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs);
/* Deleting parts of characters */
if (log_attrs[priv->current_pos].backspace_deletes_character)
{
char *cluster_text;
char *normalized_text;
glong len;
cluster_text = gtk_text_get_display_text (self, prev_pos, priv->current_pos);
normalized_text = g_utf8_normalize (cluster_text,
strlen (cluster_text),
G_NORMALIZE_NFD);
len = g_utf8_strlen (normalized_text, -1);
gtk_editable_delete_text (GTK_EDITABLE (self), prev_pos, priv->current_pos);
if (len > 1)
{
int pos = priv->current_pos;
gtk_editable_insert_text (GTK_EDITABLE (self), normalized_text,
g_utf8_offset_to_pointer (normalized_text, len - 1) - normalized_text,
&pos);
gtk_text_set_selection_bounds (self, pos, pos);
}
g_free (normalized_text);
g_free (cluster_text);
}
else
{
gtk_editable_delete_text (GTK_EDITABLE (self), prev_pos, priv->current_pos);
}
}
else
{
gtk_widget_error_bell (GTK_WIDGET (self));
}
gtk_text_pend_cursor_blink (self);
}
static void
gtk_text_copy_clipboard (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->selection_bound != priv->current_pos)
{
char *str;
if (!priv->visible)
{
gtk_widget_error_bell (GTK_WIDGET (self));
return;
}
if (priv->selection_bound < priv->current_pos)
str = gtk_text_get_display_text (self, priv->selection_bound, priv->current_pos);
else
str = gtk_text_get_display_text (self, priv->current_pos, priv->selection_bound);
gdk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (self)), str);
g_free (str);
}
}
static void
gtk_text_cut_clipboard (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (!priv->visible)
{
gtk_widget_error_bell (GTK_WIDGET (self));
return;
}
gtk_text_copy_clipboard (self);
if (priv->editable)
{
if (priv->selection_bound != priv->current_pos)
gtk_text_delete_selection (self);
}
else
{
gtk_widget_error_bell (GTK_WIDGET (self));
}
gtk_text_selection_bubble_popup_unset (self);
gtk_text_update_handles (self);
}
static void
gtk_text_paste_clipboard (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->editable)
gtk_text_paste (self, gtk_widget_get_clipboard (GTK_WIDGET (self)));
else
gtk_widget_error_bell (GTK_WIDGET (self));
gtk_text_update_handles (self);
}
static void
gtk_text_delete_cb (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->editable)
{
if (priv->selection_bound != priv->current_pos)
gtk_text_delete_selection (self);
}
}
static void
gtk_text_toggle_overwrite (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
priv->overwrite_mode = !priv->overwrite_mode;
if (priv->overwrite_mode)
{
if (!priv->block_cursor_node)
{
GtkCssNode *widget_node = gtk_widget_get_css_node (GTK_WIDGET (self));
priv->block_cursor_node = gtk_css_node_new ();
gtk_css_node_set_name (priv->block_cursor_node, g_quark_from_static_string ("block-cursor"));
gtk_css_node_set_parent (priv->block_cursor_node, widget_node);
gtk_css_node_set_state (priv->block_cursor_node, gtk_css_node_get_state (widget_node));
g_object_unref (priv->block_cursor_node);
}
}
else
{
if (priv->block_cursor_node)
{
gtk_css_node_set_parent (priv->block_cursor_node, NULL);
priv->block_cursor_node = NULL;
}
}
gtk_text_pend_cursor_blink (self);
gtk_widget_queue_draw (GTK_WIDGET (self));
}
static void
gtk_text_select_all (GtkText *self)
{
gtk_text_select_line (self);
}
static void
gtk_text_real_activate (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->activates_default)
gtk_widget_activate_default (GTK_WIDGET (self));
}
static void
direction_changed (GdkDevice *device,
GParamSpec *pspec,
GtkText *self)
{
gtk_text_recompute (self);
}
/* IM Context Callbacks
*/
static void
gtk_text_commit_cb (GtkIMContext *context,
const char *str,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->editable)
{
gtk_text_enter_text (self, str);
gtk_text_obscure_mouse_cursor (self);
}
}
static void
gtk_text_preedit_changed_cb (GtkIMContext *context,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->editable)
{
char *preedit_string;
int cursor_pos;
gtk_text_obscure_mouse_cursor (self);
gtk_im_context_get_preedit_string (priv->im_context,
&preedit_string, NULL,
&cursor_pos);
g_signal_emit (self, signals[PREEDIT_CHANGED], 0, preedit_string);
priv->preedit_length = strlen (preedit_string);
cursor_pos = CLAMP (cursor_pos, 0, g_utf8_strlen (preedit_string, -1));
priv->preedit_cursor = cursor_pos;
g_free (preedit_string);
gtk_text_recompute (self);
update_placeholder_visibility (self);
}
}
static gboolean
gtk_text_retrieve_surrounding_cb (GtkIMContext *context,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
char *text;
/* XXXX ??? does this even make sense when text is not visible? Should we return FALSE? */
text = gtk_text_get_display_text (self, 0, -1);
gtk_im_context_set_surrounding (context, text, strlen (text), /* Length in bytes */
g_utf8_offset_to_pointer (text, priv->current_pos) - text);
g_free (text);
return TRUE;
}
static gboolean
gtk_text_delete_surrounding_cb (GtkIMContext *context,
int offset,
int n_chars,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->editable)
gtk_editable_delete_text (GTK_EDITABLE (self),
priv->current_pos + offset,
priv->current_pos + offset + n_chars);
return TRUE;
}
/* Internal functions
*/
/* Used for im_commit_cb and inserting Unicode chars */
void
gtk_text_enter_text (GtkText *self,
const char *str)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int tmp_pos;
gboolean old_need_im_reset;
guint text_length;
old_need_im_reset = priv->need_im_reset;
priv->need_im_reset = FALSE;
if (priv->selection_bound != priv->current_pos)
gtk_text_delete_selection (self);
else
{
if (priv->overwrite_mode)
{
text_length = gtk_entry_buffer_get_length (get_buffer (self));
if (priv->current_pos < text_length)
gtk_text_delete_from_cursor (self, GTK_DELETE_CHARS, 1);
}
}
tmp_pos = priv->current_pos;
gtk_editable_insert_text (GTK_EDITABLE (self), str, strlen (str), &tmp_pos);
gtk_text_set_selection_bounds (self, tmp_pos, tmp_pos);
priv->need_im_reset = old_need_im_reset;
}
/* All changes to priv->current_pos and priv->selection_bound
* should go through this function.
*/
void
gtk_text_set_positions (GtkText *self,
int current_pos,
int selection_bound)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gboolean changed = FALSE;
g_object_freeze_notify (G_OBJECT (self));
if (current_pos != -1 &&
priv->current_pos != current_pos)
{
priv->current_pos = current_pos;
changed = TRUE;
g_object_notify (G_OBJECT (self), "cursor-position");
}
if (selection_bound != -1 &&
priv->selection_bound != selection_bound)
{
priv->selection_bound = selection_bound;
changed = TRUE;
g_object_notify (G_OBJECT (self), "selection-bound");
}
g_object_thaw_notify (G_OBJECT (self));
if (priv->current_pos != priv->selection_bound)
{
if (!priv->selection_node)
{
GtkCssNode *widget_node = gtk_widget_get_css_node (GTK_WIDGET (self));
priv->selection_node = gtk_css_node_new ();
gtk_css_node_set_name (priv->selection_node, g_quark_from_static_string ("selection"));
gtk_css_node_set_parent (priv->selection_node, widget_node);
gtk_css_node_set_state (priv->selection_node, gtk_css_node_get_state (widget_node));
g_object_unref (priv->selection_node);
}
}
else
{
if (priv->selection_node)
{
gtk_css_node_set_parent (priv->selection_node, NULL);
priv->selection_node = NULL;
}
}
if (changed)
{
gtk_text_update_clipboard_actions (self);
gtk_text_recompute (self);
}
}
static void
gtk_text_reset_layout (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->cached_layout)
{
g_object_unref (priv->cached_layout);
priv->cached_layout = NULL;
}
}
static void
gtk_text_recompute (GtkText *self)
{
gtk_text_reset_layout (self);
gtk_widget_queue_draw (GTK_WIDGET (self));
if (!gtk_widget_get_mapped (GTK_WIDGET (self)))
return;
gtk_text_check_cursor_blink (self);
gtk_text_adjust_scroll (self);
update_im_cursor_location (self);
gtk_text_update_handles (self);
}
static PangoLayout *
gtk_text_create_layout (GtkText *self,
gboolean include_preedit)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkWidget *widget = GTK_WIDGET (self);
PangoLayout *layout;
PangoAttrList *tmp_attrs = NULL;
char *preedit_string = NULL;
int preedit_length = 0;
PangoAttrList *preedit_attrs = NULL;
char *display_text;
guint n_bytes;
layout = gtk_widget_create_pango_layout (widget, NULL);
pango_layout_set_single_paragraph_mode (layout, TRUE);
tmp_attrs = gtk_css_style_get_pango_attributes (gtk_css_node_get_style (gtk_widget_get_css_node (widget)));
tmp_attrs = _gtk_pango_attr_list_merge (tmp_attrs, priv->attrs);
if (!tmp_attrs)
tmp_attrs = pango_attr_list_new ();
display_text = gtk_text_get_display_text (self, 0, -1);
n_bytes = strlen (display_text);
if (include_preedit)
{
gtk_im_context_get_preedit_string (priv->im_context,
&preedit_string, &preedit_attrs, NULL);
preedit_length = priv->preedit_length;
}
if (preedit_length)
{
GString *tmp_string = g_string_new (display_text);
int pos;
pos = g_utf8_offset_to_pointer (display_text, priv->current_pos) - display_text;
g_string_insert (tmp_string, pos, preedit_string);
pango_layout_set_text (layout, tmp_string->str, tmp_string->len);
pango_attr_list_splice (tmp_attrs, preedit_attrs, pos, preedit_length);
g_string_free (tmp_string, TRUE);
}
else
{
PangoDirection pango_dir;
if (gtk_text_get_display_mode (self) == DISPLAY_NORMAL)
pango_dir = gdk_find_base_dir (display_text, n_bytes);
else
pango_dir = PANGO_DIRECTION_NEUTRAL;
if (pango_dir == PANGO_DIRECTION_NEUTRAL)
{
if (gtk_widget_has_focus (widget))
{
GdkDisplay *display;
GdkSeat *seat;
GdkDevice *keyboard = NULL;
PangoDirection direction = PANGO_DIRECTION_LTR;
display = gtk_widget_get_display (widget);
seat = gdk_display_get_default_seat (display);
if (seat)
keyboard = gdk_seat_get_keyboard (seat);
if (keyboard)
direction = gdk_device_get_direction (keyboard);
if (direction == PANGO_DIRECTION_RTL)
pango_dir = PANGO_DIRECTION_RTL;
else
pango_dir = PANGO_DIRECTION_LTR;
}
else
{
if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
pango_dir = PANGO_DIRECTION_RTL;
else
pango_dir = PANGO_DIRECTION_LTR;
}
}
pango_context_set_base_dir (gtk_widget_get_pango_context (widget), pango_dir);
priv->resolved_dir = pango_dir;
pango_layout_set_text (layout, display_text, n_bytes);
}
pango_layout_set_attributes (layout, tmp_attrs);
if (priv->tabs)
pango_layout_set_tabs (layout, priv->tabs);
g_free (preedit_string);
g_free (display_text);
pango_attr_list_unref (preedit_attrs);
pango_attr_list_unref (tmp_attrs);
return layout;
}
static PangoLayout *
gtk_text_ensure_layout (GtkText *self,
gboolean include_preedit)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->preedit_length > 0 &&
!include_preedit != !priv->cache_includes_preedit)
gtk_text_reset_layout (self);
if (!priv->cached_layout)
{
priv->cached_layout = gtk_text_create_layout (self, include_preedit);
priv->cache_includes_preedit = include_preedit;
}
return priv->cached_layout;
}
static void
get_layout_position (GtkText *self,
int *x,
int *y)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_height = gtk_widget_get_height (GTK_WIDGET (self));
PangoLayout *layout;
PangoRectangle logical_rect;
int y_pos, area_height;
PangoLayoutLine *line;
layout = gtk_text_ensure_layout (self, TRUE);
area_height = PANGO_SCALE * text_height;
line = pango_layout_get_lines_readonly (layout)->data;
pango_layout_line_get_extents (line, NULL, &logical_rect);
/* Align primarily for locale's ascent/descent */
if (priv->text_baseline < 0)
y_pos = ((area_height - priv->ascent - priv->descent) / 2 +
priv->ascent + logical_rect.y);
else
y_pos = PANGO_SCALE * priv->text_baseline - pango_layout_get_baseline (layout);
/* Now see if we need to adjust to fit in actual drawn string */
if (logical_rect.height > area_height)
y_pos = (area_height - logical_rect.height) / 2;
else if (y_pos < 0)
y_pos = 0;
else if (y_pos + logical_rect.height > area_height)
y_pos = area_height - logical_rect.height;
y_pos = y_pos / PANGO_SCALE;
if (x)
*x = - priv->scroll_offset;
if (y)
*y = y_pos;
}
#define GRAPHENE_RECT_FROM_RECT(_r) (GRAPHENE_RECT_INIT ((_r)->x, (_r)->y, (_r)->width, (_r)->height))
static void
gtk_text_draw_text (GtkText *self,
GtkSnapshot *snapshot)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkWidget *widget = GTK_WIDGET (self);
GtkStyleContext *context;
PangoLayout *layout;
int x, y;
/* Nothing to display at all */
if (gtk_text_get_display_mode (self) == DISPLAY_BLANK)
return;
context = gtk_widget_get_style_context (widget);
layout = gtk_text_ensure_layout (self, TRUE);
gtk_text_get_layout_offsets (self, &x, &y);
gtk_snapshot_render_layout (snapshot, context, x, y, layout);
if (priv->selection_bound != priv->current_pos)
{
const char *text = pango_layout_get_text (layout);
int start_index = g_utf8_offset_to_pointer (text, priv->selection_bound) - text;
int end_index = g_utf8_offset_to_pointer (text, priv->current_pos) - text;
cairo_region_t *clip;
cairo_rectangle_int_t clip_extents;
int range[2];
int width, height;
width = gtk_widget_get_width (widget);
height = gtk_widget_get_height (widget);
range[0] = MIN (start_index, end_index);
range[1] = MAX (start_index, end_index);
gtk_style_context_save_to_node (context, priv->selection_node);
clip = gdk_pango_layout_get_clip_region (layout, x, y, range, 1);
cairo_region_get_extents (clip, &clip_extents);
gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_FROM_RECT (&clip_extents));
gtk_snapshot_render_background (snapshot, context, 0, 0, width, height);
gtk_snapshot_render_layout (snapshot, context, x, y, layout);
gtk_snapshot_pop (snapshot);
cairo_region_destroy (clip);
gtk_style_context_restore (context);
}
}
static void
gtk_text_draw_cursor (GtkText *self,
GtkSnapshot *snapshot,
CursorType type)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkWidget *widget = GTK_WIDGET (self);
GtkStyleContext *context;
PangoRectangle cursor_rect;
int cursor_index;
gboolean block;
gboolean block_at_line_end;
PangoLayout *layout;
const char *text;
int x, y;
context = gtk_widget_get_style_context (widget);
layout = g_object_ref (gtk_text_ensure_layout (self, TRUE));
text = pango_layout_get_text (layout);
gtk_text_get_layout_offsets (self, &x, &y);
if (type == CURSOR_DND)
cursor_index = g_utf8_offset_to_pointer (text, priv->dnd_position) - text;
else
cursor_index = g_utf8_offset_to_pointer (text, priv->current_pos + priv->preedit_cursor) - text;
if (!priv->overwrite_mode)
block = FALSE;
else
block = _gtk_text_util_get_block_cursor_location (layout,
cursor_index, &cursor_rect, &block_at_line_end);
if (!block)
{
gtk_snapshot_render_insertion_cursor (snapshot, context,
x, y,
layout, cursor_index, priv->resolved_dir);
}
else /* overwrite_mode */
{
int width = gtk_widget_get_width (widget);
int height = gtk_widget_get_height (widget);
graphene_rect_t bounds;
bounds.origin.x = PANGO_PIXELS (cursor_rect.x) + x;
bounds.origin.y = PANGO_PIXELS (cursor_rect.y) + y;
bounds.size.width = PANGO_PIXELS (cursor_rect.width);
bounds.size.height = PANGO_PIXELS (cursor_rect.height);
gtk_style_context_save_to_node (context, priv->block_cursor_node);
gtk_snapshot_push_clip (snapshot, &bounds);
gtk_snapshot_render_background (snapshot, context, 0, 0, width, height);
gtk_snapshot_render_layout (snapshot, context, x, y, layout);
gtk_snapshot_pop (snapshot);
gtk_style_context_restore (context);
}
g_object_unref (layout);
}
static void
gtk_text_handle_dragged (GtkTextHandle *handle,
int x,
int y,
GtkText *self)
{
int cursor_pos, selection_bound_pos, tmp_pos, *old_pos;
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gtk_text_selection_bubble_popup_unset (self);
cursor_pos = priv->current_pos;
selection_bound_pos = priv->selection_bound;
tmp_pos = gtk_text_find_position (self, x + priv->scroll_offset);
if (handle == priv->text_handles[TEXT_HANDLE_CURSOR])
{
/* Avoid running past the other handle in selection mode */
if (tmp_pos >= selection_bound_pos &&
gtk_widget_is_visible (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])))
{
tmp_pos = selection_bound_pos - 1;
}
old_pos = &cursor_pos;
}
else if (handle == priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])
{
/* Avoid running past the other handle */
if (tmp_pos <= cursor_pos)
tmp_pos = cursor_pos + 1;
old_pos = &selection_bound_pos;
}
else
g_assert_not_reached ();
if (tmp_pos != *old_pos)
{
*old_pos = tmp_pos;
if (handle == priv->text_handles[TEXT_HANDLE_CURSOR] &&
!gtk_widget_is_visible (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])))
gtk_text_set_positions (self, cursor_pos, cursor_pos);
else
gtk_text_set_positions (self, cursor_pos, selection_bound_pos);
if (handle == priv->text_handles[TEXT_HANDLE_CURSOR])
priv->cursor_handle_dragged = TRUE;
else if (handle == priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])
priv->selection_handle_dragged = TRUE;
gtk_text_update_handles (self);
}
gtk_text_show_magnifier (self, x, y);
}
static void
gtk_text_handle_drag_started (GtkTextHandle *handle,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
priv->cursor_handle_dragged = FALSE;
priv->selection_handle_dragged = FALSE;
}
static void
gtk_text_handle_drag_finished (GtkTextHandle *handle,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (!priv->cursor_handle_dragged && !priv->selection_handle_dragged)
{
GtkSettings *settings;
guint double_click_time;
settings = gtk_widget_get_settings (GTK_WIDGET (self));
g_object_get (settings, "gtk-double-click-time", &double_click_time, NULL);
if (g_get_monotonic_time() - priv->handle_place_time < double_click_time * 1000)
{
gtk_text_select_word (self);
gtk_text_update_handles (self);
}
else
gtk_text_selection_bubble_popup_set (self);
}
if (priv->magnifier_popover)
gtk_popover_popdown (GTK_POPOVER (priv->magnifier_popover));
}
static void
gtk_text_schedule_im_reset (GtkText *self)
{
GtkTextPrivate *priv;
priv = gtk_text_get_instance_private (self);
priv->need_im_reset = TRUE;
}
void
gtk_text_reset_im_context (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->need_im_reset)
{
priv->need_im_reset = FALSE;
gtk_im_context_reset (priv->im_context);
}
}
static int
gtk_text_find_position (GtkText *self,
int x)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
PangoLayout *layout;
PangoLayoutLine *line;
int index;
int pos;
int trailing;
const char *text;
int cursor_index;
layout = gtk_text_ensure_layout (self, TRUE);
text = pango_layout_get_text (layout);
cursor_index = g_utf8_offset_to_pointer (text, priv->current_pos) - text;
line = pango_layout_get_lines_readonly (layout)->data;
pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing);
if (index >= cursor_index && priv->preedit_length)
{
if (index >= cursor_index + priv->preedit_length)
index -= priv->preedit_length;
else
{
index = cursor_index;
trailing = 0;
}
}
pos = g_utf8_pointer_to_offset (text, text + index);
pos += trailing;
return pos;
}
static void
gtk_text_get_cursor_locations (GtkText *self,
int *strong_x,
int *weak_x)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
DisplayMode mode = gtk_text_get_display_mode (self);
/* Nothing to display at all, so no cursor is relevant */
if (mode == DISPLAY_BLANK)
{
if (strong_x)
*strong_x = 0;
if (weak_x)
*weak_x = 0;
}
else
{
PangoLayout *layout = gtk_text_ensure_layout (self, TRUE);
const char *text = pango_layout_get_text (layout);
PangoRectangle strong_pos, weak_pos;
int index;
index = g_utf8_offset_to_pointer (text, priv->current_pos + priv->preedit_cursor) - text;
pango_layout_get_cursor_pos (layout, index, &strong_pos, &weak_pos);
if (strong_x)
*strong_x = strong_pos.x / PANGO_SCALE;
if (weak_x)
*weak_x = weak_pos.x / PANGO_SCALE;
}
}
static gboolean
gtk_text_get_is_selection_handle_dragged (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkTextHandle *handle;
if (priv->current_pos >= priv->selection_bound)
handle = priv->text_handles[TEXT_HANDLE_CURSOR];
else
handle = priv->text_handles[TEXT_HANDLE_SELECTION_BOUND];
return handle && gtk_text_handle_get_is_dragged (handle);
}
static void
gtk_text_get_scroll_limits (GtkText *self,
int *min_offset,
int *max_offset)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
float xalign;
PangoLayout *layout;
PangoLayoutLine *line;
PangoRectangle logical_rect;
int text_width, width;
layout = gtk_text_ensure_layout (self, TRUE);
line = pango_layout_get_lines_readonly (layout)->data;
pango_layout_line_get_extents (line, NULL, &logical_rect);
/* Display as much text as we can */
if (priv->resolved_dir == PANGO_DIRECTION_LTR)
xalign = priv->xalign;
else
xalign = 1.0 - priv->xalign;
text_width = PANGO_PIXELS(logical_rect.width);
width = gtk_widget_get_width (GTK_WIDGET (self));
if (text_width > width)
{
*min_offset = 0;
*max_offset = text_width - width;
}
else
{
*min_offset = (text_width - width) * xalign;
*max_offset = *min_offset;
}
}
static void
gtk_text_adjust_scroll (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_width = gtk_widget_get_width (GTK_WIDGET (self));
int min_offset, max_offset;
int strong_x, weak_x;
int strong_xoffset, weak_xoffset;
if (!gtk_widget_get_realized (GTK_WIDGET (self)))
return;
gtk_text_get_scroll_limits (self, &min_offset, &max_offset);
priv->scroll_offset = CLAMP (priv->scroll_offset, min_offset, max_offset);
if (gtk_text_get_is_selection_handle_dragged (self))
{
/* 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_text_get_selection_bound_location (self);
}
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 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_text_get_cursor_locations (self, &strong_x, &weak_x);
}
strong_xoffset = strong_x - priv->scroll_offset;
if (strong_xoffset < 0)
{
priv->scroll_offset += strong_xoffset;
strong_xoffset = 0;
}
else if (strong_xoffset > text_width)
{
priv->scroll_offset += strong_xoffset - text_width;
strong_xoffset = text_width;
}
weak_xoffset = weak_x - priv->scroll_offset;
if (weak_xoffset < 0 && strong_xoffset - weak_xoffset <= text_width)
{
priv->scroll_offset += weak_xoffset;
}
else if (weak_xoffset > text_width &&
strong_xoffset - (weak_xoffset - text_width) >= 0)
{
priv->scroll_offset += weak_xoffset - text_width;
}
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_SCROLL_OFFSET]);
gtk_text_update_handles (self);
}
static int
gtk_text_move_visually (GtkText *self,
int start,
int count)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int index;
PangoLayout *layout = gtk_text_ensure_layout (self, FALSE);
const char *text;
text = pango_layout_get_text (layout);
index = g_utf8_offset_to_pointer (text, start) - text;
while (count != 0)
{
int new_index, new_trailing;
gboolean split_cursor;
gboolean strong;
g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
"gtk-split-cursor", &split_cursor,
NULL);
if (split_cursor)
strong = TRUE;
else
{
GdkDisplay *display;
GdkSeat *seat;
GdkDevice *keyboard = NULL;
PangoDirection direction = PANGO_DIRECTION_LTR;
display = gtk_widget_get_display (GTK_WIDGET (self));
seat = gdk_display_get_default_seat (display);
if (seat)
keyboard = gdk_seat_get_keyboard (seat);
if (keyboard)
direction = gdk_device_get_direction (keyboard);
strong = direction == priv->resolved_dir;
}
if (count > 0)
{
pango_layout_move_cursor_visually (layout, strong, index, 0, 1, &new_index, &new_trailing);
count--;
}
else
{
pango_layout_move_cursor_visually (layout, strong, index, 0, -1, &new_index, &new_trailing);
count++;
}
if (new_index < 0)
index = 0;
else if (new_index != G_MAXINT)
index = new_index;
while (new_trailing--)
index = g_utf8_next_char (text + index) - text;
}
return g_utf8_pointer_to_offset (text, text + index);
}
static int
gtk_text_move_logically (GtkText *self,
int start,
int count)
{
int new_pos = start;
guint length;
length = gtk_entry_buffer_get_length (get_buffer (self));
/* Prevent any leak of information */
if (gtk_text_get_display_mode (self) != DISPLAY_NORMAL)
{
new_pos = CLAMP (start + count, 0, length);
}
else
{
PangoLayout *layout = gtk_text_ensure_layout (self, FALSE);
const PangoLogAttr *log_attrs;
int n_attrs;
log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs);
while (count > 0 && new_pos < length)
{
do
new_pos++;
while (new_pos < length && !log_attrs[new_pos].is_cursor_position);
count--;
}
while (count < 0 && new_pos > 0)
{
do
new_pos--;
while (new_pos > 0 && !log_attrs[new_pos].is_cursor_position);
count++;
}
}
return new_pos;
}
static int
gtk_text_move_forward_word (GtkText *self,
int start,
gboolean allow_whitespace)
{
int new_pos = start;
guint length;
length = gtk_entry_buffer_get_length (get_buffer (self));
/* Prevent any leak of information */
if (gtk_text_get_display_mode (self) != DISPLAY_NORMAL)
{
new_pos = length;
}
else if (new_pos < length)
{
PangoLayout *layout = gtk_text_ensure_layout (self, FALSE);
const PangoLogAttr *log_attrs;
int n_attrs;
log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs);
/* Find the next word boundary */
new_pos++;
while (new_pos < n_attrs - 1 && !(log_attrs[new_pos].is_word_end ||
(log_attrs[new_pos].is_word_start && allow_whitespace)))
new_pos++;
}
return new_pos;
}
static int
gtk_text_move_backward_word (GtkText *self,
int start,
gboolean allow_whitespace)
{
int new_pos = start;
/* Prevent any leak of information */
if (gtk_text_get_display_mode (self) != DISPLAY_NORMAL)
{
new_pos = 0;
}
else if (start > 0)
{
PangoLayout *layout = gtk_text_ensure_layout (self, FALSE);
const PangoLogAttr *log_attrs;
int n_attrs;
log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs);
new_pos = start - 1;
/* Find the previous word boundary */
while (new_pos > 0 && !(log_attrs[new_pos].is_word_start ||
(log_attrs[new_pos].is_word_end && allow_whitespace)))
new_pos--;
}
return new_pos;
}
static void
gtk_text_delete_whitespace (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
PangoLayout *layout = gtk_text_ensure_layout (self, FALSE);
const PangoLogAttr *log_attrs;
int n_attrs;
int start, end;
log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs);
start = end = priv->current_pos;
while (start > 0 && log_attrs[start-1].is_white)
start--;
while (end < n_attrs && log_attrs[end].is_white)
end++;
if (start != end)
gtk_editable_delete_text (GTK_EDITABLE (self), start, end);
}
static void
gtk_text_select_word (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int start_pos = gtk_text_move_backward_word (self, priv->current_pos, TRUE);
int end_pos = gtk_text_move_forward_word (self, priv->current_pos, TRUE);
gtk_text_set_selection_bounds (self, start_pos, end_pos);
}
static void
gtk_text_select_line (GtkText *self)
{
gtk_text_set_selection_bounds (self, 0, -1);
}
static int
truncate_multiline (const char *text)
{
int length;
for (length = 0;
text[length] && text[length] != '\n' && text[length] != '\r';
length++);
return length;
}
static void
paste_received (GObject *clipboard,
GAsyncResult *result,
gpointer data)
{
GtkText *self = GTK_TEXT (data);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
char *text;
int pos, start, end;
int length = -1;
text = gdk_clipboard_read_text_finish (GDK_CLIPBOARD (clipboard), result, NULL);
if (text == NULL)
{
gtk_widget_error_bell (GTK_WIDGET (self));
return;
}
if (priv->insert_pos >= 0)
{
pos = priv->insert_pos;
start = priv->selection_bound;
end = priv->current_pos;
if (!((start <= pos && pos <= end) || (end <= pos && pos <= start)))
gtk_text_set_selection_bounds (self, pos, pos);
priv->insert_pos = -1;
}
if (priv->truncate_multiline)
length = truncate_multiline (text);
begin_change (self);
if (priv->selection_bound != priv->current_pos)
gtk_text_delete_selection (self);
pos = priv->current_pos;
gtk_editable_insert_text (GTK_EDITABLE (self), text, length, &pos);
gtk_text_set_selection_bounds (self, pos, pos);
end_change (self);
g_free (text);
g_object_unref (self);
}
static void
gtk_text_paste (GtkText *self,
GdkClipboard *clipboard)
{
gdk_clipboard_read_text_async (clipboard, NULL, paste_received, g_object_ref (self));
}
static void
gtk_text_update_primary_selection (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GdkClipboard *clipboard;
if (!gtk_widget_get_realized (GTK_WIDGET (self)))
return;
clipboard = gtk_widget_get_primary_clipboard (GTK_WIDGET (self));
if (priv->selection_bound != priv->current_pos)
{
gdk_clipboard_set_content (clipboard, priv->selection_content);
}
else
{
if (gdk_clipboard_get_content (clipboard) == priv->selection_content)
gdk_clipboard_set_content (clipboard, NULL);
}
}
/* Public API
*/
/**
* gtk_text_new:
*
* Creates a new self.
*
* Returns: a new #GtkText.
*/
GtkWidget *
gtk_text_new (void)
{
return g_object_new (GTK_TYPE_TEXT, NULL);
}
/**
* gtk_text_new_with_buffer:
* @buffer: The buffer to use for the new #GtkText.
*
* Creates a new self with the specified text buffer.
*
* Returns: a new #GtkText
*/
GtkWidget *
gtk_text_new_with_buffer (GtkEntryBuffer *buffer)
{
g_return_val_if_fail (GTK_IS_ENTRY_BUFFER (buffer), NULL);
return g_object_new (GTK_TYPE_TEXT, "buffer", buffer, NULL);
}
static GtkEntryBuffer *
get_buffer (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->buffer == NULL)
{
GtkEntryBuffer *buffer;
buffer = gtk_entry_buffer_new (NULL, 0);
gtk_text_set_buffer (self, buffer);
g_object_unref (buffer);
}
return priv->buffer;
}
/**
* gtk_text_get_buffer:
* @self: a #GtkText
*
* Get the #GtkEntryBuffer object which holds the text for
* this self.
*
* Returns: (transfer none): A #GtkEntryBuffer object.
*/
GtkEntryBuffer *
gtk_text_get_buffer (GtkText *self)
{
g_return_val_if_fail (GTK_IS_TEXT (self), NULL);
return get_buffer (self);
}
/**
* gtk_text_set_buffer:
* @self: a #GtkText
* @buffer: a #GtkEntryBuffer
*
* Set the #GtkEntryBuffer object which holds the text for
* this widget.
*/
void
gtk_text_set_buffer (GtkText *self,
GtkEntryBuffer *buffer)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GObject *obj;
gboolean had_buffer = FALSE;
guint old_length = 0;
guint new_length = 0;
g_return_if_fail (GTK_IS_TEXT (self));
if (buffer)
{
g_return_if_fail (GTK_IS_ENTRY_BUFFER (buffer));
g_object_ref (buffer);
}
if (priv->buffer)
{
had_buffer = TRUE;
old_length = gtk_entry_buffer_get_length (priv->buffer);
buffer_disconnect_signals (self);
g_object_unref (priv->buffer);
}
priv->buffer = buffer;
if (priv->buffer)
{
new_length = gtk_entry_buffer_get_length (priv->buffer);
buffer_connect_signals (self);
}
obj = G_OBJECT (self);
g_object_freeze_notify (obj);
g_object_notify_by_pspec (obj, text_props[PROP_BUFFER]);
g_object_notify_by_pspec (obj, text_props[PROP_MAX_LENGTH]);
if (old_length != 0 || new_length != 0)
g_object_notify (obj, "text");
if (had_buffer)
{
gtk_text_set_selection_bounds (self, 0, 0);
gtk_text_recompute (self);
}
g_object_thaw_notify (obj);
}
static void
gtk_text_set_editable (GtkText *self,
gboolean is_editable)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (is_editable != priv->editable)
{
GtkWidget *widget = GTK_WIDGET (self);
if (!is_editable)
{
gtk_text_reset_im_context (self);
if (gtk_widget_has_focus (widget))
gtk_im_context_focus_out (priv->im_context);
priv->preedit_length = 0;
priv->preedit_cursor = 0;
gtk_widget_remove_css_class (GTK_WIDGET (self), "read-only");
}
else
{
gtk_widget_add_css_class (GTK_WIDGET (self), "read-only");
}
priv->editable = is_editable;
if (is_editable && gtk_widget_has_focus (widget))
gtk_im_context_focus_in (priv->im_context);
gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY (priv->key_controller),
is_editable ? priv->im_context : NULL);
gtk_text_update_clipboard_actions (self);
gtk_text_update_emoji_action (self);
gtk_accessible_update_property (GTK_ACCESSIBLE (self),
GTK_ACCESSIBLE_PROPERTY_READ_ONLY, !priv->editable,
-1);
g_object_notify (G_OBJECT (self), "editable");
}
}
static void
gtk_text_set_text (GtkText *self,
const char *text)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int tmp_pos;
g_return_if_fail (GTK_IS_TEXT (self));
g_return_if_fail (text != NULL);
/* Actually setting the text will affect the cursor and selection;
* if the contents don't actually change, this will look odd to the user.
*/
if (strcmp (gtk_entry_buffer_get_text (get_buffer (self)), text) == 0)
return;
gtk_text_history_begin_irreversible_action (priv->history);
begin_change (self);
g_object_freeze_notify (G_OBJECT (self));
gtk_editable_delete_text (GTK_EDITABLE (self), 0, -1);
tmp_pos = 0;
gtk_editable_insert_text (GTK_EDITABLE (self), text, strlen (text), &tmp_pos);
g_object_thaw_notify (G_OBJECT (self));
end_change (self);
gtk_text_history_end_irreversible_action (priv->history);
}
/**
* gtk_text_set_visibility:
* @self: a #GtkText
* @visible: %TRUE if the contents of the self are displayed
* as plaintext
*
* Sets whether the contents of the self are visible or not.
* When visibility is set to %FALSE, characters are displayed
* as the invisible char, and will also appear that way when
* the text in the self widget is copied to the clipboard.
*
* By default, GTK picks the best invisible character available
* in the current font, but it can be changed with
* gtk_text_set_invisible_char().
*
* Note that you probably want to set #GtkText:input-purpose
* to %GTK_INPUT_PURPOSE_PASSWORD or %GTK_INPUT_PURPOSE_PIN to
* inform input methods about the purpose of this self,
* in addition to setting visibility to %FALSE.
*/
void
gtk_text_set_visibility (GtkText *self,
gboolean visible)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
visible = visible != FALSE;
if (priv->visible != visible)
{
priv->visible = visible;
g_object_notify (G_OBJECT (self), "visibility");
gtk_text_update_cached_style_values (self);
gtk_text_recompute (self);
/* disable undo when invisible text is used */
gtk_text_history_set_enabled (priv->history, visible);
gtk_text_update_clipboard_actions (self);
}
}
/**
* gtk_text_get_visibility:
* @self: a #GtkText
*
* Retrieves whether the text in @self is visible.
* See gtk_text_set_visibility().
*
* Returns: %TRUE if the text is currently visible
**/
gboolean
gtk_text_get_visibility (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return priv->visible;
}
/**
* gtk_text_set_invisible_char:
* @self: a #GtkText
* @ch: a Unicode character
*
* Sets the character to use in place of the actual text when
* gtk_text_set_visibility() has been called to set text visibility
* to %FALSE. i.e. this is the character used in “password mode” to
* show the user how many characters have been typed.
*
* By default, GTK picks the best invisible char available in the
* current font. If you set the invisible char to 0, then the user
* will get no feedback at all; there will be no text on the screen
* as they type.
**/
void
gtk_text_set_invisible_char (GtkText *self,
gunichar ch)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (!priv->invisible_char_set)
{
priv->invisible_char_set = TRUE;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INVISIBLE_CHAR_SET]);
}
if (ch == priv->invisible_char)
return;
priv->invisible_char = ch;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INVISIBLE_CHAR]);
gtk_text_recompute (self);
}
/**
* gtk_text_get_invisible_char:
* @self: a #GtkText
*
* Retrieves the character displayed in place of the real characters
* for entries with visibility set to false. Note that GTK does not
* compute this value unless it needs it, so the value returned by
* this function is not very useful unless it has been explicitly
* set with gtk_text_set_invisible_char()
*
* Returns: the current invisible char, or 0, if @text does not
* show invisible text at all.
**/
gunichar
gtk_text_get_invisible_char (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), 0);
return priv->invisible_char;
}
/**
* gtk_text_unset_invisible_char:
* @self: a #GtkText
*
* Unsets the invisible char previously set with
* gtk_text_set_invisible_char(). So that the
* default invisible char is used again.
**/
void
gtk_text_unset_invisible_char (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gunichar ch;
g_return_if_fail (GTK_IS_TEXT (self));
if (!priv->invisible_char_set)
return;
priv->invisible_char_set = FALSE;
ch = find_invisible_char (GTK_WIDGET (self));
if (priv->invisible_char != ch)
{
priv->invisible_char = ch;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INVISIBLE_CHAR]);
}
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INVISIBLE_CHAR_SET]);
gtk_text_recompute (self);
}
/**
* gtk_text_set_overwrite_mode:
* @self: a #GtkText
* @overwrite: new value
*
* Sets whether the text is overwritten when typing in the #GtkText.
**/
void
gtk_text_set_overwrite_mode (GtkText *self,
gboolean overwrite)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->overwrite_mode == overwrite)
return;
gtk_text_toggle_overwrite (self);
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_OVERWRITE_MODE]);
}
/**
* gtk_text_get_overwrite_mode:
* @self: a #GtkText
*
* Gets the value set by gtk_text_set_overwrite_mode().
*
* Returns: whether the text is overwritten when typing.
**/
gboolean
gtk_text_get_overwrite_mode (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return priv->overwrite_mode;
}
/**
* gtk_text_set_max_length:
* @self: a #GtkText
* @length: the maximum length of the self, or 0 for no maximum.
* (other than the maximum length of entries.) The value passed in will
* be clamped to the range 0-65536.
*
* Sets the maximum allowed length of the contents of the widget.
*
* If the current contents are longer than the given length, then
* they will be truncated to fit.
*
* This is equivalent to getting @self's #GtkEntryBuffer and
* calling gtk_entry_buffer_set_max_length() on it.
* ]|
**/
void
gtk_text_set_max_length (GtkText *self,
int length)
{
g_return_if_fail (GTK_IS_TEXT (self));
gtk_entry_buffer_set_max_length (get_buffer (self), length);
}
/**
* gtk_text_get_max_length:
* @self: a #GtkText
*
* Retrieves the maximum allowed length of the text in
* @self. See gtk_text_set_max_length().
*
* This is equivalent to getting @self's #GtkEntryBuffer and
* calling gtk_entry_buffer_get_max_length() on it.
*
* Returns: the maximum allowed number of characters
* in #GtkText, or 0 if there is no maximum.
**/
int
gtk_text_get_max_length (GtkText *self)
{
g_return_val_if_fail (GTK_IS_TEXT (self), 0);
return gtk_entry_buffer_get_max_length (get_buffer (self));
}
/**
* gtk_text_get_text_length:
* @self: a #GtkText
*
* Retrieves the current length of the text in
* @self.
*
* This is equivalent to getting @self's #GtkEntryBuffer and
* calling gtk_entry_buffer_get_length() on it.
*
* Returns: the current number of characters
* in #GtkText, or 0 if there are none.
**/
guint16
gtk_text_get_text_length (GtkText *self)
{
g_return_val_if_fail (GTK_IS_TEXT (self), 0);
return gtk_entry_buffer_get_length (get_buffer (self));
}
/**
* gtk_text_set_activates_default:
* @self: a #GtkText
* @activates: %TRUE to activate windows default widget on Enter keypress
*
* If @activates is %TRUE, pressing Enter in the @self will activate the default
* widget for the window containing the self. This usually means that
* the dialog box containing the self will be closed, since the default
* widget is usually one of the dialog buttons.
**/
void
gtk_text_set_activates_default (GtkText *self,
gboolean activates)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
activates = activates != FALSE;
if (priv->activates_default != activates)
{
priv->activates_default = activates;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_ACTIVATES_DEFAULT]);
}
}
/**
* gtk_text_get_activates_default:
* @self: a #GtkText
*
* Retrieves the value set by gtk_text_set_activates_default().
*
* Returns: %TRUE if the self will activate the default widget
*/
gboolean
gtk_text_get_activates_default (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return priv->activates_default;
}
static void
gtk_text_set_width_chars (GtkText *self,
int n_chars)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->width_chars != n_chars)
{
priv->width_chars = n_chars;
g_object_notify (G_OBJECT (self), "width-chars");
gtk_widget_queue_resize (GTK_WIDGET (self));
}
}
static void
gtk_text_set_max_width_chars (GtkText *self,
int n_chars)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->max_width_chars != n_chars)
{
priv->max_width_chars = n_chars;
g_object_notify (G_OBJECT (self), "max-width-chars");
gtk_widget_queue_resize (GTK_WIDGET (self));
}
}
PangoLayout *
gtk_text_get_layout (GtkText *self)
{
PangoLayout *layout;
g_return_val_if_fail (GTK_IS_TEXT (self), NULL);
layout = gtk_text_ensure_layout (self, TRUE);
return layout;
}
void
gtk_text_get_layout_offsets (GtkText *self,
int *x,
int *y)
{
g_return_if_fail (GTK_IS_TEXT (self));
get_layout_position (self, x, y);
}
static void
gtk_text_set_alignment (GtkText *self,
float xalign)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (xalign < 0.0)
xalign = 0.0;
else if (xalign > 1.0)
xalign = 1.0;
if (xalign != priv->xalign)
{
priv->xalign = xalign;
gtk_text_recompute (self);
g_object_notify (G_OBJECT (self), "xalign");
}
}
static void
hide_selection_bubble (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->selection_bubble && gtk_widget_get_visible (priv->selection_bubble))
gtk_widget_hide (priv->selection_bubble);
}
static void
gtk_text_activate_clipboard_cut (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkText *self = GTK_TEXT (widget);
g_signal_emit_by_name (self, "cut-clipboard");
hide_selection_bubble (self);
}
static void
gtk_text_activate_clipboard_copy (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkText *self = GTK_TEXT (widget);
g_signal_emit_by_name (self, "copy-clipboard");
hide_selection_bubble (self);
}
static void
gtk_text_activate_clipboard_paste (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkText *self = GTK_TEXT (widget);
g_signal_emit_by_name (self, "paste-clipboard");
hide_selection_bubble (self);
}
static void
gtk_text_activate_selection_delete (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkText *self = GTK_TEXT (widget);
gtk_text_delete_cb (self);
hide_selection_bubble (self);
}
static void
gtk_text_activate_selection_select_all (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkText *self = GTK_TEXT (widget);
gtk_text_select_all (self);
}
static void
gtk_text_activate_misc_insert_emoji (GtkWidget *widget,
const char *action_name,
GVariant *parameter)
{
GtkText *self = GTK_TEXT (widget);
gtk_text_insert_emoji (self);
hide_selection_bubble (self);
}
static void
gtk_text_update_clipboard_actions (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
DisplayMode mode;
GdkClipboard *clipboard;
gboolean has_clipboard;
gboolean has_selection;
gboolean has_content;
gboolean visible;
clipboard = gtk_widget_get_clipboard (GTK_WIDGET (self));
mode = gtk_text_get_display_mode (self);
has_clipboard = gdk_content_formats_contain_gtype (gdk_clipboard_get_formats (clipboard), G_TYPE_STRING);
has_selection = priv->current_pos != priv->selection_bound;
has_content = priv->buffer && (gtk_entry_buffer_get_length (priv->buffer) > 0);
visible = mode == DISPLAY_NORMAL;
gtk_widget_action_set_enabled (GTK_WIDGET (self), "clipboard.cut",
visible && priv->editable && has_selection);
gtk_widget_action_set_enabled (GTK_WIDGET (self), "clipboard.copy",
visible && has_selection);
gtk_widget_action_set_enabled (GTK_WIDGET (self), "clipboard.paste",
priv->editable && has_clipboard);
gtk_widget_action_set_enabled (GTK_WIDGET (self), "selection.delete",
priv->editable && has_selection);
gtk_widget_action_set_enabled (GTK_WIDGET (self), "selection.select-all",
has_content);
}
static void
gtk_text_update_emoji_action (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gtk_widget_action_set_enabled (GTK_WIDGET (self), "misc.insert-emoji",
priv->editable &&
(gtk_text_get_input_hints (self) & GTK_INPUT_HINT_NO_EMOJI) == 0);
}
static GMenuModel *
gtk_text_get_menu_model (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GMenu *menu, *section;
GMenuItem *item;
menu = g_menu_new ();
section = g_menu_new ();
item = g_menu_item_new (_("Cu_t"), "clipboard.cut");
g_menu_item_set_attribute (item, "touch-icon", "s", "edit-cut-symbolic");
g_menu_append_item (section, item);
g_object_unref (item);
item = g_menu_item_new (_("_Copy"), "clipboard.copy");
g_menu_item_set_attribute (item, "touch-icon", "s", "edit-copy-symbolic");
g_menu_append_item (section, item);
g_object_unref (item);
item = g_menu_item_new (_("_Paste"), "clipboard.paste");
g_menu_item_set_attribute (item, "touch-icon", "s", "edit-paste-symbolic");
g_menu_append_item (section, item);
g_object_unref (item);
item = g_menu_item_new (_("_Delete"), "selection.delete");
g_menu_item_set_attribute (item, "touch-icon", "s", "edit-delete-symbolic");
g_menu_append_item (section, item);
g_object_unref (item);
g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
g_object_unref (section);
section = g_menu_new ();
item = g_menu_item_new (_("Select _All"), "selection.select-all");
g_menu_item_set_attribute (item, "touch-icon", "s", "edit-select-all-symbolic");
g_menu_append_item (section, item);
g_object_unref (item);
item = g_menu_item_new ( _("Insert _Emoji"), "misc.insert-emoji");
g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled");
g_menu_item_set_attribute (item, "touch-icon", "s", "face-smile-symbolic");
g_menu_append_item (section, item);
g_object_unref (item);
g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
g_object_unref (section);
if (priv->extra_menu)
g_menu_append_section (menu, NULL, priv->extra_menu);
return G_MENU_MODEL (menu);
}
static gboolean
gtk_text_mnemonic_activate (GtkWidget *widget,
gboolean group_cycling)
{
gtk_widget_grab_focus (widget);
return GDK_EVENT_STOP;
}
static void
gtk_text_popup_menu (GtkWidget *widget,
const char *action_name,
GVariant *parameters)
{
gtk_text_do_popup (GTK_TEXT (widget), -1, -1);
}
static void
show_or_hide_handles (GtkWidget *popover,
GParamSpec *pspec,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
gboolean visible;
visible = gtk_widget_get_visible (popover);
priv->text_handles_enabled = !visible;
gtk_text_update_handles (self);
}
static void
append_bubble_item (GtkText *self,
GtkWidget *toolbar,
GMenuModel *model,
int index)
{
GtkActionMuxer *muxer;
GtkWidget *item, *image;
GVariant *att;
const char *icon_name;
const char *action_name;
GMenuModel *link;
gboolean enabled;
link = g_menu_model_get_item_link (model, index, "section");
if (link)
{
int i;
for (i = 0; i < g_menu_model_get_n_items (link); i++)
append_bubble_item (self, toolbar, link, i);
g_object_unref (link);
return;
}
att = g_menu_model_get_item_attribute_value (model, index, "touch-icon", G_VARIANT_TYPE_STRING);
if (att == NULL)
return;
icon_name = g_variant_get_string (att, NULL);
g_variant_unref (att);
att = g_menu_model_get_item_attribute_value (model, index, "action", G_VARIANT_TYPE_STRING);
if (att == NULL)
return;
action_name = g_variant_get_string (att, NULL);
g_variant_unref (att);
muxer = _gtk_widget_get_action_muxer (GTK_WIDGET (self), FALSE);
if (!gtk_action_muxer_query_action (muxer, action_name, &enabled,
NULL, NULL, NULL, NULL) ||
!enabled)
return;
item = gtk_button_new ();
gtk_widget_set_focus_on_click (item, FALSE);
image = gtk_image_new_from_icon_name (icon_name);
gtk_widget_show (image);
gtk_button_set_child (GTK_BUTTON (item), image);
gtk_widget_add_css_class (item, "image-button");
gtk_actionable_set_action_name (GTK_ACTIONABLE (item), action_name);
gtk_widget_show (GTK_WIDGET (item));
gtk_box_append (GTK_BOX (toolbar), item);
}
static gboolean
gtk_text_selection_bubble_popup_show (gpointer user_data)
{
GtkText *self = user_data;
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
const int text_width = gtk_widget_get_width (GTK_WIDGET (self));
const int text_height = gtk_widget_get_height (GTK_WIDGET (self));
cairo_rectangle_int_t rect;
GtkAllocation allocation;
gboolean has_selection;
int start_x, end_x;
GtkWidget *box;
GtkWidget *toolbar;
GMenuModel *model;
int i;
gtk_text_update_clipboard_actions (self);
has_selection = priv->selection_bound != priv->current_pos;
if (!has_selection && !priv->editable)
{
priv->selection_bubble_timeout_id = 0;
return G_SOURCE_REMOVE;
}
g_clear_pointer (&priv->selection_bubble, gtk_widget_unparent);
priv->selection_bubble = gtk_popover_new ();
gtk_widget_set_parent (priv->selection_bubble, GTK_WIDGET (self));
gtk_widget_add_css_class (priv->selection_bubble, "touch-selection");
gtk_popover_set_position (GTK_POPOVER (priv->selection_bubble), GTK_POS_BOTTOM);
gtk_popover_set_autohide (GTK_POPOVER (priv->selection_bubble), FALSE);
g_signal_connect (priv->selection_bubble, "notify::visible",
G_CALLBACK (show_or_hide_handles), self);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
gtk_widget_set_margin_start (box, 10);
gtk_widget_set_margin_end (box, 10);
gtk_widget_set_margin_top (box, 10);
gtk_widget_set_margin_bottom (box, 10);
gtk_widget_show (box);
toolbar = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class (toolbar, "linked");
gtk_popover_set_child (GTK_POPOVER (priv->selection_bubble), box);
gtk_box_append (GTK_BOX (box), toolbar);
model = gtk_text_get_menu_model (self);
for (i = 0; i < g_menu_model_get_n_items (model); i++)
append_bubble_item (self, toolbar, model, i);
g_object_unref (model);
gtk_widget_get_allocation (GTK_WIDGET (self), &allocation);
gtk_text_get_cursor_locations (self, &start_x, NULL);
start_x -= priv->scroll_offset;
start_x = CLAMP (start_x, 0, text_width);
rect.y = - allocation.y;
rect.height = text_height;
if (has_selection)
{
end_x = gtk_text_get_selection_bound_location (self) - priv->scroll_offset;
end_x = CLAMP (end_x, 0, text_width);
rect.x = - allocation.x + MIN (start_x, end_x);
rect.width = ABS (end_x - start_x);
}
else
{
rect.x = - allocation.x + start_x;
rect.width = 0;
}
rect.x -= 5;
rect.y -= 5;
rect.width += 10;
rect.height += 10;
gtk_popover_set_pointing_to (GTK_POPOVER (priv->selection_bubble), &rect);
gtk_popover_popup (GTK_POPOVER (priv->selection_bubble));
priv->selection_bubble_timeout_id = 0;
return G_SOURCE_REMOVE;
}
static void
gtk_text_selection_bubble_popup_unset (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->selection_bubble)
gtk_widget_hide (priv->selection_bubble);
if (priv->selection_bubble_timeout_id)
{
g_source_remove (priv->selection_bubble_timeout_id);
priv->selection_bubble_timeout_id = 0;
}
}
static void
gtk_text_selection_bubble_popup_set (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->selection_bubble_timeout_id)
g_source_remove (priv->selection_bubble_timeout_id);
priv->selection_bubble_timeout_id =
g_timeout_add (50, gtk_text_selection_bubble_popup_show, self);
g_source_set_name_by_id (priv->selection_bubble_timeout_id, "[gtk] gtk_text_selection_bubble_popup_cb");
}
static void
gtk_text_drag_leave (GtkDropTarget *dest,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkWidget *widget = GTK_WIDGET (self);
priv->dnd_position = -1;
gtk_widget_queue_draw (widget);
}
static gboolean
gtk_text_drag_drop (GtkDropTarget *dest,
const GValue *value,
double x,
double y,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int drop_position;
int length;
const char *str;
if (!priv->editable)
return FALSE;
drop_position = gtk_text_find_position (self, x + priv->scroll_offset);
str = g_value_get_string (value);
if (priv->truncate_multiline)
length = truncate_multiline (str);
else
length = -1;
if (priv->selection_bound == priv->current_pos ||
drop_position < priv->selection_bound ||
drop_position > priv->current_pos)
{
gtk_editable_insert_text (GTK_EDITABLE (self), str, length, &drop_position);
}
else
{
int pos;
/* Replacing selection */
begin_change (self);
gtk_text_delete_selection (self);
pos = MIN (priv->selection_bound, priv->current_pos);
gtk_editable_insert_text (GTK_EDITABLE (self), str, length, &pos);
end_change (self);
}
return TRUE;
}
static gboolean
gtk_text_drag_accept (GtkDropTarget *dest,
GdkDrop *drop,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (!priv->editable)
return FALSE;
if ((gdk_drop_get_actions (drop) & gtk_drop_target_get_actions (dest)) == 0)
return FALSE;
return gdk_content_formats_match (gtk_drop_target_get_formats (dest), gdk_drop_get_formats (drop));
}
static GdkDragAction
gtk_text_drag_motion (GtkDropTarget *target,
double x,
double y,
GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
int new_position, old_position;
if (!priv->editable)
{
gtk_drop_target_reject (target);
return 0;
}
old_position = priv->dnd_position;
new_position = gtk_text_find_position (self, x + priv->scroll_offset);
if (priv->selection_bound == priv->current_pos ||
new_position < priv->selection_bound ||
new_position > priv->current_pos)
{
priv->dnd_position = new_position;
}
else
{
priv->dnd_position = -1;
}
if (priv->dnd_position != old_position)
gtk_widget_queue_draw (GTK_WIDGET (self));
if (priv->drag)
return GDK_ACTION_MOVE;
else
return GDK_ACTION_COPY;
}
/* We display the cursor when
*
* - the selection is empty, AND
* - the widget has focus
*/
#define CURSOR_ON_MULTIPLIER 2
#define CURSOR_OFF_MULTIPLIER 1
#define CURSOR_PEND_MULTIPLIER 3
#define CURSOR_DIVIDER 3
static gboolean
cursor_blinks (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (gtk_widget_has_focus (GTK_WIDGET (self)) &&
priv->editable &&
priv->selection_bound == priv->current_pos)
{
GtkSettings *settings;
gboolean blink;
settings = gtk_widget_get_settings (GTK_WIDGET (self));
g_object_get (settings, "gtk-cursor-blink", &blink, NULL);
return blink;
}
else
return FALSE;
}
static gboolean
get_middle_click_paste (GtkText *self)
{
GtkSettings *settings;
gboolean paste;
settings = gtk_widget_get_settings (GTK_WIDGET (self));
g_object_get (settings, "gtk-enable-primary-paste", &paste, NULL);
return paste;
}
static int
get_cursor_time (GtkText *self)
{
GtkSettings *settings = gtk_widget_get_settings (GTK_WIDGET (self));
int time;
g_object_get (settings, "gtk-cursor-blink-time", &time, NULL);
return time;
}
static int
get_cursor_blink_timeout (GtkText *self)
{
GtkSettings *settings = gtk_widget_get_settings (GTK_WIDGET (self));
int timeout;
g_object_get (settings, "gtk-cursor-blink-timeout", &timeout, NULL);
return timeout;
}
typedef struct {
guint64 start;
guint64 end;
} BlinkData;
static gboolean blink_cb (GtkWidget *widget,
GdkFrameClock *clock,
gpointer user_data);
static void
add_blink_timeout (GtkText *self,
gboolean delay)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
BlinkData *data;
int blink_time;
priv->blink_start_time = g_get_monotonic_time ();
priv->cursor_alpha = 1.0;
blink_time = get_cursor_time (self);
data = g_new (BlinkData, 1);
data->start = priv->blink_start_time;
if (delay)
data->start += blink_time * 1000 / 2;
data->end = data->start + blink_time * 1000;
priv->blink_tick = gtk_widget_add_tick_callback (GTK_WIDGET (self),
blink_cb,
data,
g_free);
}
static void
remove_blink_timeout (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (priv->blink_tick)
{
gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->blink_tick);
priv->blink_tick = 0;
}
}
/*
* Blink!
*/
static float
blink_alpha (float phase)
{
/* keep it simple, and split the blink cycle evenly
* into visible, fading out, invisible, fading in
*/
if (phase < 0.25)
return 1;
else if (phase < 0.5)
return 1 - 4 * (phase - 0.25);
else if (phase < 0.75)
return 0;
else
return 4 * (phase - 0.75);
}
static gboolean
blink_cb (GtkWidget *widget,
GdkFrameClock *clock,
gpointer user_data)
{
GtkText *self = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
BlinkData *data = user_data;
int blink_timeout;
int blink_time;
guint64 now;
float phase;
float alpha;
if (!gtk_widget_has_focus (GTK_WIDGET (self)))
{
g_warning ("GtkText - did not receive a focus-out event.\n"
"If you handle this event, you must return\n"
"GDK_EVENT_PROPAGATE so the self gets the event as well");
gtk_text_check_cursor_blink (self);
return G_SOURCE_REMOVE;
}
g_assert (priv->selection_bound == priv->current_pos);
blink_timeout = get_cursor_blink_timeout (self);
blink_time = get_cursor_time (self);
now = g_get_monotonic_time ();
if (now > priv->blink_start_time + blink_timeout * 1000000)
{
/* we've blinked enough without the user doing anything, stop blinking */
priv->cursor_alpha = 1.0;
remove_blink_timeout (self);
gtk_widget_queue_draw (widget);
return G_SOURCE_REMOVE;
}
phase = (now - data->start) / (float) (data->end - data->start);
if (now >= data->end)
{
data->start = data->end;
data->end = data->start + blink_time * 1000;
}
alpha = blink_alpha (phase);
if (priv->cursor_alpha != alpha)
{
priv->cursor_alpha = alpha;
gtk_widget_queue_draw (widget);
}
return G_SOURCE_CONTINUE;
}
static void
gtk_text_check_cursor_blink (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
if (cursor_blinks (self))
{
if (!priv->blink_tick)
add_blink_timeout (self, FALSE);
}
else
{
if (priv->blink_tick)
remove_blink_timeout (self);
}
}
static void
gtk_text_pend_cursor_blink (GtkText *self)
{
if (cursor_blinks (self))
{
remove_blink_timeout (self);
add_blink_timeout (self, TRUE);
}
}
static void
gtk_text_reset_blink_time (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
priv->blink_start_time = g_get_monotonic_time ();
}
/**
* gtk_text_set_placeholder_text:
* @self: a #GtkText
* @text: (nullable): a string to be displayed when @self is empty and unfocused, or %NULL
*
* Sets text to be displayed in @self when it is empty.
*
* This can be used to give a visual hint of the expected
* contents of the self.
**/
void
gtk_text_set_placeholder_text (GtkText *self,
const char *text)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->placeholder == NULL)
{
priv->placeholder = g_object_new (GTK_TYPE_LABEL,
"label", text,
"css-name", "placeholder",
"xalign", 0.0f,
"ellipsize", PANGO_ELLIPSIZE_END,
NULL);
gtk_label_set_attributes (GTK_LABEL (priv->placeholder), priv->attrs);
gtk_widget_insert_after (priv->placeholder, GTK_WIDGET (self), NULL);
}
else
{
gtk_label_set_text (GTK_LABEL (priv->placeholder), text);
}
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_PLACEHOLDER_TEXT]);
}
/**
* gtk_text_get_placeholder_text:
* @self: a #GtkText
*
* Retrieves the text that will be displayed when @self is empty and unfocused
*
* Returns: (nullable) (transfer none): a pointer to the placeholder text as a string.
* This string points to internally allocated storage in the widget and must
* not be freed, modified or stored. If no placeholder text has been set,
* %NULL will be returned.
**/
const char *
gtk_text_get_placeholder_text (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), NULL);
if (!priv->placeholder)
return NULL;
return gtk_label_get_text (GTK_LABEL (priv->placeholder));
}
/**
* gtk_text_set_input_purpose:
* @self: a #GtkText
* @purpose: the purpose
*
* Sets the #GtkText:input-purpose property which
* can be used by on-screen keyboards and other input
* methods to adjust their behaviour.
*/
void
gtk_text_set_input_purpose (GtkText *self,
GtkInputPurpose purpose)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (gtk_text_get_input_purpose (self) != purpose)
{
g_object_set (G_OBJECT (priv->im_context),
"input-purpose", purpose,
NULL);
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INPUT_PURPOSE]);
}
}
/**
* gtk_text_get_input_purpose:
* @self: a #GtkText
*
* Gets the value of the #GtkText:input-purpose property.
*/
GtkInputPurpose
gtk_text_get_input_purpose (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkInputPurpose purpose;
g_return_val_if_fail (GTK_IS_TEXT (self), GTK_INPUT_PURPOSE_FREE_FORM);
g_object_get (G_OBJECT (priv->im_context),
"input-purpose", &purpose,
NULL);
return purpose;
}
/**
* gtk_text_set_input_hints:
* @self: a #GtkText
* @hints: the hints
*
* Sets the #GtkText:input-hints property, which
* allows input methods to fine-tune their behaviour.
*/
void
gtk_text_set_input_hints (GtkText *self,
GtkInputHints hints)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (gtk_text_get_input_hints (self) != hints)
{
g_object_set (G_OBJECT (priv->im_context),
"input-hints", hints,
NULL);
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_INPUT_HINTS]);
gtk_text_update_emoji_action (self);
}
}
/**
* gtk_text_get_input_hints:
* @self: a #GtkText
*
* Gets the value of the #GtkText:input-hints property.
*/
GtkInputHints
gtk_text_get_input_hints (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
GtkInputHints hints;
g_return_val_if_fail (GTK_IS_TEXT (self), GTK_INPUT_HINT_NONE);
g_object_get (G_OBJECT (priv->im_context),
"input-hints", &hints,
NULL);
return hints;
}
/**
* gtk_text_set_attributes:
* @self: a #GtkText
* @attrs: (nullable): a #PangoAttrList or %NULL to unset
*
* Sets a #PangoAttrList; the attributes in the list are applied to the
* text.
*/
void
gtk_text_set_attributes (GtkText *self,
PangoAttrList *attrs)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (attrs)
pango_attr_list_ref (attrs);
if (priv->attrs)
pango_attr_list_unref (priv->attrs);
priv->attrs = attrs;
if (priv->placeholder)
gtk_label_set_attributes (GTK_LABEL (priv->placeholder), attrs);
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_ATTRIBUTES]);
gtk_text_recompute (self);
gtk_widget_queue_resize (GTK_WIDGET (self));
}
/**
* gtk_text_get_attributes:
* @self: a #GtkText
*
* Gets the attribute list that was set on the self using
* gtk_text_set_attributes(), if any.
*
* Returns: (transfer none) (nullable): the attribute list, or %NULL
* if none was set.
*/
PangoAttrList *
gtk_text_get_attributes (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), NULL);
return priv->attrs;
}
/**
* gtk_text_set_tabs:
* @self: a #GtkText
* @tabs: (nullable): a #PangoTabArray
*
* Sets a #PangoTabArray; the tabstops in the array are applied to the self
* text.
*/
void
gtk_text_set_tabs (GtkText *self,
PangoTabArray *tabs)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->tabs)
pango_tab_array_free(priv->tabs);
if (tabs)
priv->tabs = pango_tab_array_copy (tabs);
else
priv->tabs = NULL;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_TABS]);
gtk_text_recompute (self);
gtk_widget_queue_resize (GTK_WIDGET (self));
}
/**
* gtk_text_get_tabs:
* @self: a #GtkText
*
* Gets the tabstops that were set on the self using gtk_text_set_tabs(), if
* any.
*
* Returns: (nullable) (transfer none): the tabstops, or %NULL if none was set.
*/
PangoTabArray *
gtk_text_get_tabs (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), NULL);
return priv->tabs;
}
static void
emoji_picked (GtkEmojiChooser *chooser,
const char *text,
GtkText *self)
{
int pos;
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
begin_change (self);
if (priv->selection_bound != priv->current_pos)
gtk_text_delete_selection (self);
pos = priv->current_pos;
gtk_editable_insert_text (GTK_EDITABLE (self), text, -1, &pos);
gtk_text_set_selection_bounds (self, pos, pos);
end_change (self);
}
static void
gtk_text_insert_emoji (GtkText *self)
{
GtkWidget *chooser;
if (gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_EMOJI_CHOOSER) != NULL)
return;
chooser = GTK_WIDGET (g_object_get_data (G_OBJECT (self), "gtk-emoji-chooser"));
if (!chooser)
{
chooser = gtk_emoji_chooser_new ();
g_object_set_data (G_OBJECT (self), "gtk-emoji-chooser", chooser);
gtk_widget_set_parent (chooser, GTK_WIDGET (self));
g_signal_connect (chooser, "emoji-picked", G_CALLBACK (emoji_picked), self);
g_signal_connect_swapped (chooser, "hide", G_CALLBACK (gtk_text_grab_focus_without_selecting), self);
}
gtk_popover_popup (GTK_POPOVER (chooser));
}
static void
set_text_cursor (GtkWidget *widget)
{
gtk_widget_set_cursor_from_name (widget, "text");
}
GtkEventController *
gtk_text_get_key_controller (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
return priv->key_controller;
}
/**
* gtk_text_set_extra_menu:
* @self: a #GtkText
* @model: (allow-none): a #GMenuModel
*
* Sets a menu model to add when constructing
* the context menu for @self.
*/
void
gtk_text_set_extra_menu (GtkText *self,
GMenuModel *model)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (g_set_object (&priv->extra_menu, model))
{
g_clear_pointer (&priv->popup_menu, gtk_widget_unparent);
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_EXTRA_MENU]);
}
}
/**
* gtk_text_get_extra_menu:
* @self: a #GtkText
*
* Gets the menu model set with gtk_text_set_extra_menu().
*
* Returns: (transfer none) (nullable): the menu model
*/
GMenuModel *
gtk_text_get_extra_menu (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), NULL);
return priv->extra_menu;
}
/**
* gtk_text_set_enable_emoji_completion:
* @self: a #GtkText
* @enable_emoji_completion: %TRUE to enable Emoji completion
*
* Sets whether Emoji completion is enabled. If it is,
* typing ':', followed by a recognized keyword, will pop
* up a window with suggested Emojis matching the keyword.
*/
void
gtk_text_set_enable_emoji_completion (GtkText *self,
gboolean enable_emoji_completion)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->enable_emoji_completion == enable_emoji_completion)
return;
priv->enable_emoji_completion = enable_emoji_completion;
if (priv->enable_emoji_completion)
priv->emoji_completion = gtk_emoji_completion_new (self);
else
g_clear_pointer (&priv->emoji_completion, gtk_widget_unparent);
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_ENABLE_EMOJI_COMPLETION]);
}
/**
* gtk_text_get_enable_emoji_completion:
* @self: a #GtkText
*
* Returns whether Emoji completion is enabled for this
* GtkText widget.
*
* Returns: %TRUE if Emoji completion is enabled
*/
gboolean
gtk_text_get_enable_emoji_completion (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return priv->enable_emoji_completion;
}
/**
* gtk_text_set_propagate_text_width:
* @self: a #GtkText
* @propagate_text_width: %TRUE to propagate the text width
*
* Sets whether the GtkText should grow and shrink with the content.
*/
void
gtk_text_set_propagate_text_width (GtkText *self,
gboolean propagate_text_width)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->propagate_text_width == propagate_text_width)
return;
priv->propagate_text_width = propagate_text_width;
gtk_widget_queue_resize (GTK_WIDGET (self));
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_PROPAGATE_TEXT_WIDTH]);
}
/**
* gtk_text_get_propagate_text_width:
* @self: a #GtkText
*
* Returns whether the #GtkText will grow and shrink
* with the content.
*
* Returns: %TRUE if @self will propagate the text width
*/
gboolean
gtk_text_get_propagate_text_width (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return priv->propagate_text_width;
}
/**
* gtk_text_set_truncate_multiline:
* @self: a #GtkText
* @truncate_multiline: %TRUE to truncate multi-line text
*
* Sets whether the GtkText should truncate multi-line text
* that is pasted into the widget.
*/
void
gtk_text_set_truncate_multiline (GtkText *self,
gboolean truncate_multiline)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_if_fail (GTK_IS_TEXT (self));
if (priv->truncate_multiline == truncate_multiline)
return;
priv->truncate_multiline = truncate_multiline;
g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_TRUNCATE_MULTILINE]);
}
/**
* gtk_text_get_truncate_multiline:
* @self: a #GtkText
*
* Returns whether the #GtkText will truncate multi-line text
* that is pasted into the widget
*
* Returns: %TRUE if @self will truncate multi-line text
*/
gboolean
gtk_text_get_truncate_multiline (GtkText *self)
{
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
g_return_val_if_fail (GTK_IS_TEXT (self), FALSE);
return priv->truncate_multiline;
}
static void
gtk_text_real_undo (GtkWidget *widget,
const char *action_name,
GVariant *parameters)
{
GtkText *text = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (text);
gtk_text_history_undo (priv->history);
}
static void
gtk_text_real_redo (GtkWidget *widget,
const char *action_name,
GVariant *parameters)
{
GtkText *text = GTK_TEXT (widget);
GtkTextPrivate *priv = gtk_text_get_instance_private (text);
gtk_text_history_redo (priv->history);
}
static void
gtk_text_history_change_state_cb (gpointer funcs_data,
gboolean is_modified,
gboolean can_undo,
gboolean can_redo)
{
/* Do nothing */
}
static void
gtk_text_history_insert_cb (gpointer funcs_data,
guint begin,
guint end,
const char *str,
guint len)
{
GtkText *text = funcs_data;
int location = begin;
gtk_editable_insert_text (GTK_EDITABLE (text), str, len, &location);
}
static void
gtk_text_history_delete_cb (gpointer funcs_data,
guint begin,
guint end,
const char *expected_text,
guint len)
{
GtkText *text = funcs_data;
gtk_editable_delete_text (GTK_EDITABLE (text), begin, end);
}
static void
gtk_text_history_select_cb (gpointer funcs_data,
int selection_insert,
int selection_bound)
{
GtkText *text = funcs_data;
gtk_editable_select_region (GTK_EDITABLE (text),
selection_insert,
selection_bound);
}