From b668f45fd0d7c0a614a1947458eac4d4e992715d Mon Sep 17 00:00:00 2001 From: Koyper Date: Tue, 22 Apr 2025 10:02:07 -0500 Subject: [PATCH] Fix LineEdit and TextEdit composite character backspace delete. --- doc/classes/LineEdit.xml | 19 ++++++++++++++++++ doc/classes/TextEdit.xml | 21 ++++++++++++++++++++ scene/gui/code_edit.cpp | 9 ++++++++- scene/gui/line_edit.cpp | 40 ++++++++++++++++++++++++++++++++++--- scene/gui/line_edit.h | 6 ++++++ scene/gui/text_edit.cpp | 43 +++++++++++++++++++++++++++++++++++++++- scene/gui/text_edit.h | 6 ++++++ 7 files changed, 139 insertions(+), 5 deletions(-) diff --git a/doc/classes/LineEdit.xml b/doc/classes/LineEdit.xml index 426c3bbb608..469601f51fe 100644 --- a/doc/classes/LineEdit.xml +++ b/doc/classes/LineEdit.xml @@ -133,6 +133,22 @@ [b]Warning:[/b] This is a required internal node, removing and freeing it may cause a crash. If you wish to hide it or any of its children, use their [member Window.visible] property. + + + + + Returns the correct column at the end of a composite character like ❤️‍🩹 (mending heart; Unicode: [code]U+2764 U+FE0F U+200D U+1FA79[/code]) which is comprised of more than one Unicode code point, if the caret is at the start of the composite character. Also returns the correct column with the caret at mid grapheme and for non-composite characters. + [b]Note:[/b] To check at caret location use [code]get_next_composite_character_column(get_caret_column())[/code] + + + + + + + Returns the correct column at the start of a composite character like ❤️‍🩹 (mending heart; Unicode: [code]U+2764 U+FE0F U+200D U+1FA79[/code]) which is comprised of more than one Unicode code point, if the caret is at the end of the composite character. Also returns the correct column with the caret at mid grapheme and for non-composite characters. + [b]Note:[/b] To check at caret location use [code]get_previous_composite_character_column(get_caret_column())[/code] + + @@ -246,6 +262,9 @@ Text alignment as defined in the [enum HorizontalAlignment] enum. + + If [code]true[/code] and [member caret_mid_grapheme] is [code]false[/code], backspace deletes an entire composite character such as ❤️‍🩹, instead of deleting part of the composite character. + If [code]true[/code], makes the caret blink. diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index e04a8284aff..463cc936e93 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -517,6 +517,15 @@ Returns the number of lines that may be drawn on the minimap. + + + + + + Returns the correct column at the end of a composite character like ❤️‍🩹 (mending heart; Unicode: [code]U+2764 U+FE0F U+200D U+1FA79[/code]) which is comprised of more than one Unicode code point, if the caret is at the start of the composite character. Also returns the correct column with the caret at mid grapheme and for non-composite characters. + [b]Note:[/b] To check at caret location use [code]get_next_composite_character_column(get_caret_line(), get_caret_column())[/code] + + @@ -543,6 +552,15 @@ [b]Note:[/b] The Y position corresponds to the bottom side of the line. Use [method get_rect_at_line_column] to get the top side position. + + + + + + Returns the correct column at the start of a composite character like ❤️‍🩹 (mending heart; Unicode: [code]U+2764 U+FE0F U+200D U+1FA79[/code]) which is comprised of more than one Unicode code point, if the caret is at the end of the composite character. Also returns the correct column with the caret at mid grapheme and for non-composite characters. + [b]Note:[/b] To check at caret location use [code]get_previous_composite_character_column(get_caret_line(), get_caret_column())[/code] + + @@ -1265,6 +1283,9 @@ If [member wrap_mode] is set to [constant LINE_WRAPPING_BOUNDARY], sets text wrapping mode. To see how each mode behaves, see [enum TextServer.AutowrapMode]. + + If [code]true[/code] and [member caret_mid_grapheme] is [code]false[/code], backspace deletes an entire composite character such as ❤️‍🩹, instead of deleting part of the composite character. + If [code]true[/code], makes the caret blink. diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index a5d4cfc1f5e..c2aaf8e6728 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -808,7 +808,14 @@ void CodeEdit::_backspace_internal(int p_caret) { } int from_line = to_column > 0 ? to_line : to_line - 1; - int from_column = to_column > 0 ? (to_column - 1) : (get_line(to_line - 1).length()); + int from_column = 0; + if (to_column == 0) { + from_column = get_line(to_line - 1).length(); + } else if (TextEdit::is_caret_mid_grapheme_enabled() || !TextEdit::is_backspace_deletes_composite_character_enabled()) { + from_column = to_column - 1; + } else { + from_column = TextEdit::get_previous_composite_character_column(to_line, to_column); + } merge_gutters(from_line, to_line); diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 87148d5e62d..df53061a29d 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -1955,11 +1955,14 @@ void LineEdit::delete_char() { if (text.is_empty() || caret_column == 0) { return; } - - text = text.left(caret_column - 1) + text.substr(caret_column); + int delete_char_offset = 1; + if (!caret_mid_grapheme_enabled && backspace_deletes_composite_character_enabled) { + delete_char_offset = caret_column - get_previous_composite_character_column(caret_column); + } + text = text.left(caret_column - delete_char_offset) + text.substr(caret_column); _shape(); - set_caret_column(get_caret_column() - 1); + set_caret_column(get_caret_column() - delete_char_offset); _text_changed(); } @@ -2213,6 +2216,24 @@ int LineEdit::get_caret_column() const { return caret_column; } +int LineEdit::get_next_composite_character_column(int p_column) const { + ERR_FAIL_INDEX_V(p_column, text.length() + 1, -1); + if (p_column == text.length()) { + return p_column; + } else { + return TS->shaped_text_next_character_pos(text_rid, p_column); + } +} + +int LineEdit::get_previous_composite_character_column(int p_column) const { + ERR_FAIL_INDEX_V(p_column, text.length() + 1, -1); + if (p_column == 0) { + return 0; + } else { + return TS->shaped_text_prev_character_pos(text_rid, p_column); + } +} + void LineEdit::set_scroll_offset(float p_pos) { scroll_offset = p_pos; if (scroll_offset < 0.0) { @@ -2629,6 +2650,14 @@ bool LineEdit::is_emoji_menu_enabled() const { return emoji_menu_enabled; } +void LineEdit::set_backspace_deletes_composite_character_enabled(bool p_enabled) { + backspace_deletes_composite_character_enabled = p_enabled; +} + +bool LineEdit::is_backspace_deletes_composite_character_enabled() const { + return backspace_deletes_composite_character_enabled; +} + bool LineEdit::is_menu_visible() const { return menu && menu->is_visible(); } @@ -3092,6 +3121,8 @@ void LineEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_placeholder"), &LineEdit::get_placeholder); ClassDB::bind_method(D_METHOD("set_caret_column", "position"), &LineEdit::set_caret_column); ClassDB::bind_method(D_METHOD("get_caret_column"), &LineEdit::get_caret_column); + ClassDB::bind_method(D_METHOD("get_next_composite_character_column", "column"), &LineEdit::get_next_composite_character_column); + ClassDB::bind_method(D_METHOD("get_previous_composite_character_column", "column"), &LineEdit::get_previous_composite_character_column); ClassDB::bind_method(D_METHOD("get_scroll_offset"), &LineEdit::get_scroll_offset); ClassDB::bind_method(D_METHOD("set_expand_to_text_length_enabled", "enabled"), &LineEdit::set_expand_to_text_length_enabled); ClassDB::bind_method(D_METHOD("is_expand_to_text_length_enabled"), &LineEdit::is_expand_to_text_length_enabled); @@ -3124,6 +3155,8 @@ void LineEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_context_menu_enabled"), &LineEdit::is_context_menu_enabled); ClassDB::bind_method(D_METHOD("set_emoji_menu_enabled", "enable"), &LineEdit::set_emoji_menu_enabled); ClassDB::bind_method(D_METHOD("is_emoji_menu_enabled"), &LineEdit::is_emoji_menu_enabled); + ClassDB::bind_method(D_METHOD("set_backspace_deletes_composite_character_enabled", "enable"), &LineEdit::set_backspace_deletes_composite_character_enabled); + ClassDB::bind_method(D_METHOD("is_backspace_deletes_composite_character_enabled"), &LineEdit::is_backspace_deletes_composite_character_enabled); ClassDB::bind_method(D_METHOD("set_virtual_keyboard_enabled", "enable"), &LineEdit::set_virtual_keyboard_enabled); ClassDB::bind_method(D_METHOD("is_virtual_keyboard_enabled"), &LineEdit::is_virtual_keyboard_enabled); ClassDB::bind_method(D_METHOD("set_virtual_keyboard_type", "type"), &LineEdit::set_virtual_keyboard_type); @@ -3203,6 +3236,7 @@ void LineEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand_to_text_length"), "set_expand_to_text_length_enabled", "is_expand_to_text_length_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emoji_menu_enabled"), "set_emoji_menu_enabled", "is_emoji_menu_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "backspace_deletes_composite_character_enabled"), "set_backspace_deletes_composite_character_enabled", "is_backspace_deletes_composite_character_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "virtual_keyboard_enabled"), "set_virtual_keyboard_enabled", "is_virtual_keyboard_enabled"); ADD_PROPERTY(PropertyInfo(Variant::INT, "virtual_keyboard_type", PROPERTY_HINT_ENUM, "Default,Multiline,Number,Decimal,Phone,Email,Password,URL"), "set_virtual_keyboard_type", "get_virtual_keyboard_type"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clear_button_enabled"), "set_clear_button_enabled", "is_clear_button_enabled"); diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index 672c66cd32b..709c64dc6ee 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -122,6 +122,7 @@ private: bool context_menu_enabled = true; bool emoji_menu_enabled = true; + bool backspace_deletes_composite_character_enabled = false; PopupMenu *menu = nullptr; PopupMenu *menu_dir = nullptr; PopupMenu *menu_ctl = nullptr; @@ -309,6 +310,9 @@ public: void set_emoji_menu_enabled(bool p_enabled); bool is_emoji_menu_enabled() const; + void set_backspace_deletes_composite_character_enabled(bool p_enabled); + bool is_backspace_deletes_composite_character_enabled() const; + void select(int p_from = 0, int p_to = -1); void select_all(); void selection_delete(); @@ -345,6 +349,8 @@ public: void set_caret_column(int p_column); int get_caret_column() const; + int get_next_composite_character_column(int p_column) const; + int get_previous_composite_character_column(int p_column) const; void set_max_length(int p_max_length); int get_max_length() const; diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 7e3bf4519dd..4bc00d88578 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -3723,6 +3723,14 @@ bool TextEdit::is_emoji_menu_enabled() const { return emoji_menu_enabled; } +void TextEdit::set_backspace_deletes_composite_character_enabled(bool p_enabled) { + backspace_deletes_composite_character_enabled = p_enabled; +} + +bool TextEdit::is_backspace_deletes_composite_character_enabled() const { + return backspace_deletes_composite_character_enabled; +} + void TextEdit::set_shortcut_keys_enabled(bool p_enabled) { shortcut_keys_enabled = p_enabled; } @@ -6056,6 +6064,26 @@ int TextEdit::get_selection_origin_column(int p_caret) const { return carets[p_caret].selection.origin_column; } +int TextEdit::get_next_composite_character_column(int p_line, int p_column) const { + ERR_FAIL_INDEX_V(p_line, text.size(), -1); + ERR_FAIL_INDEX_V(p_column, text[p_line].length() + 1, -1); + if (p_column == text[p_line].length()) { + return p_column; + } else { + return TS->shaped_text_next_character_pos(text.get_line_data(p_line)->get_rid(), (p_column)); + } +} + +int TextEdit::get_previous_composite_character_column(int p_line, int p_column) const { + ERR_FAIL_INDEX_V(p_line, text.size(), -1); + ERR_FAIL_INDEX_V(p_column, text[p_line].length() + 1, -1); + if (p_column == 0) { + return 0; + } else { + return TS->shaped_text_prev_character_pos(text.get_line_data(p_line)->get_rid(), p_column); + } +} + int TextEdit::get_selection_from_line(int p_caret) const { ERR_FAIL_INDEX_V(p_caret, carets.size(), -1); if (!has_selection(p_caret)) { @@ -6957,6 +6985,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_emoji_menu_enabled", "enable"), &TextEdit::set_emoji_menu_enabled); ClassDB::bind_method(D_METHOD("is_emoji_menu_enabled"), &TextEdit::is_emoji_menu_enabled); + ClassDB::bind_method(D_METHOD("set_backspace_deletes_composite_character_enabled", "enable"), &TextEdit::set_backspace_deletes_composite_character_enabled); + ClassDB::bind_method(D_METHOD("is_backspace_deletes_composite_character_enabled"), &TextEdit::is_backspace_deletes_composite_character_enabled); + ClassDB::bind_method(D_METHOD("set_shortcut_keys_enabled", "enabled"), &TextEdit::set_shortcut_keys_enabled); ClassDB::bind_method(D_METHOD("is_shortcut_keys_enabled"), &TextEdit::is_shortcut_keys_enabled); @@ -7150,6 +7181,8 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_caret_column", "column", "adjust_viewport", "caret_index"), &TextEdit::set_caret_column, DEFVAL(true), DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_caret_column", "caret_index"), &TextEdit::get_caret_column, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_next_composite_character_column", "line", "column"), &TextEdit::get_next_composite_character_column); + ClassDB::bind_method(D_METHOD("get_previous_composite_character_column", "line", "column"), &TextEdit::get_previous_composite_character_column); ClassDB::bind_method(D_METHOD("get_caret_wrap_index", "caret_index"), &TextEdit::get_caret_wrap_index, DEFVAL(0)); @@ -7359,6 +7392,7 @@ void TextEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable"), "set_editable", "is_editable"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emoji_menu_enabled"), "set_emoji_menu_enabled", "is_emoji_menu_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "backspace_deletes_composite_character_enabled"), "set_backspace_deletes_composite_character_enabled", "is_backspace_deletes_composite_character_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selecting_enabled"), "set_selecting_enabled", "is_selecting_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deselect_on_focus_loss_enabled"), "set_deselect_on_focus_loss_enabled", "is_deselect_on_focus_loss_enabled"); @@ -7595,7 +7629,14 @@ void TextEdit::_backspace_internal(int p_caret) { } int from_line = to_column > 0 ? to_line : to_line - 1; - int from_column = to_column > 0 ? (to_column - 1) : (text[to_line - 1].length()); + int from_column = 0; + if (to_column == 0) { + from_column = text[to_line - 1].length(); + } else if (caret_mid_grapheme_enabled || !backspace_deletes_composite_character_enabled) { + from_column = to_column - 1; + } else { + from_column = get_previous_composite_character_column(to_line, to_column); + } merge_gutters(from_line, to_line); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 8f6a5be6e7f..e0b785e7cf1 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -340,6 +340,7 @@ private: bool overtype_mode = false; bool context_menu_enabled = true; bool emoji_menu_enabled = true; + bool backspace_deletes_composite_character_enabled = false; bool shortcut_keys_enabled = true; bool virtual_keyboard_enabled = true; bool middle_mouse_paste_enabled = true; @@ -813,6 +814,9 @@ public: void set_emoji_menu_enabled(bool p_enabled); bool is_emoji_menu_enabled() const; + void set_backspace_deletes_composite_character_enabled(bool p_enabled); + bool is_backspace_deletes_composite_character_enabled() const; + void set_shortcut_keys_enabled(bool p_enabled); bool is_shortcut_keys_enabled() const; @@ -960,6 +964,8 @@ public: void set_caret_column(int p_column, bool p_adjust_viewport = true, int p_caret = 0); int get_caret_column(int p_caret = 0) const; + int get_next_composite_character_column(int p_line, int p_column) const; + int get_previous_composite_character_column(int p_line, int p_column) const; int get_caret_wrap_index(int p_caret = 0) const;