diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index 0d4b5571e1f..b3e9fd08944 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -797,6 +797,7 @@ Returns [code]true[/code] if the caret is visible, [code]false[/code] otherwise. A caret will be considered hidden if it is outside the scrollable area when scrolling is enabled. [b]Note:[/b] [method is_caret_visible] does not account for a caret being off-screen if it is still within the scrollable area. It will return [code]true[/code] even if the caret is off-screen as long as it meets [TextEdit]'s own conditions for being visible. This includes uses of [member scroll_fit_content_width] and [member scroll_fit_content_height] that cause the [TextEdit] to expand beyond the viewport's bounds. + [b]Note:[/b] This method does [i]not[/i] guarantee an accurate visibility check immediately after setting the caret position. The correct value may only be available in the next frame after the [TextEdit] has finished drawing. This also applies to any operation that causes the [TextEdit] to change in size. @@ -840,6 +841,13 @@ Returns [code]true[/code] if the gutter at the given index on the given line is clickable. See [method set_line_gutter_clickable]. + + + + + Returns [code]true[/code] if the given line is within the scope of the scrollable area of the viewport. + + diff --git a/editor/gui/code_editor.cpp b/editor/gui/code_editor.cpp index 13a4381f00b..7c783cb8c8c 100644 --- a/editor/gui/code_editor.cpp +++ b/editor/gui/code_editor.cpp @@ -1378,6 +1378,24 @@ void CodeTextEditor::toggle_inline_comment(const String &delimiter) { text_editor->end_complex_operation(); } +void CodeTextEditor::adjust_viewport_to_caret() { + call_on_all_layout_pending_finished(callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).bind(0)); +} + +void CodeTextEditor::center_viewport_to_caret() { + call_on_all_layout_pending_finished(callable_mp((TextEdit *)text_editor, &TextEdit::center_viewport_to_caret).bind(0)); +} + +void CodeTextEditor::center_viewport_to_caret_if_line_invisible(int p_line) { + if (text_editor->is_layout_pending_in_tree()) { + text_editor->call_on_all_layout_pending_finished(callable_mp(this, &CodeTextEditor::center_viewport_to_caret_if_line_invisible).bind(0)); + return; + } + if (!text_editor->is_line_in_viewport(p_line)) { + text_editor->center_viewport_to_caret(); + } +} + void CodeTextEditor::goto_line(int p_line, int p_column) { text_editor->remove_secondary_carets(); text_editor->deselect(); @@ -1386,8 +1404,7 @@ void CodeTextEditor::goto_line(int p_line, int p_column) { text_editor->set_caret_column(p_column, false); text_editor->set_code_hint(""); text_editor->cancel_code_completion(); - // Defer in case the CodeEdit was just created and needs to be resized. - callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).call_deferred(0); + adjust_viewport_to_caret(); } void CodeTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { @@ -1396,7 +1413,7 @@ void CodeTextEditor::goto_line_selection(int p_line, int p_begin, int p_end) { text_editor->select(p_line, p_begin, p_line, p_end); text_editor->set_code_hint(""); text_editor->cancel_code_completion(); - callable_mp((TextEdit *)text_editor, &TextEdit::adjust_viewport_to_caret).call_deferred(0); + adjust_viewport_to_caret(); } void CodeTextEditor::goto_line_centered(int p_line, int p_column) { @@ -1407,7 +1424,7 @@ void CodeTextEditor::goto_line_centered(int p_line, int p_column) { text_editor->set_caret_column(p_column, false); text_editor->set_code_hint(""); text_editor->cancel_code_completion(); - callable_mp((TextEdit *)text_editor, &TextEdit::center_viewport_to_caret).call_deferred(0); + center_viewport_to_caret(); } void CodeTextEditor::set_executing_line(int p_line) { @@ -1458,15 +1475,21 @@ void CodeTextEditor::set_edit_state(const Variant &p_state) { Dictionary state = p_state; /* update the row first as it sets the column to 0 */ - text_editor->set_caret_line(state["row"]); - text_editor->set_caret_column(state["column"]); - if (int(state["scroll_position"]) == -1) { - // Special case for previous state. - text_editor->center_viewport_to_caret(); + if (state.get("ensure_caret_visible", false)) { + text_editor->set_caret_line(state["row"], false); + text_editor->set_caret_column(state["column"], false); + center_viewport_to_caret_if_line_invisible(state["row"]); } else { - text_editor->set_v_scroll(state["scroll_position"]); + text_editor->set_caret_line(state["row"]); + text_editor->set_caret_column(state["column"]); + if (int(state["scroll_position"]) == -1) { + // Special case for previous state. + center_viewport_to_caret(); + } else { + text_editor->set_v_scroll(state["scroll_position"]); + } + text_editor->set_h_scroll(state["h_scroll_position"]); } - text_editor->set_h_scroll(state["h_scroll_position"]); if (state.get("selection", false)) { text_editor->select(state["selection_from_line"], state["selection_from_column"], state["selection_to_line"], state["selection_to_column"]); diff --git a/editor/gui/code_editor.h b/editor/gui/code_editor.h index 7f99820216a..f1a6d41b2a9 100644 --- a/editor/gui/code_editor.h +++ b/editor/gui/code_editor.h @@ -256,6 +256,10 @@ public: /// by adding or removing comment delimiter void toggle_inline_comment(const String &delimiter); + void adjust_viewport_to_caret(); + void center_viewport_to_caret(); + void center_viewport_to_caret_if_line_invisible(int p_line); + void goto_line(int p_line, int p_column = 0); void goto_line_selection(int p_line, int p_begin, int p_end); void goto_line_centered(int p_line, int p_column = 0); diff --git a/editor/script/script_editor_plugin.cpp b/editor/script/script_editor_plugin.cpp index d51b0776815..e0899f56446 100644 --- a/editor/script/script_editor_plugin.cpp +++ b/editor/script/script_editor_plugin.cpp @@ -203,12 +203,6 @@ void ScriptEditor::_goto_script_line(Ref p_script, int p_line) { if (scr.is_valid() && (scr->has_source_code() || scr->get_path().is_resource_file())) { if (edit(p_script, p_line, 0)) { EditorNode::get_singleton()->push_item(p_script.ptr()); - - if (TextEditorBase *current = Object::cast_to(_get_current_editor())) { - current->goto_line_centered(p_line); - } - - _save_history(); } } } @@ -316,7 +310,9 @@ void ScriptEditor::_save_history() { Node *n = tab_container->get_current_tab_control(); if (Object::cast_to(n)) { - history.write[history_pos].state = Object::cast_to(n)->get_navigation_state(); + Dictionary nav_state = Object::cast_to(n)->get_navigation_state(); + nav_state["ensure_caret_visible"] = true; + history.write[history_pos].state = nav_state; } if (Object::cast_to(n)) { history.write[history_pos].state = Object::cast_to(n)->get_scroll(); @@ -375,7 +371,9 @@ void ScriptEditor::_go_to_tab(int p_idx) { Node *n = tab_container->get_current_tab_control(); if (Object::cast_to(n)) { - history.write[history_pos].state = Object::cast_to(n)->get_navigation_state(); + Dictionary nav_state = Object::cast_to(n)->get_navigation_state(); + nav_state["ensure_caret_visible"] = true; + history.write[history_pos].state = nav_state; } if (Object::cast_to(n)) { history.write[history_pos].state = Object::cast_to(n)->get_scroll(); @@ -2231,7 +2229,7 @@ bool ScriptEditor::edit(const Ref &p_resource, int p_line, int p_col, } if (p_line >= 0) { - teb->goto_line(p_line, p_col); + teb->goto_line_centered(p_line, p_col); } } else if (tab_container->get_current_tab() != i) { _go_to_tab(i); @@ -2358,7 +2356,7 @@ bool ScriptEditor::edit(const Ref &p_resource, int p_line, int p_col, if (TextEditorBase *teb = Object::cast_to(seb)) { if (p_line >= 0) { - teb->goto_line(p_line, p_col); + teb->goto_line_centered(p_line, p_col); } } @@ -3442,7 +3440,9 @@ void ScriptEditor::_update_history_pos(int p_new_pos) { Node *n = tab_container->get_current_tab_control(); if (Object::cast_to(n)) { - history.write[history_pos].state = Object::cast_to(n)->get_navigation_state(); + Dictionary nav_state = Object::cast_to(n)->get_navigation_state(); + nav_state["ensure_caret_visible"] = true; + history.write[history_pos].state = nav_state; } if (Object::cast_to(n)) { history.write[history_pos].state = Object::cast_to(n)->get_scroll(); diff --git a/editor/script/script_text_editor.cpp b/editor/script/script_text_editor.cpp index fac2a90e075..0134b068c98 100644 --- a/editor/script/script_text_editor.cpp +++ b/editor/script/script_text_editor.cpp @@ -1161,11 +1161,18 @@ void ScriptTextEditor::_on_caret_moved() { if (code_editor->is_previewing_navigation_change()) { return; } + if (is_layout_pending_in_tree()) { + call_on_all_layout_pending_finished(callable_mp(this, &ScriptTextEditor::_on_caret_moved)); + return; + } + // When previous_line < 0, it means the user has just switched to this editor from a different one + // (which already saved a state in the history). In this case, we should not save this editor's previous state. int current_line = code_editor->get_text_editor()->get_caret_line(); - if (Math::abs(current_line - previous_line) >= 10) { + if (previous_line >= 0 && Math::abs(current_line - previous_line) >= 10) { Dictionary nav_state = get_navigation_state(); nav_state["row"] = previous_line; nav_state["scroll_position"] = -1; + nav_state["ensure_caret_visible"] = true; emit_signal(SNAME("request_save_previous_state"), nav_state); store_previous_state(); } @@ -1888,6 +1895,11 @@ void ScriptTextEditor::_notification(int p_what) { case NOTIFICATION_DRAG_END: { drag_info_label->hide(); } break; + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible()) { + previous_line = -1; + } + } break; } } diff --git a/editor/script/script_text_editor.h b/editor/script/script_text_editor.h index cbecce231fd..b764a636bfa 100644 --- a/editor/script/script_text_editor.h +++ b/editor/script/script_text_editor.h @@ -94,7 +94,7 @@ class ScriptTextEditor : public CodeEditorBase { Color marked_line_color = Color(1, 1, 1); Color warning_line_color = Color(1, 1, 1); Color folded_code_region_color = Color(1, 1, 1); - int previous_line = 0; + int previous_line = -1; // Previous caret line number when user continuously operates in this editor. Affects history state. Reset to -1 on editor switch. PopupPanel *color_panel = nullptr; ColorPicker *color_picker = nullptr; diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index 27ca14eb680..4745896d733 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -94,6 +94,7 @@ void Container::_sort_children() { notification(NOTIFICATION_SORT_CHILDREN); emit_signal(SceneStringName(sort_children)); pending_sort = false; + layout_pending_finish(); } void Container::fit_child_in_rect(RequiredParam rp_child, const Rect2 &p_rect) { @@ -140,6 +141,7 @@ void Container::queue_sort() { return; } + layout_pending_start(); callable_mp(this, &Container::_sort_children).call_deferred(); pending_sort = true; } diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 2c42bc3074a..baed743fe82 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -1752,6 +1752,55 @@ Size2 Control::get_custom_minimum_size() const { return data.custom_minimum_size; } +bool Control::is_layout_pending() const { + ERR_MAIN_THREAD_GUARD_V(false); + return data.layout_pending; +} + +bool Control::is_layout_pending_in_tree() const { + ERR_MAIN_THREAD_GUARD_V(false); + const Control *current_node = this; + while (current_node != nullptr) { + if (current_node->is_layout_pending()) { + return true; + } + current_node = current_node->get_parent_control(); + } + return false; +} + +void Control::layout_pending_start() { + ERR_MAIN_THREAD_GUARD; + data.layout_pending = true; +} + +void Control::layout_pending_finish() { + ERR_MAIN_THREAD_GUARD; + data.layout_pending = false; + emit_signal(SNAME("_layout_pending_finished")); +} + +Control *Control::get_layout_pending_control_in_tree() const { + Control *current_node = const_cast(this); + while (current_node != nullptr) { + if (current_node->is_layout_pending()) { + return current_node; + } + current_node = current_node->get_parent_control(); + } + return nullptr; +} + +void Control::call_on_all_layout_pending_finished(const Callable &p_callable) { + Control *pending_control = get_layout_pending_control_in_tree(); + if (pending_control != nullptr) { + Callable recheck = callable_mp(this, &Control::call_on_all_layout_pending_finished).bind(p_callable); + pending_control->connect(SNAME("_layout_pending_finished"), recheck, CONNECT_ONE_SHOT | CONNECT_REFERENCE_COUNTED); + } else { + p_callable.call(); + } +} + void Control::_update_minimum_size_cache() const { Size2 minsize = get_minimum_size(); minsize = minsize.max(data.custom_minimum_size); @@ -4059,6 +4108,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_screen_position"), &Control::get_screen_position); ClassDB::bind_method(D_METHOD("get_rect"), &Control::get_rect); ClassDB::bind_method(D_METHOD("get_global_rect"), &Control::get_global_rect); + ClassDB::bind_method(D_METHOD("set_focus_mode", "mode"), &Control::set_focus_mode); ClassDB::bind_method(D_METHOD("get_focus_mode"), &Control::get_focus_mode); ClassDB::bind_method(D_METHOD("get_focus_mode_with_override"), &Control::get_focus_mode_with_override); @@ -4429,6 +4479,7 @@ void Control::_bind_methods() { BIND_ENUM_CONSTANT(TEXT_DIRECTION_RTL); ADD_SIGNAL(MethodInfo("resized")); + ADD_SIGNAL(MethodInfo("_layout_pending_finished")); ADD_SIGNAL(MethodInfo("gui_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, InputEvent::get_class_static()))); ADD_SIGNAL(MethodInfo("mouse_entered")); ADD_SIGNAL(MethodInfo("mouse_exited")); diff --git a/scene/gui/control.h b/scene/gui/control.h index b00903f67bb..e7659c34fc3 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -225,6 +225,8 @@ private: bool updating_last_minimum_size = false; bool block_minimum_size_adjust = false; + bool layout_pending = false; + bool size_warning = true; // Container sizing. @@ -554,6 +556,13 @@ public: void set_custom_minimum_size(const Size2 &p_custom); Size2 get_custom_minimum_size() const; + bool is_layout_pending() const; + bool is_layout_pending_in_tree() const; + void layout_pending_start(); + void layout_pending_finish(); + Control *get_layout_pending_control_in_tree() const; + void call_on_all_layout_pending_finished(const Callable &p_callable); + // Container sizing. void set_h_size_flags(BitField p_flags); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index e682166ff8b..270369a2c70 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -271,7 +271,17 @@ void TabContainer::_on_theme_changed() { theme_changing = false; } +void TabContainer::_repaint_call_deferred() { + layout_pending_start(); + callable_mp(this, &TabContainer::_repaint_internal).call_deferred(); +} + void TabContainer::_repaint() { + layout_pending_start(); + _repaint_internal(); +} + +void TabContainer::_repaint_internal() { Vector controls = _get_tab_controls(); int current = get_current_tab(); @@ -315,6 +325,7 @@ void TabContainer::_repaint() { updating_visibility = false; update_minimum_size(); + layout_pending_finish(); } void TabContainer::_update_margins() { @@ -494,7 +505,7 @@ void TabContainer::_on_tab_hovered(int p_tab) { } void TabContainer::_on_tab_changed(int p_tab) { - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); queue_redraw(); queue_accessibility_update(); @@ -503,7 +514,7 @@ void TabContainer::_on_tab_changed(int p_tab) { void TabContainer::_on_tab_selected(int p_tab) { if (p_tab != get_previous_tab()) { - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); } emit_signal(SNAME("tab_selected"), p_tab); @@ -595,7 +606,7 @@ void TabContainer::add_child_notify(Node *p_child) { // TabBar won't emit the "tab_changed" signal when not inside the tree. if (!is_inside_tree()) { - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); } notify_property_list_changed(); } @@ -657,7 +668,7 @@ void TabContainer::remove_child_notify(Node *p_child) { // TabBar won't emit the "tab_changed" signal when not inside the tree. if (!is_inside_tree()) { - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); } notify_property_list_changed(); } @@ -774,7 +785,7 @@ void TabContainer::set_tabs_position(TabPosition p_tabs_position) { tab_bar->set_tab_style_v_flip(tabs_position == POSITION_BOTTOM); - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); queue_redraw(); } @@ -806,7 +817,7 @@ void TabContainer::set_tabs_visible(bool p_visible) { tabs_visible = p_visible; tab_bar->set_visible(tabs_visible); - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); queue_redraw(); } @@ -947,7 +958,7 @@ void TabContainer::set_tab_hidden(int p_tab, bool p_hidden) { if (!get_clip_tabs()) { update_minimum_size(); } - callable_mp(this, &TabContainer::_repaint).call_deferred(); + _repaint_call_deferred(); } bool TabContainer::is_tab_hidden(int p_tab) const { diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 16780ec4ae4..f3c405643ba 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -127,7 +127,9 @@ private: Control *_as_tab_control(Node *p_child) const; Vector _get_tab_controls() const; void _on_theme_changed(); + void _repaint_call_deferred(); void _repaint(); + void _repaint_internal(); void _refresh_tab_indices(); void _refresh_tab_names(); void _update_margins(); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 046feb85a2c..9cae3825fc2 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -839,7 +839,9 @@ void TextEdit::_notification(int p_what) { case NOTIFICATION_ENTER_TREE: { _update_caches(); if (caret_pos_dirty) { - callable_mp(this, &TextEdit::_emit_caret_changed).call_deferred(); + // No need to emit "caret_changed" signal (otherwise will save an unnecessary state to history), + // so we use `_set_caret_pos_dirty()` instead of `_emit_caret_changed()`. + callable_mp(this, &TextEdit::_set_caret_pos_dirty).call_deferred(false); } if (text_changed_dirty) { callable_mp(this, &TextEdit::_emit_text_changed).call_deferred(); @@ -6651,6 +6653,26 @@ int TextEdit::get_total_visible_line_count() const { } // Auto adjust. +bool TextEdit::is_line_in_viewport(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + + int line_wrap = get_line_wrap_index_at_column(p_line, 0); + + int first_vis_line = get_first_visible_line(); + int first_vis_wrap = first_visible_line_wrap_ofs; + int last_vis_line = get_last_full_visible_line(); + int last_vis_wrap = get_last_full_visible_line_wrap_index(); + + if (p_line < first_vis_line || (p_line == first_vis_line && p_line < first_vis_wrap)) { + // Caret is above screen. + return false; + } else if (p_line > last_vis_line || (p_line == last_vis_line && line_wrap > last_vis_wrap)) { + // Caret is below screen. + return false; + } + return true; +} + void TextEdit::adjust_viewport_to_caret(int p_caret) { ERR_FAIL_INDEX(p_caret, carets.size()); @@ -7460,6 +7482,7 @@ void TextEdit::_bind_methods() { // Visible lines. ClassDB::bind_method(D_METHOD("set_line_as_first_visible", "line", "wrap_index"), &TextEdit::set_line_as_first_visible, DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_first_visible_line"), &TextEdit::get_first_visible_line); + ClassDB::bind_method(D_METHOD("is_line_in_viewport", "line"), &TextEdit::is_line_in_viewport); ClassDB::bind_method(D_METHOD("set_line_as_center_visible", "line", "wrap_index"), &TextEdit::set_line_as_center_visible, DEFVAL(0)); @@ -8243,6 +8266,10 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con } /* Caret */ +void TextEdit::_set_caret_pos_dirty(bool p_dirty) { + caret_pos_dirty = p_dirty; +} + void TextEdit::_caret_changed(int p_caret) { queue_redraw(); diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index cbd75c6b5be..4a990a0eb4a 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -457,6 +457,7 @@ private: bool setting_caret_line = false; bool caret_pos_dirty = false; + void _set_caret_pos_dirty(bool p_dirty); int multicaret_edit_count = 0; bool multicaret_edit_merge_queued = false; @@ -1093,6 +1094,7 @@ public: int get_total_visible_line_count() const; // Auto Adjust + bool is_line_in_viewport(int p_line) const; void adjust_viewport_to_caret(int p_caret = 0); void center_viewport_to_caret(int p_caret = 0);