From b32395ade9455756b20d55d6c69641b35ebb2812 Mon Sep 17 00:00:00 2001 From: mosquito chang Date: Sun, 18 Aug 2024 14:46:34 +0200 Subject: [PATCH 01/52] Fix `InputMap::event_get_index` to handle unmatched events correctly --- core/input/input_map.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index ddeee9d765a2..5cd02cf39d8e 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -253,8 +253,8 @@ bool InputMap::event_is_action(const Ref &p_event, const StringName int InputMap::event_get_index(const Ref &p_event, const StringName &p_action, bool p_exact_match) const { int index = -1; - event_get_action_status(p_event, p_action, p_exact_match, nullptr, nullptr, nullptr, &index); - return index; + bool valid = event_get_action_status(p_event, p_action, p_exact_match, nullptr, nullptr, nullptr, &index); + return valid ? index : -1; } bool InputMap::event_get_action_status(const Ref &p_event, const StringName &p_action, bool p_exact_match, bool *r_pressed, float *r_strength, float *r_raw_strength, int *r_event_index) const { From 6a12fac44cf3bbc020689b4309841ee96ecd56c3 Mon Sep 17 00:00:00 2001 From: Mark Wilson <23439518+wlsnmrk@users.noreply.github.com> Date: Sun, 10 Sep 2023 22:13:04 -0400 Subject: [PATCH 02/52] Fix button up and down events with focus changes Adds a flag to guard button_up and button_down events based on whether button_down has been previously emitted. Buttons now emit button_up signals if they have emitted button_down and subsequently lose focus, do not emit button_up if they gain focus while ui_accept is still pressed, and do not emit multiple up/down signals if multiple ui_accept keys are pressed simultaneously. --- scene/gui/base_button.cpp | 11 +++++++++-- scene/gui/base_button.h | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index 34f50954932e..85aa90cfee33 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -115,6 +115,11 @@ void BaseButton::_notification(int p_what) { } else if (status.hovering) { queue_redraw(); } + + if (status.pressed_down_with_focus) { + status.pressed_down_with_focus = false; + emit_signal(SNAME("button_up")); + } } break; case NOTIFICATION_VISIBILITY_CHANGED: @@ -147,9 +152,10 @@ void BaseButton::_toggled(bool p_pressed) { void BaseButton::on_action_event(Ref p_event) { Ref mouse_button = p_event; - if (p_event->is_pressed() && (mouse_button.is_null() || status.hovering)) { + if (!status.pressed_down_with_focus && p_event->is_pressed() && (mouse_button.is_null() || status.hovering)) { status.press_attempt = true; status.pressing_inside = true; + status.pressed_down_with_focus = true; emit_signal(SNAME("button_down")); } @@ -176,9 +182,10 @@ void BaseButton::on_action_event(Ref p_event) { } } - if (!p_event->is_pressed()) { + if (status.pressed_down_with_focus && !p_event->is_pressed()) { status.press_attempt = false; status.pressing_inside = false; + status.pressed_down_with_focus = false; emit_signal(SNAME("button_up")); } diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 8405acb21db8..e15539208b40 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -61,7 +61,7 @@ class BaseButton : public Control { bool hovering = false; bool press_attempt = false; bool pressing_inside = false; - + bool pressed_down_with_focus = false; bool disabled = false; } status; From 3cc43abab07971349c8f6c7b40179485cf430442 Mon Sep 17 00:00:00 2001 From: Florent Guiocheau Date: Mon, 2 Dec 2024 12:11:20 +0100 Subject: [PATCH 03/52] Document inverse aspect ratio convention of `Projection::get_fovy()` --- doc/classes/Projection.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/classes/Projection.xml b/doc/classes/Projection.xml index 091e0bf54f46..d32fdc9bb8f5 100644 --- a/doc/classes/Projection.xml +++ b/doc/classes/Projection.xml @@ -194,6 +194,7 @@ Returns the vertical field of view of the projection (in degrees) associated with the given horizontal field of view (in degrees) and aspect ratio. + [b]Note:[/b] Unlike most methods of [Projection], [param aspect] is expected to be 1 divided by the X:Y aspect ratio. From b3e970dde8c07168ef3927b3cd5dd157b403b281 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 12 Dec 2024 12:06:09 -0500 Subject: [PATCH 04/52] Adds `get_selection_line_offset` to `RichTextLabel` This new method allow you to get the line offset of the current selection (returns -1 if nothing is selected.) This is useful if you want to pop up a control or menu above the currently selected text. Previously there was no accurate way to get this information. The logic is moved from the implementation of `scroll_to_selection` verbatim, and that method has been adjusted to avoid repetition. --- doc/classes/RichTextLabel.xml | 6 +++++ scene/gui/rich_text_label.cpp | 48 +++++++++++++++++++++-------------- scene/gui/rich_text_label.h | 1 + 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml index d75d3043e029..b982808ff0ea 100644 --- a/doc/classes/RichTextLabel.xml +++ b/doc/classes/RichTextLabel.xml @@ -190,6 +190,12 @@ Returns the current selection first character index if a selection is active, [code]-1[/code] otherwise. Does not include BBCodes. + + + + Returns the current selection vertical line offset if a selection is active, [code]-1.0[/code] otherwise. + + diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index be54ef7c94ed..99a959104750 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -5449,25 +5449,8 @@ void RichTextLabel::append_text(const String &p_bbcode) { } void RichTextLabel::scroll_to_selection() { - if (selection.active && selection.from_frame && selection.from_line >= 0 && selection.from_line < (int)selection.from_frame->lines.size()) { - // Selected frame paragraph offset. - float line_offset = selection.from_frame->lines[selection.from_line].offset.y; - - // Add wrapped line offset. - for (int i = 0; i < selection.from_frame->lines[selection.from_line].text_buf->get_line_count(); i++) { - Vector2i range = selection.from_frame->lines[selection.from_line].text_buf->get_line_range(i); - if (range.x <= selection.from_char && range.y >= selection.from_char) { - break; - } - line_offset += selection.from_frame->lines[selection.from_line].text_buf->get_line_ascent(i) + selection.from_frame->lines[selection.from_line].text_buf->get_line_descent(i) + theme_cache.line_separation; - } - - // Add nested frame (e.g. table cell) offset. - ItemFrame *it = selection.from_frame; - while (it->parent_frame != nullptr) { - line_offset += it->parent_frame->lines[it->line].offset.y; - it = it->parent_frame; - } + float line_offset = get_selection_line_offset(); + if (line_offset != -1.0) { vscroll->set_value(line_offset); } } @@ -5978,6 +5961,32 @@ int RichTextLabel::get_selection_to() const { return selection.to_frame->lines[selection.to_line].char_offset + selection.to_char - 1; } +float RichTextLabel::get_selection_line_offset() const { + if (selection.active && selection.from_frame && selection.from_line >= 0 && selection.from_line < (int)selection.from_frame->lines.size()) { + // Selected frame paragraph offset. + float line_offset = selection.from_frame->lines[selection.from_line].offset.y; + + // Add wrapped line offset. + for (int i = 0; i < selection.from_frame->lines[selection.from_line].text_buf->get_line_count(); i++) { + Vector2i range = selection.from_frame->lines[selection.from_line].text_buf->get_line_range(i); + if (range.x <= selection.from_char && range.y >= selection.from_char) { + break; + } + line_offset += selection.from_frame->lines[selection.from_line].text_buf->get_line_ascent(i) + selection.from_frame->lines[selection.from_line].text_buf->get_line_descent(i) + theme_cache.line_separation; + } + + // Add nested frame (e.g. table cell) offset. + ItemFrame *it = selection.from_frame; + while (it->parent_frame != nullptr) { + line_offset += it->parent_frame->lines[it->line].offset.y; + it = it->parent_frame; + } + return line_offset; + } + + return -1.0; +} + void RichTextLabel::set_text(const String &p_bbcode) { // Allow clearing the tag stack. if (!p_bbcode.is_empty() && text == p_bbcode) { @@ -6412,6 +6421,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_selection_from"), &RichTextLabel::get_selection_from); ClassDB::bind_method(D_METHOD("get_selection_to"), &RichTextLabel::get_selection_to); + ClassDB::bind_method(D_METHOD("get_selection_line_offset"), &RichTextLabel::get_selection_line_offset); ClassDB::bind_method(D_METHOD("select_all"), &RichTextLabel::select_all); ClassDB::bind_method(D_METHOD("get_selected_text"), &RichTextLabel::get_selected_text); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index d9aac546ca07..12ae17ad4ad4 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -787,6 +787,7 @@ class RichTextLabel : public Control { bool is_selection_enabled() const; int get_selection_from() const; int get_selection_to() const; + float get_selection_line_offset() const; String get_selected_text() const; void select_all(); void selection_copy(); From 7c182a15441e01b9fb0804890ec145b98225b9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pa=CC=84vels=20Nadtoc=CC=8Cajevs?= <7645683+bruvzg@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:37:26 +0200 Subject: [PATCH 05/52] [TextServer] Fix space trimming around mandatory line breaks. --- servers/text_server.cpp | 12 +++++------ tests/servers/test_text_server.h | 37 ++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/servers/text_server.cpp b/servers/text_server.cpp index c758e25d36c7..ab69ef71b49d 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -890,10 +890,10 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks_adv(const RID &p_shaped if (last_end <= l_gl[start_pos].start) { lines.push_back(l_gl[start_pos].start); lines.push_back(l_gl[end_pos].end); - last_end = l_gl[end_pos].end; - cur_safe_brk = end_pos; + last_end = l_gl[i].end; + cur_safe_brk = i; } - trim_next = false; + trim_next = true; } else { if (last_end <= line_start) { lines.push_back(line_start); @@ -1057,15 +1057,15 @@ PackedInt32Array TextServer::shaped_text_get_line_breaks(const RID &p_shaped, do while ((start_pos < end_pos) && ((l_gl[end_pos].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE || (l_gl[end_pos].flags & GRAPHEME_IS_BREAK_HARD) == GRAPHEME_IS_BREAK_HARD || (l_gl[end_pos].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT)) { end_pos -= l_gl[end_pos].count; } - trim_next = false; + trim_next = true; if (last_end <= l_gl[start_pos].start) { lines.push_back(l_gl[start_pos].start); lines.push_back(l_gl[end_pos].end); if (p_width > indent) { l_width = p_width - indent; } - last_end = l_gl[end_pos].end; - cur_safe_brk = end_pos; + last_end = l_gl[i].end; + cur_safe_brk = i; } } else { if (last_end <= line_start) { diff --git a/tests/servers/test_text_server.h b/tests/servers/test_text_server.h index 4e20d43efc29..da21399cb8a6 100644 --- a/tests/servers/test_text_server.h +++ b/tests/servers/test_text_server.h @@ -461,7 +461,7 @@ TEST_SUITE("[TextServer]") { ts->free_rid(ctx); } - if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { + if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { // Line breaking opportunities. String test = U"เป็นภาษาราชการและภาษา"; RID ctx = ts->create_shaped_text(); CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); @@ -489,7 +489,7 @@ TEST_SUITE("[TextServer]") { ts->free_rid(ctx); } - if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { + if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { // Break line. struct TestCase { String text; PackedInt32Array breaks; @@ -504,15 +504,48 @@ TEST_SUITE("[TextServer]") { { U"الحمدا لحمدا لحمـــد", { 0, 13, 13, 20 } }, { U" الحمد test", { 0, 15, 15, 19 } }, { U"الحمـد الرياضي العربي", { 0, 7, 7, 15, 15, 21 } }, + { U"test \rtest", { 0, 6, 6, 10 } }, + { U"test\r test", { 0, 5, 5, 10 } }, + { U"test\r test \r test", { 0, 5, 5, 12, 12, 17 } }, }; for (size_t j = 0; j < sizeof(cases) / sizeof(TestCase); j++) { RID ctx = ts->create_shaped_text(); CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); bool ok = ts->shaped_text_add_string(ctx, cases[j].text, font, 16); CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed."); + PackedInt32Array breaks = ts->shaped_text_get_line_breaks(ctx, 90.0); + CHECK_FALSE_MESSAGE(breaks != cases[j].breaks, "Invalid break points."); + breaks = ts->shaped_text_get_line_breaks_adv(ctx, { 90.0 }, 0, false); CHECK_FALSE_MESSAGE(breaks != cases[j].breaks, "Invalid break points."); + + ts->free_rid(ctx); + } + } + + if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { // Break line and trim spaces. + struct TestCase { + String text; + PackedInt32Array breaks; + }; + TestCase cases[] = { + { U"test \rtest", { 0, 4, 6, 10 } }, + { U"test\r test", { 0, 4, 6, 10 } }, + { U"test\r test \r test", { 0, 4, 6, 10, 13, 17 } }, + }; + for (size_t j = 0; j < sizeof(cases) / sizeof(TestCase); j++) { + RID ctx = ts->create_shaped_text(); + CHECK_FALSE_MESSAGE(ctx == RID(), "Creating text buffer failed."); + bool ok = ts->shaped_text_add_string(ctx, cases[j].text, font, 16); + CHECK_FALSE_MESSAGE(!ok, "Adding text to the buffer failed."); + + PackedInt32Array breaks = ts->shaped_text_get_line_breaks(ctx, 90.0, 0, TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_TRIM_EDGE_SPACES); + CHECK_FALSE_MESSAGE(breaks != cases[j].breaks, "Invalid break points."); + + breaks = ts->shaped_text_get_line_breaks_adv(ctx, { 90.0 }, 0, false, TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_TRIM_EDGE_SPACES); + CHECK_FALSE_MESSAGE(breaks != cases[j].breaks, "Invalid break points."); + ts->free_rid(ctx); } } From 1e982a49c3bfccae7aeea0aafb6104fae4a31108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pa=CC=84vels=20Nadtoc=CC=8Cajevs?= <7645683+bruvzg@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:37:26 +0200 Subject: [PATCH 06/52] [Dictionary Property Editor] Use property editors instead of labels to display keys. --- doc/classes/EditorProperty.xml | 6 +++ editor/editor_inspector.cpp | 34 +++++++++++++- editor/editor_inspector.h | 8 ++++ editor/editor_properties_array_dict.cpp | 59 +++++++++++++++++++++++++ editor/editor_properties_array_dict.h | 10 ++++- 5 files changed, 114 insertions(+), 3 deletions(-) diff --git a/doc/classes/EditorProperty.xml b/doc/classes/EditorProperty.xml index 2a9e4088a6b5..cf8296841d44 100644 --- a/doc/classes/EditorProperty.xml +++ b/doc/classes/EditorProperty.xml @@ -109,6 +109,12 @@ Used by the inspector, set to [code]true[/code] when the property can be deleted by the user. + + Used by the inspector, set to [code]true[/code] when the property label is drawn. + + + Used by the inspector, set to [code]true[/code] when the property background is drawn. + Used by the inspector, set to [code]true[/code] when the property is drawn with the editor theme's warning color. This is used for editable children's properties. diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 3a899719edd1..d939be84128d 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -164,6 +164,9 @@ void EditorProperty::_notification(int p_what) { if (no_children) { text_size = size.width; rect = Rect2(size.width - 1, 0, 1, height); + } else if (!draw_label) { + text_size = 0; + rect = Rect2(1, 0, size.width - 1, height); } else { text_size = MAX(0, size.width - (child_room + 4 * EDSCALE)); if (is_layout_rtl()) { @@ -268,10 +271,10 @@ void EditorProperty::_notification(int p_what) { } Ref bg_stylebox = get_theme_stylebox(SNAME("child_bg")); - if (draw_top_bg && right_child_rect != Rect2()) { + if (draw_top_bg && right_child_rect != Rect2() && draw_background) { draw_style_box(bg_stylebox, right_child_rect); } - if (bottom_child_rect != Rect2()) { + if (bottom_child_rect != Rect2() && draw_background) { draw_style_box(bg_stylebox, bottom_child_rect); } @@ -605,6 +608,25 @@ bool EditorProperty::use_keying_next() const { return false; } +void EditorProperty::set_draw_label(bool p_draw_label) { + draw_label = p_draw_label; + queue_redraw(); + queue_sort(); +} + +bool EditorProperty::is_draw_label() const { + return draw_label; +} + +void EditorProperty::set_draw_background(bool p_draw_background) { + draw_background = p_draw_background; + queue_redraw(); +} + +bool EditorProperty::is_draw_background() const { + return draw_background; +} + void EditorProperty::set_checkable(bool p_checkable) { checkable = p_checkable; queue_redraw(); @@ -1070,6 +1092,12 @@ void EditorProperty::_bind_methods() { ClassDB::bind_method(D_METHOD("set_read_only", "read_only"), &EditorProperty::set_read_only); ClassDB::bind_method(D_METHOD("is_read_only"), &EditorProperty::is_read_only); + ClassDB::bind_method(D_METHOD("set_draw_label", "draw_label"), &EditorProperty::set_draw_label); + ClassDB::bind_method(D_METHOD("is_draw_label"), &EditorProperty::is_draw_label); + + ClassDB::bind_method(D_METHOD("set_draw_background", "draw_background"), &EditorProperty::set_draw_background); + ClassDB::bind_method(D_METHOD("is_draw_background"), &EditorProperty::is_draw_background); + ClassDB::bind_method(D_METHOD("set_checkable", "checkable"), &EditorProperty::set_checkable); ClassDB::bind_method(D_METHOD("is_checkable"), &EditorProperty::is_checkable); @@ -1112,6 +1140,8 @@ void EditorProperty::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "label"), "set_label", "get_label"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "read_only"), "set_read_only", "is_read_only"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_label"), "set_draw_label", "is_draw_label"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_background"), "set_draw_background", "is_draw_background"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "checkable"), "set_checkable", "is_checkable"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "checked"), "set_checked", "is_checked"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_warning"), "set_draw_warning", "is_draw_warning"); diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index bc2c8112937c..6807b2f55e7d 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -89,6 +89,8 @@ class EditorProperty : public Container { int property_usage; + bool draw_label = true; + bool draw_background = true; bool read_only = false; bool checkable = false; bool checked = false; @@ -170,6 +172,12 @@ class EditorProperty : public Container { void set_read_only(bool p_read_only); bool is_read_only() const; + void set_draw_label(bool p_draw_label); + bool is_draw_label() const; + + void set_draw_background(bool p_draw_background); + bool is_draw_background() const; + Object *get_edited_object(); StringName get_edited_property() const; inline Variant get_edited_property_value() const { diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index 3cc3a0f7c2ad..876b3b04d20e 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -146,6 +146,16 @@ bool EditorPropertyDictionaryObject::get_by_property_name(const String &p_name, return true; } + if (name == "new_item_key_name") { + r_ret = TTR("New Key:"); + return true; + } + + if (name == "new_item_value_name") { + r_ret = TTR("New Value:"); + return true; + } + if (name.begins_with("indices")) { int index = name.get_slicec('/', 1).to_int(); Variant key = dict.get_key_at_index(index); @@ -153,6 +163,13 @@ bool EditorPropertyDictionaryObject::get_by_property_name(const String &p_name, return true; } + if (name.begins_with("keys")) { + int index = name.get_slicec('/', 1).to_int(); + Variant key = dict.get_key_at_index(index); + r_ret = key; + return true; + } + return false; } @@ -191,6 +208,17 @@ String EditorPropertyDictionaryObject::get_property_name_for_index(int p_index) } } +String EditorPropertyDictionaryObject::get_key_name_for_index(int p_index) { + switch (p_index) { + case NEW_KEY_INDEX: + return "new_item_key_name"; + case NEW_VALUE_INDEX: + return "new_item_value_name"; + default: + return "keys/" + itos(p_index); + } +} + String EditorPropertyDictionaryObject::get_label_for_index(int p_index) { switch (p_index) { case NEW_KEY_INDEX: @@ -931,7 +959,31 @@ void EditorPropertyDictionary::_add_key_value() { void EditorPropertyDictionary::_create_new_property_slot(int p_idx) { HBoxContainer *hbox = memnew(HBoxContainer); + + EditorProperty *prop_key = nullptr; + if (p_idx != EditorPropertyDictionaryObject::NEW_KEY_INDEX && p_idx != EditorPropertyDictionaryObject::NEW_VALUE_INDEX) { + if (key_subtype == Variant::OBJECT) { + EditorPropertyObjectID *editor = memnew(EditorPropertyObjectID); + editor->setup("Object"); + prop_key = editor; + } else { + prop_key = EditorInspector::instantiate_property_editor(this, key_subtype, "", key_subtype_hint, key_subtype_hint_string, PROPERTY_USAGE_NONE); + } + prop_key->set_read_only(true); + prop_key->set_selectable(false); + prop_key->set_focus_mode(Control::FOCUS_NONE); + prop_key->set_draw_background(false); + prop_key->set_use_folding(is_using_folding()); + prop_key->set_h_size_flags(SIZE_EXPAND_FILL); + prop_key->set_draw_label(false); + hbox->add_child(prop_key); + } + EditorProperty *prop = memnew(EditorPropertyNil); + prop->set_h_size_flags(SIZE_EXPAND_FILL); + if (p_idx != EditorPropertyDictionaryObject::NEW_KEY_INDEX && p_idx != EditorPropertyDictionaryObject::NEW_VALUE_INDEX) { + prop->set_draw_label(false); + } hbox->add_child(prop); bool use_key = p_idx == EditorPropertyDictionaryObject::NEW_KEY_INDEX; @@ -959,6 +1011,7 @@ void EditorPropertyDictionary::_create_new_property_slot(int p_idx) { Slot slot; slot.prop = prop; + slot.prop_key = prop_key; slot.object = object; slot.container = hbox; int index = p_idx + (p_idx >= 0 ? page_index * page_length : 0); @@ -1171,6 +1224,9 @@ void EditorPropertyDictionary::update_property() { new_prop->connect(SNAME("property_changed"), callable_mp(this, &EditorPropertyDictionary::_property_changed)); new_prop->connect(SNAME("object_id_selected"), callable_mp(this, &EditorPropertyDictionary::_object_id_selected)); new_prop->set_h_size_flags(SIZE_EXPAND_FILL); + if (slot.index != EditorPropertyDictionaryObject::NEW_KEY_INDEX && slot.index != EditorPropertyDictionaryObject::NEW_VALUE_INDEX) { + new_prop->set_draw_label(false); + } new_prop->set_read_only(is_read_only()); slot.set_prop(new_prop); } else if (slot.index != EditorPropertyDictionaryObject::NEW_KEY_INDEX && slot.index != EditorPropertyDictionaryObject::NEW_VALUE_INDEX) { @@ -1188,6 +1244,9 @@ void EditorPropertyDictionary::update_property() { } slot.prop->update_property(); + if (slot.prop_key) { + slot.prop_key->update_property(); + } } updating = false; diff --git a/editor/editor_properties_array_dict.h b/editor/editor_properties_array_dict.h index 84c3f975be9e..abc28d91d51e 100644 --- a/editor/editor_properties_array_dict.h +++ b/editor/editor_properties_array_dict.h @@ -89,6 +89,7 @@ class EditorPropertyDictionaryObject : public RefCounted { String get_label_for_index(int p_index); String get_property_name_for_index(int p_index); + String get_key_name_for_index(int p_index); EditorPropertyDictionaryObject(); }; @@ -184,11 +185,14 @@ class EditorPropertyDictionary : public EditorProperty { Variant::Type type = Variant::VARIANT_MAX; bool as_id = false; EditorProperty *prop = nullptr; + EditorProperty *prop_key = nullptr; String prop_name; + String key_name; void set_index(int p_idx) { index = p_idx; prop_name = object->get_property_name_for_index(p_idx); + key_name = object->get_key_name_for_index(p_idx); update_prop_or_index(); } @@ -201,7 +205,11 @@ class EditorPropertyDictionary : public EditorProperty { void update_prop_or_index() { prop->set_object_and_property(object.ptr(), prop_name); - prop->set_label(object->get_label_for_index(index)); + if (prop_key) { + prop_key->set_object_and_property(object.ptr(), key_name); + } else { + prop->set_label(object->get_label_for_index(index)); + } } }; From 143d8c87bb4c004c0078c5db93e57f89338ff133 Mon Sep 17 00:00:00 2001 From: demolke Date: Sat, 30 Nov 2024 00:05:05 +0100 Subject: [PATCH 07/52] Move reimport check to EditorImportPlugin reimport_append is used by gltf_document, fbx_document and editor_import_plugin. The first two will never call it when importing == false. It's only the editor_import_plugin that should guard against that. https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html#class-editorimportplugin-method-append-import-external-resource The motivation of removing the check from gltf_document call path is to be able to test nested imports (texture embedded in gltf). --- editor/editor_file_system.cpp | 1 - editor/import/editor_import_plugin.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp index 4911d5c702dc..b3abbb9e70fa 100644 --- a/editor/editor_file_system.cpp +++ b/editor/editor_file_system.cpp @@ -3205,7 +3205,6 @@ void EditorFileSystem::reimport_files(const Vector &p_files) { } Error EditorFileSystem::reimport_append(const String &p_file, const HashMap &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) { - ERR_FAIL_COND_V_MSG(!importing, ERR_INVALID_PARAMETER, "Can only append files to import during a current reimport process."); Vector reloads; reloads.append(p_file); diff --git a/editor/import/editor_import_plugin.cpp b/editor/import/editor_import_plugin.cpp index 27b59d3bef56..82710103e1f0 100644 --- a/editor/import/editor_import_plugin.cpp +++ b/editor/import/editor_import_plugin.cpp @@ -214,6 +214,7 @@ Error EditorImportPlugin::_append_import_external_resource(const String &p_file, } Error EditorImportPlugin::append_import_external_resource(const String &p_file, const HashMap &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) { + ERR_FAIL_COND_V_MSG(!EditorFileSystem::get_singleton()->is_importing(), ERR_INVALID_PARAMETER, "Can only append files to import during a current reimport process."); return EditorFileSystem::get_singleton()->reimport_append(p_file, p_custom_options, p_custom_importer, p_generator_parameters); } From e649e7e3c59fc173fb080a30e643a25040cd7755 Mon Sep 17 00:00:00 2001 From: demolke Date: Fri, 29 Nov 2024 21:33:00 +0100 Subject: [PATCH 08/52] GLTF: Don't duplicate textures when importing blend files Blender imports will always start within `.godot/imported` folder because we first convert the .blend file to .gltf, store it in `.godot/imported` and run the import from there, so on-disk resources linked from .blend files end up with duplicate textures. --- .../editor/editor_scene_importer_gltf.cpp | 3 + modules/gltf/gltf_document.cpp | 75 +++++--- modules/gltf/gltf_document.h | 2 +- .../embedded_texture.gltf | 147 +++++++++++++++ .../gltf_placed_in_dot_godot_imported.gltf | 147 +++++++++++++++ .../texture.png | Bin 0 -> 92 bytes ...ting_to_texture_outside_of_res_folder.gltf | 147 +++++++++++++++ .../texture_source.png | Bin 0 -> 92 bytes modules/gltf/tests/test_gltf.h | 165 +++++++++++++++++ modules/gltf/tests/test_gltf_extras.h | 83 ++------- modules/gltf/tests/test_gltf_images.h | 169 ++++++++++++++++++ 11 files changed, 845 insertions(+), 93 deletions(-) create mode 100644 modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf create mode 100644 modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf create mode 100644 modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png create mode 100644 modules/gltf/tests/data/gltf_pointing_to_texture_outside_of_res_folder/gltf_pointing_to_texture_outside_of_res_folder.gltf create mode 100644 modules/gltf/tests/data/gltf_pointing_to_texture_outside_of_res_folder/texture_source.png create mode 100644 modules/gltf/tests/test_gltf.h create mode 100644 modules/gltf/tests/test_gltf_images.h diff --git a/modules/gltf/editor/editor_scene_importer_gltf.cpp b/modules/gltf/editor/editor_scene_importer_gltf.cpp index 41e294cfc6e1..3e75017fe65b 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.cpp +++ b/modules/gltf/editor/editor_scene_importer_gltf.cpp @@ -62,6 +62,9 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { state->set_import_as_skeleton_bones(true); } + if (p_options.has(SNAME("extract_path"))) { + state->set_extract_path(p_options["extract_path"]); + } state->set_bake_fps(p_options["animation/fps"]); Error err = gltf->append_from_file(p_path, state, p_flags); if (err != OK) { diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index bfd21891299e..e5d8955fb6a7 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -3932,7 +3932,7 @@ Ref GLTFDocument::_parse_image_bytes_into_image(Ref p_state, c return r_image; } -void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_file_extension, int p_index, Ref p_image) { +void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_resource_uri, const String &p_file_extension, int p_index, Ref p_image) { GLTFState::GLTFHandleBinary handling = GLTFState::GLTFHandleBinary(p_state->handle_binary_image); if (p_image->is_empty() || handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_DISCARD_TEXTURES) { p_state->images.push_back(Ref()); @@ -3950,33 +3950,46 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< WARN_PRINT(vformat("glTF: Image index '%d' did not have a name. It will be automatically given a name based on its index.", p_index)); p_image->set_name(itos(p_index)); } - bool must_import = true; + bool must_write = true; // If the resource does not exist on the disk within res:// directory write it. + bool must_import = true; // Trigger import. Vector img_data = p_image->get_data(); Dictionary generator_parameters; - String file_path = p_state->get_extract_path().path_join(p_state->get_extract_prefix() + "_" + p_image->get_name()); - file_path += p_file_extension.is_empty() ? ".png" : p_file_extension; - if (FileAccess::exists(file_path + ".import")) { - Ref config; - config.instantiate(); - config->load(file_path + ".import"); - if (config->has_section_key("remap", "generator_parameters")) { - generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters"); - } - if (!generator_parameters.has("md5")) { - must_import = false; // Didn't come from a gltf document; don't overwrite. + String file_path; + // If resource_uri is within res:// folder but outside of .godot/imported folder, use it. + if (!p_resource_uri.is_empty() && !p_resource_uri.begins_with("res://.godot/imported") && !p_resource_uri.begins_with("res://..")) { + file_path = p_resource_uri; + must_import = true; + must_write = !FileAccess::exists(file_path); + } else { + // Texture data has to be written to the res:// folder and imported. + file_path = p_state->get_extract_path().path_join(p_state->get_extract_prefix() + "_" + p_image->get_name()); + file_path += p_file_extension.is_empty() ? ".png" : p_file_extension; + if (FileAccess::exists(file_path + ".import")) { + Ref config; + config.instantiate(); + config->load(file_path + ".import"); + if (config->has_section_key("remap", "generator_parameters")) { + generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters"); + } + if (!generator_parameters.has("md5")) { + must_write = false; // Didn't come from a gltf document; don't overwrite. + must_import = false; // And don't import. + } } } - if (must_import) { + + if (must_write) { String existing_md5 = generator_parameters["md5"]; unsigned char md5_hash[16]; CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash); String new_md5 = String::hex_encode_buffer(md5_hash, 16); generator_parameters["md5"] = new_md5; if (new_md5 == existing_md5) { + must_write = false; must_import = false; } } - if (must_import) { + if (must_write) { Error err = OK; if (p_file_extension.is_empty()) { // If a file extension was not specified, save the image data to a PNG file. @@ -3989,10 +4002,13 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< file->store_buffer(p_bytes); file->close(); } + } + if (must_import) { // ResourceLoader::import will crash if not is_editor_hint(), so this case is protected above and will fall through to uncompressed. HashMap custom_options; custom_options[SNAME("mipmaps/generate")] = true; // Will only use project settings defaults if custom_importer is empty. + EditorFileSystem::get_singleton()->update_file(file_path); EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters); } @@ -4002,7 +4018,7 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< p_state->source_images.push_back(saved_image->get_image()); return; } else { - WARN_PRINT(vformat("glTF: Image index '%d' with the name '%s' couldn't be imported. It will be loaded directly instead, uncompressed.", p_index, p_image->get_name())); + WARN_PRINT(vformat("glTF: Image index '%d' with the name '%s' resolved to %s couldn't be imported. It will be loaded directly instead, uncompressed.", p_index, p_image->get_name(), file_path)); } } } @@ -4070,6 +4086,9 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p while (used_names.has(image_name)) { image_name += "_" + itos(i); } + + String resource_uri; + used_names.insert(image_name); // Load the image data. If we get a byte array, store here for later. Vector data; @@ -4087,14 +4106,14 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER); uri = uri.uri_decode(); uri = p_base_path.path_join(uri).replace("\\", "/"); // Fix for Windows. - // If the image is in the .godot/imported directory, we can't use ResourceLoader. - if (!p_base_path.begins_with("res://.godot/imported")) { - // ResourceLoader will rely on the file extension to use the relevant loader. - // The spec says that if mimeType is defined, it should take precedence (e.g. - // there could be a `.png` image which is actually JPEG), but there's no easy - // API for that in Godot, so we'd have to load as a buffer (i.e. embedded in - // the material), so we only do that only as fallback. - Ref texture = ResourceLoader::load(uri, "Texture2D"); + resource_uri = uri.simplify_path(); + // ResourceLoader will rely on the file extension to use the relevant loader. + // The spec says that if mimeType is defined, it should take precedence (e.g. + // there could be a `.png` image which is actually JPEG), but there's no easy + // API for that in Godot, so we'd have to load as a buffer (i.e. embedded in + // the material), so we only do that only as fallback. + if (ResourceLoader::exists(resource_uri)) { + Ref texture = ResourceLoader::load(resource_uri, "Texture2D"); if (texture.is_valid()) { p_state->images.push_back(texture); p_state->source_images.push_back(texture->get_image()); @@ -4105,13 +4124,13 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p // If the mimeType does not match with the file extension, either it should be // specified in the file, or the GLTFDocumentExtension should handle it. if (mime_type.is_empty()) { - mime_type = "image/" + uri.get_extension(); + mime_type = "image/" + resource_uri.get_extension(); } // Fallback to loading as byte array. This enables us to support the // spec's requirement that we honor mimetype regardless of file URI. - data = FileAccess::get_file_as_bytes(uri); + data = FileAccess::get_file_as_bytes(resource_uri); if (data.size() == 0) { - WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded as a buffer of MIME type '%s' from URI: %s because there was no data to load. Skipping it.", i, mime_type, uri)); + WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded as a buffer of MIME type '%s' from URI: %s because there was no data to load. Skipping it.", i, mime_type, resource_uri)); p_state->images.push_back(Ref()); // Placeholder to keep count. p_state->source_images.push_back(Ref()); continue; @@ -4141,7 +4160,7 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p String file_extension; Ref img = _parse_image_bytes_into_image(p_state, data, mime_type, i, file_extension); img->set_name(image_name); - _parse_image_save_image(p_state, data, file_extension, i, img); + _parse_image_save_image(p_state, data, resource_uri, file_extension, i, img); } print_verbose("glTF: Total images: " + itos(p_state->images.size())); diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index a6d6caa3f0a7..f17d1a1d7328 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -190,7 +190,7 @@ class GLTFDocument : public Resource { Error _serialize_images(Ref p_state); Error _serialize_lights(Ref p_state); Ref _parse_image_bytes_into_image(Ref p_state, const Vector &p_bytes, const String &p_mime_type, int p_index, String &r_file_extension); - void _parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_file_extension, int p_index, Ref p_image); + void _parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_resource_uri, const String &p_file_extension, int p_index, Ref p_image); Error _parse_images(Ref p_state, const String &p_base_path); Error _parse_textures(Ref p_state); Error _parse_texture_samplers(Ref p_state); diff --git a/modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf b/modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf new file mode 100644 index 000000000000..59b154fb3d67 --- /dev/null +++ b/modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf @@ -0,0 +1,147 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.2.70", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 1 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"mesh_instance_3d" + }, + { + "children":[ + 0 + ], + "name":"_Node3D_6" + } + ], + "materials":[ + { + "name":"material", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.9999998807907104, + 0.9999998807907104, + 0.9999998807907104, + 1 + ], + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0 + } + } + ], + "meshes":[ + { + "name":"Mesh_0", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"material_albedo000", + "uri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABZJREFUCJljZGBg+P+fgYGBBUIxMAAAKCAEAplLvcoAAAAASUVORK5CYII=" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":48, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":48, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":96, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":128, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":140, + "uri":"data:application/octet-stream;base64,AACAPwAAAAAAAIA/AACAvwAAAAAAAIA/AACAPwAAAAAAAIC/AACAvwAAAAAAAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAACAAEAAAACAAMAAQA=" + } + ] +} diff --git a/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf b/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf new file mode 100644 index 000000000000..3c03102eea36 --- /dev/null +++ b/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf @@ -0,0 +1,147 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.2.70", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 1 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"mesh_instance_3d" + }, + { + "children":[ + 0 + ], + "name":"_Node3D_6" + } + ], + "materials":[ + { + "name":"material", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.9999998807907104, + 0.9999998807907104, + 0.9999998807907104, + 1 + ], + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0 + } + } + ], + "meshes":[ + { + "name":"Mesh_0", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"material_albedo000", + "uri":"texture.png", + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":48, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":48, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":96, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":128, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":140, + "uri":"data:application/octet-stream;base64,AACAPwAAAAAAAIA/AACAvwAAAAAAAIA/AACAPwAAAAAAAIC/AACAvwAAAAAAAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAACAAEAAAACAAMAAQA=" + } + ] +} diff --git a/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png b/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png new file mode 100644 index 0000000000000000000000000000000000000000..cb872e7419b7904df8cc02fa9fb7f84a8885bb0a GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nR#^NA%Cx&(BWL^R}VxBIJAsjQ4 nQxXz>{GZ?0*vRT+Xu!arp}@j4(|hkJpb7?0S3j3^P6 import_scene; + import_scene.instantiate("PackedScene", true); + ResourceFormatImporter::get_singleton()->add_importer(import_scene); + Ref import_gltf; + import_gltf.instantiate(); + ResourceImporterScene::add_scene_importer(import_gltf); + + // Support processing png files in editor import. + Ref import_texture; + import_texture.instantiate(true); + ResourceFormatImporter::get_singleton()->add_importer(import_texture); + + // Once editor import convert pngs to ctex, we will need to load it as ctex resource. + Ref resource_loader_stream_texture; + resource_loader_stream_texture.instantiate(); + ResourceLoader::add_resource_format_loader(resource_loader_stream_texture); + + HashMap options(21); + options["nodes/root_type"] = ""; + options["nodes/root_name"] = ""; + options["nodes/apply_root_scale"] = true; + options["nodes/root_scale"] = 1.0; + options["meshes/ensure_tangents"] = true; + options["meshes/generate_lods"] = false; + options["meshes/create_shadow_meshes"] = true; + options["meshes/light_baking"] = 1; + options["meshes/lightmap_texel_size"] = 0.2; + options["meshes/force_disable_compression"] = false; + options["skins/use_named_skins"] = true; + options["animation/import"] = true; + options["animation/fps"] = 30; + options["animation/trimming"] = false; + options["animation/remove_immutable_tracks"] = true; + options["import_script/path"] = ""; + options["extract_path"] = "res://"; + options["_subresources"] = Dictionary(); + options["gltf/naming_version"] = 1; + + // Process gltf file, note that this generates `.scn` resource from the 2nd argument. + String scene_file = "res://" + p_file.get_file().get_basename(); + Error err = import_scene->import(0, p_file, scene_file, options, nullptr, nullptr, nullptr); + CHECK_MESSAGE(err == OK, "GLTF import failed."); + + Ref packed_scene = ResourceLoader::load(scene_file + ".scn", "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err); + CHECK_MESSAGE(err == OK, "Loading scene failed."); + Node *p_scene = packed_scene->instantiate(); + + ResourceImporterScene::remove_scene_importer(import_gltf); + ResourceFormatImporter::get_singleton()->remove_importer(import_texture); + ResourceLoader::remove_resource_format_loader(resource_loader_stream_texture); + return p_scene; +} + +static Node *gltf_export_then_import(Node *p_root, const String &p_test_name) { + String tempfile = TestUtils::get_temp_path(p_test_name); + + Ref doc; + doc.instantiate(); + Ref state; + state.instantiate(); + Error err = doc->append_from_scene(p_root, state, EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS); + CHECK_MESSAGE(err == OK, "GLTF state generation failed."); + + err = doc->write_to_filesystem(state, tempfile + ".gltf"); + CHECK_MESSAGE(err == OK, "Writing GLTF to cache dir failed."); + + return gltf_import(tempfile + ".gltf"); +} + +void init(const String &p_test, const String &p_copy_target = String()) { + Error err; + + // Setup project settings since it's needed for the import process. + String project_folder = TestUtils::get_temp_path(p_test.get_file().get_basename()); + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + da->make_dir_recursive(project_folder.path_join(".godot").path_join("imported")); + // Initialize res:// to `project_folder`. + TestProjectSettingsInternalsAccessor::resource_path() = project_folder; + err = ProjectSettings::get_singleton()->setup(project_folder, String(), true); + + if (p_copy_target.is_empty()) { + return; + } + + // Copy all the necessary test data files to the res:// directory. + da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + String test_data = String("modules/gltf/tests/data/").path_join(p_test); + da = DirAccess::open(test_data); + CHECK_MESSAGE(da.is_valid(), "Unable to open folder."); + da->list_dir_begin(); + for (String item = da->get_next(); !item.is_empty(); item = da->get_next()) { + if (!FileAccess::exists(test_data.path_join(item))) { + continue; + } + Ref output = FileAccess::open(p_copy_target.path_join(item), FileAccess::WRITE, &err); + CHECK_MESSAGE(err == OK, "Unable to open output file."); + output->store_buffer(FileAccess::get_file_as_bytes(test_data.path_join(item))); + output->close(); + } + da->list_dir_end(); +} + +} //namespace TestGltf + +#endif // TOOLS_ENABLED + +#endif // TEST_GLTF_H diff --git a/modules/gltf/tests/test_gltf_extras.h b/modules/gltf/tests/test_gltf_extras.h index 73ef02e9f1b4..84a4dff76b96 100644 --- a/modules/gltf/tests/test_gltf_extras.h +++ b/modules/gltf/tests/test_gltf_extras.h @@ -31,6 +31,7 @@ #ifndef TEST_GLTF_EXTRAS_H #define TEST_GLTF_EXTRAS_H +#include "test_gltf.h" #include "tests/test_macros.h" #ifdef TOOLS_ENABLED @@ -47,61 +48,10 @@ #include "scene/resources/material.h" #include "scene/resources/packed_scene.h" -namespace TestGltfExtras { - -static Node *_gltf_export_then_import(Node *p_root, String &p_tempfilebase) { - Ref doc; - doc.instantiate(); - Ref state; - state.instantiate(); - Error err = doc->append_from_scene(p_root, state, EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS); - CHECK_MESSAGE(err == OK, "GLTF state generation failed."); - err = doc->write_to_filesystem(state, p_tempfilebase + ".gltf"); - CHECK_MESSAGE(err == OK, "Writing GLTF to cache dir failed."); - - // Setting up importers. - Ref import_scene = memnew(ResourceImporterScene("PackedScene", true)); - ResourceFormatImporter::get_singleton()->add_importer(import_scene); - Ref import_gltf; - import_gltf.instantiate(); - ResourceImporterScene::add_scene_importer(import_gltf); - - // GTLF importer behaves differently outside of editor, it's too late to modify Engine::get_editor_hint - // as the registration of runtime extensions already happened, so remove them. See modules/gltf/register_types.cpp - GLTFDocument::unregister_all_gltf_document_extensions(); - - HashMap options(20); - options["nodes/root_type"] = ""; - options["nodes/root_name"] = ""; - options["nodes/apply_root_scale"] = true; - options["nodes/root_scale"] = 1.0; - options["meshes/ensure_tangents"] = true; - options["meshes/generate_lods"] = false; - options["meshes/create_shadow_meshes"] = true; - options["meshes/light_baking"] = 1; - options["meshes/lightmap_texel_size"] = 0.2; - options["meshes/force_disable_compression"] = false; - options["skins/use_named_skins"] = true; - options["animation/import"] = true; - options["animation/fps"] = 30; - options["animation/trimming"] = false; - options["animation/remove_immutable_tracks"] = true; - options["import_script/path"] = ""; - options["_subresources"] = Dictionary(); - options["gltf/naming_version"] = 1; - - // Process gltf file, note that this generates `.scn` resource from the 2nd argument. - err = import_scene->import(0, p_tempfilebase + ".gltf", p_tempfilebase, options, nullptr, nullptr, nullptr); - CHECK_MESSAGE(err == OK, "GLTF import failed."); - ResourceImporterScene::remove_scene_importer(import_gltf); - - Ref packed_scene = ResourceLoader::load(p_tempfilebase + ".scn", "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err); - CHECK_MESSAGE(err == OK, "Loading scene failed."); - Node *p_scene = packed_scene->instantiate(); - return p_scene; -} +namespace TestGltf { TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import") { + init("gltf_mesh_material_extras"); // Setup scene. Ref original_material = memnew(StandardMaterial3D); original_material->set_albedo(Color(1.0, .0, .0)); @@ -133,9 +83,11 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" original->set_meta("extras", node_dict); original->set_meta("meta_not_nested_under_extras", "should not propagate"); + original->set_owner(SceneTree::get_singleton()->get_root()); + original_mesh_instance->set_owner(SceneTree::get_singleton()->get_root()); + // Convert to GLFT and back. - String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_extras"); - Node *loaded = _gltf_export_then_import(original, tempfile); + Node *loaded = gltf_export_then_import(original, "gltf_extras"); // Compare the results. CHECK(loaded->get_name() == "node3d"); @@ -161,6 +113,7 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" } TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { + init("gltf_skeleton_extras"); // Setup scene. Skeleton3D *skeleton = memnew(Skeleton3D); skeleton->set_name("skeleton"); @@ -189,18 +142,20 @@ TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { mesh->set_mesh(meshdata); mesh->set_name("mesh_instance_3d"); - Node3D *scene = memnew(Node3D); - SceneTree::get_singleton()->get_root()->add_child(scene); - scene->add_child(skeleton); - scene->add_child(mesh); - scene->set_name("node3d"); + Node3D *original = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(original); + original->add_child(skeleton); + original->add_child(mesh); + original->set_name("node3d"); // Now that both skeleton and mesh are part of scene, link them. mesh->set_skeleton_path(mesh->get_path_to(skeleton)); + mesh->set_owner(SceneTree::get_singleton()->get_root()); + original->set_owner(SceneTree::get_singleton()->get_root()); + // Convert to GLFT and back. - String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras"); - Node *loaded = _gltf_export_then_import(scene, tempfile); + Node *loaded = gltf_export_then_import(original, "gltf_bone_extras"); // Compare the results. CHECK(loaded->get_name() == "node3d"); @@ -212,10 +167,10 @@ TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { memdelete(skeleton); memdelete(mesh); - memdelete(scene); + memdelete(original); memdelete(loaded); } -} // namespace TestGltfExtras +} //namespace TestGltf #endif // TOOLS_ENABLED diff --git a/modules/gltf/tests/test_gltf_images.h b/modules/gltf/tests/test_gltf_images.h new file mode 100644 index 000000000000..969ed3966560 --- /dev/null +++ b/modules/gltf/tests/test_gltf_images.h @@ -0,0 +1,169 @@ +/**************************************************************************/ +/* test_gltf_images.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_GLTF_IMAGES_H +#define TEST_GLTF_IMAGES_H + +#include "test_gltf.h" + +#ifdef TOOLS_ENABLED + +#include "editor/editor_file_system.h" +#include "editor/editor_paths.h" +#include "scene/resources/image_texture.h" + +namespace TestGltf { +Ref _check_texture(Node *p_node) { + MeshInstance3D *mesh_instance_3d = Object::cast_to(p_node->find_child("mesh_instance_3d", true, true)); + Ref material = mesh_instance_3d->get_active_material(0); + Ref texture = material->get_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO); + + CHECK_MESSAGE(texture->get_size().x == 2, "Texture width not correct."); + CHECK_MESSAGE(texture->get_size().y == 2, "Texture height not correct."); + + // Check if the loaded texture pixels are exactly as we expect. + for (int x = 0; x < 2; ++x) { + for (int y = 0; y < 2; ++y) { + Color c = texture->get_image()->get_pixel(x, y); + CHECK_MESSAGE(c == Color(x, y, y), "Texture content is incorrect."); + } + } + return texture; +} + +TEST_CASE("[SceneTree][Node] Export GLTF with external texture and import") { + init("gltf_images_external_export_import"); + // Setup scene. + Ref original_texture; + original_texture.instantiate(); + Ref image; + image.instantiate(); + image->initialize_data(2, 2, false, Image::FORMAT_RGBA8); + for (int x = 0; x < 2; ++x) { + for (int y = 0; y < 2; ++y) { + image->set_pixel(x, y, Color(x, y, y)); + } + } + + original_texture->set_image(image); + + Ref original_material; + original_material.instantiate(); + original_material->set_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO, original_texture); + original_material->set_name("material"); + + Ref original_meshdata; + original_meshdata.instantiate(); + original_meshdata->set_name("planemesh"); + original_meshdata->surface_set_material(0, original_material); + + MeshInstance3D *original_mesh_instance = memnew(MeshInstance3D); + original_mesh_instance->set_mesh(original_meshdata); + original_mesh_instance->set_name("mesh_instance_3d"); + + Node3D *original = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(original); + original->add_child(original_mesh_instance); + original->set_owner(SceneTree::get_singleton()->get_root()); + original_mesh_instance->set_owner(SceneTree::get_singleton()->get_root()); + + // Convert to GLFT and back. + Node *loaded = gltf_export_then_import(original, "gltf_images"); + _check_texture(loaded); + + memdelete(original_mesh_instance); + memdelete(original); + memdelete(loaded); +} + +TEST_CASE("[SceneTree][Node][Editor] Import GLTF from .godot/imported folder with external texture") { + init("gltf_placed_in_dot_godot_imported", "res://.godot/imported"); + + EditorFileSystem *efs = memnew(EditorFileSystem); + EditorResourcePreview *erp = memnew(EditorResourcePreview); + + Node *loaded = gltf_import("res://.godot/imported/gltf_placed_in_dot_godot_imported.gltf"); + Ref texture = _check_texture(loaded); + + // In-editor imports of gltf and texture from .godot/imported folder should end up in res:// if extract_path is defined. + CHECK_MESSAGE(texture->get_path() == "res://gltf_placed_in_dot_godot_imported_material_albedo000.png", "Texture not parsed as resource."); + + memdelete(loaded); + memdelete(erp); + memdelete(efs); +} + +TEST_CASE("[SceneTree][Node][Editor] Import GLTF with texture outside of res:// directory") { + init("gltf_pointing_to_texture_outside_of_res_folder", "res://"); + + EditorFileSystem *efs = memnew(EditorFileSystem); + EditorResourcePreview *erp = memnew(EditorResourcePreview); + + // Copy texture to the parent folder of res:// - i.e. to res://.. where we can't import from. + String oneup = TestUtils::get_temp_path("texture.png"); + Error err; + Ref output = FileAccess::open(oneup, FileAccess::WRITE, &err); + CHECK_MESSAGE(err == OK, "Unable to open texture file."); + output->store_buffer(FileAccess::get_file_as_bytes("res://texture_source.png")); + output->close(); + + Node *loaded = gltf_import("res://gltf_pointing_to_texture_outside_of_res_folder.gltf"); + Ref texture = _check_texture(loaded); + + // Imports of gltf with texture from outside of res:// folder should end up being copied to res:// + CHECK_MESSAGE(texture->get_path() == "res://gltf_pointing_to_texture_outside_of_res_folder_material_albedo000.png", "Texture not parsed as resource."); + + memdelete(loaded); + memdelete(erp); + memdelete(efs); +} + +TEST_CASE("[SceneTree][Node][Editor] Import GLTF with embedded texture, check how it got extracted") { + init("gltf_embedded_texture", "res://"); + + EditorFileSystem *efs = memnew(EditorFileSystem); + EditorResourcePreview *erp = memnew(EditorResourcePreview); + + Node *loaded = gltf_import("res://embedded_texture.gltf"); + Ref texture = _check_texture(loaded); + + // In-editor imports of texture embedded in file should end up with a resource. + CHECK_MESSAGE(texture->get_path() == "res://embedded_texture_material_albedo000.png", "Texture not parsed as resource."); + + memdelete(loaded); + memdelete(erp); + memdelete(efs); +} + +} //namespace TestGltf + +#endif // TOOLS_ENABLED + +#endif // TEST_GLTF_IMAGES_H From 06efe84bcae33c5bba48c6daf093e7882f0932ed Mon Sep 17 00:00:00 2001 From: Kiro Date: Sun, 1 Dec 2024 11:05:53 +0100 Subject: [PATCH 09/52] Remove duplicate `utf8()` calls --- core/io/pck_packer.cpp | 5 +++-- modules/gltf/gltf_document.cpp | 10 ++++++---- platform/linuxbsd/x11/display_server_x11.cpp | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/io/pck_packer.cpp b/core/io/pck_packer.cpp index b9fe121ea6d5..c7cfca190d71 100644 --- a/core/io/pck_packer.cpp +++ b/core/io/pck_packer.cpp @@ -198,11 +198,12 @@ Error PCKPacker::flush(bool p_verbose) { } for (int i = 0; i < files.size(); i++) { - int string_len = files[i].path.utf8().length(); + CharString utf8_string = files[i].path.utf8(); + int string_len = utf8_string.length(); int pad = _get_pad(4, string_len); fhead->store_32(uint32_t(string_len + pad)); - fhead->store_buffer((const uint8_t *)files[i].path.utf8().get_data(), string_len); + fhead->store_buffer((const uint8_t *)utf8_string.get_data(), string_len); for (int j = 0; j < pad; j++) { fhead->store_8(0); } diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index bfd21891299e..ccca9c701bcc 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -8235,11 +8235,10 @@ PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Erro const int32_t header_size = 12; const int32_t chunk_header_size = 8; - int32_t padding = (chunk_header_size + json.utf8().length()) % 4; - json += String(" ").repeat(padding); - CharString cs = json.utf8(); - const uint32_t text_chunk_length = cs.length(); + int32_t padding = (chunk_header_size + cs.length()) % 4; + + const uint32_t text_chunk_length = cs.length() + padding; const uint32_t text_chunk_type = 0x4E4F534A; //JSON int32_t binary_data_length = 0; @@ -8257,6 +8256,9 @@ PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Erro buffer->put_32(text_chunk_length); buffer->put_32(text_chunk_type); buffer->put_data((uint8_t *)&cs[0], cs.length()); + for (int i = 0; i < padding; i++) { + buffer->put_8(' '); + } if (binary_chunk_length) { buffer->put_32(binary_chunk_length); buffer->put_32(binary_chunk_type); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index f8716c6f34cc..ce081d75b27d 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -1939,7 +1939,8 @@ void DisplayServerX11::window_set_title(const String &p_title, WindowID p_window Atom _net_wm_name = XInternAtom(x11_display, "_NET_WM_NAME", false); Atom utf8_string = XInternAtom(x11_display, "UTF8_STRING", false); if (_net_wm_name != None && utf8_string != None) { - XChangeProperty(x11_display, wd.x11_window, _net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char *)p_title.utf8().get_data(), p_title.utf8().length()); + CharString utf8_title = p_title.utf8(); + XChangeProperty(x11_display, wd.x11_window, _net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char *)utf8_title.get_data(), utf8_title.length()); } } From 38ff1500c790194846de0a7a472d61466bd9347c Mon Sep 17 00:00:00 2001 From: Michael Alexsander Date: Thu, 7 Nov 2024 12:24:14 -0300 Subject: [PATCH 10/52] Add toggle to hide filtered out parents in the "SceneTree" dock --- doc/classes/EditorSettings.xml | 3 + editor/debugger/editor_debugger_tree.cpp | 113 +++++++++++------ editor/debugger/editor_debugger_tree.h | 13 +- editor/editor_settings.cpp | 1 + editor/gui/scene_tree_editor.cpp | 152 +++++++++++++++++------ editor/gui/scene_tree_editor.h | 4 +- editor/scene_tree_dock.cpp | 15 ++- editor/scene_tree_dock.h | 2 +- 8 files changed, 223 insertions(+), 80 deletions(-) diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index e1c85d3595f5..bd9f7a665aca 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -241,6 +241,9 @@ If [code]true[/code], new node created when reparenting node(s) will be positioned at the average position of the selected node(s). + + If [code]true[/code], the scene tree dock will only show nodes that match the filter, without showing parents that don't. This settings can also be changed in the Scene dock's top menu. + If [code]true[/code], the Create dialog (Create New Node/Create New Resource) will start with all its sections expanded. Otherwise, sections will be collapsed until the user starts searching (which will automatically expand sections as needed). diff --git a/editor/debugger/editor_debugger_tree.cpp b/editor/debugger/editor_debugger_tree.cpp index a9e4adf674c2..24c505aec4bc 100644 --- a/editor/debugger/editor_debugger_tree.cpp +++ b/editor/debugger/editor_debugger_tree.cpp @@ -32,6 +32,7 @@ #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_node.h" +#include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/gui/editor_file_dialog.h" #include "editor/scene_tree_dock.h" @@ -146,24 +147,50 @@ void EditorDebuggerTree::_scene_tree_rmb_selected(const Vector2 &p_position, Mou /// |-E /// void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger) { + set_hide_root(false); + updating_scene_tree = true; const String last_path = get_selected_path(); const String filter = SceneTreeDock::get_singleton()->get_filter(); + TreeItem *select_item = nullptr; + bool hide_filtered_out_parents = EDITOR_GET("docks/scene_tree/hide_filtered_out_parents"); + bool should_scroll = scrolling_to_item || filter != last_filter; scrolling_to_item = false; TreeItem *scroll_item = nullptr; // Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion. - List> parents; + List parents; for (const SceneDebuggerTree::RemoteNode &node : p_tree->nodes) { TreeItem *parent = nullptr; + Pair move_from_to; if (parents.size()) { // Find last parent. - Pair &p = parents.front()->get(); - parent = p.first; - if (!(--p.second)) { // If no child left, remove it. + ParentItem &p = parents.front()->get(); + parent = p.tree_item; + if (!(--p.child_count)) { // If no child left, remove it. parents.pop_front(); + + if (hide_filtered_out_parents && !filter.is_subsequence_ofn(parent->get_text(0))) { + if (parent == get_root()) { + set_hide_root(true); + } else { + move_from_to.first = parent; + // Find the closest ancestor that matches the filter. + for (const ParentItem p2 : parents) { + move_from_to.second = p2.tree_item; + if (p2.matches_filter || move_from_to.second == get_root()) { + break; + } + } + + if (!move_from_to.second) { + move_from_to.second = get_root(); + } + } + } } } + // Add this node. TreeItem *item = create_item(parent); item->set_text(0, node.name); @@ -178,12 +205,17 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int } item->set_metadata(0, node.id); - // Set current item as collapsed if necessary (root is never collapsed). + String current_path; if (parent) { + current_path += (String)parent->get_meta("node_path"); + + // Set current item as collapsed if necessary (root is never collapsed). if (!unfold_cache.has(node.id)) { item->set_collapsed(true); } } + item->set_meta("node_path", current_path + "/" + item->get_text(0)); + // Select previously selected node. if (debugger_id == p_debugger) { // Can use remote id. if (node.id == inspected_object_id) { @@ -196,21 +228,18 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int updating_scene_tree = true; } - item->select(0); - + select_item = item; if (should_scroll) { scroll_item = item; } } - } else { // Must use path - if (last_path == _get_path(item)) { - updating_scene_tree = false; // Force emission of new selection. - item->select(0); - if (should_scroll) { - scroll_item = item; - } - updating_scene_tree = true; + } else if (last_path == (String)item->get_meta("node_path")) { // Must use path. + updating_scene_tree = false; // Force emission of new selection. + select_item = item; + if (should_scroll) { + scroll_item = item; } + updating_scene_tree = true; } // Add buttons. @@ -242,7 +271,7 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int // Add in front of the parents stack if children are expected. if (node.child_count) { - parents.push_front(Pair(item, node.child_count)); + parents.push_front(ParentItem(item, node.child_count, filter.is_subsequence_ofn(item->get_text(0)))); } else { // Apply filters. while (parent) { @@ -250,31 +279,60 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int if (filter.is_subsequence_ofn(item->get_text(0))) { break; // Filter matches, must survive. } + parent->remove_child(item); memdelete(item); - if (scroll_item == item) { + if (select_item == item || scroll_item == item) { + select_item = nullptr; scroll_item = nullptr; } + if (had_siblings) { break; // Parent must survive. } + item = parent; parent = item->get_parent(); // Check if parent expects more children. - for (const Pair &pair : parents) { - if (pair.first == item) { + for (ParentItem &pair : parents) { + if (pair.tree_item == item) { parent = nullptr; break; // Might have more children. } } } } + + // Move all children to the ancestor that matches the filter, if picked. + if (move_from_to.first) { + TreeItem *from = move_from_to.first; + TypedArray children = from->get_children(); + if (!children.is_empty()) { + for (Variant &c : children) { + TreeItem *ti = Object::cast_to(c); + from->remove_child(ti); + move_from_to.second->add_child(ti); + } + + from->get_parent()->remove_child(from); + memdelete(from); + if (select_item == from || scroll_item == from) { + select_item = nullptr; + scroll_item = nullptr; + } + } + } } debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree. + + if (select_item) { + select_item->select(0); + } if (scroll_item) { scroll_to_item(scroll_item, false); } + last_filter = filter; updating_scene_tree = false; } @@ -338,22 +396,7 @@ String EditorDebuggerTree::get_selected_path() { if (!get_selected()) { return ""; } - return _get_path(get_selected()); -} - -String EditorDebuggerTree::_get_path(TreeItem *p_item) { - ERR_FAIL_NULL_V(p_item, ""); - - if (p_item->get_parent() == nullptr) { - return "/root"; - } - String text = p_item->get_text(0); - TreeItem *cur = p_item->get_parent(); - while (cur) { - text = cur->get_text(0) + "/" + text; - cur = cur->get_parent(); - } - return "/" + text; + return get_selected()->get_meta("node_path"); } void EditorDebuggerTree::_item_menu_id_pressed(int p_option) { diff --git a/editor/debugger/editor_debugger_tree.h b/editor/debugger/editor_debugger_tree.h index d048688cad24..46893d6dc3bd 100644 --- a/editor/debugger/editor_debugger_tree.h +++ b/editor/debugger/editor_debugger_tree.h @@ -40,6 +40,18 @@ class EditorDebuggerTree : public Tree { GDCLASS(EditorDebuggerTree, Tree); private: + struct ParentItem { + TreeItem *tree_item; + int child_count; + bool matches_filter; + + ParentItem(TreeItem *p_tree_item = nullptr, int p_child_count = 0, bool p_matches_filter = false) { + tree_item = p_tree_item; + child_count = p_child_count; + matches_filter = p_matches_filter; + } + }; + enum ItemMenu { ITEM_MENU_SAVE_REMOTE_NODE, ITEM_MENU_COPY_NODE_PATH, @@ -56,7 +68,6 @@ class EditorDebuggerTree : public Tree { EditorFileDialog *file_dialog = nullptr; String last_filter; - String _get_path(TreeItem *p_item); void _scene_tree_folded(Object *p_obj); void _scene_tree_selected(); void _scene_tree_rmb_selected(const Vector2 &p_position, MouseButton p_button); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index bd886291bb9f..aa77128fff78 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -633,6 +633,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { _initial_set("docks/scene_tree/start_create_dialog_fully_expanded", false); _initial_set("docks/scene_tree/auto_expand_to_selected", true); _initial_set("docks/scene_tree/center_node_on_reparent", false); + _initial_set("docks/scene_tree/hide_filtered_out_parents", true); // FileSystem EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "docks/filesystem/thumbnail_size", 64, "32,128,16") diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp index 6e65d4d8ee49..cf443d6f7b2a 100644 --- a/editor/gui/scene_tree_editor.cpp +++ b/editor/gui/scene_tree_editor.cpp @@ -952,47 +952,60 @@ bool SceneTreeEditor::_update_filter(TreeItem *p_parent, bool p_scroll_to_select return false; } - bool keep_for_children = false; - for (TreeItem *child = p_parent->get_first_child(); child; child = child->get_next()) { - // Always keep if at least one of the children are kept. - keep_for_children = _update_filter(child, p_scroll_to_selected) || keep_for_children; - } - // Now find other reasons to keep this Node, too. PackedStringArray terms = filter.to_lower().split_spaces(); bool keep = _item_matches_all_terms(p_parent, terms); bool selectable = keep; - if (keep && !valid_types.is_empty()) { - selectable = false; + bool is_root = p_parent == tree->get_root(); + + if (keep) { Node *n = get_node(p_parent->get_metadata(0)); + if (!p_parent->is_visible() || (is_root && tree->is_root_hidden())) { + // Place back moved out children from when this item has hidden. + HashMap::Iterator I = node_cache.get(n, false); + if (I && I->value.has_moved_children) { + _update_node_subtree(I->value.node, nullptr, true); + } + } - for (const StringName &E : valid_types) { - if (n->is_class(E) || - EditorNode::get_singleton()->is_object_of_custom_type(n, E)) { - selectable = true; - break; - } else { - Ref