| /* -*- 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 "gtktypebuiltins.h" |
| #include "gtkwidgetprivate.h" |
| #include "gtkwindow.h" |
| #include "gtknative.h" |
| #include "gtkactionmuxerprivate.h" |
| |
| #include <cairo-gobject.h> |
| #include <string.h> |
| |
| /** |
| * SECTION:gtktext |
| * @Short_description: A simple single-line text entry field |
| * @Title: GtkText |
| * @See_also: #GtkEntry, #GtkTextView |
| * |
| * The #GtkText widget is a single line text entry widget. |
| * |
| * A fairly large set of key bindings are supported by default. If the |
| * entered text is longer than the allocation of the widget, the widget |
| * will scroll so that the cursor position is visible. |
| * |
| * When using an entry for passwords and other sensitive information, |
| * it can be put into “password mode” using gtk_text_set_visibility(). |
| * In this mode, entered text is displayed using a “invisible” character. |
| * By default, GTK picks the best invisible character that is available |
| * in the current font, but it can be changed with gtk_text_set_invisible_char(). |
| * |
| * If you are looking to add icons or progress display in an entry, look |
| * at #GtkEntry. There other alternatives for more specialized use cases, |
| * such as #GtkSearchEntry. |
| * |
| * If you need multi-line editable text, look at #GtkTextView. |
| * |
| * # CSS nodes |
| * |
| * |[<!-- language="plain" --> |
| * text[.read-only] |
| * ├── placeholder |
| * ├── undershoot.left |
| * ├── undershoot.right |
| * ├── [selection] |
| * ├── [block-cursor] |
| * ╰── [window.popup] |
| * ]| |
| * |
| * GtkText has a main node with the name text. Depending on the properties |
| * of the widget, the .read-only style class may appear. |
| * |
| * When the entry has a selection, it adds a subnode with the name selection. |
| * |
| * When the entry is in overwrite mode, it adds a subnode with the name block-cursor |
| * that determines how the block cursor is drawn. |
| * |
| * The CSS node for a context menu is added as a subnode below text as well. |
| * |
| * The undershoot nodes are used to draw the underflow indication when content |
| * is scrolled out of view. These nodes get the .left and .right style classes |
| * added depending on where the indication is drawn. |
| * |
| * When touch is used and touch selection handles are shown, they are using |
| * CSS nodes with name cursor-handle. They get the .top or .bottom style class |
| * depending on where they are shown in relation to the selection. If there is |
| * just a single handle for the text cursor, it gets the style class .insertion-cursor. |
| */ |
| |
| #define NAT_ENTRY_WIDTH 150 |
| |
| #define UNDERSHOOT_SIZE 20 |
| |
| #define DEFAULT_MAX_UNDO 200 |
| |
| static GQuark quark_password_hint = 0; |
| |
| enum |
| { |
| TEXT_HANDLE_CURSOR, |
| TEXT_HANDLE_SELECTION_BOUND, |
| TEXT_HANDLE_N_HANDLES |
| }; |
| |
| typedef struct _GtkTextPasswordHint GtkTextPasswordHint; |
| |
| typedef struct _GtkTextPrivate GtkTextPrivate; |
| struct _GtkTextPrivate |
| { |
| GtkEntryBuffer *buffer; |
| GtkIMContext *im_context; |
| |
| int text_baseline; |
| |
| PangoLayout *cached_layout; |
| PangoAttrList *attrs; |
| PangoTabArray *tabs; |
| |
| GdkContentProvider *selection_content; |
| |
| char *im_module; |
| |
| GtkWidget *emoji_completion; |
| GtkTextHandle *text_handles[TEXT_HANDLE_N_HANDLES]; |
| GtkWidget *selection_bubble; |
| guint selection_bubble_timeout_id; |
| |
| GtkWidget *magnifier_popover; |
| GtkWidget *magnifier; |
| |
| GtkWidget *placeholder; |
| |
| GtkGesture *drag_gesture; |
| GtkEventController *key_controller; |
| |
| GtkCssNode *selection_node; |
| GtkCssNode *block_cursor_node; |
| GtkCssNode *undershoot_node[2]; |
| |
| GtkWidget *popup_menu; |
| GMenuModel *extra_menu; |
| |
| GtkTextHistory *history; |
| |
| GdkDrag *drag; |
| |
| float xalign; |
| |
| int ascent; /* font ascent in pango units */ |
| int current_pos; |
| int descent; /* font descent in pango units */ |
| int dnd_position; /* In chars, -1 == no DND cursor */ |
| int drag_start_x; |
| int drag_start_y; |
| int insert_pos; |
| int selection_bound; |
| int scroll_offset; |
| int width_chars; |
| int max_width_chars; |
| |
| gunichar invisible_char; |
| |
| guint64 blink_start_time; |
| guint blink_tick; |
| float cursor_alpha; |
| |
| guint16 preedit_length; /* length of preedit string, in bytes */ |
| guint16 preedit_cursor; /* offset of cursor within preedit string, in chars */ |
| |
| gint64 handle_place_time; |
| |
| guint editable : 1; |
| guint enable_emoji_completion : 1; |
| guint in_drag : 1; |
| guint overwrite_mode : 1; |
| guint visible : 1; |
| |
| guint activates_default : 1; |
| guint cache_includes_preedit : 1; |
| guint change_count : 8; |
| guint in_click : 1; /* Flag so we don't select all when clicking in entry to focus in */ |
| guint invisible_char_set : 1; |
| guint mouse_cursor_obscured : 1; |
| guint need_im_reset : 1; |
| guint real_changed : 1; |
| guint resolved_dir : 4; /* PangoDirection */ |
| guint select_words : 1; |
| guint select_lines : 1; |
| guint truncate_multiline : 1; |
| guint cursor_handle_dragged : 1; |
| guint selection_handle_dragged : 1; |
| guint populate_all : 1; |
| guint propagate_text_width : 1; |
| guint text_handles_enabled : 1; |
| }; |
| |
| struct _GtkTextPasswordHint |
| { |
| int position; /* Position (in text) of the last password hint */ |
| guint source_id; /* Timeout source id */ |
| }; |
| |
| enum { |
| ACTIVATE, |
| MOVE_CURSOR, |
| INSERT_AT_CURSOR, |
| DELETE_FROM_CURSOR, |
| BACKSPACE, |
| CUT_CLIPBOARD, |
| COPY_CLIPBOARD, |
| PASTE_CLIPBOARD, |
| TOGGLE_OVERWRITE, |
| PREEDIT_CHANGED, |
| INSERT_EMOJI, |
| LAST_SIGNAL |
| }; |
| |
| enum { |
| PROP_0, |
| PROP_BUFFER, |
| PROP_MAX_LENGTH, |
| PROP_VISIBILITY, |
| PROP_INVISIBLE_CHAR, |
| PROP_INVISIBLE_CHAR_SET, |
| PROP_ACTIVATES_DEFAULT, |
| PROP_SCROLL_OFFSET, |
| PROP_TRUNCATE_MULTILINE, |
| PROP_OVERWRITE_MODE, |
| PROP_IM_MODULE, |
| PROP_PLACEHOLDER_TEXT, |
| PROP_INPUT_PURPOSE, |
| PROP_INPUT_HINTS, |
| PROP_ATTRIBUTES, |
| PROP_TABS, |
| PROP_ENABLE_EMOJI_COMPLETION, |
| PROP_PROPAGATE_TEXT_WIDTH, |
| PROP_EXTRA_MENU, |
| NUM_PROPERTIES |
| }; |
| |
| static GParamSpec *text_props[NUM_PROPERTIES] = { NULL, }; |
| |
| static guint signals[LAST_SIGNAL] = { 0 }; |
| |
| typedef enum { |
| CURSOR_STANDARD, |
| CURSOR_DND |
| } CursorType; |
| |
| typedef enum |
| { |
| DISPLAY_NORMAL, /* The text is being shown */ |
| DISPLAY_INVISIBLE, /* In invisible mode, text replaced by (eg) bullets */ |
| DISPLAY_BLANK /* In invisible mode, nothing shown at all */ |
| } DisplayMode; |
| |
| /* GObject methods |
| */ |
| static void gtk_text_set_property (GObject *object, |
| guint prop_id, |
| const GValue *value, |
| GParamSpec *pspec); |
| static void gtk_text_get_property (GObject *object, |
| guint prop_id, |
| GValue *value, |
| GParamSpec *pspec); |
| static void gtk_text_finalize (GObject *object); |
| static void gtk_text_dispose (GObject *object); |
| |
| /* GtkWidget methods |
| */ |
| static void gtk_text_realize (GtkWidget *widget); |
| static void gtk_text_unrealize (GtkWidget *widget); |
| static void gtk_text_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->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 modifier does not. |
| * There are too many key combinations to list them all here. |
| * - Arrow keys move by individual characters/lines |
| * - Ctrl-arrow key combinations move by words/paragraphs |
| * - Home/End keys move to the ends of the buffer |
| */ |
| signals[MOVE_CURSOR] = |
| g_signal_new (I_("move-cursor"), |
| G_OBJECT_CLASS_TYPE (gobject_class), |
| G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, |
| G_STRUCT_OFFSET (GtkTextClass, move_cursor), |
| NULL, NULL, |
| _gtk_marshal_VOID__ENUM_INT_BOOLEAN, |
| G_TYPE_NONE, 3, |
| GTK_TYPE_MOVEMENT_STEP, |
| G_TYPE_INT, |
| G_TYPE_BOOLEAN); |
| |
| /** |
| * GtkText::insert-at-cursor: |
| * @self: the object which received the signal |
| * @string: the string to insert |
| * |
| * The ::insert-at-cursor signal is a |
| * [keybinding signal][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); |
| |
| /* |
| * Actions |
| */ |
| |
| /** |
| * GtkText|clipboard.cut: |
| * |
| * Copies the contents to the clipboard and deletes it from the widget. |
| */ |
| gtk_widget_class_install_action (widget_class, "clipboard.cut", NULL, |
| gtk_text_activate_clipboard_cut); |
| |
| /** |
| * GtkText|clipboard.copy: |
| * |
| * Copies the contents to the clipboard. |
| */ |
| gtk_widget_class_install_action (widget_class, "clipboard.copy", NULL, |
| gtk_text_activate_clipboard_copy); |
| |
| /** |
| * GtkText|clipboard.paste: |
| * |
| * Inserts the contents of the clipboard into the widget. |
| */ |
| gtk_widget_class_install_action (widget_class, "clipboard.paste", NULL, |
| gtk_text_activate_clipboard_paste); |
| |
| /** |
| * GtkText|selection.delete: |
| * |
| * Deletes the current selection. |
| */ |
| gtk_widget_class_install_action (widget_class, "selection.delete", NULL, |
| gtk_text_activate_selection_delete); |
| |
| /** |
| * GtkText|selection.select-all: |
| * |
| * Selects all of the widgets content. |
| */ |
| gtk_widget_class_install_action (widget_class, "selection.select-all", NULL, |
| gtk_text_activate_selection_select_all); |
| |
| /** |
| * GtkText|misc.insert-emoji: |
| * |
| * Opens the Emoji chooser. |
| */ |
| gtk_widget_class_install_action (widget_class, "misc.insert-emoji", NULL, |
| gtk_text_activate_misc_insert_emoji); |
| |
| /** |
| * GtkText|misc.toggle-visibility: |
| * |
| * Toggles the #GtkText:visibility property. |
| */ |
| gtk_widget_class_install_property_action (widget_class, |
| "misc.toggle-visibility", |
| "visibility"); |
| |
| /** |
| * GtkText|text.undo: |
| * |
| * Undoes the last change to the contents. |
| */ |
| gtk_widget_class_install_action (widget_class, "text.undo", NULL, gtk_text_real_undo); |
| |
| /** |
| * GtkText|text.redo: |
| * |
| * Redoes the last change to the contents. |
| */ |
| gtk_widget_class_install_action (widget_class, "text.redo", NULL, gtk_text_real_redo); |
| |
| /** |
| * GtkText|menu.popup: |
| * |
| * Opens the context menu. |
| */ |
| gtk_widget_class_install_action (widget_class, "menu.popup", NULL, gtk_text_popup_menu); |
| |
| /* |
| * Key bindings |
| */ |
| |
| gtk_widget_class_add_binding_action (widget_class, |
| GDK_KEY_F10, GDK_SHIFT_MASK, |
| "menu.popup", |
| NULL); |
| gtk_widget_class_add_binding_action (widget_class, |
| GDK_KEY_Menu, 0, |
| "menu.popup", |
| NULL); |
| |
| /* Moving the insertion point */ |
| add_move_binding (widget_class, GDK_KEY_Right, 0, |
| GTK_MOVEMENT_VISUAL_POSITIONS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_Left, 0, |
| GTK_MOVEMENT_VISUAL_POSITIONS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_Right, 0, |
| GTK_MOVEMENT_VISUAL_POSITIONS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_Left, 0, |
| GTK_MOVEMENT_VISUAL_POSITIONS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_Right, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_WORDS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_Left, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_WORDS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_Right, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_WORDS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_Left, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_WORDS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_Home, 0, |
| GTK_MOVEMENT_DISPLAY_LINE_ENDS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_End, 0, |
| GTK_MOVEMENT_DISPLAY_LINE_ENDS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_Home, 0, |
| GTK_MOVEMENT_DISPLAY_LINE_ENDS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_End, 0, |
| GTK_MOVEMENT_DISPLAY_LINE_ENDS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_Home, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_BUFFER_ENDS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_End, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_BUFFER_ENDS, 1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_Home, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_BUFFER_ENDS, -1); |
| |
| add_move_binding (widget_class, GDK_KEY_KP_End, GDK_CONTROL_MASK, |
| GTK_MOVEMENT_BUFFER_ENDS, 1); |
| |
| /* Select all |
| */ |
| gtk_widget_class_add_binding (widget_class, |
| GDK_KEY_a, GDK_CONTROL_MASK, |
| (GtkShortcutFunc) gtk_text_select_all, |
| NULL); |
| |
| gtk_widget_class_add_binding (widget_class, |
| GDK_KEY_slash, GDK_CONTROL_MASK, |
| (GtkShortcutFunc) gtk_text_select_all, |
| NULL); |
| /* Unselect all |
| */ |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_backslash, GDK_CONTROL_MASK, |
| "move-cursor", |
| "(iib)", GTK_MOVEMENT_VISUAL_POSITIONS, 0, FALSE); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_a, GDK_SHIFT_MASK | GDK_CONTROL_MASK, |
| "move-cursor", |
| "(iib)", GTK_MOVEMENT_VISUAL_POSITIONS, 0, FALSE); |
| |
| /* Activate |
| */ |
| gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_Return, 0, |
| "activate", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_ISO_Enter, 0, |
| "activate", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, GDK_KEY_KP_Enter, 0, |
| "activate", |
| NULL); |
| |
| /* Deleting text */ |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_Delete, 0, |
| "delete-from-cursor", |
| "(ii)", GTK_DELETE_CHARS, 1); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_KP_Delete, 0, |
| "delete-from-cursor", |
| "(ii)", GTK_DELETE_CHARS, 1); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_BackSpace, 0, |
| "backspace", |
| NULL); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_u, GDK_CONTROL_MASK, |
| "delete-from-cursor", |
| "(ii)", GTK_DELETE_PARAGRAPH_ENDS, -1); |
| |
| /* Make this do the same as Backspace, to help with mis-typing */ |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_BackSpace, GDK_SHIFT_MASK, |
| "backspace", |
| NULL); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_Delete, GDK_CONTROL_MASK, |
| "delete-from-cursor", |
| "(ii)", GTK_DELETE_WORD_ENDS, 1); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_KP_Delete, GDK_CONTROL_MASK, |
| "delete-from-cursor", |
| "(ii)", GTK_DELETE_WORD_ENDS, 1); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_BackSpace, GDK_CONTROL_MASK, |
| "delete-from-cursor", |
| "(ii)", GTK_DELETE_WORD_ENDS, -1); |
| |
| /* Cut/copy/paste */ |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_x, GDK_CONTROL_MASK, |
| "cut-clipboard", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_c, GDK_CONTROL_MASK, |
| "copy-clipboard", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_v, GDK_CONTROL_MASK, |
| "paste-clipboard", |
| NULL); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_Delete, GDK_SHIFT_MASK, |
| "cut-clipboard", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_Insert, GDK_CONTROL_MASK, |
| "copy-clipboard", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_Insert, GDK_SHIFT_MASK, |
| "paste-clipboard", |
| NULL); |
| |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_KP_Delete, GDK_SHIFT_MASK, |
| "cut-clipboard", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_KP_Insert, GDK_CONTROL_MASK, |
| "copy-clipboard", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_KP_Insert, GDK_SHIFT_MASK, |
| "paste-clipboard", |
| NULL); |
| |
| /* Overwrite */ |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_Insert, 0, |
| "toggle-overwrite", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_KP_Insert, 0, |
| "toggle-overwrite", |
| NULL); |
| |
| /* Emoji */ |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_period, GDK_CONTROL_MASK, |
| "insert-emoji", |
| NULL); |
| gtk_widget_class_add_binding_signal (widget_class, |
| GDK_KEY_semicolon, GDK_CONTROL_MASK, |
| "insert-emoji", |
| NULL); |
| |
| /* Undo/Redo */ |
| gtk_widget_class_add_binding_action (widget_class, |
| GDK_KEY_z, GDK_CONTROL_MASK, |
| "text.undo", NULL); |
| gtk_widget_class_add_binding_action (widget_class, |
| GDK_KEY_y, GDK_CONTROL_MASK, |
| "text.redo", NULL); |
| gtk_widget_class_add_binding_action (widget_class, |
| GDK_KEY_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK, |
| "text.redo", NULL); |
| |
| gtk_widget_class_set_css_name (widget_class, I_("text")); |
| } |
| |
| 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_focusable (GTK_WIDGET (self), TRUE); |
| gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN); |
| |
| priv->editable = TRUE; |
| priv->visible = TRUE; |
| priv->dnd_position = -1; |
| priv->width_chars = -1; |
| priv->max_width_chars = -1; |
| priv->truncate_multiline = FALSE; |
| priv->xalign = 0.0; |
| priv->insert_pos = -1; |
| priv->cursor_alpha = 1.0; |
| priv->invisible_char = 0; |
| priv->history = gtk_text_history_new (&history_funcs, self); |
| |
| gtk_text_history_set_max_undo_levels (priv->history, DEFAULT_MAX_UNDO); |
| |
| priv->selection_content = g_object_new (GTK_TYPE_TEXT_CONTENT, NULL); |
| GTK_TEXT_CONTENT (priv->selection_content)->self = self; |
| |
| target = gtk_drop_target_new (G_TYPE_STRING, GDK_ACTION_COPY | GDK_ACTION_MOVE); |
| g_signal_connect (target, "accept", G_CALLBACK (gtk_text_drag_accept), self); |
| g_signal_connect (target, "enter", G_CALLBACK (gtk_text_drag_motion), self); |
| g_signal_connect (target, "motion", G_CALLBACK (gtk_text_drag_motion), self); |
| g_signal_connect (target, "leave", G_CALLBACK (gtk_text_drag_leave), self); |
| g_signal_connect (target, "drop", G_CALLBACK (gtk_text_drag_drop), self); |
| gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (target)); |
| |
| /* This object is completely private. No external entity can gain a reference |
| * to it; so we create it here and destroy it in finalize(). |
| */ |
| priv->im_context = gtk_im_multicontext_new (); |
| |
| g_signal_connect (priv->im_context, "commit", |
| G_CALLBACK (gtk_text_commit_cb), self); |
| g_signal_connect (priv->im_context, "preedit-changed", |
| G_CALLBACK (gtk_text_preedit_changed_cb), self); |
| g_signal_connect (priv->im_context, "retrieve-surrounding", |
| G_CALLBACK (gtk_text_retrieve_surrounding_cb), self); |
| g_signal_connect (priv->im_context, "delete-surrounding", |
| G_CALLBACK (gtk_text_delete_surrounding_cb), self); |
| |
| priv->drag_gesture = gtk_gesture_drag_new (); |
| g_signal_connect (priv->drag_gesture, "drag-update", |
| G_CALLBACK (gtk_text_drag_gesture_update), self); |
| g_signal_connect (priv->drag_gesture, "drag-end", |
| G_CALLBACK (gtk_text_drag_gesture_end), self); |
| gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (priv->drag_gesture), 0); |
| gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (priv->drag_gesture), TRUE); |
| gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (priv->drag_gesture)); |
| |
| gesture = gtk_gesture_click_new (); |
| gtk_event_controller_set_name (GTK_EVENT_CONTROLLER (gesture), "gtk-text-click-gesture"); |
| g_signal_connect (gesture, "pressed", |
| G_CALLBACK (gtk_text_click_gesture_pressed), self); |
| gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), 0); |
| gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (gesture), TRUE); |
| gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture)); |
| |
| controller = gtk_event_controller_motion_new (); |
| gtk_event_controller_set_name (controller, "gtk-text-motion-controller"); |
| g_signal_connect (controller, "motion", |
| G_CALLBACK (gtk_text_motion_controller_motion), self); |
| gtk_widget_add_controller (GTK_WIDGET (self), controller); |
| |
| priv->key_controller = gtk_event_controller_key_new (); |
| gtk_event_controller_set_propagation_phase (priv->key_controller, GTK_PHASE_TARGET); |
| gtk_event_controller_set_name (priv->key_controller, "gtk-text-key-controller"); |
| g_signal_connect (priv->key_controller, "key-pressed", |
| G_CALLBACK (gtk_text_key_controller_key_pressed), self); |
| g_signal_connect_swapped (priv->key_controller, "im-update", |
| G_CALLBACK (gtk_text_schedule_im_reset), self); |
| gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY (priv->key_controller), |
| priv->im_context); |
| gtk_widget_add_controller (GTK_WIDGET (self), priv->key_controller); |
| |
| controller = gtk_event_controller_focus_new (); |
| gtk_event_controller_set_name (controller, "gtk-text-focus-controller"); |
| g_signal_connect_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_dispose (GObject *object) |
| { |
| GtkText *self = GTK_TEXT (object); |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GdkSeat *seat; |
| GdkDevice *keyboard = NULL; |
| GtkWidget *chooser; |
| |
| priv->current_pos = priv->selection_bound = 0; |
| gtk_text_reset_im_context (self); |
| gtk_text_reset_layout (self); |
| |
| if (priv->blink_tick) |
| { |
| gtk_widget_remove_tick_callback (GTK_WIDGET (object), priv->blink_tick); |
| priv->blink_tick = 0; |
| } |
| |
| if (priv->magnifier) |
| _gtk_magnifier_set_inspected (GTK_MAGNIFIER (priv->magnifier), NULL); |
| |
| if (priv->buffer) |
| { |
| buffer_disconnect_signals (self); |
| g_object_unref (priv->buffer); |
| priv->buffer = NULL; |
| } |
| |
| g_clear_pointer (&priv->emoji_completion, gtk_widget_unparent); |
| chooser = g_object_get_data (object, "gtk-emoji-chooser"); |
| if (chooser) |
| gtk_widget_unparent (chooser); |
| |
| seat = gdk_display_get_default_seat (gtk_widget_get_display (GTK_WIDGET (object))); |
| if (seat) |
| keyboard = gdk_seat_get_keyboard (seat); |
| if (keyboard) |
| g_signal_handlers_disconnect_by_func (keyboard, direction_changed, self); |
| |
| g_clear_pointer (&priv->selection_bubble, gtk_widget_unparent); |
| g_clear_pointer (&priv->popup_menu, gtk_widget_unparent); |
| g_clear_pointer ((GtkWidget **) &priv->text_handles[TEXT_HANDLE_CURSOR], gtk_widget_unparent); |
| g_clear_pointer ((GtkWidget **) &priv->text_handles[TEXT_HANDLE_SELECTION_BOUND], gtk_widget_unparent); |
| g_clear_object (&priv->extra_menu); |
| |
| g_clear_pointer (&priv->magnifier_popover, gtk_widget_unparent); |
| g_clear_pointer (&priv->placeholder, gtk_widget_unparent); |
| |
| G_OBJECT_CLASS (gtk_text_parent_class)->dispose (object); |
| } |
| |
| static void |
| gtk_text_finalize (GObject *object) |
| { |
| GtkText *self = GTK_TEXT (object); |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| g_clear_object (&priv->selection_content); |
| |
| g_clear_object (&priv->history); |
| g_clear_object (&priv->cached_layout); |
| g_clear_object (&priv->im_context); |
| g_free (priv->im_module); |
| |
| if (priv->tabs) |
| pango_tab_array_free (priv->tabs); |
| |
| if (priv->attrs) |
| pango_attr_list_unref (priv->attrs); |
| |
| |
| G_OBJECT_CLASS (gtk_text_parent_class)->finalize (object); |
| } |
| |
| static void |
| gtk_text_ensure_magnifier (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->magnifier_popover) |
| return; |
| |
| priv->magnifier = _gtk_magnifier_new (GTK_WIDGET (self)); |
| gtk_widget_set_size_request (priv->magnifier, 100, 60); |
| _gtk_magnifier_set_magnification (GTK_MAGNIFIER (priv->magnifier), 2.0); |
| priv->magnifier_popover = gtk_popover_new (); |
| gtk_popover_set_position (GTK_POPOVER (priv->magnifier_popover), GTK_POS_TOP); |
| gtk_widget_set_parent (priv->magnifier_popover, GTK_WIDGET (self)); |
| gtk_widget_add_css_class (priv->magnifier_popover, "magnifier"); |
| gtk_popover_set_autohide (GTK_POPOVER (priv->magnifier_popover), FALSE); |
| gtk_popover_set_child (GTK_POPOVER (priv->magnifier_popover), priv->magnifier); |
| gtk_widget_show (priv->magnifier); |
| } |
| |
| static void |
| begin_change (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| priv->change_count++; |
| |
| g_object_freeze_notify (G_OBJECT (self)); |
| } |
| |
| static void |
| end_change (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| g_return_if_fail (priv->change_count > 0); |
| |
| g_object_thaw_notify (G_OBJECT (self)); |
| |
| priv->change_count--; |
| |
| if (priv->change_count == 0) |
| { |
| if (priv->real_changed) |
| { |
| g_signal_emit_by_name (self, "changed"); |
| priv->real_changed = FALSE; |
| } |
| } |
| } |
| |
| static void |
| emit_changed (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->change_count == 0) |
| g_signal_emit_by_name (self, "changed"); |
| else |
| priv->real_changed = TRUE; |
| } |
| |
| static DisplayMode |
| gtk_text_get_display_mode (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->visible) |
| return DISPLAY_NORMAL; |
| |
| if (priv->invisible_char == 0 && priv->invisible_char_set) |
| return DISPLAY_BLANK; |
| |
| return DISPLAY_INVISIBLE; |
| } |
| |
| char * |
| gtk_text_get_display_text (GtkText *self, |
| int start_pos, |
| int end_pos) |
| { |
| GtkTextPasswordHint *password_hint; |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| gunichar invisible_char; |
| const char *start; |
| const char *end; |
| const char *text; |
| char char_str[7]; |
| int char_len; |
| GString *str; |
| guint length; |
| int i; |
| |
| text = gtk_entry_buffer_get_text (get_buffer (self)); |
| length = gtk_entry_buffer_get_length (get_buffer (self)); |
| |
| if (end_pos < 0 || end_pos > length) |
| end_pos = length; |
| if (start_pos > length) |
| start_pos = length; |
| |
| if (end_pos <= start_pos) |
| return g_strdup (""); |
| else if (priv->visible) |
| { |
| start = g_utf8_offset_to_pointer (text, start_pos); |
| end = g_utf8_offset_to_pointer (start, end_pos - start_pos); |
| return g_strndup (start, end - start); |
| } |
| else |
| { |
| str = g_string_sized_new (length * 2); |
| |
| /* Figure out what our invisible char is and encode it */ |
| if (!priv->invisible_char) |
| invisible_char = priv->invisible_char_set ? ' ' : '*'; |
| else |
| invisible_char = priv->invisible_char; |
| char_len = g_unichar_to_utf8 (invisible_char, char_str); |
| |
| /* |
| * Add hidden characters for each character in the text |
| * buffer. If there is a password hint, then keep that |
| * character visible. |
| */ |
| |
| password_hint = g_object_get_qdata (G_OBJECT (self), quark_password_hint); |
| for (i = start_pos; i < end_pos; ++i) |
| { |
| if (password_hint && i == password_hint->position) |
| { |
| start = g_utf8_offset_to_pointer (text, i); |
| g_string_append_len (str, start, g_utf8_next_char (start) - start); |
| } |
| else |
| { |
| g_string_append_len (str, char_str, char_len); |
| } |
| } |
| |
| return g_string_free (str, FALSE); |
| } |
| } |
| |
| static void |
| gtk_text_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); |
| gtk_text_update_emoji_action (self); |
| |
| if (!priv->popup_menu) |
| { |
| GMenuModel *model; |
| |
| model = gtk_text_get_menu_model (self); |
| priv->popup_menu = gtk_popover_menu_new_from_model (model); |
| gtk_widget_set_parent (priv->popup_menu, GTK_WIDGET (self)); |
| gtk_popover_set_position (GTK_POPOVER (priv->popup_menu), GTK_POS_BOTTOM); |
| |
| gtk_popover_set_has_arrow (GTK_POPOVER (priv->popup_menu), FALSE); |
| gtk_widget_set_halign (priv->popup_menu, GTK_ALIGN_START); |
| |
| g_object_unref (model); |
| } |
| |
| if (x != -1 && y != -1) |
| { |
| GdkRectangle rect = { x, y, 1, 1 }; |
| gtk_popover_set_pointing_to (GTK_POPOVER (priv->popup_menu), &rect); |
| } |
| else |
| gtk_popover_set_pointing_to (GTK_POPOVER (priv->popup_menu), NULL); |
| |
| gtk_popover_popup (GTK_POPOVER (priv->popup_menu)); |
| } |
| |
| static void |
| gtk_text_click_gesture_pressed (GtkGestureClick *gesture, |
| int n_press, |
| double widget_x, |
| double widget_y, |
| GtkText *self) |
| { |
| GtkWidget *widget = GTK_WIDGET (self); |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GdkEventSequence *current; |
| GdkEvent *event; |
| int x, y, sel_start, sel_end; |
| guint button; |
| int tmp_pos; |
| |
| button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture)); |
| current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); |
| event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), current); |
| |
| gesture_get_current_point_in_layout (GTK_GESTURE_SINGLE (gesture), self, &x, &y); |
| gtk_text_reset_blink_time (self); |
| |
| if (!gtk_widget_has_focus (widget)) |
| { |
| priv->in_click = TRUE; |
| gtk_widget_grab_focus (widget); |
| gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); |
| priv->in_click = FALSE; |
| } |
| |
| tmp_pos = gtk_text_find_position (self, x); |
| |
| if (gdk_event_triggers_context_menu (event)) |
| { |
| gtk_text_do_popup (self, widget_x, widget_y); |
| gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); |
| } |
| else if (n_press == 1 && button == GDK_BUTTON_MIDDLE && |
| get_middle_click_paste (self)) |
| { |
| if (priv->editable) |
| { |
| priv->insert_pos = tmp_pos; |
| gtk_text_paste (self, gtk_widget_get_primary_clipboard (widget)); |
| } |
| else |
| { |
| gtk_widget_error_bell (widget); |
| } |
| |
| gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); |
| } |
| else if (button == GDK_BUTTON_PRIMARY) |
| { |
| gboolean have_selection; |
| gboolean is_touchscreen, extend_selection; |
| GdkDevice *source; |
| guint state; |
| |
| sel_start = priv->selection_bound; |
| sel_end = priv->current_pos; |
| have_selection = sel_start != sel_end; |
| |
| source = gdk_event_get_device (event); |
| is_touchscreen = gtk_simulate_touchscreen () || |
| gdk_device_get_source (source) == GDK_SOURCE_TOUCHSCREEN; |
| |
| priv->text_handles_enabled = is_touchscreen; |
| |
| priv->in_drag = FALSE; |
| priv->select_words = FALSE; |
| priv->select_lines = FALSE; |
| |
| state = gdk_event_get_modifier_state (event); |
| |
| extend_selection = (state & GDK_SHIFT_MASK) != 0; |
| |
| /* Always emit reset when preedit is shown */ |
| priv->need_im_reset = TRUE; |
| gtk_text_reset_im_context (self); |
| |
| switch (n_press) |
| { |
| case 1: |
| if (in_selection (self, x)) |
| { |
| if (is_touchscreen) |
| { |
| if (priv->selection_bubble && |
| gtk_widget_get_visible (priv->selection_bubble)) |
| gtk_text_selection_bubble_popup_unset (self); |
| else |
| gtk_text_selection_bubble_popup_set (self); |
| } |
| else if (extend_selection) |
| { |
| /* Truncate current selection, but keep it as big as possible */ |
| if (tmp_pos - sel_start > sel_end - tmp_pos) |
| gtk_text_set_positions (self, sel_start, tmp_pos); |
| else |
| gtk_text_set_positions (self, tmp_pos, sel_end); |
| |
| /* all done, so skip the extend_to_left stuff later */ |
| extend_selection = FALSE; |
| } |
| else |
| { |
| /* We'll either start a drag, or clear the selection */ |
| priv->in_drag = TRUE; |
| priv->drag_start_x = x; |
| priv->drag_start_y = y; |
| } |
| } |
| else |
| { |
| gtk_text_selection_bubble_popup_unset (self); |
| |
| if (!extend_selection) |
| { |
| gtk_text_set_selection_bounds (self, tmp_pos, tmp_pos); |
| priv->handle_place_time = g_get_monotonic_time (); |
| } |
| else |
| { |
| /* select from the current position to the clicked position */ |
| if (!have_selection) |
| sel_start = sel_end = priv->current_pos; |
| |
| gtk_text_set_positions (self, tmp_pos, tmp_pos); |
| } |
| } |
| |
| break; |
| |
| case 2: |
| priv->select_words = TRUE; |
| gtk_text_select_word (self); |
| break; |
| |
| case 3: |
| priv->select_lines = TRUE; |
| gtk_text_select_line (self); |
| break; |
| |
| default: |
| break; |
| } |
| |
| if (extend_selection) |
| { |
| gboolean extend_to_left; |
| int start, end; |
| |
| start = MIN (priv->current_pos, priv->selection_bound); |
| start = MIN (sel_start, start); |
| |
| end = MAX (priv->current_pos, priv->selection_bound); |
| end = MAX (sel_end, end); |
| |
| if (tmp_pos == sel_start || tmp_pos == sel_end) |
| extend_to_left = (tmp_pos == start); |
| else |
| extend_to_left = (end == sel_end); |
| |
| if (extend_to_left) |
| gtk_text_set_positions (self, start, end); |
| else |
| gtk_text_set_positions (self, end, start); |
| } |
| |
| gtk_text_update_handles (self); |
| } |
| |
| if (n_press >= 3) |
| gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture)); |
| } |
| |
| static char * |
| _gtk_text_get_selected_text (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->selection_bound != priv->current_pos) |
| { |
| const int start = MIN (priv->selection_bound, priv->current_pos); |
| const int end = MAX (priv->selection_bound, priv->current_pos); |
| const char *text = gtk_entry_buffer_get_text (get_buffer (self)); |
| const int start_index = g_utf8_offset_to_pointer (text, start) - text; |
| const int end_index = g_utf8_offset_to_pointer (text, end) - text; |
| |
| return g_strndup (text + start_index, end_index - start_index); |
| } |
| |
| return NULL; |
| } |
| |
| static void |
| gtk_text_show_magnifier (GtkText *self, |
| int x, |
| int y) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| const int text_height = gtk_widget_get_height (GTK_WIDGET (self)); |
| cairo_rectangle_int_t rect; |
| |
| gtk_text_ensure_magnifier (self); |
| |
| rect.x = x; |
| rect.width = 1; |
| rect.y = 0; |
| rect.height = text_height; |
| |
| _gtk_magnifier_set_coords (GTK_MAGNIFIER (priv->magnifier), rect.x, |
| rect.y + rect.height / 2); |
| gtk_popover_set_pointing_to (GTK_POPOVER (priv->magnifier_popover), |
| &rect); |
| gtk_popover_popup (GTK_POPOVER (priv->magnifier_popover)); |
| } |
| |
| static void |
| gtk_text_motion_controller_motion (GtkEventControllerMotion *controller, |
| double x, |
| double y, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->mouse_cursor_obscured) |
| { |
| set_text_cursor (GTK_WIDGET (self)); |
| priv->mouse_cursor_obscured = FALSE; |
| } |
| } |
| |
| static void |
| dnd_finished_cb (GdkDrag *drag, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (gdk_drag_get_selected_action (drag) == GDK_ACTION_MOVE) |
| gtk_text_delete_selection (self); |
| |
| priv->drag = NULL; |
| } |
| |
| static void |
| 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); |
| |
| g_assert (n_ranges > 0); |
| |
| if (priv->editable) |
| actions = GDK_ACTION_COPY|GDK_ACTION_MOVE; |
| else |
| actions = GDK_ACTION_COPY; |
| |
| content = gdk_content_provider_new_typed (G_TYPE_STRING, text); |
| |
| drag = gdk_drag_begin (gdk_event_get_surface ((GdkEvent*) event), |
| gdk_event_get_device ((GdkEvent*) event), |
| content, |
| actions, |
| priv->drag_start_x, |
| priv->drag_start_y); |
| g_object_unref (content); |
| |
| g_signal_connect (drag, "dnd-finished", G_CALLBACK (dnd_finished_cb), self); |
| |
| 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_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 = NULL; |
| GdkDevice *keyboard = NULL; |
| |
| gtk_widget_queue_draw (widget); |
| |
| seat = gdk_display_get_default_seat (gtk_widget_get_display (widget)); |
| if (seat) |
| keyboard = gdk_seat_get_keyboard (seat); |
| if (keyboard) |
| g_signal_connect (keyboard, "notify::direction", |
| G_CALLBACK (direction_changed), self); |
| |
| |
| if (priv->editable) |
| { |
| gtk_text_schedule_im_reset (self); |
| gtk_im_context_focus_in (priv->im_context); |
| } |
| |
| gtk_text_reset_blink_time (self); |
| 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 = NULL; |
| GdkDevice *keyboard = NULL; |
| |
| 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)); |
| if (seat) |
| keyboard = gdk_seat_get_keyboard (seat); |
| if (keyboard) |
| g_signal_handlers_disconnect_by_func (keyboard, direction_changed, self); |
| |
| if (priv->editable) |
| { |
| gtk_text_schedule_im_reset (self); |
| gtk_im_context_focus_out (priv->im_context); |
| } |
| |
| gtk_text_check_cursor_blink (self); |
| } |
| |
| static gboolean |
| gtk_text_grab_focus (GtkWidget *widget) |
| { |
| GtkText *self = GTK_TEXT (widget); |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| gboolean select_on_focus; |
| GtkWidget *prev_focus; |
| |
| prev_focus = gtk_root_get_focus (gtk_widget_get_root (widget)); |
| |
| if (!GTK_WIDGET_CLASS (gtk_text_parent_class)->grab_focus (GTK_WIDGET (self))) |
| return FALSE; |
| |
| if (priv->editable && !priv->in_click && |
| !(prev_focus && gtk_widget_is_ancestor (prev_focus, widget))) |
| { |
| 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, |
| priv->preedit_length == 0 && |
| gtk_entry_buffer_get_length (priv->buffer) == 0); |
| } |
| |
| /* GtkEntryBuffer signal handlers |
| */ |
| static void |
| buffer_inserted_text (GtkEntryBuffer *buffer, |
| guint position, |
| const char *chars, |
| guint n_chars, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| guint password_hint_timeout; |
| guint current_pos; |
| int selection_bound; |
| |
| current_pos = priv->current_pos; |
| if (current_pos > position) |
| current_pos += n_chars; |
| |
| selection_bound = priv->selection_bound; |
| if (selection_bound > position) |
| selection_bound += n_chars; |
| |
| gtk_text_set_positions (self, current_pos, selection_bound); |
| gtk_text_recompute (self); |
| |
| gtk_text_history_text_inserted (priv->history, position, chars, -1); |
| |
| /* Calculate the password hint if it needs to be displayed. */ |
| if (n_chars == 1 && !priv->visible) |
| { |
| g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)), |
| "gtk-entry-password-hint-timeout", &password_hint_timeout, |
| NULL); |
| |
| if (password_hint_timeout > 0) |
| { |
| GtkTextPasswordHint *password_hint = g_object_get_qdata (G_OBJECT (self), |
| quark_password_hint); |
| if (!password_hint) |
| { |
| password_hint = g_slice_new0 (GtkTextPasswordHint); |
| g_object_set_qdata_full (G_OBJECT (self), quark_password_hint, password_hint, |
| (GDestroyNotify)gtk_text_password_hint_free); |
| } |
| |
| password_hint->position = position; |
| if (password_hint->source_id) |
| g_source_remove (password_hint->source_id); |
| password_hint->source_id = g_timeout_add (password_hint_timeout, |
| (GSourceFunc)gtk_text_remove_password_hint, |
| self); |
| g_source_set_name_by_id (password_hint->source_id, "[gtk] gtk_text_remove_password_hint"); |
| } |
| } |
| } |
| |
| static void |
| buffer_deleted_text (GtkEntryBuffer *buffer, |
| guint position, |
| guint n_chars, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| guint end_pos = position + n_chars; |
| |
| if (gtk_text_history_get_enabled (priv->history)) |
| { |
| char *deleted_text; |
| |
| deleted_text = gtk_editable_get_chars (GTK_EDITABLE (self), |
| position, |
| end_pos); |
| gtk_text_history_selection_changed (priv->history, |
| priv->current_pos, |
| priv->selection_bound); |
| gtk_text_history_text_deleted (priv->history, |
| position, |
| end_pos, |
| deleted_text, |
| -1); |
| |
| g_free (deleted_text); |
| } |
| } |
| |
| static void |
| buffer_deleted_text_after (GtkEntryBuffer *buffer, |
| guint position, |
| guint n_chars, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| guint end_pos = position + n_chars; |
| int selection_bound; |
| guint current_pos; |
| |
| current_pos = priv->current_pos; |
| if (current_pos > position) |
| current_pos -= MIN (current_pos, end_pos) - position; |
| |
| selection_bound = priv->selection_bound; |
| if (selection_bound > position) |
| selection_bound -= MIN (selection_bound, end_pos) - position; |
| |
| gtk_text_set_positions (self, current_pos, selection_bound); |
| gtk_text_recompute (self); |
| |
| /* We might have deleted the selection */ |
| gtk_text_update_primary_selection (self); |
| |
| /* Disable the password hint if one exists. */ |
| if (!priv->visible) |
| { |
| GtkTextPasswordHint *password_hint = g_object_get_qdata (G_OBJECT (self), |
| quark_password_hint); |
| if (password_hint) |
| { |
| if (password_hint->source_id) |
| g_source_remove (password_hint->source_id); |
| password_hint->source_id = 0; |
| password_hint->position = -1; |
| } |
| } |
| } |
| |
| static void |
| buffer_notify_text (GtkEntryBuffer *buffer, |
| GParamSpec *spec, |
| GtkText *self) |
| { |
| emit_changed (self); |
| g_object_notify (G_OBJECT (self), "text"); |
| } |
| |
| static void |
| buffer_notify_max_length (GtkEntryBuffer *buffer, |
| GParamSpec *spec, |
| GtkText *self) |
| { |
| g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_MAX_LENGTH]); |
| } |
| |
| static void |
| buffer_connect_signals (GtkText *self) |
| { |
| g_signal_connect (get_buffer (self), "inserted-text", G_CALLBACK (buffer_inserted_text), self); |
| g_signal_connect (get_buffer (self), "deleted-text", G_CALLBACK (buffer_deleted_text), self); |
| g_signal_connect_after (get_buffer (self), "deleted-text", G_CALLBACK (buffer_deleted_text_after), self); |
| g_signal_connect (get_buffer (self), "notify::text", G_CALLBACK (buffer_notify_text), self); |
| g_signal_connect (get_buffer (self), "notify::max-length", G_CALLBACK (buffer_notify_max_length), self); |
| } |
| |
| static void |
| buffer_disconnect_signals (GtkText *self) |
| { |
| g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_inserted_text, self); |
| g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_deleted_text, self); |
| g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_deleted_text_after, self); |
| g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_notify_text, self); |
| g_signal_handlers_disconnect_by_func (get_buffer (self), buffer_notify_max_length, self); |
| } |
| |
| /* Compute the X position for an offset that corresponds to the "more important |
| * cursor position for that offset. We use this when trying to guess to which |
| * end of the selection we should go to when the user hits the left or |
| * right arrow key. |
| */ |
| static int |
| get_better_cursor_x (GtkText *self, |
| int offset) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GdkSeat *seat; |
| GdkDevice *keyboard = NULL; |
| PangoDirection direction = PANGO_DIRECTION_LTR; |
| gboolean split_cursor; |
| PangoLayout *layout = gtk_text_ensure_layout (self, TRUE); |
| const char *text = pango_layout_get_text (layout); |
| int index = g_utf8_offset_to_pointer (text, offset) - text; |
| PangoRectangle strong_pos, weak_pos; |
| |
| seat = gdk_display_get_default_seat (gtk_widget_get_display (GTK_WIDGET (self))); |
| if (seat) |
| keyboard = gdk_seat_get_keyboard (seat); |
| if (keyboard) |
| direction = gdk_device_get_direction (keyboard); |
| |
| g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)), |
| "gtk-split-cursor", &split_cursor, |
| NULL); |
| |
| pango_layout_get_cursor_pos (layout, index, &strong_pos, &weak_pos); |
| |
| if (split_cursor) |
| return strong_pos.x / PANGO_SCALE; |
| else |
| return (direction == priv->resolved_dir) ? strong_pos.x / PANGO_SCALE : weak_pos.x / PANGO_SCALE; |
| } |
| |
| static void |
| gtk_text_move_cursor (GtkText *self, |
| GtkMovementStep step, |
| int count, |
| gboolean extend_selection) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| int new_pos = priv->current_pos; |
| |
| gtk_text_reset_im_context (self); |
| |
| if (priv->current_pos != priv->selection_bound && !extend_selection) |
| { |
| /* If we have a current selection and aren't extending it, move to the |
| * start/or end of the selection as appropriate |
| */ |
| switch (step) |
| { |
| case GTK_MOVEMENT_VISUAL_POSITIONS: |
| { |
| int current_x = get_better_cursor_x (self, priv->current_pos); |
| int bound_x = get_better_cursor_x (self, priv->selection_bound); |
| |
| if (count <= 0) |
| new_pos = current_x < bound_x ? priv->current_pos : priv->selection_bound; |
| else |
| new_pos = current_x > bound_x ? priv->current_pos : priv->selection_bound; |
| } |
| break; |
| |
| case GTK_MOVEMENT_WORDS: |
| if (priv->resolved_dir == PANGO_DIRECTION_RTL) |
| count *= -1; |
| G_GNUC_FALLTHROUGH; |
| |
| case GTK_MOVEMENT_LOGICAL_POSITIONS: |
| if (count < 0) |
| new_pos = MIN (priv->current_pos, priv->selection_bound); |
| else |
| new_pos = MAX (priv->current_pos, priv->selection_bound); |
| |
| break; |
| |
| case GTK_MOVEMENT_DISPLAY_LINE_ENDS: |
| case GTK_MOVEMENT_PARAGRAPH_ENDS: |
| case GTK_MOVEMENT_BUFFER_ENDS: |
| new_pos = count < 0 ? 0 : gtk_entry_buffer_get_length (get_buffer (self)); |
| break; |
| |
| case GTK_MOVEMENT_DISPLAY_LINES: |
| case GTK_MOVEMENT_PARAGRAPHS: |
| case GTK_MOVEMENT_PAGES: |
| case GTK_MOVEMENT_HORIZONTAL_PAGES: |
| default: |
| break; |
| } |
| } |
| else |
| { |
| switch (step) |
| { |
| case GTK_MOVEMENT_LOGICAL_POSITIONS: |
| new_pos = gtk_text_move_logically (self, new_pos, count); |
| break; |
| |
| case GTK_MOVEMENT_VISUAL_POSITIONS: |
| new_pos = gtk_text_move_visually (self, new_pos, count); |
| |
| if (priv->current_pos == new_pos) |
| { |
| if (!extend_selection) |
| { |
| if (!gtk_widget_keynav_failed (GTK_WIDGET (self), |
| count > 0 ? |
| GTK_DIR_RIGHT : GTK_DIR_LEFT)) |
| { |
| GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self))); |
| |
| if (toplevel) |
| gtk_widget_child_focus (toplevel, |
| count > 0 ? |
| GTK_DIR_RIGHT : GTK_DIR_LEFT); |
| } |
| } |
| else |
| { |
| gtk_widget_error_bell (GTK_WIDGET (self)); |
| } |
| } |
| break; |
| |
| case GTK_MOVEMENT_WORDS: |
| if (priv->resolved_dir == PANGO_DIRECTION_RTL) |
| count *= -1; |
| |
| while (count > 0) |
| { |
| new_pos = gtk_text_move_forward_word (self, new_pos, FALSE); |
| count--; |
| } |
| |
| while (count < 0) |
| { |
| new_pos = gtk_text_move_backward_word (self, new_pos, FALSE); |
| count++; |
| } |
| |
| if (priv->current_pos == new_pos) |
| gtk_widget_error_bell (GTK_WIDGET (self)); |
| |
| break; |
| |
| case GTK_MOVEMENT_DISPLAY_LINE_ENDS: |
| case GTK_MOVEMENT_PARAGRAPH_ENDS: |
| case GTK_MOVEMENT_BUFFER_ENDS: |
| new_pos = count < 0 ? 0 : gtk_entry_buffer_get_length (get_buffer (self)); |
| |
| if (priv->current_pos == new_pos) |
| gtk_widget_error_bell (GTK_WIDGET (self)); |
| |
| break; |
| |
| case GTK_MOVEMENT_DISPLAY_LINES: |
| case GTK_MOVEMENT_PARAGRAPHS: |
| case GTK_MOVEMENT_PAGES: |
| case GTK_MOVEMENT_HORIZONTAL_PAGES: |
| default: |
| break; |
| } |
| } |
| |
| if (extend_selection) |
| gtk_text_set_selection_bounds (self, priv->selection_bound, new_pos); |
| else |
| gtk_text_set_selection_bounds (self, new_pos, new_pos); |
| |
| gtk_text_pend_cursor_blink (self); |
| } |
| |
| static void |
| gtk_text_insert_at_cursor (GtkText *self, |
| const char *str) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| int pos = priv->current_pos; |
| |
| if (priv->editable) |
| { |
| gtk_text_reset_im_context (self); |
| gtk_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, beginning of next word */ |
| start_pos = gtk_text_move_forward_word (self, start_pos, FALSE); |
| start_pos = gtk_text_move_backward_word (self, start_pos, FALSE); |
| } |
| G_GNUC_FALLTHROUGH; |
| case GTK_DELETE_WORD_ENDS: |
| while (count < 0) |
| { |
| start_pos = gtk_text_move_backward_word (self, start_pos, FALSE); |
| count++; |
| } |
| |
| while (count > 0) |
| { |
| end_pos = gtk_text_move_forward_word (self, end_pos, FALSE); |
| count--; |
| } |
| |
| gtk_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; |
| } |
| |
| if (priv->selection_bound < priv->current_pos) |
| str = gtk_text_get_display_text (self, priv->selection_bound, priv->current_pos); |
| else |
| str = gtk_text_get_display_text (self, priv->current_pos, priv->selection_bound); |
| |
| gdk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (self)), str); |
| g_free (str); |
| } |
| } |
| |
| static void |
| gtk_text_cut_clipboard (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (!priv->visible) |
| { |
| gtk_widget_error_bell (GTK_WIDGET (self)); |
| return; |
| } |
| |
| gtk_text_copy_clipboard (self); |
| |
| if (priv->editable) |
| { |
| if (priv->selection_bound != priv->current_pos) |
| gtk_text_delete_selection (self); |
| } |
| else |
| { |
| gtk_widget_error_bell (GTK_WIDGET (self)); |
| } |
| |
| gtk_text_selection_bubble_popup_unset (self); |
| |
| gtk_text_update_handles (self); |
| } |
| |
| static void |
| gtk_text_paste_clipboard (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->editable) |
| gtk_text_paste (self, gtk_widget_get_clipboard (GTK_WIDGET (self))); |
| else |
| gtk_widget_error_bell (GTK_WIDGET (self)); |
| |
| gtk_text_update_handles (self); |
| } |
| |
| static void |
| gtk_text_delete_cb (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->editable) |
| { |
| if (priv->selection_bound != priv->current_pos) |
| gtk_text_delete_selection (self); |
| } |
| } |
| |
| static void |
| gtk_text_toggle_overwrite (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| priv->overwrite_mode = !priv->overwrite_mode; |
| |
| if (priv->overwrite_mode) |
| { |
| if (!priv->block_cursor_node) |
| { |
| GtkCssNode *widget_node = gtk_widget_get_css_node (GTK_WIDGET (self)); |
| |
| priv->block_cursor_node = gtk_css_node_new (); |
| gtk_css_node_set_name (priv->block_cursor_node, g_quark_from_static_string ("block-cursor")); |
| gtk_css_node_set_parent (priv->block_cursor_node, widget_node); |
| gtk_css_node_set_state (priv->block_cursor_node, gtk_css_node_get_state (widget_node)); |
| g_object_unref (priv->block_cursor_node); |
| } |
| } |
| else |
| { |
| if (priv->block_cursor_node) |
| { |
| gtk_css_node_set_parent (priv->block_cursor_node, NULL); |
| priv->block_cursor_node = NULL; |
| } |
| } |
| |
| gtk_text_pend_cursor_blink (self); |
| gtk_widget_queue_draw (GTK_WIDGET (self)); |
| } |
| |
| static void |
| gtk_text_select_all (GtkText *self) |
| { |
| gtk_text_select_line (self); |
| } |
| |
| static void |
| gtk_text_real_activate (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->activates_default) |
| gtk_widget_activate_default (GTK_WIDGET (self)); |
| } |
| |
| static void |
| direction_changed (GdkDevice *device, |
| GParamSpec *pspec, |
| GtkText *self) |
| { |
| gtk_text_recompute (self); |
| } |
| |
| /* IM Context Callbacks |
| */ |
| |
| static void |
| gtk_text_commit_cb (GtkIMContext *context, |
| const char *str, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->editable) |
| { |
| gtk_text_enter_text (self, str); |
| gtk_text_obscure_mouse_cursor (self); |
| } |
| } |
| |
| static void |
| gtk_text_preedit_changed_cb (GtkIMContext *context, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->editable) |
| { |
| char *preedit_string; |
| int cursor_pos; |
| |
| gtk_text_obscure_mouse_cursor (self); |
| |
| gtk_im_context_get_preedit_string (priv->im_context, |
| &preedit_string, NULL, |
| &cursor_pos); |
| g_signal_emit (self, signals[PREEDIT_CHANGED], 0, preedit_string); |
| priv->preedit_length = strlen (preedit_string); |
| cursor_pos = CLAMP (cursor_pos, 0, g_utf8_strlen (preedit_string, -1)); |
| priv->preedit_cursor = cursor_pos; |
| g_free (preedit_string); |
| |
| gtk_text_recompute (self); |
| update_placeholder_visibility (self); |
| } |
| } |
| |
| static gboolean |
| gtk_text_retrieve_surrounding_cb (GtkIMContext *context, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| char *text; |
| |
| /* XXXX ??? does this even make sense when text is not visible? Should we return FALSE? */ |
| text = gtk_text_get_display_text (self, 0, -1); |
| gtk_im_context_set_surrounding (context, text, strlen (text), /* Length in bytes */ |
| g_utf8_offset_to_pointer (text, priv->current_pos) - text); |
| g_free (text); |
| |
| return TRUE; |
| } |
| |
| static gboolean |
| gtk_text_delete_surrounding_cb (GtkIMContext *context, |
| int offset, |
| int n_chars, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->editable) |
| gtk_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); |
| PangoLayout *layout; |
| PangoAttrList *tmp_attrs = NULL; |
| char *preedit_string = NULL; |
| int preedit_length = 0; |
| PangoAttrList *preedit_attrs = NULL; |
| char *display_text; |
| guint n_bytes; |
| |
| layout = gtk_widget_create_pango_layout (widget, NULL); |
| pango_layout_set_single_paragraph_mode (layout, TRUE); |
| |
| tmp_attrs = gtk_css_style_get_pango_attributes (gtk_css_node_get_style (gtk_widget_get_css_node (widget))); |
| tmp_attrs = _gtk_pango_attr_list_merge (tmp_attrs, priv->attrs); |
| if (!tmp_attrs) |
| tmp_attrs = pango_attr_list_new (); |
| |
| display_text = gtk_text_get_display_text (self, 0, -1); |
| |
| n_bytes = strlen (display_text); |
| |
| if (include_preedit) |
| { |
| gtk_im_context_get_preedit_string (priv->im_context, |
| &preedit_string, &preedit_attrs, NULL); |
| preedit_length = priv->preedit_length; |
| } |
| |
| if (preedit_length) |
| { |
| GString *tmp_string = g_string_new (display_text); |
| int pos; |
| |
| pos = g_utf8_offset_to_pointer (display_text, priv->current_pos) - display_text; |
| g_string_insert (tmp_string, pos, preedit_string); |
| pango_layout_set_text (layout, tmp_string->str, tmp_string->len); |
| pango_attr_list_splice (tmp_attrs, preedit_attrs, pos, preedit_length); |
| g_string_free (tmp_string, TRUE); |
| } |
| else |
| { |
| PangoDirection pango_dir; |
| |
| if (gtk_text_get_display_mode (self) == DISPLAY_NORMAL) |
| pango_dir = gdk_find_base_dir (display_text, n_bytes); |
| else |
| pango_dir = PANGO_DIRECTION_NEUTRAL; |
| |
| if (pango_dir == PANGO_DIRECTION_NEUTRAL) |
| { |
| if (gtk_widget_has_focus (widget)) |
| { |
| GdkDisplay *display; |
| GdkSeat *seat; |
| GdkDevice *keyboard = NULL; |
| PangoDirection direction = PANGO_DIRECTION_LTR; |
| |
| display = gtk_widget_get_display (widget); |
| seat = gdk_display_get_default_seat (display); |
| if (seat) |
| keyboard = gdk_seat_get_keyboard (seat); |
| if (keyboard) |
| direction = gdk_device_get_direction (keyboard); |
| |
| if (direction == PANGO_DIRECTION_RTL) |
| pango_dir = PANGO_DIRECTION_RTL; |
| else |
| pango_dir = PANGO_DIRECTION_LTR; |
| } |
| else |
| { |
| if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL) |
| pango_dir = PANGO_DIRECTION_RTL; |
| else |
| pango_dir = PANGO_DIRECTION_LTR; |
| } |
| } |
| |
| pango_context_set_base_dir (gtk_widget_get_pango_context (widget), pango_dir); |
| |
| priv->resolved_dir = pango_dir; |
| |
| pango_layout_set_text (layout, display_text, n_bytes); |
| } |
| |
| pango_layout_set_attributes (layout, tmp_attrs); |
| |
| if (priv->tabs) |
| pango_layout_set_tabs (layout, priv->tabs); |
| |
| g_free (preedit_string); |
| g_free (display_text); |
| |
| pango_attr_list_unref (preedit_attrs); |
| pango_attr_list_unref (tmp_attrs); |
| |
| return layout; |
| } |
| |
| static PangoLayout * |
| gtk_text_ensure_layout (GtkText *self, |
| gboolean include_preedit) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->preedit_length > 0 && |
| !include_preedit != !priv->cache_includes_preedit) |
| gtk_text_reset_layout (self); |
| |
| if (!priv->cached_layout) |
| { |
| priv->cached_layout = gtk_text_create_layout (self, include_preedit); |
| priv->cache_includes_preedit = include_preedit; |
| } |
| |
| return priv->cached_layout; |
| } |
| |
| static void |
| get_layout_position (GtkText *self, |
| int *x, |
| int *y) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| const int text_height = gtk_widget_get_height (GTK_WIDGET (self)); |
| PangoLayout *layout; |
| PangoRectangle logical_rect; |
| int y_pos, area_height; |
| PangoLayoutLine *line; |
| |
| layout = gtk_text_ensure_layout (self, TRUE); |
| |
| area_height = PANGO_SCALE * text_height; |
| |
| line = pango_layout_get_lines_readonly (layout)->data; |
| pango_layout_line_get_extents (line, NULL, &logical_rect); |
| |
| /* Align primarily for locale's ascent/descent */ |
| if (priv->text_baseline < 0) |
| y_pos = ((area_height - priv->ascent - priv->descent) / 2 + |
| priv->ascent + logical_rect.y); |
| else |
| y_pos = PANGO_SCALE * priv->text_baseline - pango_layout_get_baseline (layout); |
| |
| /* Now see if we need to adjust to fit in actual drawn string */ |
| if (logical_rect.height > area_height) |
| y_pos = (area_height - logical_rect.height) / 2; |
| else if (y_pos < 0) |
| y_pos = 0; |
| else if (y_pos + logical_rect.height > area_height) |
| y_pos = area_height - logical_rect.height; |
| |
| y_pos = y_pos / PANGO_SCALE; |
| |
| if (x) |
| *x = - priv->scroll_offset; |
| |
| if (y) |
| *y = y_pos; |
| } |
| |
| #define GRAPHENE_RECT_FROM_RECT(_r) (GRAPHENE_RECT_INIT ((_r)->x, (_r)->y, (_r)->width, (_r)->height)) |
| |
| static void |
| gtk_text_draw_text (GtkText *self, |
| GtkSnapshot *snapshot) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GtkWidget *widget = GTK_WIDGET (self); |
| GtkStyleContext *context; |
| PangoLayout *layout; |
| int x, y; |
| |
| /* Nothing to display at all */ |
| if (gtk_text_get_display_mode (self) == DISPLAY_BLANK) |
| return; |
| |
| context = gtk_widget_get_style_context (widget); |
| layout = gtk_text_ensure_layout (self, TRUE); |
| |
| gtk_text_get_layout_offsets (self, &x, &y); |
| |
| gtk_snapshot_render_layout (snapshot, context, x, y, layout); |
| |
| if (priv->selection_bound != priv->current_pos) |
| { |
| const char *text = pango_layout_get_text (layout); |
| int start_index = g_utf8_offset_to_pointer (text, priv->selection_bound) - text; |
| int end_index = g_utf8_offset_to_pointer (text, priv->current_pos) - text; |
| cairo_region_t *clip; |
| cairo_rectangle_int_t clip_extents; |
| int range[2]; |
| int width, height; |
| |
| width = gtk_widget_get_width (widget); |
| height = gtk_widget_get_height (widget); |
| |
| range[0] = MIN (start_index, end_index); |
| range[1] = MAX (start_index, end_index); |
| |
| gtk_style_context_save_to_node (context, priv->selection_node); |
| |
| clip = gdk_pango_layout_get_clip_region (layout, x, y, range, 1); |
| cairo_region_get_extents (clip, &clip_extents); |
| |
| gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_FROM_RECT (&clip_extents)); |
| gtk_snapshot_render_background (snapshot, context, 0, 0, width, height); |
| gtk_snapshot_render_layout (snapshot, context, x, y, layout); |
| gtk_snapshot_pop (snapshot); |
| |
| cairo_region_destroy (clip); |
| |
| gtk_style_context_restore (context); |
| } |
| } |
| |
| static void |
| gtk_text_draw_cursor (GtkText *self, |
| GtkSnapshot *snapshot, |
| CursorType type) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GtkWidget *widget = GTK_WIDGET (self); |
| GtkStyleContext *context; |
| PangoRectangle cursor_rect; |
| int cursor_index; |
| gboolean block; |
| gboolean block_at_line_end; |
| PangoLayout *layout; |
| const char *text; |
| int x, y; |
| |
| context = gtk_widget_get_style_context (widget); |
| |
| layout = g_object_ref (gtk_text_ensure_layout (self, TRUE)); |
| text = pango_layout_get_text (layout); |
| gtk_text_get_layout_offsets (self, &x, &y); |
| |
| if (type == CURSOR_DND) |
| cursor_index = g_utf8_offset_to_pointer (text, priv->dnd_position) - text; |
| else |
| cursor_index = g_utf8_offset_to_pointer (text, priv->current_pos + priv->preedit_cursor) - text; |
| |
| if (!priv->overwrite_mode) |
| block = FALSE; |
| else |
| block = _gtk_text_util_get_block_cursor_location (layout, |
| cursor_index, &cursor_rect, &block_at_line_end); |
| if (!block) |
| { |
| gtk_snapshot_render_insertion_cursor (snapshot, context, |
| x, y, |
| layout, cursor_index, priv->resolved_dir); |
| } |
| else /* overwrite_mode */ |
| { |
| int width = gtk_widget_get_width (widget); |
| int height = gtk_widget_get_height (widget); |
| graphene_rect_t bounds; |
| |
| bounds.origin.x = PANGO_PIXELS (cursor_rect.x) + x; |
| bounds.origin.y = PANGO_PIXELS (cursor_rect.y) + y; |
| bounds.size.width = PANGO_PIXELS (cursor_rect.width); |
| bounds.size.height = PANGO_PIXELS (cursor_rect.height); |
| |
| gtk_style_context_save_to_node (context, priv->block_cursor_node); |
| |
| gtk_snapshot_push_clip (snapshot, &bounds); |
| gtk_snapshot_render_background (snapshot, context, 0, 0, width, height); |
| gtk_snapshot_render_layout (snapshot, context, x, y, layout); |
| gtk_snapshot_pop (snapshot); |
| |
| gtk_style_context_restore (context); |
| } |
| |
| g_object_unref (layout); |
| } |
| |
| static void |
| gtk_text_handle_dragged (GtkTextHandle *handle, |
| int x, |
| int y, |
| GtkText *self) |
| { |
| int cursor_pos, selection_bound_pos, tmp_pos, *old_pos; |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| gtk_text_selection_bubble_popup_unset (self); |
| |
| cursor_pos = priv->current_pos; |
| selection_bound_pos = priv->selection_bound; |
| |
| tmp_pos = gtk_text_find_position (self, x + priv->scroll_offset); |
| |
| if (handle == priv->text_handles[TEXT_HANDLE_CURSOR]) |
| { |
| /* Avoid running past the other handle in selection mode */ |
| if (tmp_pos >= selection_bound_pos && |
| gtk_widget_is_visible (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]))) |
| { |
| tmp_pos = selection_bound_pos - 1; |
| } |
| |
| old_pos = &cursor_pos; |
| } |
| else if (handle == priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]) |
| { |
| /* Avoid running past the other handle */ |
| if (tmp_pos <= cursor_pos) |
| tmp_pos = cursor_pos + 1; |
| |
| old_pos = &selection_bound_pos; |
| } |
| else |
| g_assert_not_reached (); |
| |
| if (tmp_pos != *old_pos) |
| { |
| *old_pos = tmp_pos; |
| |
| if (handle == priv->text_handles[TEXT_HANDLE_CURSOR] && |
| !gtk_widget_is_visible (GTK_WIDGET (priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]))) |
| gtk_text_set_positions (self, cursor_pos, cursor_pos); |
| else |
| gtk_text_set_positions (self, cursor_pos, selection_bound_pos); |
| |
| if (handle == priv->text_handles[TEXT_HANDLE_CURSOR]) |
| priv->cursor_handle_dragged = TRUE; |
| else if (handle == priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]) |
| priv->selection_handle_dragged = TRUE; |
| |
| gtk_text_update_handles (self); |
| } |
| |
| gtk_text_show_magnifier (self, x, y); |
| } |
| |
| static void |
| gtk_text_handle_drag_started (GtkTextHandle *handle, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| priv->cursor_handle_dragged = FALSE; |
| priv->selection_handle_dragged = FALSE; |
| } |
| |
| static void |
| gtk_text_handle_drag_finished (GtkTextHandle *handle, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (!priv->cursor_handle_dragged && !priv->selection_handle_dragged) |
| { |
| GtkSettings *settings; |
| guint double_click_time; |
| |
| settings = gtk_widget_get_settings (GTK_WIDGET (self)); |
| g_object_get (settings, "gtk-double-click-time", &double_click_time, NULL); |
| if (g_get_monotonic_time() - priv->handle_place_time < double_click_time * 1000) |
| { |
| gtk_text_select_word (self); |
| gtk_text_update_handles (self); |
| } |
| else |
| gtk_text_selection_bubble_popup_set (self); |
| } |
| |
| if (priv->magnifier_popover) |
| gtk_popover_popdown (GTK_POPOVER (priv->magnifier_popover)); |
| } |
| |
| static void |
| gtk_text_schedule_im_reset (GtkText *self) |
| { |
| GtkTextPrivate *priv; |
| |
| priv = gtk_text_get_instance_private (self); |
| |
| priv->need_im_reset = TRUE; |
| } |
| |
| void |
| gtk_text_reset_im_context (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| g_return_if_fail (GTK_IS_TEXT (self)); |
| |
| if (priv->need_im_reset) |
| { |
| priv->need_im_reset = FALSE; |
| gtk_im_context_reset (priv->im_context); |
| } |
| } |
| |
| static int |
| gtk_text_find_position (GtkText *self, |
| int x) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| PangoLayout *layout; |
| PangoLayoutLine *line; |
| int index; |
| int pos; |
| int trailing; |
| const char *text; |
| int cursor_index; |
| |
| layout = gtk_text_ensure_layout (self, TRUE); |
| text = pango_layout_get_text (layout); |
| cursor_index = g_utf8_offset_to_pointer (text, priv->current_pos) - text; |
| |
| line = pango_layout_get_lines_readonly (layout)->data; |
| pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing); |
| |
| if (index >= cursor_index && priv->preedit_length) |
| { |
| if (index >= cursor_index + priv->preedit_length) |
| index -= priv->preedit_length; |
| else |
| { |
| index = cursor_index; |
| trailing = 0; |
| } |
| } |
| |
| pos = g_utf8_pointer_to_offset (text, text + index); |
| pos += trailing; |
| |
| return pos; |
| } |
| |
| static void |
| gtk_text_get_cursor_locations (GtkText *self, |
| int *strong_x, |
| int *weak_x) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| DisplayMode mode = gtk_text_get_display_mode (self); |
| |
| /* Nothing to display at all, so no cursor is relevant */ |
| if (mode == DISPLAY_BLANK) |
| { |
| if (strong_x) |
| *strong_x = 0; |
| |
| if (weak_x) |
| *weak_x = 0; |
| } |
| else |
| { |
| PangoLayout *layout = gtk_text_ensure_layout (self, TRUE); |
| const char *text = pango_layout_get_text (layout); |
| PangoRectangle strong_pos, weak_pos; |
| int index; |
| |
| index = g_utf8_offset_to_pointer (text, priv->current_pos + priv->preedit_cursor) - text; |
| |
| pango_layout_get_cursor_pos (layout, index, &strong_pos, &weak_pos); |
| |
| if (strong_x) |
| *strong_x = strong_pos.x / PANGO_SCALE; |
| |
| if (weak_x) |
| *weak_x = weak_pos.x / PANGO_SCALE; |
| } |
| } |
| |
| static gboolean |
| gtk_text_get_is_selection_handle_dragged (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GtkTextHandle *handle; |
| |
| if (priv->current_pos >= priv->selection_bound) |
| handle = priv->text_handles[TEXT_HANDLE_CURSOR]; |
| else |
| handle = priv->text_handles[TEXT_HANDLE_SELECTION_BOUND]; |
| |
| return handle && gtk_text_handle_get_is_dragged (handle); |
| } |
| |
| static void |
| gtk_text_get_scroll_limits (GtkText *self, |
| int *min_offset, |
| int *max_offset) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| float xalign; |
| PangoLayout *layout; |
| PangoLayoutLine *line; |
| PangoRectangle logical_rect; |
| int text_width, width; |
| |
| layout = gtk_text_ensure_layout (self, TRUE); |
| line = pango_layout_get_lines_readonly (layout)->data; |
| |
| pango_layout_line_get_extents (line, NULL, &logical_rect); |
| |
| /* Display as much text as we can */ |
| |
| if (priv->resolved_dir == PANGO_DIRECTION_LTR) |
| xalign = priv->xalign; |
| else |
| xalign = 1.0 - priv->xalign; |
| |
| text_width = PANGO_PIXELS(logical_rect.width); |
| width = gtk_widget_get_width (GTK_WIDGET (self)); |
| |
| if (text_width > width) |
| { |
| *min_offset = 0; |
| *max_offset = text_width - width; |
| } |
| else |
| { |
| *min_offset = (text_width - width) * xalign; |
| *max_offset = *min_offset; |
| } |
| } |
| |
| static void |
| gtk_text_adjust_scroll (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| const int text_width = gtk_widget_get_width (GTK_WIDGET (self)); |
| int min_offset, max_offset; |
| int strong_x, weak_x; |
| int strong_xoffset, weak_xoffset; |
| |
| if (!gtk_widget_get_realized (GTK_WIDGET (self))) |
| return; |
| |
| gtk_text_get_scroll_limits (self, &min_offset, &max_offset); |
| |
| priv->scroll_offset = CLAMP (priv->scroll_offset, min_offset, max_offset); |
| |
| if (gtk_text_get_is_selection_handle_dragged (self)) |
| { |
| /* The text handle corresponding to the selection bound is |
| * being dragged, ensure it stays onscreen even if we scroll |
| * cursors away, this is so both handles can cause content |
| * to scroll. |
| */ |
| strong_x = weak_x = gtk_text_get_selection_bound_location (self); |
| } |
| else |
| { |
| /* And make sure cursors are on screen. Note that the cursor is |
| * actually drawn one pixel into the INNER_BORDER space on |
| * the right, when the scroll is at the utmost right. This |
| * looks better to me than confining the cursor inside the |
| * border entirely, though it means that the cursor gets one |
| * pixel closer to the edge of the widget on the right than |
| * on the left. This might need changing if one changed |
| * INNER_BORDER from 2 to 1, as one would do on a |
| * small-screen-real-estate display. |
| * |
| * We always make sure that the strong cursor is on screen, and |
| * put the weak cursor on screen if possible. |
| */ |
| gtk_text_get_cursor_locations (self, &strong_x, &weak_x); |
| } |
| |
| strong_xoffset = strong_x - priv->scroll_offset; |
| |
| if (strong_xoffset < 0) |
| { |
| priv->scroll_offset += strong_xoffset; |
| strong_xoffset = 0; |
| } |
| else if (strong_xoffset > text_width) |
| { |
| priv->scroll_offset += strong_xoffset - text_width; |
| strong_xoffset = text_width; |
| } |
| |
| weak_xoffset = weak_x - priv->scroll_offset; |
| |
| if (weak_xoffset < 0 && strong_xoffset - weak_xoffset <= text_width) |
| { |
| priv->scroll_offset += weak_xoffset; |
| } |
| else if (weak_xoffset > text_width && |
| strong_xoffset - (weak_xoffset - text_width) >= 0) |
| { |
| priv->scroll_offset += weak_xoffset - text_width; |
| } |
| |
| g_object_notify_by_pspec (G_OBJECT (self), text_props[PROP_SCROLL_OFFSET]); |
| |
| gtk_text_update_handles (self); |
| } |
| |
| static int |
| gtk_text_move_visually (GtkText *self, |
| int start, |
| int count) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| int index; |
| PangoLayout *layout = gtk_text_ensure_layout (self, FALSE); |
| const char *text; |
| |
| text = pango_layout_get_text (layout); |
| |
| index = g_utf8_offset_to_pointer (text, start) - text; |
| |
| while (count != 0) |
| { |
| int new_index, new_trailing; |
| gboolean split_cursor; |
| gboolean strong; |
| |
| g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)), |
| "gtk-split-cursor", &split_cursor, |
| NULL); |
| |
| if (split_cursor) |
| strong = TRUE; |
| else |
| { |
| GdkDisplay *display; |
| GdkSeat *seat; |
| GdkDevice *keyboard = NULL; |
| PangoDirection direction = PANGO_DIRECTION_LTR; |
| |
| display = gtk_widget_get_display (GTK_WIDGET (self)); |
| seat = gdk_display_get_default_seat (display); |
| if (seat) |
| keyboard = gdk_seat_get_keyboard (seat); |
| if (keyboard) |
| direction = gdk_device_get_direction (keyboard); |
| |
| strong = direction == priv->resolved_dir; |
| } |
| |
| if (count > 0) |
| { |
| pango_layout_move_cursor_visually (layout, strong, index, 0, 1, &new_index, &new_trailing); |
| count--; |
| } |
| else |
| { |
| pango_layout_move_cursor_visually (layout, strong, index, 0, -1, &new_index, &new_trailing); |
| count++; |
| } |
| |
| if (new_index < 0) |
| index = 0; |
| else if (new_index != G_MAXINT) |
| index = new_index; |
| |
| while (new_trailing--) |
| index = g_utf8_next_char (text + index) - text; |
| } |
| |
| return g_utf8_pointer_to_offset (text, text + index); |
| } |
| |
| static int |
| gtk_text_move_logically (GtkText *self, |
| int start, |
| int count) |
| { |
| int new_pos = start; |
| guint length; |
| |
| length = gtk_entry_buffer_get_length (get_buffer (self)); |
| |
| /* Prevent any leak of information */ |
| if (gtk_text_get_display_mode (self) != DISPLAY_NORMAL) |
| { |
| new_pos = CLAMP (start + count, 0, length); |
| } |
| else |
| { |
| PangoLayout *layout = gtk_text_ensure_layout (self, FALSE); |
| const PangoLogAttr *log_attrs; |
| int n_attrs; |
| |
| log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs); |
| |
| while (count > 0 && new_pos < length) |
| { |
| do |
| new_pos++; |
| while (new_pos < length && !log_attrs[new_pos].is_cursor_position); |
| |
| count--; |
| } |
| while (count < 0 && new_pos > 0) |
| { |
| do |
| new_pos--; |
| while (new_pos > 0 && !log_attrs[new_pos].is_cursor_position); |
| |
| count++; |
| } |
| } |
| |
| return new_pos; |
| } |
| |
| static int |
| gtk_text_move_forward_word (GtkText *self, |
| int start, |
| gboolean allow_whitespace) |
| { |
| int new_pos = start; |
| guint length; |
| |
| length = gtk_entry_buffer_get_length (get_buffer (self)); |
| |
| /* Prevent any leak of information */ |
| if (gtk_text_get_display_mode (self) != DISPLAY_NORMAL) |
| { |
| new_pos = length; |
| } |
| else if (new_pos < length) |
| { |
| PangoLayout *layout = gtk_text_ensure_layout (self, FALSE); |
| const PangoLogAttr *log_attrs; |
| int n_attrs; |
| |
| log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs); |
| |
| /* Find the next word boundary */ |
| new_pos++; |
| while (new_pos < n_attrs - 1 && !(log_attrs[new_pos].is_word_end || |
| (log_attrs[new_pos].is_word_start && allow_whitespace))) |
| new_pos++; |
| } |
| |
| return new_pos; |
| } |
| |
| |
| static int |
| gtk_text_move_backward_word (GtkText *self, |
| int start, |
| gboolean allow_whitespace) |
| { |
| int new_pos = start; |
| |
| /* Prevent any leak of information */ |
| if (gtk_text_get_display_mode (self) != DISPLAY_NORMAL) |
| { |
| new_pos = 0; |
| } |
| else if (start > 0) |
| { |
| PangoLayout *layout = gtk_text_ensure_layout (self, FALSE); |
| const PangoLogAttr *log_attrs; |
| int n_attrs; |
| |
| log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs); |
| |
| new_pos = start - 1; |
| |
| /* Find the previous word boundary */ |
| while (new_pos > 0 && !(log_attrs[new_pos].is_word_start || |
| (log_attrs[new_pos].is_word_end && allow_whitespace))) |
| new_pos--; |
| } |
| |
| return new_pos; |
| } |
| |
| static void |
| gtk_text_delete_whitespace (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| PangoLayout *layout = gtk_text_ensure_layout (self, FALSE); |
| const PangoLogAttr *log_attrs; |
| int n_attrs; |
| int start, end; |
| |
| log_attrs = pango_layout_get_log_attrs_readonly (layout, &n_attrs); |
| |
| start = end = priv->current_pos; |
| |
| while (start > 0 && log_attrs[start-1].is_white) |
| start--; |
| |
| while (end < n_attrs && log_attrs[end].is_white) |
| end++; |
| |
| if (start != end) |
| gtk_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); |
| |
| gtk_text_update_clipboard_actions (self); |
| gtk_text_update_emoji_action (self); |
| |
| 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) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| gtk_widget_action_set_enabled (GTK_WIDGET (self), "misc.insert-emoji", |
| priv->editable && |
| (gtk_text_get_input_hints (self) & GTK_INPUT_HINT_NO_EMOJI) == 0); |
| } |
| |
| static GMenuModel * |
| gtk_text_get_menu_model (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GMenu *menu, *section; |
| GMenuItem *item; |
| |
| menu = g_menu_new (); |
| |
| section = g_menu_new (); |
| item = g_menu_item_new (_("Cu_t"), "clipboard.cut"); |
| g_menu_item_set_attribute (item, "touch-icon", "s", "edit-cut-symbolic"); |
| g_menu_append_item (section, item); |
| g_object_unref (item); |
| item = g_menu_item_new (_("_Copy"), "clipboard.copy"); |
| g_menu_item_set_attribute (item, "touch-icon", "s", "edit-copy-symbolic"); |
| g_menu_append_item (section, item); |
| g_object_unref (item); |
| item = g_menu_item_new (_("_Paste"), "clipboard.paste"); |
| g_menu_item_set_attribute (item, "touch-icon", "s", "edit-paste-symbolic"); |
| g_menu_append_item (section, item); |
| g_object_unref (item); |
| item = g_menu_item_new (_("_Delete"), "selection.delete"); |
| g_menu_item_set_attribute (item, "touch-icon", "s", "edit-delete-symbolic"); |
| g_menu_append_item (section, item); |
| g_object_unref (item); |
| g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); |
| g_object_unref (section); |
| |
| section = g_menu_new (); |
| |
| item = g_menu_item_new (_("Select _All"), "selection.select-all"); |
| g_menu_item_set_attribute (item, "touch-icon", "s", "edit-select-all-symbolic"); |
| g_menu_append_item (section, item); |
| g_object_unref (item); |
| |
| item = g_menu_item_new ( _("Insert _Emoji"), "misc.insert-emoji"); |
| g_menu_item_set_attribute (item, "hidden-when", "s", "action-disabled"); |
| g_menu_item_set_attribute (item, "touch-icon", "s", "face-smile-symbolic"); |
| g_menu_append_item (section, item); |
| g_object_unref (item); |
| g_menu_append_section (menu, NULL, G_MENU_MODEL (section)); |
| g_object_unref (section); |
| |
| if (priv->extra_menu) |
| g_menu_append_section (menu, NULL, priv->extra_menu); |
| |
| return G_MENU_MODEL (menu); |
| } |
| |
| static gboolean |
| gtk_text_mnemonic_activate (GtkWidget *widget, |
| gboolean group_cycling) |
| { |
| gtk_widget_grab_focus (widget); |
| return GDK_EVENT_STOP; |
| } |
| |
| static void |
| gtk_text_popup_menu (GtkWidget *widget, |
| const char *action_name, |
| GVariant *parameters) |
| { |
| gtk_text_do_popup (GTK_TEXT (widget), -1, -1); |
| } |
| |
| static void |
| show_or_hide_handles (GtkWidget *popover, |
| GParamSpec *pspec, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| gboolean visible; |
| |
| visible = gtk_widget_get_visible (popover); |
| priv->text_handles_enabled = !visible; |
| gtk_text_update_handles (self); |
| } |
| |
| static void |
| append_bubble_item (GtkText *self, |
| GtkWidget *toolbar, |
| GMenuModel *model, |
| int index) |
| { |
| GtkActionMuxer *muxer; |
| GtkWidget *item, *image; |
| GVariant *att; |
| const char *icon_name; |
| const char *action_name; |
| GMenuModel *link; |
| gboolean enabled; |
| |
| link = g_menu_model_get_item_link (model, index, "section"); |
| if (link) |
| { |
| int i; |
| for (i = 0; i < g_menu_model_get_n_items (link); i++) |
| append_bubble_item (self, toolbar, link, i); |
| g_object_unref (link); |
| return; |
| } |
| |
| att = g_menu_model_get_item_attribute_value (model, index, "touch-icon", G_VARIANT_TYPE_STRING); |
| if (att == NULL) |
| return; |
| |
| icon_name = g_variant_get_string (att, NULL); |
| g_variant_unref (att); |
| |
| att = g_menu_model_get_item_attribute_value (model, index, "action", G_VARIANT_TYPE_STRING); |
| if (att == NULL) |
| return; |
| action_name = g_variant_get_string (att, NULL); |
| g_variant_unref (att); |
| |
| muxer = _gtk_widget_get_action_muxer (GTK_WIDGET (self), FALSE); |
| if (!gtk_action_muxer_query_action (muxer, action_name, &enabled, |
| NULL, NULL, NULL, NULL) || |
| !enabled) |
| return; |
| |
| item = gtk_button_new (); |
| gtk_widget_set_focus_on_click (item, FALSE); |
| image = gtk_image_new_from_icon_name (icon_name); |
| gtk_widget_show (image); |
| gtk_button_set_child (GTK_BUTTON (item), image); |
| gtk_widget_add_css_class (item, "image-button"); |
| gtk_actionable_set_action_name (GTK_ACTIONABLE (item), action_name); |
| gtk_widget_show (GTK_WIDGET (item)); |
| gtk_box_append (GTK_BOX (toolbar), item); |
| } |
| |
| static gboolean |
| gtk_text_selection_bubble_popup_show (gpointer user_data) |
| { |
| GtkText *self = user_data; |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| const int text_width = gtk_widget_get_width (GTK_WIDGET (self)); |
| const int text_height = gtk_widget_get_height (GTK_WIDGET (self)); |
| cairo_rectangle_int_t rect; |
| GtkAllocation allocation; |
| gboolean has_selection; |
| int start_x, end_x; |
| GtkWidget *box; |
| GtkWidget *toolbar; |
| GMenuModel *model; |
| int i; |
| |
| gtk_text_update_clipboard_actions (self); |
| |
| has_selection = priv->selection_bound != priv->current_pos; |
| |
| if (!has_selection && !priv->editable) |
| { |
| priv->selection_bubble_timeout_id = 0; |
| return G_SOURCE_REMOVE; |
| } |
| |
| g_clear_pointer (&priv->selection_bubble, gtk_widget_unparent); |
| |
| priv->selection_bubble = gtk_popover_new (); |
| gtk_widget_set_parent (priv->selection_bubble, GTK_WIDGET (self)); |
| gtk_widget_add_css_class (priv->selection_bubble, 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_popover_set_child (GTK_POPOVER (priv->selection_bubble), box); |
| gtk_box_append (GTK_BOX (box), toolbar); |
| |
| model = gtk_text_get_menu_model (self); |
| |
| for (i = 0; i < g_menu_model_get_n_items (model); i++) |
| append_bubble_item (self, toolbar, model, i); |
| |
| g_object_unref (model); |
| |
| gtk_widget_get_allocation (GTK_WIDGET (self), &allocation); |
| |
| gtk_text_get_cursor_locations (self, &start_x, NULL); |
| |
| start_x -= priv->scroll_offset; |
| start_x = CLAMP (start_x, 0, text_width); |
| rect.y = - allocation.y; |
| rect.height = text_height; |
| |
| if (has_selection) |
| { |
| end_x = gtk_text_get_selection_bound_location (self) - priv->scroll_offset; |
| end_x = CLAMP (end_x, 0, text_width); |
| |
| rect.x = - allocation.x + MIN (start_x, end_x); |
| rect.width = ABS (end_x - start_x); |
| } |
| else |
| { |
| rect.x = - allocation.x + start_x; |
| rect.width = 0; |
| } |
| |
| rect.x -= 5; |
| rect.y -= 5; |
| rect.width += 10; |
| rect.height += 10; |
| |
| gtk_popover_set_pointing_to (GTK_POPOVER (priv->selection_bubble), &rect); |
| gtk_popover_popup (GTK_POPOVER (priv->selection_bubble)); |
| |
| priv->selection_bubble_timeout_id = 0; |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| gtk_text_selection_bubble_popup_unset (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->selection_bubble) |
| gtk_widget_hide (priv->selection_bubble); |
| |
| if (priv->selection_bubble_timeout_id) |
| { |
| g_source_remove (priv->selection_bubble_timeout_id); |
| priv->selection_bubble_timeout_id = 0; |
| } |
| } |
| |
| static void |
| gtk_text_selection_bubble_popup_set (GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| |
| if (priv->selection_bubble_timeout_id) |
| g_source_remove (priv->selection_bubble_timeout_id); |
| |
| priv->selection_bubble_timeout_id = |
| g_timeout_add (50, gtk_text_selection_bubble_popup_show, self); |
| g_source_set_name_by_id (priv->selection_bubble_timeout_id, "[gtk] gtk_text_selection_bubble_popup_cb"); |
| } |
| |
| static void |
| gtk_text_drag_leave (GtkDropTarget *dest, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| GtkWidget *widget = GTK_WIDGET (self); |
| |
| priv->dnd_position = -1; |
| gtk_widget_queue_draw (widget); |
| } |
| |
| static gboolean |
| gtk_text_drag_drop (GtkDropTarget *dest, |
| const GValue *value, |
| double x, |
| double y, |
| GtkText *self) |
| { |
| GtkTextPrivate *priv = gtk_text_get_instance_private (self); |
| int drop_position; |
| int length; |
| const char *str; |
| |
| if (!priv->editable) |
| return FALSE; |
| |
| drop_position = gtk_text_find_position (self, x + priv->scroll_offset); |
| |
| str = g_value_get_string (value); |
| if (priv->truncate_multiline) |
| length = truncate_multiline (str); |
| else |
| length = -1; |
| |
| if (priv->selection_bound == priv->current_pos || |
| drop_position < priv->selection_bound || |
| drop_position > priv->current_pos) |
| { |
| gtk_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); |
| } |