diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index ddeee9d765a2..9a772c87c9b1 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -400,6 +400,7 @@ static const _BuiltinActionDisplayName _builtin_action_display_names[] = { { "ui_filedialog_refresh", TTRC("Refresh") }, { "ui_filedialog_show_hidden", TTRC("Show Hidden") }, { "ui_swap_input_direction ", TTRC("Swap Input Direction") }, + { "ui_unicode_start", TTRC("Start Unicode Character Input") }, { "", ""} /* clang-format on */ }; @@ -754,6 +755,10 @@ const HashMap>> &InputMap::get_builtins() { inputs.push_back(InputEventKey::create_reference(Key::KP_ENTER)); default_builtin_cache.insert("ui_text_submit", inputs); + inputs = List>(); + inputs.push_back(InputEventKey::create_reference(Key::U | KeyModifierMask::CTRL | KeyModifierMask::SHIFT)); + default_builtin_cache.insert("ui_unicode_start", inputs); + // ///// UI Graph Shortcuts ///// inputs = List>(); diff --git a/doc/classes/EditorSpinSlider.xml b/doc/classes/EditorSpinSlider.xml index 783f1243e243..83c65b736e75 100644 --- a/doc/classes/EditorSpinSlider.xml +++ b/doc/classes/EditorSpinSlider.xml @@ -51,4 +51,12 @@ + + + Single texture representing both the up and down buttons. + + + Single texture representing both the up and down buttons, when the control is readonly or disabled. + + diff --git a/doc/classes/LineEdit.xml b/doc/classes/LineEdit.xml index 77fff2215794..ca51162efcc4 100644 --- a/doc/classes/LineEdit.xml +++ b/doc/classes/LineEdit.xml @@ -4,7 +4,14 @@ An input field for single-line text. - [LineEdit] provides an input field for editing a single line of text. It features many built-in shortcuts that are always available ([kbd]Ctrl[/kbd] here maps to [kbd]Cmd[/kbd] on macOS): + [LineEdit] provides an input field for editing a single line of text. + - When the [LineEdit] control is focused using the keyboard arrow keys, it will only gain focus and not enter edit mode. + - To enter edit mode, click on the control with the mouse or press the "ui_text_submit" action (default: [kbd]Enter[/kbd] or [kbd]Kp Enter[/kbd]). + - To exit edit mode, press "ui_text_submit" or "ui_cancel" (default: [kbd]Escape[/kbd]) actions. + - Check [method is_editing] and [signal editing_toggled] for more information. + [b]Important:[/b] + - Focusing the [LineEdit] with "ui_focus_next" (default: [kbd]Tab[/kbd]) or "ui_focus_prev" (default: [kbd]Shift + Tab[/kbd]) or [method Control.grab_focus] still enters edit mode (for compatibility). + [LineEdit] features many built-in shortcuts that are always available ([kbd]Ctrl[/kbd] here maps to [kbd]Cmd[/kbd] on macOS): - [kbd]Ctrl + C[/kbd]: Copy - [kbd]Ctrl + X[/kbd]: Cut - [kbd]Ctrl + V[/kbd] or [kbd]Ctrl + Y[/kbd]: Paste/"yank" @@ -30,6 +37,18 @@ + + + + Applies text from the [url=https://en.wikipedia.org/wiki/Input_method]Input Method Editor[/url] (IME) and closes the IME if it is open. + + + + + + Closes the [url=https://en.wikipedia.org/wiki/Input_method]Input Method Editor[/url] (IME) if it is open. Any text in the IME will be lost. + + @@ -126,6 +145,12 @@ Returns the selection end column. + + + + Returns [code]true[/code] if the user has text in the [url=https://en.wikipedia.org/wiki/Input_method]Input Method Editor[/url] (IME). + + @@ -139,6 +164,12 @@ Inserts [param text] at the caret. If the resulting value is longer than [member max_length], nothing happens. + + + + Returns whether the [LineEdit] is being edited. + + @@ -301,6 +332,12 @@ + + + + Emitted when the [LineEdit] switches in or out of edit mode. + + diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index aceafa2a16d6..acd7cd2cb211 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1388,6 +1388,10 @@ Default [InputEventAction] to undo the most recent action. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + + Default [InputEventAction] to start Unicode character hexadecimal code input in a text field. + [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. + Default [InputEventAction] to move up in the UI. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. diff --git a/doc/classes/SpinBox.xml b/doc/classes/SpinBox.xml index 03e247ec8a70..c3b06784e8fa 100644 --- a/doc/classes/SpinBox.xml +++ b/doc/classes/SpinBox.xml @@ -25,7 +25,7 @@ The above code will create a [SpinBox], disable context menu on it and set the text alignment to right. See [Range] class for more options over the [SpinBox]. [b]Note:[/b] With the [SpinBox]'s context menu disabled, you can right-click the bottom half of the spinbox to set the value to its minimum, while right-clicking the top half sets the value to its maximum. - [b]Note:[/b] [SpinBox] relies on an underlying [LineEdit] node. To theme a [SpinBox]'s background, add theme items for [LineEdit] and customize them. + [b]Note:[/b] [SpinBox] relies on an underlying [LineEdit] node. To theme a [SpinBox]'s background, add theme items for [LineEdit] and customize them. The [LineEdit] has the [code]SpinBoxInnerLineEdit[/code] theme variation, so that you can give it a distinct appearance from regular [LineEdit]s. [b]Note:[/b] If you want to implement drag and drop for the underlying [LineEdit], you can use [method Control.set_drag_forwarding] on the node returned by [method get_line_edit]. @@ -71,8 +71,98 @@ + + Down button icon modulation color, when the button is disabled. + + + Down button icon modulation color, when the button is hovered. + + + Down button icon modulation color. + + + Down button icon modulation color, when the button is being pressed. + + + Up button icon modulation color, when the button is disabled. + + + Up button icon modulation color, when the button is hovered. + + + Up button icon modulation color. + + + Up button icon modulation color, when the button is being pressed. + + + Vertical separation between the up and down buttons. + + + Width of the up and down buttons. If smaller than any icon set on the buttons, the respective icon may overlap neighboring elements, unless [theme_item set_min_buttons_width_from_icons] is different than [code]0[/code]. + + + Width of the horizontal separation between the text input field ([LineEdit]) and the buttons. + + + If not [code]0[/code], the minimum button width corresponds to the widest of all icons set on those buttons, even if [theme_item buttons_width] is smaller. + + + Down button icon, displayed in the middle of the down (value-decreasing) button. + + + Down button icon when the button is disabled. + + + Down button icon when the button is hovered. + + + Down button icon when the button is being pressed. + + + Up button icon, displayed in the middle of the up (value-increasing) button. + + + Up button icon when the button is disabled. + + + Up button icon when the button is hovered. + + + Up button icon when the button is being pressed. + - Sets a custom [Texture2D] for up and down arrows of the [SpinBox]. + Single texture representing both the up and down buttons icons. It is displayed in the middle of the buttons and does not change upon interaction. It is recommended to use individual [theme_item up] and [theme_item down] graphics for better usability. This can also be used as additional decoration between the two buttons. + + + Background style of the down button. + + + Background style of the down button when disabled. + + + Background style of the down button when hovered. + + + Background style of the down button when being pressed. + + + [StyleBox] drawn in the space occupied by the separation between the input field and the buttons. + + + Background style of the up button. + + + Background style of the up button when disabled. + + + Background style of the up button when hovered. + + + Background style of the up button when being pressed. + + + [StyleBox] drawn in the space occupied by the separation between the up and down buttons. diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index d425e38b5e65..48c9b0f71b65 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -90,7 +90,6 @@ void EditorPropertyText::_text_submitted(const String &p_string) { } if (text->has_focus()) { - text->release_focus(); _text_changed(p_string); } } diff --git a/editor/gui/editor_spin_slider.cpp b/editor/gui/editor_spin_slider.cpp index 5788ee1a5f7c..a3162fbd2c96 100644 --- a/editor/gui/editor_spin_slider.cpp +++ b/editor/gui/editor_spin_slider.cpp @@ -37,6 +37,7 @@ #include "editor/themes/editor_scale.h" #include "scene/gui/line_edit.h" #include "scene/gui/texture_rect.h" +#include "scene/theme/theme_db.h" bool EditorSpinSlider::is_text_field() const { return true; @@ -385,7 +386,7 @@ void EditorSpinSlider::_draw_spin_slider() { if (!hide_slider) { if (get_step() == 1) { - Ref updown2 = get_theme_icon(is_read_only() ? SNAME("updown_disabled") : SNAME("updown"), SNAME("SpinBox")); + Ref updown2 = is_read_only() ? theme_cache.updown_disabled_icon : theme_cache.updown_icon; int updown_vofs = (size.height - updown2->get_height()) / 2; if (rtl) { updown_offset = sb->get_margin(SIDE_LEFT); @@ -707,6 +708,9 @@ void EditorSpinSlider::_bind_methods() { ADD_SIGNAL(MethodInfo("ungrabbed")); ADD_SIGNAL(MethodInfo("value_focus_entered")); ADD_SIGNAL(MethodInfo("value_focus_exited")); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, EditorSpinSlider, updown_icon, "updown"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, EditorSpinSlider, updown_disabled_icon, "updown_disabled"); } void EditorSpinSlider::_ensure_input_popup() { diff --git a/editor/gui/editor_spin_slider.h b/editor/gui/editor_spin_slider.h index 6f35836989c2..4939cdaba0b9 100644 --- a/editor/gui/editor_spin_slider.h +++ b/editor/gui/editor_spin_slider.h @@ -88,6 +88,11 @@ class EditorSpinSlider : public Range { void _ensure_input_popup(); void _draw_spin_slider(); + struct ThemeCache { + Ref updown_icon; + Ref updown_disabled_icon; + } theme_cache; + protected: void _notification(int p_what); virtual void gui_input(const Ref &p_event) override; diff --git a/editor/icons/GuiSpinboxDown.svg b/editor/icons/GuiSpinboxDown.svg new file mode 100644 index 000000000000..f8f473ce1a42 --- /dev/null +++ b/editor/icons/GuiSpinboxDown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/GuiSpinboxUp.svg b/editor/icons/GuiSpinboxUp.svg new file mode 100644 index 000000000000..28bd0505d4be --- /dev/null +++ b/editor/icons/GuiSpinboxUp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp index 1aa361b5f6ef..93eac48dc28a 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1515,8 +1515,44 @@ void EditorThemeManager::_populate_standard_styles(const Ref &p_the } // SpinBox. - p_theme->set_icon("updown", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUpdown"), EditorStringName(EditorIcons))); - p_theme->set_icon("updown_disabled", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUpdownDisabled"), EditorStringName(EditorIcons))); + { + Ref empty_icon = memnew(ImageTexture); + p_theme->set_icon("updown", "SpinBox", empty_icon); + p_theme->set_icon("up", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("up_hover", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("up_pressed", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("up_disabled", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxUp"), EditorStringName(EditorIcons))); + p_theme->set_icon("down", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + p_theme->set_icon("down_hover", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + p_theme->set_icon("down_pressed", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + p_theme->set_icon("down_disabled", "SpinBox", p_theme->get_icon(SNAME("GuiSpinboxDown"), EditorStringName(EditorIcons))); + + p_theme->set_stylebox("up_background", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("up_background_hovered", "SpinBox", p_config.button_style_hover); + p_theme->set_stylebox("up_background_pressed", "SpinBox", p_config.button_style_pressed); + p_theme->set_stylebox("up_background_disabled", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("down_background", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("down_background_hovered", "SpinBox", p_config.button_style_hover); + p_theme->set_stylebox("down_background_pressed", "SpinBox", p_config.button_style_pressed); + p_theme->set_stylebox("down_background_disabled", "SpinBox", make_empty_stylebox()); + + p_theme->set_color("up_icon_modulate", "SpinBox", p_config.font_color); + p_theme->set_color("up_hover_icon_modulate", "SpinBox", p_config.font_hover_color); + p_theme->set_color("up_pressed_icon_modulate", "SpinBox", p_config.font_pressed_color); + p_theme->set_color("up_disabled_icon_modulate", "SpinBox", p_config.font_disabled_color); + p_theme->set_color("down_icon_modulate", "SpinBox", p_config.font_color); + p_theme->set_color("down_hover_icon_modulate", "SpinBox", p_config.font_hover_color); + p_theme->set_color("down_pressed_icon_modulate", "SpinBox", p_config.font_pressed_color); + p_theme->set_color("down_disabled_icon_modulate", "SpinBox", p_config.font_disabled_color); + + p_theme->set_stylebox("field_and_buttons_separator", "SpinBox", make_empty_stylebox()); + p_theme->set_stylebox("up_down_buttons_separator", "SpinBox", make_empty_stylebox()); + + p_theme->set_constant("buttons_vertical_separation", "SpinBox", 0); + p_theme->set_constant("field_and_buttons_separation", "SpinBox", 2); + p_theme->set_constant("buttons_width", "SpinBox", 16); + p_theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1); + } // ProgressBar. p_theme->set_stylebox("background", "ProgressBar", make_stylebox(p_theme->get_icon(SNAME("GuiProgressBar"), EditorStringName(EditorIcons)), 4, 4, 4, 4, 0, 0, 0, 0)); @@ -1904,6 +1940,10 @@ void EditorThemeManager::_populate_editor_styles(const Ref &p_theme editor_spin_label_bg->set_border_width_all(0); p_theme->set_stylebox("label_bg", "EditorSpinSlider", editor_spin_label_bg); + // TODO Use separate arrows instead like on SpinBox. Planned for a different PR. + p_theme->set_icon("updown", "EditorSpinSlider", p_theme->get_icon(SNAME("GuiSpinboxUpdown"), EditorStringName(EditorIcons))); + p_theme->set_icon("updown_disabled", "EditorSpinSlider", p_theme->get_icon(SNAME("GuiSpinboxUpdownDisabled"), EditorStringName(EditorIcons))); + // Launch Pad and Play buttons. Ref style_launch_pad = make_flat_stylebox(p_config.dark_color_1, 2 * EDSCALE, 0, 2 * EDSCALE, 0, p_config.corner_radius); style_launch_pad->set_corner_radius_all(p_config.corner_radius * EDSCALE); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 4071f9dea173..e83374a1237f 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -3046,7 +3046,7 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win XWindowAttributes xwa; XSync(x11_display, False); XGetWindowAttributes(x11_display, wd.x11_xim_window, &xwa); - if (xwa.map_state == IsViewable) { + if (xwa.map_state == IsViewable && _window_focus_check()) { _set_input_focus(wd.x11_xim_window, RevertToParent); } XSetICFocus(wd.xic); @@ -4315,7 +4315,7 @@ bool DisplayServerX11::_window_focus_check() { bool has_focus = false; for (const KeyValue &wid : windows) { - if (wid.value.x11_window == focused_window) { + if (wid.value.x11_window == focused_window || (wid.value.xic && wid.value.ime_active && wid.value.x11_xim_window == focused_window)) { has_focus = true; break; } diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index cf8a39341d78..a84031826a85 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -5393,7 +5393,7 @@ void DisplayServerWindows::_process_key_events() { k->set_physical_keycode(physical_keycode); k->set_key_label(key_label); k->set_unicode(fix_unicode(unicode)); - if (k->get_unicode() && ke.altgr) { + if (k->get_unicode() && ke.altgr && windows[ke.window_id].ime_active) { k->set_alt_pressed(false); k->set_ctrl_pressed(false); } @@ -5469,7 +5469,7 @@ void DisplayServerWindows::_process_key_events() { } k->set_unicode(fix_unicode(unicode)); } - if (k->get_unicode() && ke.altgr) { + if (k->get_unicode() && ke.altgr && windows[ke.window_id].ime_active) { k->set_alt_pressed(false); k->set_ctrl_pressed(false); } diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 6a5c04b1eed4..45e2f8222a01 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -46,6 +46,113 @@ #include "editor/editor_settings.h" #endif +void LineEdit::_edit() { + if (!is_inside_tree()) { + return; + } + + if (!has_focus()) { + grab_focus(); + } + + if (!editable || editing) { + return; + } + + editing = true; + _validate_caret_can_draw(); + + show_virtual_keyboard(); + queue_redraw(); + emit_signal(SNAME("editing_toggled"), true); +} + +void LineEdit::_unedit() { + if (!editing) { + return; + } + + editing = false; + _validate_caret_can_draw(); + + apply_ime(); + set_caret_column(caret_column); // Update scroll_offset. + + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + DisplayServer::get_singleton()->virtual_keyboard_hide(); + } + + if (deselect_on_focus_loss_enabled && !selection.drag_attempt) { + deselect(); + } + + emit_signal(SNAME("editing_toggled"), false); +} + +bool LineEdit::is_editing() const { + return editing; +} + +void LineEdit::_close_ime_window() { + DisplayServer::WindowID wid = get_window() ? get_window()->get_window_id() : DisplayServer::INVALID_WINDOW_ID; + if (wid == DisplayServer::INVALID_WINDOW_ID || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { + return; + } + DisplayServer::get_singleton()->window_set_ime_position(Point2(), wid); + DisplayServer::get_singleton()->window_set_ime_active(false, wid); +} + +void LineEdit::_update_ime_window_position() { + DisplayServer::WindowID wid = get_window() ? get_window()->get_window_id() : DisplayServer::INVALID_WINDOW_ID; + if (wid == DisplayServer::INVALID_WINDOW_ID || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { + return; + } + DisplayServer::get_singleton()->window_set_ime_active(true, wid); + Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + theme_cache.font->get_height(theme_cache.font_size)) / 2) + get_global_position(); + if (get_window()->get_embedder()) { + pos += get_viewport()->get_popup_base_transform().get_origin(); + } + // The window will move to the updated position the next time the IME is updated, not immediately. + DisplayServer::get_singleton()->window_set_ime_position(pos, wid); +} + +bool LineEdit::has_ime_text() const { + return !ime_text.is_empty(); +} + +void LineEdit::cancel_ime() { + if (!has_ime_text()) { + _close_ime_window(); + return; + } + ime_text = String(); + ime_selection = Vector2i(); + alt_start = false; + alt_start_no_hold = false; + _close_ime_window(); + _shape(); +} + +void LineEdit::apply_ime() { + if (!has_ime_text()) { + _close_ime_window(); + return; + } + + // Force apply the current IME text. + if (alt_start || alt_start_no_hold) { + cancel_ime(); + if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { + char32_t ucodestr[2] = { (char32_t)alt_code, 0 }; + insert_text_at_caret(ucodestr); + } + } else { + String insert_ime_text = ime_text; + cancel_ime(); + insert_text_at_caret(insert_ime_text); + } +} + void LineEdit::_swap_current_input_direction() { if (input_direction == TEXT_DIRECTION_LTR) { input_direction = TEXT_DIRECTION_RTL; @@ -53,7 +160,6 @@ void LineEdit::_swap_current_input_direction() { input_direction = TEXT_DIRECTION_LTR; } set_caret_column(get_caret_column()); - queue_redraw(); } void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) { @@ -69,10 +175,15 @@ void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < cc) { - cc = words[i]; - break; + if (words.is_empty() || cc <= words[0]) { + // Move to the start when there are no more words. + cc = 0; + } else { + for (int i = words.size() - 2; i >= 0; i = i - 2) { + if (words[i] < cc) { + cc = words[i]; + break; + } } } @@ -102,10 +213,15 @@ void LineEdit::_move_caret_right(bool p_select, bool p_move_by_word) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > cc) { - cc = words[i]; - break; + if (words.is_empty() || cc >= words[words.size() - 1]) { + // Move to the end when there are no more words. + cc = text.length(); + } else { + for (int i = 1; i < words.size(); i = i + 2) { + if (words[i] > cc) { + cc = words[i]; + break; + } } } @@ -160,10 +276,15 @@ void LineEdit::_backspace(bool p_word, bool p_all_to_left) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < cc) { - cc = words[i]; - break; + if (words.is_empty() || cc <= words[0]) { + // Delete to the start when there are no more words. + cc = 0; + } else { + for (int i = words.size() - 2; i >= 0; i = i - 2) { + if (words[i] < cc) { + cc = words[i]; + break; + } } } @@ -199,10 +320,15 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) { if (p_word) { int cc = caret_column; PackedInt32Array words = TS->shaped_text_get_word_breaks(text_rid); - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > cc) { - cc = words[i]; - break; + if (words.is_empty() || cc >= words[words.size() - 1]) { + // Delete to the end when there are no more words. + cc = text.length(); + } else { + for (int i = 1; i < words.size(); i = i + 2) { + if (words[i] > cc) { + cc = words[i]; + break; + } } } @@ -221,6 +347,11 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) { } void LineEdit::unhandled_key_input(const Ref &p_event) { + // Return to prevent editing if just focused. + if (!editing) { + return; + } + Ref k = p_event; if (k.is_valid()) { @@ -247,25 +378,40 @@ void LineEdit::gui_input(const Ref &p_event) { Ref b = p_event; if (b.is_valid()) { - if (ime_text.length() != 0) { - // Ignore mouse clicks in IME input mode. - return; - } - if (b->is_pressed() && b->get_button_index() == MouseButton::RIGHT && context_menu_enabled) { - _update_context_menu(); - menu->set_position(get_screen_position() + get_local_mouse_position()); - menu->reset_size(); - menu->popup(); - grab_focus(); + if (b->is_pressed() && b->get_button_index() == MouseButton::RIGHT) { + apply_ime(); + + if (editable && !selection.enabled) { + set_caret_at_pixel_pos(b->get_position().x); + } + + if (context_menu_enabled) { + _update_context_menu(); + menu->set_position(get_screen_position() + get_local_mouse_position()); + menu->reset_size(); + menu->popup(); + } + + if (editable && !editing) { + _edit(); + } + accept_event(); return; } - if (is_middle_mouse_paste_enabled() && b->is_pressed() && b->get_button_index() == MouseButton::MIDDLE && is_editable() && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { + if (editable && is_middle_mouse_paste_enabled() && b->is_pressed() && b->get_button_index() == MouseButton::MIDDLE && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { + apply_ime(); + String paste_buffer = DisplayServer::get_singleton()->clipboard_get_primary().strip_escapes(); deselect(); set_caret_at_pixel_pos(b->get_position().x); + + if (!editing) { + _edit(); + } + if (!paste_buffer.is_empty()) { insert_text_at_caret(paste_buffer); @@ -276,7 +422,6 @@ void LineEdit::gui_input(const Ref &p_event) { text_changed_dirty = true; } } - grab_focus(); accept_event(); return; } @@ -285,10 +430,15 @@ void LineEdit::gui_input(const Ref &p_event) { return; } - _reset_caret_blink_timer(); + if (editing) { + _reset_caret_blink_timer(); + } + if (b->is_pressed()) { + apply_ime(); + accept_event(); // Don't pass event further when clicked on text field. - if (!text.is_empty() && is_editable() && _is_over_clear_button(b->get_position())) { + if (editable && !text.is_empty() && _is_over_clear_button(b->get_position())) { clear_button_status.press_attempt = true; clear_button_status.pressing_inside = true; queue_redraw(); @@ -311,7 +461,7 @@ void LineEdit::gui_input(const Ref &p_event) { const int triple_click_tolerance = 5; const bool is_triple_click = !b->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && b->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance; - if (is_triple_click && text.length()) { + if (is_triple_click && !text.is_empty()) { // Triple-click select all. selection.enabled = true; selection.begin = 0; @@ -358,13 +508,17 @@ void LineEdit::gui_input(const Ref &p_event) { } } + if (editable && !editing) { + _edit(); + return; + } queue_redraw(); } else { if (selection.enabled && !pass && b->get_button_index() == MouseButton::LEFT && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } - if (!text.is_empty() && is_editable() && clear_button_enabled) { + if (editable && !text.is_empty() && clear_button_enabled) { bool press_attempt = clear_button_status.press_attempt; clear_button_status.press_attempt = false; if (press_attempt && clear_button_status.pressing_inside && _is_over_clear_button(b->get_position())) { @@ -397,7 +551,7 @@ void LineEdit::gui_input(const Ref &p_event) { Ref m = p_event; if (m.is_valid()) { - if (!text.is_empty() && is_editable() && clear_button_enabled) { + if (editable && !text.is_empty() && clear_button_enabled) { bool last_press_inside = clear_button_status.pressing_inside; clear_button_status.pressing_inside = clear_button_status.press_attempt && _is_over_clear_button(m->get_position()); if (last_press_inside != clear_button_status.pressing_inside) { @@ -443,221 +597,305 @@ void LineEdit::gui_input(const Ref &p_event) { Ref k = p_event; - if (k.is_valid()) { - if (!k->is_pressed()) { - if (alt_start && k->get_keycode() == Key::ALT) { - alt_start = false; - if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { - char32_t ucodestr[2] = { (char32_t)alt_code, 0 }; - insert_text_at_caret(ucodestr); - } - accept_event(); - return; - } - return; - } + if (k.is_null()) { + return; + } - // Alt + Unicode input: - if (k->is_alt_pressed()) { - if (!alt_start) { - if (k->get_keycode() == Key::KP_ADD) { - alt_start = true; - alt_code = 0; - accept_event(); - return; - } - } else { - if (k->get_keycode() >= Key::KEY_0 && k->get_keycode() <= Key::KEY_9) { - alt_code = alt_code << 4; - alt_code += (uint32_t)(k->get_keycode() - Key::KEY_0); - } - if (k->get_keycode() >= Key::KP_0 && k->get_keycode() <= Key::KP_9) { - alt_code = alt_code << 4; - alt_code += (uint32_t)(k->get_keycode() - Key::KP_0); - } - if (k->get_keycode() >= Key::A && k->get_keycode() <= Key::F) { - alt_code = alt_code << 4; - alt_code += (uint32_t)(k->get_keycode() - Key::A) + 10; - } - accept_event(); - return; - } - } + if (editable && !editing && k->is_action_pressed("ui_text_submit", false)) { + _edit(); + return; + } - if (context_menu_enabled) { - if (k->is_action("ui_menu", true)) { - _update_context_menu(); - Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + theme_cache.font->get_height(theme_cache.font_size)) / 2); - menu->set_position(get_screen_position() + pos); - menu->reset_size(); - menu->popup(); - menu->grab_focus(); + if (!editing) { + return; + } - accept_event(); - return; - } + // Start Unicode input (hold). + if (k->is_alt_pressed() && k->get_keycode() == Key::KP_ADD && !alt_start && !alt_start_no_hold) { + if (selection.enabled) { + selection_delete(); } + alt_start = true; + alt_code = 0; + ime_text = "u"; + ime_selection = Vector2i(0, -1); + _shape(); + queue_redraw(); + accept_event(); + return; + } - // Default is ENTER and KP_ENTER. Cannot use ui_accept as default includes SPACE. - if (k->is_action("ui_text_submit", false)) { - emit_signal(SNAME("text_submitted"), text); - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_hide(); - } - accept_event(); - return; + // Start Unicode input (press). + if (k->is_action("ui_unicode_start", true) && !alt_start && !alt_start_no_hold) { + if (selection.enabled) { + selection_delete(); } + alt_start_no_hold = true; + alt_code = 0; + ime_text = "u"; + ime_selection = Vector2i(0, -1); + _shape(); + queue_redraw(); + accept_event(); + return; + } - if (k->is_action("ui_cancel")) { - callable_mp((Control *)this, &Control::release_focus).call_deferred(); - accept_event(); - return; + // Update Unicode input. + if (k->is_pressed() && ((k->is_alt_pressed() && alt_start) || alt_start_no_hold)) { + if (k->get_keycode() >= Key::KEY_0 && k->get_keycode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KEY_0); + } else if (k->get_keycode() >= Key::KP_0 && k->get_keycode() <= Key::KP_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KP_0); + } else if (k->get_keycode() >= Key::A && k->get_keycode() <= Key::F) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::A) + 10; + } else if ((Key)k->get_unicode() >= Key::KEY_0 && (Key)k->get_unicode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)((Key)k->get_unicode() - Key::KEY_0); + } else if ((Key)k->get_unicode() >= Key::A && (Key)k->get_unicode() <= Key::F) { + alt_code = alt_code << 4; + alt_code += (uint32_t)((Key)k->get_unicode() - Key::A) + 10; + } else if (k->get_physical_keycode() >= Key::KEY_0 && k->get_physical_keycode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_physical_keycode() - Key::KEY_0); } + if (k->get_keycode() == Key::BACKSPACE) { + alt_code = alt_code >> 4; + } + if (alt_code > 0x10ffff) { + alt_code = 0x10ffff; + } + if (alt_code > 0) { + ime_text = vformat("u%s", String::num_int64(alt_code, 16, true)); + } else { + ime_text = "u"; + } + ime_selection = Vector2i(0, -1); + _shape(); + queue_redraw(); + accept_event(); + return; + } - if (is_shortcut_keys_enabled()) { - if (k->is_action("ui_copy", true)) { - copy_text(); - accept_event(); - return; - } - - if (k->is_action("ui_text_select_all", true)) { - select(); - accept_event(); - return; - } - - // Cut / Paste - if (k->is_action("ui_cut", true)) { - cut_text(); - accept_event(); - return; - } + // Submit Unicode input. + if ((!k->is_pressed() && alt_start && k->get_keycode() == Key::ALT) || (alt_start_no_hold && (k->is_action("ui_text_submit", true) || k->is_action("ui_accept", true)))) { + alt_start = false; + alt_start_no_hold = false; + if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { + ime_text = String(); + ime_selection = Vector2i(); + char32_t ucodestr[2] = { (char32_t)alt_code, 0 }; + insert_text_at_caret(ucodestr); + } else { + ime_text = String(); + ime_selection = Vector2i(); + _shape(); + } + queue_redraw(); + accept_event(); + return; + } - if (k->is_action("ui_paste", true)) { - paste_text(); - accept_event(); - return; - } + // Cancel Unicode input. + if (alt_start_no_hold && k->is_action("ui_cancel", true)) { + alt_start = false; + alt_start_no_hold = false; + ime_text = String(); + ime_selection = Vector2i(); + _shape(); + queue_redraw(); + accept_event(); + return; + } - // Undo / Redo - if (k->is_action("ui_undo", true)) { - undo(); - accept_event(); - return; - } + if (!k->is_pressed()) { + return; + } - if (k->is_action("ui_redo", true)) { - redo(); - accept_event(); - return; - } - } + // Open context menu. + if (context_menu_enabled) { + if (k->is_action("ui_menu", true)) { + _update_context_menu(); + Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + theme_cache.font->get_height(theme_cache.font_size)) / 2); + menu->set_position(get_screen_position() + pos); + menu->reset_size(); + menu->popup(); + menu->grab_focus(); - // BACKSPACE - if (k->is_action("ui_text_backspace_all_to_left", true)) { - _backspace(false, true); - accept_event(); - return; - } - if (k->is_action("ui_text_backspace_word", true)) { - _backspace(true); - accept_event(); - return; - } - if (k->is_action("ui_text_backspace", true)) { - _backspace(); accept_event(); return; } + } - // DELETE - if (k->is_action("ui_text_delete_all_to_right", true)) { - _delete(false, true); - accept_event(); - return; - } - if (k->is_action("ui_text_delete_word", true)) { - _delete(true); - accept_event(); - return; - } - if (k->is_action("ui_text_delete", true)) { - _delete(); - accept_event(); - return; + // Default is ENTER and KP_ENTER. Cannot use ui_accept as default includes SPACE. + if (k->is_action_pressed("ui_text_submit")) { + emit_signal(SNAME("text_submitted"), text); + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + DisplayServer::get_singleton()->virtual_keyboard_hide(); } - // Cursor Movement + if (editing) { + _unedit(); + } - k = k->duplicate(); - bool shift_pressed = k->is_shift_pressed(); - // Remove shift or else actions will not match. Use above variable for selection. - k->set_shift_pressed(false); + accept_event(); + return; + } - if (k->is_action("ui_text_caret_word_left", true)) { - _move_caret_left(shift_pressed, true); - accept_event(); - return; + if (k->is_action("ui_cancel")) { + if (editing) { + _unedit(); } - if (k->is_action("ui_text_caret_left", true)) { - _move_caret_left(shift_pressed); + + accept_event(); + return; + } + + if (is_shortcut_keys_enabled()) { + if (k->is_action("ui_copy", true)) { + copy_text(); accept_event(); return; } - if (k->is_action("ui_text_caret_word_right", true)) { - _move_caret_right(shift_pressed, true); + + if (k->is_action("ui_text_select_all", true)) { + select(); accept_event(); return; } - if (k->is_action("ui_text_caret_right", true)) { - _move_caret_right(shift_pressed, false); + + // Cut / Paste + if (k->is_action("ui_cut", true)) { + cut_text(); accept_event(); return; } - // Up = Home, Down = End - if (k->is_action("ui_text_caret_up", true) || k->is_action("ui_text_caret_line_start", true) || k->is_action("ui_text_caret_page_up", true)) { - _move_caret_start(shift_pressed); + if (k->is_action("ui_paste", true)) { + paste_text(); accept_event(); return; } - if (k->is_action("ui_text_caret_down", true) || k->is_action("ui_text_caret_line_end", true) || k->is_action("ui_text_caret_page_down", true)) { - _move_caret_end(shift_pressed); + + // Undo / Redo + if (k->is_action("ui_undo", true)) { + undo(); accept_event(); return; } - // Misc - if (k->is_action("ui_swap_input_direction", true)) { - _swap_current_input_direction(); + if (k->is_action("ui_redo", true)) { + redo(); accept_event(); return; } + } + + // BACKSPACE + if (k->is_action("ui_text_backspace_all_to_left", true)) { + _backspace(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace_word", true)) { + _backspace(true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + _backspace(); + accept_event(); + return; + } - _reset_caret_blink_timer(); + // DELETE + if (k->is_action("ui_text_delete_all_to_right", true)) { + _delete(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_word", true)) { + _delete(true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete", true)) { + _delete(); + accept_event(); + return; + } - // Allow unicode handling if: - // * No Modifiers are pressed (except shift) - bool allow_unicode_handling = !(k->is_ctrl_pressed() || k->is_alt_pressed() || k->is_meta_pressed()); + // Cursor Movement - if (allow_unicode_handling && editable && k->get_unicode() >= 32) { - // Handle Unicode if no modifiers are active. - selection_delete(); - char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; - int prev_len = text.length(); - insert_text_at_caret(ucodestr); - if (text.length() != prev_len) { - if (!text_changed_dirty) { - if (is_inside_tree()) { - callable_mp(this, &LineEdit::_text_changed).call_deferred(); - } - text_changed_dirty = true; + k = k->duplicate(); + bool shift_pressed = k->is_shift_pressed(); + // Remove shift or else actions will not match. Use above variable for selection. + k->set_shift_pressed(false); + + if (k->is_action("ui_text_caret_word_left", true)) { + _move_caret_left(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_left", true)) { + _move_caret_left(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_word_right", true)) { + _move_caret_right(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_right", true)) { + _move_caret_right(shift_pressed, false); + accept_event(); + return; + } + + // Up = Home, Down = End + if (k->is_action("ui_text_caret_up", true) || k->is_action("ui_text_caret_line_start", true) || k->is_action("ui_text_caret_page_up", true)) { + _move_caret_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_down", true) || k->is_action("ui_text_caret_line_end", true) || k->is_action("ui_text_caret_page_down", true)) { + _move_caret_end(shift_pressed); + accept_event(); + return; + } + + // Misc + if (k->is_action("ui_swap_input_direction", true)) { + _swap_current_input_direction(); + accept_event(); + return; + } + + _reset_caret_blink_timer(); + + // Allow unicode handling if: + // * No Modifiers are pressed (except shift) + bool allow_unicode_handling = !(k->is_ctrl_pressed() || k->is_alt_pressed() || k->is_meta_pressed()); + + if (allow_unicode_handling && editable && k->get_unicode() >= 32) { + // Handle Unicode if no modifiers are active. + selection_delete(); + char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; + int prev_len = text.length(); + insert_text_at_caret(ucodestr); + if (text.length() != prev_len) { + if (!text_changed_dirty) { + if (is_inside_tree()) { + callable_mp(this, &LineEdit::_text_changed).call_deferred(); } + text_changed_dirty = true; } - accept_event(); - return; } + accept_event(); + return; } } @@ -705,7 +943,9 @@ bool LineEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { Control::drop_data(p_point, p_data); - if (p_data.get_type() == Variant::STRING && is_editable()) { + if (p_data.is_string() && is_editable()) { + apply_ime(); + set_caret_at_pixel_pos(p_point.x); int caret_column_tmp = caret_column; bool is_inside_sel = selection.enabled && caret_column >= selection.begin && caret_column <= selection.end; @@ -1088,22 +1328,12 @@ void LineEdit::_notification(int p_what) { } } - if (has_focus()) { - DisplayServer::WindowID wid = get_window() ? get_window()->get_window_id() : DisplayServer::INVALID_WINDOW_ID; - if (wid != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { - DisplayServer::get_singleton()->window_set_ime_active(true, wid); - Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + theme_cache.font->get_height(theme_cache.font_size)) / 2) + get_global_position(); - if (get_window()->get_embedder()) { - pos += get_viewport()->get_popup_base_transform().get_origin(); - } - DisplayServer::get_singleton()->window_set_ime_position(pos, wid); - } + if (editing) { + _update_ime_window_position(); } } break; case NOTIFICATION_FOCUS_ENTER: { - _validate_caret_can_draw(); - if (select_all_on_focus) { if (Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) { // Select all when the mouse button is up. @@ -1113,43 +1343,20 @@ void LineEdit::_notification(int p_what) { } } - DisplayServer::WindowID wid = get_window() ? get_window()->get_window_id() : DisplayServer::INVALID_WINDOW_ID; - if (wid != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { - DisplayServer::get_singleton()->window_set_ime_active(true, wid); - Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + theme_cache.font->get_height(theme_cache.font_size)) / 2) + get_global_position(); - if (get_window()->get_embedder()) { - pos += get_viewport()->get_popup_base_transform().get_origin(); - } - DisplayServer::get_singleton()->window_set_ime_position(pos, wid); + // Only allow editing if the LineEdit is not focused with arrow keys. + if (!(Input::get_singleton()->is_action_pressed("ui_up") || Input::get_singleton()->is_action_pressed("ui_down") || Input::get_singleton()->is_action_pressed("ui_left") || Input::get_singleton()->is_action_pressed("ui_right"))) { + _edit(); } - - show_virtual_keyboard(); } break; case NOTIFICATION_FOCUS_EXIT: { - _validate_caret_can_draw(); - - DisplayServer::WindowID wid = get_window() ? get_window()->get_window_id() : DisplayServer::INVALID_WINDOW_ID; - if (wid != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { - DisplayServer::get_singleton()->window_set_ime_position(Point2(), wid); - DisplayServer::get_singleton()->window_set_ime_active(false, wid); - } - ime_text = ""; - ime_selection = Point2(); - _shape(); - set_caret_column(caret_column); // Update scroll_offset. - - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_hide(); - } - - if (deselect_on_focus_loss_enabled && !selection.drag_attempt) { - deselect(); + if (editing) { + _unedit(); } } break; case MainLoop::NOTIFICATION_OS_IME_UPDATE: { - if (has_focus()) { + if (editing) { ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); @@ -1159,8 +1366,6 @@ void LineEdit::_notification(int p_what) { _shape(); set_caret_column(caret_column); // Update scroll_offset. - - queue_redraw(); } } break; @@ -1400,7 +1605,7 @@ Vector2 LineEdit::get_caret_pixel_pos() { Vector2 ret; CaretInfo caret; // Get position of the start of caret. - if (ime_text.length() != 0 && ime_selection.x != 0) { + if (!ime_text.is_empty() && ime_selection.x != 0) { caret = TS->shaped_text_get_carets(text_rid, caret_column + ime_selection.x); } else { caret = TS->shaped_text_get_carets(text_rid, caret_column); @@ -1413,7 +1618,7 @@ Vector2 LineEdit::get_caret_pixel_pos() { } // Get position of the end of caret. - if (ime_text.length() != 0) { + if (!ime_text.is_empty()) { if (ime_selection.y != 0) { caret = TS->shaped_text_get_carets(text_rid, caret_column + ime_selection.x + ime_selection.y); } else { @@ -1506,11 +1711,11 @@ void LineEdit::_validate_caret_can_draw() { draw_caret = true; caret_blink_timer = 0.0; } - caret_can_draw = editable && (window_has_focus || (menu && menu->has_focus())) && (has_focus() || caret_force_displayed); + caret_can_draw = editing && (window_has_focus || (menu && menu->has_focus())) && (has_focus() || caret_force_displayed); } void LineEdit::delete_char() { - if ((text.length() <= 0) || (caret_column == 0)) { + if (text.is_empty() || caret_column == 0) { return; } @@ -1642,12 +1847,14 @@ void LineEdit::clear() { _text_changed(); // This should reset virtual keyboard state if needed. - if (has_focus()) { + if (editing) { show_virtual_keyboard(); } } void LineEdit::show_virtual_keyboard() { + _update_ime_window_position(); + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { if (selection.enabled) { DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), DisplayServer::VirtualKeyboardType(virtual_keyboard_type), max_length, selection.begin, selection.end); @@ -1815,7 +2022,8 @@ Size2 LineEdit::get_minimum_size() const { Size2 min_size; // Minimum size of text. - float em_space_size = font->get_char_size('M', font_size).x; + // W is wider than M in most fonts, Using M may result in hiding the last digit when using float values in SpinBox, ie. ColorPicker RAW values. + float em_space_size = font->get_char_size('W', font_size).x; min_size.width = theme_cache.minimum_character_width * em_space_size; if (expand_to_text_length) { @@ -1913,7 +2121,8 @@ void LineEdit::select_all() { return; } - if (!text.length()) { + if (text.is_empty()) { + set_caret_column(0); return; } @@ -1929,6 +2138,10 @@ void LineEdit::set_editable(bool p_editable) { } editable = p_editable; + + if (!editable && editing) { + _unedit(); + } _validate_caret_can_draw(); update_minimum_size(); @@ -2309,6 +2522,7 @@ void LineEdit::_emit_text_change() { emit_signal(SceneStringName(text_changed), text); text_changed_dirty = false; } + PackedStringArray LineEdit::get_configuration_warnings() const { PackedStringArray warnings = Control::get_configuration_warnings(); if (secret_character.length() > 1) { @@ -2328,13 +2542,13 @@ void LineEdit::_shape() { TS->shaped_text_clear(text_rid); String t; - if (text.length() == 0 && ime_text.length() == 0) { + if (text.is_empty() && ime_text.is_empty()) { t = placeholder_translated; } else if (pass) { - String s = (secret_character.length() > 0) ? secret_character.left(1) : U"•"; + String s = secret_character.is_empty() ? U"•" : secret_character.left(1); t = s.repeat(text.length() + ime_text.length()); } else { - if (ime_text.length() > 0) { + if (!ime_text.is_empty()) { t = text.substr(0, caret_column) + ime_text + text.substr(caret_column, text.length()); } else { t = text; @@ -2540,9 +2754,14 @@ void LineEdit::_validate_property(PropertyInfo &p_property) const { } void LineEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("has_ime_text"), &LineEdit::has_ime_text); + ClassDB::bind_method(D_METHOD("cancel_ime"), &LineEdit::cancel_ime); + ClassDB::bind_method(D_METHOD("apply_ime"), &LineEdit::apply_ime); + ClassDB::bind_method(D_METHOD("set_horizontal_alignment", "alignment"), &LineEdit::set_horizontal_alignment); ClassDB::bind_method(D_METHOD("get_horizontal_alignment"), &LineEdit::get_horizontal_alignment); + ClassDB::bind_method(D_METHOD("is_editing"), &LineEdit::is_editing); ClassDB::bind_method(D_METHOD("clear"), &LineEdit::clear); ClassDB::bind_method(D_METHOD("select", "from", "to"), &LineEdit::select, DEFVAL(0), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("select_all"), &LineEdit::select_all); @@ -2623,6 +2842,7 @@ void LineEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("text_changed", PropertyInfo(Variant::STRING, "new_text"))); ADD_SIGNAL(MethodInfo("text_change_rejected", PropertyInfo(Variant::STRING, "rejected_substring"))); ADD_SIGNAL(MethodInfo("text_submitted", PropertyInfo(Variant::STRING, "new_text"))); + ADD_SIGNAL(MethodInfo("editing_toggled", PropertyInfo(Variant::BOOL, "toggled_on"))); BIND_ENUM_CONSTANT(MENU_CUT); BIND_ENUM_CONSTANT(MENU_COPY); diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index 7bf980a20a8a..cdbad088b55b 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -87,11 +87,13 @@ class LineEdit : public Control { private: HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT; + bool editing = false; bool editable = false; bool pass = false; bool text_changed_dirty = false; bool alt_start = false; + bool alt_start_no_hold = false; uint32_t alt_code = 0; String undo_text; @@ -206,6 +208,12 @@ class LineEdit : public Control { float base_scale = 1.0; } theme_cache; + void _edit(); + void _unedit(); + + void _close_ime_window(); + void _update_ime_window_position(); + void _clear_undo_stack(); void _clear_redo(); void _create_undo_state(); @@ -258,6 +266,12 @@ class LineEdit : public Control { virtual void gui_input(const Ref &p_event) override; public: + bool is_editing() const; + + bool has_ime_text() const; + void cancel_ime(); + void apply_ime(); + void set_horizontal_alignment(HorizontalAlignment p_alignment); HorizontalAlignment get_horizontal_alignment() const; diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index 865cc36647f1..5c2c7890bd9a 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -39,7 +39,7 @@ Size2 SpinBox::get_minimum_size() const { Size2 ms = line_edit->get_combined_minimum_size(); - ms.width += last_w; + ms.width += sizing_cache.buttons_block_width; return ms; } @@ -49,7 +49,7 @@ void SpinBox::_update_text(bool p_keep_line_edit) { value = TS->format_number(value); } - if (!line_edit->has_focus()) { + if (!line_edit->is_editing()) { if (!prefix.is_empty()) { value = prefix + " " + value; } @@ -131,7 +131,7 @@ void SpinBox::_range_click_timeout() { } } -void SpinBox::_release_mouse() { +void SpinBox::_release_mouse_from_drag_mode() { if (drag.enabled) { drag.enabled = false; Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_HIDDEN); @@ -140,6 +140,14 @@ void SpinBox::_release_mouse() { } } +void SpinBox::_mouse_exited() { + if (state_cache.up_button_hovered || state_cache.down_button_hovered) { + state_cache.up_button_hovered = false; + state_cache.down_button_hovered = false; + queue_redraw(); + } +} + void SpinBox::gui_input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); @@ -147,18 +155,36 @@ void SpinBox::gui_input(const Ref &p_event) { return; } + Ref me = p_event; Ref mb = p_event; + Ref mm = p_event; double step = get_custom_arrow_step() != 0.0 ? get_custom_arrow_step() : get_step(); - if (mb.is_valid() && mb->is_pressed()) { - bool up = mb->get_position().y < (get_size().height / 2); + Vector2 mpos; + bool mouse_on_up_button = false; + bool mouse_on_down_button = false; + if (mb.is_valid() || mm.is_valid()) { + Rect2 up_button_rc = Rect2(sizing_cache.buttons_left, 0, sizing_cache.buttons_width, sizing_cache.button_up_height); + Rect2 down_button_rc = Rect2(sizing_cache.buttons_left, sizing_cache.second_button_top, sizing_cache.buttons_width, sizing_cache.button_down_height); + + mpos = me->get_position(); + + mouse_on_up_button = up_button_rc.has_point(mpos); + mouse_on_down_button = down_button_rc.has_point(mpos); + } + if (mb.is_valid() && mb->is_pressed()) { switch (mb->get_button_index()) { case MouseButton::LEFT: { line_edit->grab_focus(); - set_value(get_value() + (up ? step : -step)); + if (mouse_on_up_button || mouse_on_down_button) { + set_value(get_value() + (mouse_on_up_button ? step : -step)); + } + state_cache.up_button_pressed = mouse_on_up_button; + state_cache.down_button_pressed = mouse_on_down_button; + queue_redraw(); range_click_timer->set_wait_time(0.6); range_click_timer->set_one_shot(true); @@ -169,16 +195,18 @@ void SpinBox::gui_input(const Ref &p_event) { } break; case MouseButton::RIGHT: { line_edit->grab_focus(); - set_value((up ? get_max() : get_min())); + if (mouse_on_up_button || mouse_on_down_button) { + set_value(mouse_on_up_button ? get_max() : get_min()); + } } break; case MouseButton::WHEEL_UP: { - if (line_edit->has_focus()) { + if (line_edit->is_editing()) { set_value(get_value() + step * mb->get_factor()); accept_event(); } } break; case MouseButton::WHEEL_DOWN: { - if (line_edit->has_focus()) { + if (line_edit->is_editing()) { set_value(get_value() - step * mb->get_factor()); accept_event(); } @@ -189,14 +217,30 @@ void SpinBox::gui_input(const Ref &p_event) { } if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + if (state_cache.up_button_pressed || state_cache.down_button_pressed) { + state_cache.up_button_pressed = false; + state_cache.down_button_pressed = false; + queue_redraw(); + } + //set_default_cursor_shape(CURSOR_ARROW); range_click_timer->stop(); - _release_mouse(); + _release_mouse_from_drag_mode(); drag.allowed = false; line_edit->clear_pending_select_all_on_focus(); } - Ref mm = p_event; + if (mm.is_valid()) { + bool old_up_hovered = state_cache.up_button_hovered; + bool old_down_hovered = state_cache.down_button_hovered; + + state_cache.up_button_hovered = mouse_on_up_button; + state_cache.down_button_hovered = mouse_on_down_button; + + if (old_up_hovered != state_cache.up_button_hovered || old_down_hovered != state_cache.down_button_hovered) { + queue_redraw(); + } + } if (mm.is_valid() && (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) { if (drag.enabled) { @@ -212,71 +256,153 @@ void SpinBox::gui_input(const Ref &p_event) { } } -void SpinBox::_line_edit_focus_enter() { - int col = line_edit->get_caret_column(); - _update_text(); - line_edit->set_caret_column(col); +void SpinBox::_line_edit_editing_toggled(bool p_toggled_on) { + if (p_toggled_on) { + int col = line_edit->get_caret_column(); + _update_text(); + line_edit->set_caret_column(col); - // LineEdit text might change and it clears any selection. Have to re-select here. - if (line_edit->is_select_all_on_focus() && !Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) { - line_edit->select_all(); + // LineEdit text might change and it clears any selection. Have to re-select here. + if (line_edit->is_select_all_on_focus() && !Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) { + line_edit->select_all(); + } + } else { + // Discontinue because the focus_exit was caused by canceling. + if (Input::get_singleton()->is_action_pressed("ui_cancel")) { + _update_text(); + return; + } + + line_edit->deselect(); + _text_submitted(line_edit->get_text()); } } -void SpinBox::_line_edit_focus_exit() { - // Discontinue because the focus_exit was caused by left-clicking the arrows. - const Viewport *viewport = get_viewport(); - if (!viewport || viewport->gui_get_focus_owner() == get_line_edit()) { - return; - } - // Discontinue because the focus_exit was caused by right-click context menu. - if (line_edit->is_menu_visible()) { - return; - } - // Discontinue because the focus_exit was caused by canceling. - if (Input::get_singleton()->is_action_pressed("ui_cancel")) { - _update_text(); - return; - } +inline void SpinBox::_compute_sizes() { + int buttons_block_wanted_width = theme_cache.buttons_width + theme_cache.field_and_buttons_separation; + int buttons_block_icon_enforced_width = _get_widest_button_icon_width() + theme_cache.field_and_buttons_separation; - _text_submitted(line_edit->get_text()); -} + int w = theme_cache.set_min_buttons_width_from_icons != 0 ? MAX(buttons_block_icon_enforced_width, buttons_block_wanted_width) : buttons_block_wanted_width; -inline void SpinBox::_adjust_width_for_icon(const Ref &icon) { - int w = icon->get_width(); - if ((w != last_w)) { + if (w != sizing_cache.buttons_block_width) { line_edit->set_offset(SIDE_LEFT, 0); line_edit->set_offset(SIDE_RIGHT, -w); - last_w = w; + sizing_cache.buttons_block_width = w; } + + Size2i size = get_size(); + + sizing_cache.buttons_width = w - theme_cache.field_and_buttons_separation; + sizing_cache.buttons_vertical_separation = CLAMP(theme_cache.buttons_vertical_separation, 0, size.height); + sizing_cache.buttons_left = is_layout_rtl() ? 0 : size.width - sizing_cache.buttons_width; + sizing_cache.button_up_height = (size.height - sizing_cache.buttons_vertical_separation) / 2; + sizing_cache.button_down_height = size.height - sizing_cache.button_up_height - sizing_cache.buttons_vertical_separation; + sizing_cache.second_button_top = size.height - sizing_cache.button_down_height; + + sizing_cache.buttons_separator_top = sizing_cache.button_up_height; + sizing_cache.field_and_buttons_separator_left = is_layout_rtl() ? sizing_cache.buttons_width : size.width - sizing_cache.buttons_block_width; + sizing_cache.field_and_buttons_separator_width = theme_cache.field_and_buttons_separation; +} + +inline int SpinBox::_get_widest_button_icon_width() { + int max = 0; + max = MAX(max, theme_cache.updown_icon->get_width()); + max = MAX(max, theme_cache.up_icon->get_width()); + max = MAX(max, theme_cache.up_hover_icon->get_width()); + max = MAX(max, theme_cache.up_pressed_icon->get_width()); + max = MAX(max, theme_cache.up_disabled_icon->get_width()); + max = MAX(max, theme_cache.down_icon->get_width()); + max = MAX(max, theme_cache.down_hover_icon->get_width()); + max = MAX(max, theme_cache.down_pressed_icon->get_width()); + max = MAX(max, theme_cache.down_disabled_icon->get_width()); + return max; } void SpinBox::_notification(int p_what) { switch (p_what) { case NOTIFICATION_DRAW: { _update_text(true); - _adjust_width_for_icon(theme_cache.updown_icon); + _compute_sizes(); RID ci = get_canvas_item(); Size2i size = get_size(); - if (is_layout_rtl()) { - theme_cache.updown_icon->draw(ci, Point2i(0, (size.height - theme_cache.updown_icon->get_height()) / 2)); - } else { - theme_cache.updown_icon->draw(ci, Point2i(size.width - theme_cache.updown_icon->get_width(), (size.height - theme_cache.updown_icon->get_height()) / 2)); + Ref up_stylebox = theme_cache.up_base_stylebox; + Ref down_stylebox = theme_cache.down_base_stylebox; + Ref up_icon = theme_cache.up_icon; + Ref down_icon = theme_cache.down_icon; + Color up_icon_modulate = theme_cache.up_icon_modulate; + Color down_icon_modulate = theme_cache.down_icon_modulate; + + bool is_fully_disabled = !is_editable(); + + if (state_cache.up_button_disabled || is_fully_disabled) { + up_stylebox = theme_cache.up_disabled_stylebox; + up_icon = theme_cache.up_disabled_icon; + up_icon_modulate = theme_cache.up_disabled_icon_modulate; + } else if (state_cache.up_button_pressed && !drag.enabled) { + up_stylebox = theme_cache.up_pressed_stylebox; + up_icon = theme_cache.up_pressed_icon; + up_icon_modulate = theme_cache.up_pressed_icon_modulate; + } else if (state_cache.up_button_hovered && !drag.enabled) { + up_stylebox = theme_cache.up_hover_stylebox; + up_icon = theme_cache.up_hover_icon; + up_icon_modulate = theme_cache.up_hover_icon_modulate; } + + if (state_cache.down_button_disabled || is_fully_disabled) { + down_stylebox = theme_cache.down_disabled_stylebox; + down_icon = theme_cache.down_disabled_icon; + down_icon_modulate = theme_cache.down_disabled_icon_modulate; + } else if (state_cache.down_button_pressed && !drag.enabled) { + down_stylebox = theme_cache.down_pressed_stylebox; + down_icon = theme_cache.down_pressed_icon; + down_icon_modulate = theme_cache.down_pressed_icon_modulate; + } else if (state_cache.down_button_hovered && !drag.enabled) { + down_stylebox = theme_cache.down_hover_stylebox; + down_icon = theme_cache.down_hover_icon; + down_icon_modulate = theme_cache.down_hover_icon_modulate; + } + + int updown_icon_left = sizing_cache.buttons_left + (sizing_cache.buttons_width - theme_cache.updown_icon->get_width()) / 2; + int updown_icon_top = (size.height - theme_cache.updown_icon->get_height()) / 2; + + // Compute center icon positions once we know which one is used. + int up_icon_left = sizing_cache.buttons_left + (sizing_cache.buttons_width - up_icon->get_width()) / 2; + int up_icon_top = (sizing_cache.button_up_height - up_icon->get_height()) / 2; + int down_icon_left = sizing_cache.buttons_left + (sizing_cache.buttons_width - down_icon->get_width()) / 2; + int down_icon_top = sizing_cache.second_button_top + (sizing_cache.button_down_height - down_icon->get_height()) / 2; + + // Draw separators. + draw_style_box(theme_cache.up_down_buttons_separator, Rect2(sizing_cache.buttons_left, sizing_cache.buttons_separator_top, sizing_cache.buttons_width, sizing_cache.buttons_vertical_separation)); + draw_style_box(theme_cache.field_and_buttons_separator, Rect2(sizing_cache.field_and_buttons_separator_left, 0, sizing_cache.field_and_buttons_separator_width, size.height)); + + // Draw buttons. + draw_style_box(up_stylebox, Rect2(sizing_cache.buttons_left, 0, sizing_cache.buttons_width, sizing_cache.button_up_height)); + draw_style_box(down_stylebox, Rect2(sizing_cache.buttons_left, sizing_cache.second_button_top, sizing_cache.buttons_width, sizing_cache.button_down_height)); + + // Draw arrows. + theme_cache.updown_icon->draw(ci, Point2i(updown_icon_left, updown_icon_top)); + draw_texture(up_icon, Point2i(up_icon_left, up_icon_top), up_icon_modulate); + draw_texture(down_icon, Point2i(down_icon_left, down_icon_top), down_icon_modulate); + + } break; + + case NOTIFICATION_MOUSE_EXIT: { + _mouse_exited(); } break; case NOTIFICATION_ENTER_TREE: { - _adjust_width_for_icon(theme_cache.updown_icon); + _compute_sizes(); _update_text(); + _update_buttons_state_for_current_value(); } break; case NOTIFICATION_VISIBILITY_CHANGED: drag.allowed = false; [[fallthrough]]; case NOTIFICATION_EXIT_TREE: { - _release_mouse(); + _release_mouse_from_drag_mode(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { @@ -356,6 +482,7 @@ bool SpinBox::is_select_all_on_focus() const { void SpinBox::set_editable(bool p_enabled) { line_edit->set_editable(p_enabled); + queue_redraw(); } bool SpinBox::is_editable() const { @@ -374,6 +501,22 @@ double SpinBox::get_custom_arrow_step() const { return custom_arrow_step; } +void SpinBox::_value_changed(double p_value) { + _update_buttons_state_for_current_value(); +} + +void SpinBox::_update_buttons_state_for_current_value() { + double value = get_value(); + bool should_disable_up = value == get_max() && !is_greater_allowed(); + bool should_disable_down = value == get_min() && !is_lesser_allowed(); + + if (state_cache.up_button_disabled != should_disable_up || state_cache.down_button_disabled != should_disable_down) { + state_cache.up_button_disabled = should_disable_up; + state_cache.down_button_disabled = should_disable_down; + queue_redraw(); + } +} + void SpinBox::_bind_methods() { ClassDB::bind_method(D_METHOD("set_horizontal_alignment", "alignment"), &SpinBox::set_horizontal_alignment); ClassDB::bind_method(D_METHOD("get_horizontal_alignment"), &SpinBox::get_horizontal_alignment); @@ -400,20 +543,54 @@ void SpinBox::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "custom_arrow_step", PROPERTY_HINT_RANGE, "0,10000,0.0001,or_greater"), "set_custom_arrow_step", "get_custom_arrow_step"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "select_all_on_focus"), "set_select_all_on_focus", "is_select_all_on_focus"); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, buttons_vertical_separation); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, field_and_buttons_separation); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, buttons_width); + BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SpinBox, set_min_buttons_width_from_icons); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, updown_icon, "updown"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_icon, "up"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_hover_icon, "up_hover"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_pressed_icon, "up_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, up_disabled_icon, "up_disabled"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_icon, "down"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_hover_icon, "down_hover"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_pressed_icon, "down_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, down_disabled_icon, "down_disabled"); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_base_stylebox, "up_background"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_hover_stylebox, "up_background_hovered"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_pressed_stylebox, "up_background_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_disabled_stylebox, "up_background_disabled"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_base_stylebox, "down_background"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_hover_stylebox, "down_background_hovered"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_pressed_stylebox, "down_background_pressed"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, down_disabled_stylebox, "down_background_disabled"); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_icon_modulate, "up_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_hover_icon_modulate, "up_hover_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_pressed_icon_modulate, "up_pressed_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, up_disabled_icon_modulate, "up_disabled_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_icon_modulate, "down_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_hover_icon_modulate, "down_hover_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_pressed_icon_modulate, "down_pressed_icon_modulate"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, SpinBox, down_disabled_icon_modulate, "down_disabled_icon_modulate"); + + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, field_and_buttons_separator, "field_and_buttons_separator"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, SpinBox, up_down_buttons_separator, "up_down_buttons_separator"); } SpinBox::SpinBox() { line_edit = memnew(LineEdit); add_child(line_edit, false, INTERNAL_MODE_FRONT); + line_edit->set_theme_type_variation("SpinBoxInnerLineEdit"); line_edit->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); line_edit->set_mouse_filter(MOUSE_FILTER_PASS); line_edit->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); line_edit->connect("text_submitted", callable_mp(this, &SpinBox::_text_submitted), CONNECT_DEFERRED); - line_edit->connect(SceneStringName(focus_entered), callable_mp(this, &SpinBox::_line_edit_focus_enter), CONNECT_DEFERRED); - line_edit->connect(SceneStringName(focus_exited), callable_mp(this, &SpinBox::_line_edit_focus_exit), CONNECT_DEFERRED); + line_edit->connect("editing_toggled", callable_mp(this, &SpinBox::_line_edit_editing_toggled), CONNECT_DEFERRED); line_edit->connect(SceneStringName(gui_input), callable_mp(this, &SpinBox::_line_edit_input)); range_click_timer = memnew(Timer); diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index e124f8137574..b3bf3ecdff5f 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -40,12 +40,24 @@ class SpinBox : public Range { GDCLASS(SpinBox, Range); LineEdit *line_edit = nullptr; - int last_w = 0; bool update_on_text_changed = false; + struct SizingCache { + int buttons_block_width = 0; + int buttons_width = 0; + int buttons_vertical_separation = 0; + int buttons_left = 0; + int button_up_height = 0; + int button_down_height = 0; + int second_button_top = 0; + int buttons_separator_top = 0; + int field_and_buttons_separator_left = 0; + int field_and_buttons_separator_width = 0; + } sizing_cache; + Timer *range_click_timer = nullptr; void _range_click_timeout(); - void _release_mouse(); + void _release_mouse_from_drag_mode(); void _update_text(bool p_keep_line_edit = false); void _text_submitted(const String &p_string); @@ -66,17 +78,65 @@ class SpinBox : public Range { double diff_y = 0.0; } drag; - void _line_edit_focus_enter(); - void _line_edit_focus_exit(); + struct StateCache { + bool up_button_hovered = false; + bool up_button_pressed = false; + bool up_button_disabled = false; + bool down_button_hovered = false; + bool down_button_pressed = false; + bool down_button_disabled = false; + } state_cache; + + void _line_edit_editing_toggled(bool p_toggled_on); - inline void _adjust_width_for_icon(const Ref &icon); + inline void _compute_sizes(); + inline int _get_widest_button_icon_width(); struct ThemeCache { Ref updown_icon; + Ref up_icon; + Ref up_hover_icon; + Ref up_pressed_icon; + Ref up_disabled_icon; + Ref down_icon; + Ref down_hover_icon; + Ref down_pressed_icon; + Ref down_disabled_icon; + + Ref up_base_stylebox; + Ref up_hover_stylebox; + Ref up_pressed_stylebox; + Ref up_disabled_stylebox; + Ref down_base_stylebox; + Ref down_hover_stylebox; + Ref down_pressed_stylebox; + Ref down_disabled_stylebox; + + Color up_icon_modulate; + Color up_hover_icon_modulate; + Color up_pressed_icon_modulate; + Color up_disabled_icon_modulate; + Color down_icon_modulate; + Color down_hover_icon_modulate; + Color down_pressed_icon_modulate; + Color down_disabled_icon_modulate; + + Ref field_and_buttons_separator; + Ref up_down_buttons_separator; + + int buttons_vertical_separation = 0; + int field_and_buttons_separation = 0; + int buttons_width = 0; + int set_min_buttons_width_from_icons = 0; + } theme_cache; + void _mouse_exited(); + void _update_buttons_state_for_current_value(); + protected: virtual void gui_input(const Ref &p_event) override; + void _value_changed(double p_value) override; void _notification(int p_what); static void _bind_methods(); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 36d317f8cc94..567450b02033 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -1537,7 +1537,7 @@ void TextEdit::_notification(int p_what) { carets.write[c].draw_pos.x = rect.position.x; } } - { + if (ime_selection.y > 0) { // IME caret. Vector sel = TS->shaped_text_get_selection(rid, get_caret_column(c) + ime_selection.x, get_caret_column(c) + ime_selection.x + ime_selection.y); for (int j = 0; j < sel.size(); j++) { @@ -1687,39 +1687,93 @@ void TextEdit::unhandled_key_input(const Ref &p_event) { bool TextEdit::alt_input(const Ref &p_gui_input) { Ref k = p_gui_input; if (k.is_valid()) { - if (!k->is_pressed()) { - if (alt_start && k->get_keycode() == Key::ALT) { - alt_start = false; - if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { - handle_unicode_input(alt_code); - } - return true; + // Start Unicode input (hold). + if (k->is_alt_pressed() && k->get_keycode() == Key::KP_ADD && !alt_start && !alt_start_no_hold) { + if (has_selection()) { + delete_selection(); } - return false; + alt_start = true; + alt_code = 0; + ime_text = "u"; + ime_selection = Vector2i(0, -1); + _update_ime_text(); + return true; } - if (k->is_alt_pressed()) { - if (!alt_start) { - if (k->get_keycode() == Key::KP_ADD) { - alt_start = true; - alt_code = 0; - return true; - } + // Start Unicode input (press). + if (k->is_action("ui_unicode_start", true) && !alt_start && !alt_start_no_hold) { + if (has_selection()) { + delete_selection(); + } + alt_start_no_hold = true; + alt_code = 0; + ime_text = "u"; + ime_selection = Vector2i(0, -1); + _update_ime_text(); + return true; + } + + // Update Unicode input. + if (k->is_pressed() && ((k->is_alt_pressed() && alt_start) || alt_start_no_hold)) { + if (k->get_keycode() >= Key::KEY_0 && k->get_keycode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KEY_0); + } else if (k->get_keycode() >= Key::KP_0 && k->get_keycode() <= Key::KP_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KP_0); + } else if (k->get_keycode() >= Key::A && k->get_keycode() <= Key::F) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::A) + 10; + } else if ((Key)k->get_unicode() >= Key::KEY_0 && (Key)k->get_unicode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)((Key)k->get_unicode() - Key::KEY_0); + } else if ((Key)k->get_unicode() >= Key::A && (Key)k->get_unicode() <= Key::F) { + alt_code = alt_code << 4; + alt_code += (uint32_t)((Key)k->get_unicode() - Key::A) + 10; + } else if (k->get_physical_keycode() >= Key::KEY_0 && k->get_physical_keycode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_physical_keycode() - Key::KEY_0); + } + if (k->get_keycode() == Key::BACKSPACE) { + alt_code = alt_code >> 4; + } + if (alt_code > 0x10ffff) { + alt_code = 0x10ffff; + } + if (alt_code > 0) { + ime_text = vformat("u%s", String::num_int64(alt_code, 16, true)); } else { - if (k->get_keycode() >= Key::KEY_0 && k->get_keycode() <= Key::KEY_9) { - alt_code = alt_code << 4; - alt_code += (uint32_t)(k->get_keycode() - Key::KEY_0); - } - if (k->get_keycode() >= Key::KP_0 && k->get_keycode() <= Key::KP_9) { - alt_code = alt_code << 4; - alt_code += (uint32_t)(k->get_keycode() - Key::KP_0); - } - if (k->get_keycode() >= Key::A && k->get_keycode() <= Key::F) { - alt_code = alt_code << 4; - alt_code += (uint32_t)(k->get_keycode() - Key::A) + 10; - } - return true; + ime_text = "u"; } + ime_selection = Vector2i(0, -1); + _update_ime_text(); + return true; + } + + // Submit Unicode input. + if ((!k->is_pressed() && alt_start && k->get_keycode() == Key::ALT) || (alt_start_no_hold && (k->is_action("ui_text_submit", true) || k->is_action("ui_accept", true)))) { + alt_start = false; + alt_start_no_hold = false; + if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { + ime_text = String(); + ime_selection = Vector2i(); + handle_unicode_input(alt_code); + } else { + ime_text = String(); + ime_selection = Vector2i(); + } + _update_ime_text(); + return true; + } + + // Cancel Unicode input. + if (alt_start_no_hold && k->is_action("ui_cancel", true)) { + alt_start = false; + alt_start_no_hold = false; + ime_text = String(); + ime_selection = Vector2i(); + _update_ime_text(); + return true; } } return false; @@ -2396,7 +2450,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); if (words.is_empty() || cc <= words[0]) { - // This solves the scenario where there are no words but glyfs that can be ignored. + // Move to the start when there are no more words. cc = 0; } else { for (int j = words.size() - 2; j >= 0; j = j - 2) { @@ -2453,7 +2507,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(i))->get_rid()); if (words.is_empty() || cc >= words[words.size() - 1]) { - // This solves the scenario where there are no words but glyfs that can be ignored. + // Move to the end when there are no more words. cc = text[get_caret_line(i)].length(); } else { for (int j = 1; j < words.size(); j = j + 2) { @@ -2669,7 +2723,7 @@ void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { // Get a list with the indices of the word bounds of the given text line. const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(get_caret_line(caret_index))->get_rid()); if (words.is_empty() || column <= words[0]) { - // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. + // Delete to the start when there are no more words. column = 0; } else { // Otherwise search for the first word break that is smaller than the index from we're currently deleting. @@ -2734,10 +2788,15 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { int column = get_caret_column(caret_index); PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); - for (int j = 1; j < words.size(); j = j + 2) { - if (words[j] > column) { - column = words[j]; - break; + if (words.is_empty() || column >= words[words.size() - 1]) { + // Delete to the end when there are no more words. + column = text[get_caret_line(i)].length(); + } else { + for (int j = 1; j < words.size(); j = j + 2) { + if (words[j] > column) { + column = words[j]; + break; + } } } @@ -3083,22 +3142,34 @@ bool TextEdit::has_ime_text() const { void TextEdit::cancel_ime() { if (!has_ime_text()) { + _close_ime_window(); return; } ime_text = String(); - ime_selection = Point2(); + ime_selection = Vector2i(); + alt_start = false; + alt_start_no_hold = false; _close_ime_window(); _update_ime_text(); } void TextEdit::apply_ime() { if (!has_ime_text()) { + _close_ime_window(); return; } + // Force apply the current IME text. - String insert_ime_text = ime_text; - cancel_ime(); - insert_text_at_caret(insert_ime_text); + if (alt_start || alt_start_no_hold) { + cancel_ime(); + if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { + handle_unicode_input(alt_code); + } + } else { + String insert_ime_text = ime_text; + cancel_ime(); + insert_text_at_caret(insert_ime_text); + } } void TextEdit::set_editable(const bool p_editable) { @@ -5936,7 +6007,7 @@ void TextEdit::adjust_viewport_to_caret(int p_caret) { // Get position of the end of caret. if (has_ime_text()) { - if (ime_selection.y != 0) { + if (ime_selection.y > 0) { caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_selection.x + ime_selection.y, get_caret_line(p_caret), get_caret_column(p_caret)); } else { caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_text.size(), get_caret_line(p_caret), get_caret_column(p_caret)); @@ -5988,7 +6059,7 @@ void TextEdit::center_viewport_to_caret(int p_caret) { // Get position of the end of caret. if (has_ime_text()) { - if (ime_selection.y != 0) { + if (ime_selection.y > 0) { caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_selection.x + ime_selection.y, get_caret_line(p_caret), get_caret_column(p_caret)); } else { caret_pos.y = _get_column_x_offset_for_line(get_caret_column(p_caret) + ime_text.size(), get_caret_line(p_caret), get_caret_column(p_caret)); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index d817ae9cd00b..ccf095e3ee28 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -283,6 +283,7 @@ class TextEdit : public Control { bool setting_text = false; bool alt_start = false; + bool alt_start_no_hold = false; uint32_t alt_code = 0; // Text properties. diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp index d1de19a90c9a..6206a2a76e22 100644 --- a/scene/theme/default_theme.cpp +++ b/scene/theme/default_theme.cpp @@ -613,7 +613,41 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const // SpinBox - theme->set_icon("updown", "SpinBox", icons["updown"]); + theme->set_icon("updown", "SpinBox", empty_icon); + theme->set_icon("up", "SpinBox", icons["value_up"]); + theme->set_icon("up_hover", "SpinBox", icons["value_up"]); + theme->set_icon("up_pressed", "SpinBox", icons["value_up"]); + theme->set_icon("up_disabled", "SpinBox", icons["value_up"]); + theme->set_icon("down", "SpinBox", icons["value_down"]); + theme->set_icon("down_hover", "SpinBox", icons["value_down"]); + theme->set_icon("down_pressed", "SpinBox", icons["value_down"]); + theme->set_icon("down_disabled", "SpinBox", icons["value_down"]); + + theme->set_stylebox("up_background", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("up_background_hovered", "SpinBox", button_hover); + theme->set_stylebox("up_background_pressed", "SpinBox", button_pressed); + theme->set_stylebox("up_background_disabled", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("down_background", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("down_background_hovered", "SpinBox", button_hover); + theme->set_stylebox("down_background_pressed", "SpinBox", button_pressed); + theme->set_stylebox("down_background_disabled", "SpinBox", make_empty_stylebox()); + + theme->set_color("up_icon_modulate", "SpinBox", control_font_color); + theme->set_color("up_hover_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("up_pressed_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("up_disabled_icon_modulate", "SpinBox", control_font_disabled_color); + theme->set_color("down_icon_modulate", "SpinBox", control_font_color); + theme->set_color("down_hover_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("down_pressed_icon_modulate", "SpinBox", control_font_hover_color); + theme->set_color("down_disabled_icon_modulate", "SpinBox", control_font_disabled_color); + + theme->set_stylebox("field_and_buttons_separator", "SpinBox", make_empty_stylebox()); + theme->set_stylebox("up_down_buttons_separator", "SpinBox", make_empty_stylebox()); + + theme->set_constant("buttons_vertical_separation", "SpinBox", 0); + theme->set_constant("field_and_buttons_separation", "SpinBox", 2); + theme->set_constant("buttons_width", "SpinBox", 16); + theme->set_constant("set_min_buttons_width_from_icons", "SpinBox", 1); // ScrollContainer diff --git a/scene/theme/icons/value_down.svg b/scene/theme/icons/value_down.svg new file mode 100644 index 000000000000..57837d03fdb1 --- /dev/null +++ b/scene/theme/icons/value_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scene/theme/icons/value_up.svg b/scene/theme/icons/value_up.svg new file mode 100644 index 000000000000..53fb102fe2df --- /dev/null +++ b/scene/theme/icons/value_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 8926eadf1ec3..bf20eee3cf82 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -4211,6 +4211,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(0) == 0); CHECK(text_edit->get_caret_column(0) == 4); text_edit->remove_secondary_carets(); + + // Remove when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + + SEND_GUI_ACTION("ui_text_backspace_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } SUBCASE("[TextEdit] ui_text_backspace_word same line") { @@ -4870,6 +4882,18 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { CHECK(text_edit->get_caret_line(0) == 0); CHECK(text_edit->get_caret_column(0) == 2); text_edit->remove_secondary_carets(); + + // Remove when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + SEND_GUI_ACTION("ui_text_delete_word"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK_FALSE(text_edit->has_selection()); + CHECK(text_edit->get_text() == ""); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } SUBCASE("[TextEdit] ui_text_delete_word same line") { @@ -5280,6 +5304,16 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Move when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(3); + + SEND_GUI_ACTION("ui_text_caret_word_left"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 0); } SUBCASE("[TextEdit] ui_text_caret_left") { @@ -5542,6 +5576,16 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK_FALSE("text_changed"); SIGNAL_CHECK_FALSE("lines_edited_from"); + + // Move when there are no words, only symbols. + text_edit->set_text("#{}"); + text_edit->set_caret_line(0); + text_edit->set_caret_column(0); + + SEND_GUI_ACTION("ui_text_caret_word_right"); + CHECK(text_edit->get_viewport()->is_input_handled()); + CHECK(text_edit->get_caret_line(0) == 0); + CHECK(text_edit->get_caret_column(0) == 3); } SUBCASE("[TextEdit] ui_text_caret_right") {