forked from AuroraMiddleware/gtk
c297d45b8a
Reviewing the existing settings, the only backend with some differences in the modifier intent settings is OS X, and we would rather have that implemented by interpreting the existing modifiers in the appropriate way. X11 Wayland Win32 OS X primary ctrl ctrl ctrl mod2 mnemonic alt alt alt alt context menu - - - ctrl extend sel shift shift shift shift modify sel ctrl ctrl ctrl mod2 no text alt|ctrl alt|ctrl alt|ctrl mod2|ctrl shift group varies - - alt GTK now uses the following modifiers: primary ctrl mnemonic alt extend sel shift modify sel ctrl no text alt|ctrl The context menu and shift group intents were not used in GTK at all. Update tests to no longer expect <Primary> to roundtrip through the accelerator parsing and formatting code.
6858 lines
210 KiB
C
6858 lines
210 KiB
C
/* -*- 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 "gtkcssnodeprivate.h"
|
||
#include "gtkdebug.h"
|
||
#include "gtkdragicon.h"
|
||
#include "gtkdragsource.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 "gtktreeselection.h"
|
||
#include "gtktreeview.h"
|
||
#include "gtktypebuiltins.h"
|
||
#include "gtkwidgetprivate.h"
|
||
#include "gtkwindow.h"
|
||
#include "gtknative.h"
|
||
#include "gtkactionmuxerprivate.h"
|
||
|
||
#include "a11y/gtktextaccessible.h"
|
||
|
||
#include <cairo-gobject.h>
|
||
#include <string.h>
|
||
|
||
#include "fallback-c89.c"
|
||
|
||
/**
|
||
* 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.
|
||
*/
|
||
|
||
#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_destroy (GtkWidget *widget);
|
||
static void gtk_text_realize (GtkWidget *widget);
|
||
static void gtk_text_unrealize (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_in (GtkWidget *widget);
|
||
static void gtk_text_focus_out (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 void gtk_text_root (GtkWidget *widget);
|
||
|
||
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_enable_emoji_completion (GtkText *self,
|
||
gboolean value);
|
||
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, ¤t_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->destroy = gtk_text_destroy;
|
||
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->root = gtk_text_root;
|
||
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 it’s 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 self 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][GtkBindingSignal]
|
||
* 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 modifer 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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][GtkBindingSignal]
|
||
* 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);
|
||
|
||
gtk_widget_class_install_action (widget_class, "text.undo", NULL, gtk_text_real_undo);
|
||
gtk_widget_class_install_action (widget_class, "text.redo", NULL, gtk_text_real_redo);
|
||
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_accessible_type (widget_class, GTK_TYPE_TEXT_ACCESSIBLE);
|
||
gtk_widget_class_set_css_name (widget_class, I_("text"));
|
||
|
||
gtk_widget_class_install_action (widget_class, "clipboard.cut", NULL,
|
||
gtk_text_activate_clipboard_cut);
|
||
gtk_widget_class_install_action (widget_class, "clipboard.copy", NULL,
|
||
gtk_text_activate_clipboard_copy);
|
||
gtk_widget_class_install_action (widget_class, "clipboard.paste", NULL,
|
||
gtk_text_activate_clipboard_paste);
|
||
gtk_widget_class_install_action (widget_class, "selection.delete", NULL,
|
||
gtk_text_activate_selection_delete);
|
||
gtk_widget_class_install_action (widget_class, "selection.select-all", NULL,
|
||
gtk_text_activate_selection_select_all);
|
||
gtk_widget_class_install_action (widget_class, "misc.insert-emoji", NULL,
|
||
gtk_text_activate_misc_insert_emoji);
|
||
gtk_widget_class_install_property_action (widget_class,
|
||
"misc.toggle-visibility",
|
||
"visibility");
|
||
}
|
||
|
||
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:
|
||
if (priv->truncate_multiline != g_value_get_boolean (value))
|
||
{
|
||
priv->truncate_multiline = g_value_get_boolean (value);
|
||
g_object_notify_by_pspec (object, pspec);
|
||
}
|
||
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:
|
||
set_enable_emoji_completion (self, g_value_get_boolean (value));
|
||
break;
|
||
|
||
case PROP_PROPAGATE_TEXT_WIDTH:
|
||
if (priv->propagate_text_width != g_value_get_boolean (value))
|
||
{
|
||
priv->propagate_text_width = g_value_get_boolean (value);
|
||
gtk_widget_queue_resize (GTK_WIDGET (self));
|
||
g_object_notify_by_pspec (object, pspec);
|
||
}
|
||
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_can_focus (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 ();
|
||
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 ();
|
||
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 ();
|
||
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 ();
|
||
g_signal_connect_swapped (controller, "enter",
|
||
G_CALLBACK (gtk_text_focus_in), self);
|
||
g_signal_connect_swapped (controller, "leave",
|
||
G_CALLBACK (gtk_text_focus_out), 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 ? GTK_STYLE_CLASS_LEFT : GTK_STYLE_CLASS_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_destroy (GtkWidget *widget)
|
||
{
|
||
GtkText *self = GTK_TEXT (widget);
|
||
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
|
||
|
||
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 (widget, priv->blink_tick);
|
||
priv->blink_tick = 0;
|
||
}
|
||
|
||
if (priv->magnifier)
|
||
_gtk_magnifier_set_inspected (GTK_MAGNIFIER (priv->magnifier), NULL);
|
||
|
||
GTK_WIDGET_CLASS (gtk_text_parent_class)->destroy (widget);
|
||
}
|
||
|
||
static void
|
||
gtk_text_dispose (GObject *object)
|
||
{
|
||
GtkText *self = GTK_TEXT (object);
|
||
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
|
||
GdkSeat *seat;
|
||
GdkDevice *keyboard;
|
||
GtkWidget *chooser;
|
||
|
||
priv->current_pos = 0;
|
||
|
||
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)));
|
||
keyboard = gdk_seat_get_keyboard (seat);
|
||
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_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_clear_pointer (&priv->magnifier_popover, gtk_widget_destroy);
|
||
g_free (priv->im_module);
|
||
|
||
g_clear_pointer (&priv->placeholder, gtk_widget_unparent);
|
||
|
||
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_container_add (GTK_CONTAINER (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_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_native_check_resize (GTK_NATIVE (chooser));
|
||
|
||
gtk_text_update_handles (self);
|
||
|
||
if (priv->emoji_completion)
|
||
gtk_native_check_resize (GTK_NATIVE (priv->emoji_completion));
|
||
|
||
if (priv->magnifier_popover)
|
||
gtk_native_check_resize (GTK_NATIVE (priv->magnifier_popover));
|
||
|
||
if (priv->popup_menu)
|
||
gtk_native_check_resize (GTK_NATIVE (priv->popup_menu));
|
||
|
||
if (priv->selection_bubble)
|
||
gtk_native_check_resize (GTK_NATIVE (priv->selection_bubble));
|
||
|
||
if (priv->text_handles[TEXT_HANDLE_CURSOR])
|
||
gtk_native_check_resize (GTK_NATIVE (priv->text_handles[TEXT_HANDLE_CURSOR]));
|
||
|
||
if (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND])
|
||
gtk_native_check_resize (GTK_NATIVE (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);
|
||
|
||
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);
|
||
|
||
gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), current,
|
||
GTK_EVENT_SEQUENCE_CLAIMED);
|
||
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);
|
||
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);
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
else if (button == GDK_BUTTON_PRIMARY)
|
||
{
|
||
gboolean have_selection;
|
||
gboolean is_touchscreen, extend_selection;
|
||
GdkDevice *source;
|
||
|
||
sel_start = priv->selection_bound;
|
||
sel_end = priv->current_pos;
|
||
have_selection = sel_start != sel_end;
|
||
|
||
source = gdk_event_get_source_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;
|
||
|
||
extend_selection = GDK_SHIFT_MASK;
|
||
|
||
/* 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_gesture_set_state (priv->drag_gesture,
|
||
GTK_EVENT_SEQUENCE_CLAIMED);
|
||
|
||
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
|
||
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 (widget,
|
||
priv->drag_start_x, priv->drag_start_y,
|
||
x, 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);
|
||
|
||
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);
|
||
|
||
paintable = gtk_text_util_create_drag_icon (widget, text, -1);
|
||
gtk_drag_icon_set_from_paintable (drag, paintable, ranges[0], 0);
|
||
g_clear_object (&paintable);
|
||
|
||
priv->drag = drag;
|
||
|
||
g_object_unref (drag);
|
||
|
||
g_free (ranges);
|
||
g_free (text);
|
||
|
||
priv->in_drag = FALSE;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
GdkInputSource input_source;
|
||
GdkDevice *source;
|
||
guint length;
|
||
int tmp_pos;
|
||
|
||
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_source_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_in (GtkWidget *widget)
|
||
{
|
||
GtkText *self = GTK_TEXT (widget);
|
||
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
|
||
GdkSeat *seat;
|
||
GdkDevice *keyboard;
|
||
|
||
gtk_widget_queue_draw (widget);
|
||
|
||
seat = gdk_display_get_default_seat (gtk_widget_get_display (widget));
|
||
keyboard = gdk_seat_get_keyboard (seat);
|
||
|
||
if (priv->editable)
|
||
{
|
||
gtk_text_schedule_im_reset (self);
|
||
gtk_im_context_focus_in (priv->im_context);
|
||
}
|
||
|
||
g_signal_connect (keyboard, "notify::direction",
|
||
G_CALLBACK (direction_changed), self);
|
||
|
||
gtk_text_reset_blink_time (self);
|
||
gtk_text_check_cursor_blink (self);
|
||
}
|
||
|
||
static void
|
||
gtk_text_focus_out (GtkWidget *widget)
|
||
{
|
||
GtkText *self = GTK_TEXT (widget);
|
||
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
|
||
GdkSeat *seat;
|
||
GdkDevice *keyboard;
|
||
|
||
gtk_text_selection_bubble_popup_unset (self);
|
||
|
||
priv->text_handles_enabled = FALSE;
|
||
gtk_text_update_handles (self);
|
||
|
||
gtk_widget_queue_draw (widget);
|
||
|
||
seat = gdk_display_get_default_seat (gtk_widget_get_display (widget));
|
||
keyboard = gdk_seat_get_keyboard (seat);
|
||
|
||
if (priv->editable)
|
||
{
|
||
gtk_text_schedule_im_reset (self);
|
||
gtk_im_context_focus_out (priv->im_context);
|
||
}
|
||
|
||
gtk_text_check_cursor_blink (self);
|
||
|
||
g_signal_handlers_disconnect_by_func (keyboard, direction_changed, 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;
|
||
|
||
if (!GTK_WIDGET_CLASS (gtk_text_parent_class)->grab_focus (GTK_WIDGET (self)))
|
||
return FALSE;
|
||
|
||
if (priv->editable && !priv->in_click)
|
||
{
|
||
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 the 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);
|
||
}
|
||
|
||
static void
|
||
gtk_text_root (GtkWidget *widget)
|
||
{
|
||
GTK_WIDGET_CLASS (gtk_text_parent_class)->root (widget);
|
||
|
||
gtk_text_recompute (GTK_TEXT (widget));
|
||
}
|
||
|
||
/* 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_text_delete_text (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->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 (change == NULL ||
|
||
gtk_css_style_change_affects (change, GTK_CSS_AFFECTS_TEXT |
|
||
GTK_CSS_AFFECTS_BACKGROUND |
|
||
GTK_CSS_AFFECTS_CONTENT))
|
||
gtk_widget_queue_draw (GTK_WIDGET (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,
|
||
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 = gdk_display_get_default_seat (gtk_widget_get_display (GTK_WIDGET (self)));
|
||
GdkDevice *keyboard = gdk_seat_get_keyboard (seat);
|
||
PangoDirection direction = gdk_device_get_direction (keyboard);
|
||
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;
|
||
|
||
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_text_insert_text (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_text_delete_text (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, begining 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_text_delete_text (self, start_pos, end_pos);
|
||
break;
|
||
|
||
case GTK_DELETE_DISPLAY_LINE_ENDS:
|
||
case GTK_DELETE_PARAGRAPH_ENDS:
|
||
if (count < 0)
|
||
gtk_text_delete_text (self, 0, priv->current_pos);
|
||
else
|
||
gtk_text_delete_text (self, priv->current_pos, -1);
|
||
|
||
break;
|
||
|
||
case GTK_DELETE_DISPLAY_LINES:
|
||
case GTK_DELETE_PARAGRAPHS:
|
||
gtk_text_delete_text (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_text_delete_text (self, prev_pos, priv->current_pos);
|
||
if (len > 1)
|
||
{
|
||
int pos = priv->current_pos;
|
||
|
||
gtk_text_insert_text (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_text_delete_text (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;
|
||
}
|
||
|
||
str = gtk_text_get_display_text (self, priv->selection_bound, priv->current_pos);
|
||
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);
|
||
}
|
||
}
|
||
|
||
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 *slave,
|
||
int offset,
|
||
int n_chars,
|
||
GtkText *self)
|
||
{
|
||
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
|
||
|
||
if (priv->editable)
|
||
gtk_text_delete_text (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_text_insert_text (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_text_check_cursor_blink (self);
|
||
|
||
gtk_text_adjust_scroll (self);
|
||
|
||
update_im_cursor_location (self);
|
||
|
||
gtk_text_update_handles (self);
|
||
|
||
gtk_widget_queue_draw (GTK_WIDGET (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);
|
||
GtkStyleContext *context;
|
||
PangoLayout *layout;
|
||
PangoAttrList *tmp_attrs;
|
||
char *preedit_string = NULL;
|
||
int preedit_length = 0;
|
||
PangoAttrList *preedit_attrs = NULL;
|
||
char *display_text;
|
||
guint n_bytes;
|
||
|
||
context = gtk_widget_get_style_context (widget);
|
||
|
||
layout = gtk_widget_create_pango_layout (widget, NULL);
|
||
pango_layout_set_single_paragraph_mode (layout, TRUE);
|
||
|
||
tmp_attrs = _gtk_style_context_get_pango_attributes (context);
|
||
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 = gtk_widget_get_display (widget);
|
||
GdkSeat *seat = gdk_display_get_default_seat (display);
|
||
GdkDevice *keyboard = gdk_seat_get_keyboard (seat);
|
||
|
||
if (gdk_device_get_direction (keyboard) == 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);
|
||
|
||
if (preedit_attrs)
|
||
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 = 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);
|
||
}
|
||
}
|
||
|
||
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 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 = gtk_widget_get_display (GTK_WIDGET (self));
|
||
GdkSeat *seat = gdk_display_get_default_seat (display);
|
||
GdkDevice *keyboard = gdk_seat_get_keyboard (seat);
|
||
PangoDirection 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_text_delete_text (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_text_insert_text (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), GTK_STYLE_CLASS_READ_ONLY);
|
||
}
|
||
else
|
||
{
|
||
gtk_widget_add_css_class (GTK_WIDGET (self), GTK_STYLE_CLASS_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);
|
||
|
||
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_text_delete_text (self, 0, -1);
|
||
tmp_pos = 0;
|
||
gtk_text_insert_text (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.
|
||
* See gtk_text_set_invisible_char().
|
||
*
|
||
* Returns: the current invisible char, or 0, if the self 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 window’s 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)
|
||
{
|
||
gtk_widget_action_set_enabled (GTK_WIDGET (self), "misc.insert-emoji",
|
||
(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;
|
||
|
||
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 (!g_action_group_get_action_enabled (G_ACTION_GROUP (muxer), action_name))
|
||
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_container_add (GTK_CONTAINER (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_container_add (GTK_CONTAINER (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, GTK_STYLE_CLASS_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_container_add (GTK_CONTAINER (priv->selection_bubble), box);
|
||
gtk_container_add (GTK_CONTAINER (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_text_insert_text (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_text_insert_text (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_text_insert_text (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);
|
||
}
|
||
|
||
gtk_popover_popup (GTK_POPOVER (chooser));
|
||
}
|
||
|
||
static void
|
||
set_enable_emoji_completion (GtkText *self,
|
||
gboolean value)
|
||
{
|
||
GtkTextPrivate *priv = gtk_text_get_instance_private (self);
|
||
|
||
if (priv->enable_emoji_completion == value)
|
||
return;
|
||
|
||
priv->enable_emoji_completion = value;
|
||
|
||
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]);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|