diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 611053df26..70e2b2f838 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -437,26 +437,27 @@ void DisplayServerAndroid::window_set_drop_files_callback(const Callable &p_call // Not supported on Android. } -void DisplayServerAndroid::_window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred) const { +template +void DisplayServerAndroid::_window_callback(const Callable &p_callable, bool p_deferred, const Args &...p_rest_args) const { if (p_callable.is_valid()) { if (p_deferred) { - p_callable.call_deferred(p_arg); + p_callable.call_deferred(p_rest_args...); } else { - p_callable.call(p_arg); + p_callable.call(p_rest_args...); } } } void DisplayServerAndroid::send_window_event(DisplayServer::WindowEvent p_event, bool p_deferred) const { - _window_callback(window_event_callback, int(p_event), p_deferred); + _window_callback(window_event_callback, p_deferred, int(p_event)); } void DisplayServerAndroid::send_input_event(const Ref &p_event) const { - _window_callback(input_event_callback, p_event); + _window_callback(input_event_callback, false, p_event); } void DisplayServerAndroid::send_input_text(const String &p_text) const { - _window_callback(input_text_callback, p_text); + _window_callback(input_text_callback, false, p_text, false); } void DisplayServerAndroid::_dispatch_input_events(const Ref &p_event) { diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index f5b437708a..49301d4b7f 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -96,7 +96,8 @@ class DisplayServerAndroid : public DisplayServer { Callable file_picker_callback; - void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const; + template + void _window_callback(const Callable &p_callable, bool p_deferred, const Args &...p_rest_args) const; static void _dispatch_input_events(const Ref &p_event); diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index eda004f908..4a0f026c69 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -796,7 +796,7 @@ void DisplayServerWeb::_vk_input_text_callback(const String &p_text, int p_curso return; } // Call input_text - ds->input_text_callback.call(p_text); + ds->input_text_callback.call(p_text, true); // Insert key right to reach position. Input *input = Input::get_singleton(); Ref k; diff --git a/platform/web/eslint.config.cjs b/platform/web/eslint.config.cjs index 56a45367f0..7bc1280396 100644 --- a/platform/web/eslint.config.cjs +++ b/platform/web/eslint.config.cjs @@ -157,6 +157,7 @@ module.exports = [ 'GodotFS': true, 'GodotOS': true, 'GodotAudio': true, + 'GodotInput': true, 'GodotRuntime': true, 'IDHandler': true, 'XRWebGLLayer': true, diff --git a/platform/web/js/libs/library_godot_display.js b/platform/web/js/libs/library_godot_display.js index dd812ed531..747db850e8 100644 --- a/platform/web/js/libs/library_godot_display.js +++ b/platform/web/js/libs/library_godot_display.js @@ -30,7 +30,7 @@ const GodotDisplayVK = { - $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners'], + $GodotDisplayVK__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInput'], $GodotDisplayVK__postset: 'GodotOS.atexit(function(resolve, reject) { GodotDisplayVK.clear(); resolve(); });', $GodotDisplayVK: { textinput: null, @@ -61,6 +61,17 @@ const GodotDisplayVK = { input_cb(c_str, elem.selectionEnd); GodotRuntime.free(c_str); }, false); + if (what === 'input') { + // Handling the "Enter" key. + const onKey = (pEvent, pEventName) => { + if (pEvent.key !== 'Enter') { + return; + } + GodotInput.onKeyEvent(pEventName === 'keydown', pEvent); + }; + GodotEventListeners.add(elem, 'keydown', (pEvent) => onKey(pEvent, 'keydown'), false); + GodotEventListeners.add(elem, 'keyup', (pEvent) => onKey(pEvent, 'keyup'), false); + } GodotEventListeners.add(elem, 'blur', function (evt) { elem.style.display = 'none'; elem.readonly = true; diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js index 576d1a7e21..fce4141a0f 100644 --- a/platform/web/js/libs/library_godot_input.js +++ b/platform/web/js/libs/library_godot_input.js @@ -482,9 +482,13 @@ mergeInto(LibraryManager.library, GodotInputDragDrop); const GodotInput = { $GodotInput__deps: ['$GodotRuntime', '$GodotConfig', '$GodotEventListeners', '$GodotInputGamepads', '$GodotInputDragDrop', '$GodotIME'], $GodotInput: { + inputKeyCallback: null, + setInputKeyData: null, + getModifiers: function (evt) { return (evt.shiftKey + 0) + ((evt.altKey + 0) << 1) + ((evt.ctrlKey + 0) << 2) + ((evt.metaKey + 0) << 3); }, + computePosition: function (evt, rect) { const canvas = GodotConfig.canvas; const rw = canvas.width / rect.width; @@ -493,6 +497,20 @@ const GodotInput = { const y = (evt.clientY - rect.y) * rh; return [x, y]; }, + + onKeyEvent: function (pIsPressed, pEvent) { + if (GodotInput.inputKeyCallback == null) { + throw new TypeError('GodotInput.onKeyEvent(): GodotInput.inputKeyCallback is null, cannot process key event.'); + } + if (GodotInput.setInputKeyData == null) { + throw new TypeError('GodotInput.onKeyEvent(): GodotInput.setInputKeyData is null, cannot process key event.'); + } + + const modifiers = GodotInput.getModifiers(pEvent); + GodotInput.setInputKeyData(pEvent.code, pEvent.key); + GodotInput.inputKeyCallback(pIsPressed ? 1 : 0, pEvent.repeat, modifiers); + pEvent.preventDefault(); + }, }, /* @@ -590,17 +608,14 @@ const GodotInput = { */ godot_js_input_key_cb__proxy: 'sync', godot_js_input_key_cb__sig: 'viii', - godot_js_input_key_cb: function (callback, code, key) { - const func = GodotRuntime.get_func(callback); - function key_cb(pressed, evt) { - const modifiers = GodotInput.getModifiers(evt); - GodotRuntime.stringToHeap(evt.code, code, 32); - GodotRuntime.stringToHeap(evt.key, key, 32); - func(pressed, evt.repeat, modifiers); - evt.preventDefault(); - } - GodotEventListeners.add(GodotConfig.canvas, 'keydown', key_cb.bind(null, 1), false); - GodotEventListeners.add(GodotConfig.canvas, 'keyup', key_cb.bind(null, 0), false); + godot_js_input_key_cb: function (pCallback, pCodePtr, pKeyPtr) { + GodotInput.inputKeyCallback = GodotRuntime.get_func(pCallback); + GodotInput.setInputKeyData = (pCode, pKey) => { + GodotRuntime.stringToHeap(pCode, pCodePtr, 32); + GodotRuntime.stringToHeap(pKey, pKeyPtr, 32); + }; + GodotEventListeners.add(GodotConfig.canvas, 'keydown', GodotInput.onKeyEvent.bind(null, true), false); + GodotEventListeners.add(GodotConfig.canvas, 'keyup', GodotInput.onKeyEvent.bind(null, false), false); }, /* diff --git a/pyproject.toml b/pyproject.toml index 6e477f279d..b1245da9f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ ignore-words-list = [ "ot", "outin", "parm", + "pEvent", "requestor", "streamin", "te", diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index a7d9606fa5..8df276e3af 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -2109,22 +2109,39 @@ void LineEdit::delete_text(int p_from_column, int p_to_column) { } } -void LineEdit::set_text(String p_text) { +void LineEdit::_set_text(String p_text, bool p_emit_signal) { clear_internal(); + + String previous_text = get_text(); insert_text_at_caret(p_text); - _create_undo_state(); + + if (get_text() != previous_text) { + _create_undo_state(); + if (p_emit_signal) { + _text_changed(); + } + } queue_redraw(); caret_column = 0; scroll_offset = 0.0; } +void LineEdit::set_text(String p_text) { + _set_text(p_text); +} + void LineEdit::set_text_with_selection(const String &p_text) { Selection selection_copy = selection; clear_internal(); + + String previous_text = get_text(); insert_text_at_caret(p_text); - _create_undo_state(); + + if (get_text() != previous_text) { + _create_undo_state(); + } int tlen = text.length(); selection = selection_copy; @@ -3299,6 +3316,10 @@ void LineEdit::_validate_property(PropertyInfo &p_property) const { } void LineEdit::_bind_methods() { + // Private exposed API. + ClassDB::bind_method(D_METHOD("_set_text", "text", "emit_signal"), &LineEdit::_set_text, DEFVAL(false)); + + // Public API. 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); diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index b02fc7dea7..bd3d81b318 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -342,6 +342,7 @@ public: void delete_char(); void delete_text(int p_from_column, int p_to_column); + void _set_text(String p_text, bool p_emit_signal = false); void set_text(String p_text); String get_text() const; void set_text_with_selection(const String &p_text); // Set text, while preserving selection. diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 193059b962..062d896807 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -3982,12 +3982,8 @@ void TextEdit::_clear() { emit_signal(SNAME("lines_edited_from"), old_text_size, 0); } -void TextEdit::set_text(const String &p_text) { +void TextEdit::_set_text(const String &p_text, bool p_emit_signal) { setting_text = true; - if (!undo_enabled) { - _clear(); - insert_text_at_caret(p_text); - } if (undo_enabled) { remove_secondary_carets(); @@ -3997,7 +3993,22 @@ void TextEdit::set_text(const String &p_text) { begin_complex_operation(); deselect(); _remove_text(0, 0, MAX(0, get_line_count() - 1), MAX(get_line(MAX(get_line_count() - 1, 0)).size() - 1, 0)); - insert_text_at_caret(p_text); + } else { + _clear(); + } + + String previous_text; + if (p_emit_signal) { + previous_text = get_text(); + } + + insert_text_at_caret(p_text); + + if (p_emit_signal && get_text() != previous_text) { + _text_changed(); + } + + if (undo_enabled) { end_complex_operation(); } @@ -4009,6 +4020,10 @@ void TextEdit::set_text(const String &p_text) { emit_signal(SNAME("text_set")); } +void TextEdit::set_text(const String &p_text) { + _set_text(p_text, false); +} + String TextEdit::get_text() const { StringBuilder ret_text; const int text_size = text.size(); @@ -7091,6 +7106,11 @@ Color TextEdit::get_font_color() const { } void TextEdit::_bind_methods() { + // Private API. + ClassDB::bind_method(D_METHOD("_set_text", "text", "emit_signal"), &TextEdit::_set_text, DEFVAL(false)); + + // Public API. + /* Text */ // Text properties ClassDB::bind_method(D_METHOD("has_ime_text"), &TextEdit::has_ime_text); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 3181ed9ad1..cbd75c6b5b 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -853,6 +853,7 @@ public: // Text manipulation void clear(); + void _set_text(const String &p_text, bool p_emit_signal = false); void set_text(const String &p_text); String get_text() const; diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 233f50e7c4..996d44455b 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -2828,16 +2828,22 @@ void Viewport::_post_gui_grab_click_focus() { /////////////////////////////// -void Viewport::push_text_input(const String &p_text) { +void Viewport::_push_text_input(const String &p_text, bool p_emit_signal) { ERR_MAIN_THREAD_GUARD; if (gui.subwindow_focused) { gui.subwindow_focused->push_text_input(p_text); return; } - if (gui.key_focus) { - gui.key_focus->call("set_text", p_text); + StringName set_text_method = SNAME("_set_text"); + if (!gui.key_focus || !gui.key_focus->has_method(set_text_method)) { + return; } + gui.key_focus->call(set_text_method, p_text, p_emit_signal); +} + +void Viewport::push_text_input(const String &p_text) { + _push_text_input(p_text, false); } Viewport::SubWindowResize Viewport::_sub_window_get_resize_margin(Window *p_subwindow, const Point2 &p_point) { diff --git a/scene/main/viewport.h b/scene/main/viewport.h index f45cc33753..206631e6c2 100644 --- a/scene/main/viewport.h +++ b/scene/main/viewport.h @@ -608,6 +608,7 @@ public: Vector2 get_camera_coords(const Vector2 &p_viewport_coords) const; Vector2 get_camera_rect_size() const; + void _push_text_input(const String &p_text, bool p_emit_text_changed_signal = false); void push_text_input(const String &p_text); void push_input(RequiredParam rp_event, bool p_local_coords = false); #ifndef DISABLE_DEPRECATED diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 24783f7087..81de401f00 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -1860,8 +1860,8 @@ void Window::_window_input(const Ref &p_ev) { } } -void Window::_window_input_text(const String &p_text) { - push_text_input(p_text); +void Window::_window_input_text(const String &p_text, bool p_emit_signal) { + _push_text_input(p_text, p_emit_signal); } void Window::_window_drop_files(const Vector &p_files) { diff --git a/scene/main/window.h b/scene/main/window.h index 68ab3f842a..1e01d37292 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -244,7 +244,7 @@ private: friend class Viewport; //friend back, can call the methods below void _window_input(const Ref &p_ev); - void _window_input_text(const String &p_text); + void _window_input_text(const String &p_text, bool p_emit_signal = false); void _window_drop_files(const Vector &p_files); void _rect_changed_callback(const Rect2i &p_callback); void _event_callback(DisplayServer::WindowEvent p_event);