diff options
Diffstat (limited to 'editor')
-rw-r--r-- | editor/connections_dialog.cpp | 85 | ||||
-rw-r--r-- | editor/connections_dialog.h | 2 | ||||
-rw-r--r-- | editor/create_dialog.cpp | 5 | ||||
-rw-r--r-- | editor/editor_build_profile.cpp | 29 | ||||
-rw-r--r-- | editor/editor_feature_profile.cpp | 25 | ||||
-rw-r--r-- | editor/editor_help.cpp | 295 | ||||
-rw-r--r-- | editor/editor_help.h | 31 | ||||
-rw-r--r-- | editor/editor_inspector.cpp | 182 | ||||
-rw-r--r-- | editor/editor_inspector.h | 8 | ||||
-rw-r--r-- | editor/editor_node.cpp | 2 | ||||
-rw-r--r-- | editor/editor_properties.cpp | 136 | ||||
-rw-r--r-- | editor/editor_properties.h | 20 | ||||
-rw-r--r-- | editor/editor_properties_array_dict.cpp | 3 | ||||
-rw-r--r-- | editor/icons/CodeRegionFoldDownArrow.svg | 2 | ||||
-rw-r--r-- | editor/icons/CodeRegionFoldedRightArrow.svg | 2 | ||||
-rw-r--r-- | editor/plugins/animation_player_editor_plugin.cpp | 245 | ||||
-rw-r--r-- | editor/plugins/animation_player_editor_plugin.h | 27 | ||||
-rw-r--r-- | editor/property_selector.cpp | 43 | ||||
-rw-r--r-- | editor/renames_map_3_to_4.cpp | 3 |
19 files changed, 685 insertions, 460 deletions
diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index 208253d617..31659d4d4e 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -835,35 +835,9 @@ ConnectDialog::~ConnectDialog() { ////////////////////////////////////////// -// Originally copied and adapted from EditorProperty, try to keep style in sync. Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const { - // `p_text` is expected to be something like this: - // - `class|Control||Control brief description.`; - // - `signal|gui_input|(event: InputEvent)|gui_input description.`; - // - `../../.. :: _on_gui_input()`. - // Note that the description can be empty or contain `|`. - PackedStringArray slices = p_text.split("|", true, 3); - if (slices.size() < 4) { - return nullptr; // Use default tooltip instead. - } - - String item_type = (slices[0] == "class") ? TTR("Class:") : TTR("Signal:"); - String item_name = slices[1].strip_edges(); - String item_params = slices[2].strip_edges(); - String item_descr = slices[3].strip_edges(); - - String text = item_type + " [u][b]" + item_name + "[/b][/u]" + item_params + "\n"; - if (item_descr.is_empty()) { - text += "[i]" + TTR("No description.") + "[/i]"; - } else { - text += item_descr; - } - - EditorHelpBit *help_bit = memnew(EditorHelpBit); - help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1)); - help_bit->set_text(text); - - return help_bit; + // If it's not a doc tooltip, fallback to the default one. + return p_text.contains("::") ? nullptr : memnew(EditorHelpTooltip(p_text)); } struct _ConnectionsDockMethodInfoSort { @@ -1341,7 +1315,6 @@ void ConnectionsDock::update_tree() { while (native_base != StringName()) { String class_name; String doc_class_name; - String class_brief; Ref<Texture2D> class_icon; List<MethodInfo> class_signals; @@ -1355,21 +1328,8 @@ void ConnectionsDock::update_tree() { if (doc_class_name.is_empty()) { doc_class_name = script_base->get_path().trim_prefix("res://").quote(); } - - // For a script class, the cache is filled each time. - if (!doc_class_name.is_empty()) { - if (descr_cache.has(doc_class_name)) { - descr_cache[doc_class_name].clear(); - } - HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name); - if (F) { - class_brief = F->value.brief_description; - for (int i = 0; i < F->value.signals.size(); i++) { - descr_cache[doc_class_name][F->value.signals[i].name] = F->value.signals[i].description; - } - } else { - doc_class_name = String(); - } + if (!doc_class_name.is_empty() && !doc_data->class_list.find(doc_class_name)) { + doc_class_name = String(); } class_icon = editor_data.get_script_icon(script_base); @@ -1398,18 +1358,9 @@ void ConnectionsDock::update_tree() { script_base = base; } else { class_name = native_base; - doc_class_name = class_name; - - HashMap<String, DocData::ClassDoc>::ConstIterator F = doc_data->class_list.find(doc_class_name); - if (F) { - class_brief = DTR(F->value.brief_description); - // For a native class, the cache is filled once. - if (!descr_cache.has(doc_class_name)) { - for (int i = 0; i < F->value.signals.size(); i++) { - descr_cache[doc_class_name][F->value.signals[i].name] = DTR(F->value.signals[i].description); - } - } - } else { + doc_class_name = native_base; + + if (!doc_data->class_list.find(doc_class_name)) { doc_class_name = String(); } @@ -1434,8 +1385,8 @@ void ConnectionsDock::update_tree() { section_item = tree->create_item(root); section_item->set_text(0, class_name); - // `|` separators used in `make_custom_tooltip()` for formatting. - section_item->set_tooltip_text(0, "class|" + class_name + "||" + class_brief); + // `|` separators used in `EditorHelpTooltip` for formatting. + section_item->set_tooltip_text(0, "class|" + doc_class_name + "||"); section_item->set_icon(0, class_icon); section_item->set_selectable(0, false); section_item->set_editable(0, false); @@ -1466,22 +1417,8 @@ void ConnectionsDock::update_tree() { sinfo["args"] = argnames; signal_item->set_metadata(0, sinfo); signal_item->set_icon(0, get_editor_theme_icon(SNAME("Signal"))); - - // Set tooltip with the signal's documentation. - { - String descr; - - HashMap<StringName, HashMap<StringName, String>>::ConstIterator G = descr_cache.find(doc_class_name); - if (G) { - HashMap<StringName, String>::ConstIterator F = G->value.find(signal_name); - if (F) { - descr = F->value; - } - } - - // `|` separators used in `make_custom_tooltip()` for formatting. - signal_item->set_tooltip_text(0, "signal|" + String(signal_name) + "|" + signame.trim_prefix(mi.name) + "|" + descr); - } + // `|` separators used in `EditorHelpTooltip` for formatting. + signal_item->set_tooltip_text(0, "signal|" + doc_class_name + "|" + String(signal_name) + "|" + signame.trim_prefix(mi.name)); // List existing connections. List<Object::Connection> existing_connections; diff --git a/editor/connections_dialog.h b/editor/connections_dialog.h index b07b08ecc7..7316a770ec 100644 --- a/editor/connections_dialog.h +++ b/editor/connections_dialog.h @@ -231,8 +231,6 @@ class ConnectionsDock : public VBoxContainer { PopupMenu *slot_menu = nullptr; LineEdit *search_box = nullptr; - HashMap<StringName, HashMap<StringName, String>> descr_cache; - void _filter_changed(const String &p_text); void _make_or_edit_connection(); diff --git a/editor/create_dialog.cpp b/editor/create_dialog.cpp index 1c8226e5e1..0e025b4430 100644 --- a/editor/create_dialog.cpp +++ b/editor/create_dialog.cpp @@ -500,10 +500,11 @@ void CreateDialog::select_type(const String &p_type, bool p_center_on_item) { to_select->select(0); search_options->scroll_to_item(to_select, p_center_on_item); - if (EditorHelp::get_doc_data()->class_list.has(p_type) && !DTR(EditorHelp::get_doc_data()->class_list[p_type].brief_description).is_empty()) { + String text = help_bit->get_class_description(p_type); + if (!text.is_empty()) { // Display both class name and description, since the help bit may be displayed // far away from the location (especially if the dialog was resized to be taller). - help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, DTR(EditorHelp::get_doc_data()->class_list[p_type].brief_description))); + help_bit->set_text(vformat("[b]%s[/b]: %s", p_type, text)); help_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); } else { // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. diff --git a/editor/editor_build_profile.cpp b/editor/editor_build_profile.cpp index bca30b2d2d..c3087d797a 100644 --- a/editor/editor_build_profile.cpp +++ b/editor/editor_build_profile.cpp @@ -646,24 +646,21 @@ void EditorBuildProfileManager::_class_list_item_selected() { Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { - String class_name = md; - String class_description; - - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); - if (E) { - class_description = DTR(E->value.brief_description); + String text = description_bit->get_class_description(md); + if (!text.is_empty()) { + // Display both class name and description, since the help bit may be displayed + // far away from the location (especially if the dialog was resized to be taller). + description_bit->set_text(vformat("[b]%s[/b]: %s", md, text)); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); + } else { + // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. + description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); } - - description_bit->set_text(class_description); } else if (md.get_type() == Variant::INT) { - int build_option_id = md; - String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id)); - - description_bit->set_text(TTRGET(build_option_description)); - return; - } else { - return; + String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption((int)md)); + description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(build_option_description))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); } } diff --git a/editor/editor_feature_profile.cpp b/editor/editor_feature_profile.cpp index 50406bea6a..5a44ae1aba 100644 --- a/editor/editor_feature_profile.cpp +++ b/editor/editor_feature_profile.cpp @@ -555,21 +555,22 @@ void EditorFeatureProfileManager::_class_list_item_selected() { Variant md = item->get_metadata(0); if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { - String class_name = md; - String class_description; - - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); - if (E) { - class_description = DTR(E->value.brief_description); + String text = description_bit->get_class_description(md); + if (!text.is_empty()) { + // Display both class name and description, since the help bit may be displayed + // far away from the location (especially if the dialog was resized to be taller). + description_bit->set_text(vformat("[b]%s[/b]: %s", md, text)); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); + } else { + // Use nested `vformat()` as translators shouldn't interfere with BBCode tags. + description_bit->set_text(vformat(TTR("No description available for %s."), vformat("[b]%s[/b]", md))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 0.5)); } - - description_bit->set_text(class_description); } else if (md.get_type() == Variant::INT) { - int feature_id = md; - String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature(feature_id)); + String feature_description = EditorFeatureProfile::get_feature_description(EditorFeatureProfile::Feature((int)md)); + description_bit->set_text(vformat("[b]%s[/b]: %s", TTR(item->get_text(0)), TTRGET(feature_description))); + description_bit->get_rich_text()->set_self_modulate(Color(1, 1, 1, 1)); - description_bit->set_text(TTRGET(feature_description)); return; } else { return; diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index b3bcb9f014..5d07ba7568 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -38,6 +38,7 @@ #include "doc_data_compressed.gen.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" +#include "editor/editor_property_name_processor.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" @@ -216,6 +217,12 @@ void EditorHelp::_class_desc_select(const String &p_select) { if (tag == "method") { topic = "class_method"; table = &this->method_line; + } else if (tag == "constructor") { + topic = "class_method"; + table = &this->method_line; + } else if (tag == "operator") { + topic = "class_method"; + table = &this->method_line; } else if (tag == "member") { topic = "class_property"; table = &this->property_line; @@ -410,8 +417,10 @@ String EditorHelp::_fix_constant(const String &p_constant) const { class_desc->add_text(" (" + TTR("Experimental") + ")"); \ class_desc->pop(); -void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview) { - method_line[p_method.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description +void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview, bool p_override) { + if (p_override) { + method_line[p_method.name] = class_desc->get_paragraph_count() - 2; // Gets overridden if description. + } const bool is_vararg = p_method.qualifiers.contains("vararg"); @@ -582,7 +591,7 @@ Error EditorHelp::_goto_desc(const String &p_class) { return OK; } -void EditorHelp::_update_method_list(const Vector<DocData::MethodDoc> p_methods) { +void EditorHelp::_update_method_list(const Vector<DocData::MethodDoc> p_methods, MethodType p_method_type) { class_desc->add_newline(); _push_code_font(); @@ -628,7 +637,8 @@ void EditorHelp::_update_method_list(const Vector<DocData::MethodDoc> p_methods) class_desc->pop(); // cell } - _add_method(m[i], true); + // For constructors always point to the first one. + _add_method(m[i], true, (p_method_type != METHOD_TYPE_CONSTRUCTOR || i == 0)); } any_previous = !m.is_empty(); @@ -660,7 +670,8 @@ void EditorHelp::_update_method_descriptions(const DocData::ClassDoc p_classdoc, for (int i = 0; i < methods_filtered.size(); i++) { _push_code_font(); - _add_method(methods_filtered[i], false); + // For constructors always point to the first one. + _add_method(methods_filtered[i], false, (p_method_type != METHOD_TYPE_CONSTRUCTOR || i == 0)); _pop_code_font(); class_desc->add_newline(); @@ -1151,7 +1162,7 @@ void EditorHelp::_update_doc() { class_desc->add_text(TTR("Constructors")); _pop_title_font(); - _update_method_list(cd.constructors); + _update_method_list(cd.constructors, METHOD_TYPE_CONSTRUCTOR); } if (!methods.is_empty()) { @@ -1164,7 +1175,7 @@ void EditorHelp::_update_doc() { class_desc->add_text(TTR("Methods")); _pop_title_font(); - _update_method_list(methods); + _update_method_list(methods, METHOD_TYPE_METHOD); } if (!cd.operators.is_empty()) { @@ -1177,7 +1188,7 @@ void EditorHelp::_update_doc() { class_desc->add_text(TTR("Operators")); _pop_title_font(); - _update_method_list(cd.operators); + _update_method_list(cd.operators, METHOD_TYPE_OPERATOR); } // Theme properties @@ -1991,10 +2002,10 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control // Select the correct code examples. switch ((int)EDITOR_GET("text_editor/help/class_reference_examples")) { case 0: // GDScript - bbcode = bbcode.replace("[gdscript]", "[codeblock]"); + bbcode = bbcode.replace("[gdscript", "[codeblock"); // Tag can have extra arguments. bbcode = bbcode.replace("[/gdscript]", "[/codeblock]"); - for (int pos = bbcode.find("[csharp]"); pos != -1; pos = bbcode.find("[csharp]")) { + for (int pos = bbcode.find("[csharp"); pos != -1; pos = bbcode.find("[csharp")) { int end_pos = bbcode.find("[/csharp]"); if (end_pos == -1) { WARN_PRINT("Unclosed [csharp] block or parse fail in code (search for tag errors)"); @@ -2008,10 +2019,10 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control } break; case 1: // C# - bbcode = bbcode.replace("[csharp]", "[codeblock]"); + bbcode = bbcode.replace("[csharp", "[codeblock"); // Tag can have extra arguments. bbcode = bbcode.replace("[/csharp]", "[/codeblock]"); - for (int pos = bbcode.find("[gdscript]"); pos != -1; pos = bbcode.find("[gdscript]")) { + for (int pos = bbcode.find("[gdscript"); pos != -1; pos = bbcode.find("[gdscript")) { int end_pos = bbcode.find("[/gdscript]"); if (end_pos == -1) { WARN_PRINT("Unclosed [gdscript] block or parse fail in code (search for tag errors)"); @@ -2025,8 +2036,8 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control } break; case 2: // GDScript and C# - bbcode = bbcode.replace("[csharp]", "[b]C#:[/b]\n[codeblock]"); - bbcode = bbcode.replace("[gdscript]", "[b]GDScript:[/b]\n[codeblock]"); + bbcode = bbcode.replace("[csharp", "[b]C#:[/b]\n[codeblock"); // Tag can have extra arguments. + bbcode = bbcode.replace("[gdscript", "[b]GDScript:[/b]\n[codeblock"); // Tag can have extra arguments. bbcode = bbcode.replace("[/csharp]", "[/codeblock]"); bbcode = bbcode.replace("[/gdscript]", "[/codeblock]"); @@ -2041,6 +2052,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control // Remove extra new lines around code blocks. bbcode = bbcode.replace("[codeblock]\n", "[codeblock]"); + bbcode = bbcode.replace("[codeblock skip-lint]\n", "[codeblock skip-lint]"); // Extra argument to silence validation warnings. bbcode = bbcode.replace("\n[/codeblock]", "[/codeblock]"); List<String> tag_stack; @@ -2114,7 +2126,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control p_rt->add_text("["); pos = brk_pos + 1; - } else if (tag.begins_with("method ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("annotation ") || tag.begins_with("theme_item ")) { + } else if (tag.begins_with("method ") || tag.begins_with("constructor ") || tag.begins_with("operator ") || tag.begins_with("member ") || tag.begins_with("signal ") || tag.begins_with("enum ") || tag.begins_with("constant ") || tag.begins_with("annotation ") || tag.begins_with("theme_item ")) { const int tag_end = tag.find(" "); const String link_tag = tag.substr(0, tag_end); const String link_target = tag.substr(tag_end + 1, tag.length()).lstrip(" "); @@ -2125,7 +2137,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control p_rt->push_font_size(doc_code_font_size); Color target_color = link_color; - if (link_tag == "method") { + if (link_tag == "method" || link_tag == "constructor" || link_tag == "operator") { target_color = link_method_color; } else if (link_tag == "member" || link_tag == "signal" || link_tag == "theme property") { target_color = link_property_color; @@ -2196,7 +2208,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control pos = brk_end + 1; tag_stack.push_front(tag); - } else if (tag == "code") { + } else if (tag == "code" || tag.begins_with("code ")) { // Use monospace font with darkened background color to make code easier to distinguish from other text. p_rt->push_font(doc_code_font); p_rt->push_font_size(doc_code_font_size); @@ -2205,8 +2217,8 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control code_tag = true; pos = brk_end + 1; - tag_stack.push_front(tag); - } else if (tag == "codeblock") { + tag_stack.push_front("code"); + } else if (tag == "codeblock" || tag.begins_with("codeblock ")) { // Use monospace font with darkened background color to make code easier to distinguish from other text. // Use a single-column table with cell row background color instead of `[bgcolor]`. // This makes the background color highlight cover the entire block, rather than individual lines. @@ -2221,7 +2233,7 @@ static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control codeblock_tag = true; pos = brk_end + 1; - tag_stack.push_front(tag); + tag_stack.push_front("codeblock"); } else if (tag == "kbd") { // Use keyboard font with custom color and background color. p_rt->push_font(doc_kbd_font); @@ -2587,7 +2599,7 @@ DocTools *EditorHelp::get_doc_data() { return doc; } -//// EditorHelpBit /// +/// EditorHelpBit /// void EditorHelpBit::_go_to_help(String p_what) { EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT); @@ -2620,6 +2632,179 @@ void EditorHelpBit::_meta_clicked(String p_select) { } } +String EditorHelpBit::get_class_description(const StringName &p_class_name) const { + if (doc_class_cache.has(p_class_name)) { + return doc_class_cache[p_class_name]; + } + + String description; + HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + if (E) { + // Non-native class shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + description = is_native ? DTR(E->value.brief_description) : E->value.brief_description; + + if (is_native) { + doc_class_cache[p_class_name] = description; + } + } + + return description; +} + +String EditorHelpBit::get_property_description(const StringName &p_class_name, const StringName &p_property_name) const { + if (doc_property_cache.has(p_class_name) && doc_property_cache[p_class_name].has(p_property_name)) { + return doc_property_cache[p_class_name][p_property_name]; + } + + String description; + // Non-native properties shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + DocTools *dd = EditorHelp::get_doc_data(); + HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); + if (E) { + for (int i = 0; i < E->value.properties.size(); i++) { + String description_current = is_native ? DTR(E->value.properties[i].description) : E->value.properties[i].description; + + const Vector<String> class_enum = E->value.properties[i].enumeration.split("."); + const String enum_name = class_enum.size() >= 2 ? class_enum[1] : ""; + if (!enum_name.is_empty()) { + // Classes can use enums from other classes, so check from which it came. + HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_enum[0]); + if (enum_class) { + for (DocData::ConstantDoc val : enum_class->value.constants) { + // Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector. + if (val.enumeration == enum_name && !val.name.ends_with("_MAX")) { + const String enum_value = EditorPropertyNameProcessor::get_singleton()->process_name(val.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED); + const String enum_prefix = EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " "; + const String enum_description = is_native ? DTR(val.description) : val.description; + + // Prettify the enum value display, so that "<ENUM NAME>_<VALUE>" becomes "Value". + description_current = description_current.trim_prefix("\n") + vformat("\n[b]%s:[/b] %s", enum_value.trim_prefix(enum_prefix), enum_description.is_empty() ? ("[i]" + DTR("No description available.") + "[/i]") : enum_description); + } + } + } + } + + if (E->value.properties[i].name == p_property_name) { + description = description_current; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_property_cache[p_class_name][E->value.properties[i].name] = description_current; + } + } + } + + return description; +} + +String EditorHelpBit::get_method_description(const StringName &p_class_name, const StringName &p_method_name) const { + if (doc_method_cache.has(p_class_name) && doc_method_cache[p_class_name].has(p_method_name)) { + return doc_method_cache[p_class_name][p_method_name]; + } + + String description; + HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + if (E) { + // Non-native methods shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + + for (int i = 0; i < E->value.methods.size(); i++) { + String description_current = is_native ? DTR(E->value.methods[i].description) : E->value.methods[i].description; + + if (E->value.methods[i].name == p_method_name) { + description = description_current; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_method_cache[p_class_name][E->value.methods[i].name] = description_current; + } + } + } + + return description; +} + +String EditorHelpBit::get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const { + if (doc_signal_cache.has(p_class_name) && doc_signal_cache[p_class_name].has(p_signal_name)) { + return doc_signal_cache[p_class_name][p_signal_name]; + } + + String description; + HashMap<String, DocData::ClassDoc>::ConstIterator E = EditorHelp::get_doc_data()->class_list.find(p_class_name); + if (E) { + // Non-native signals shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + + for (int i = 0; i < E->value.signals.size(); i++) { + String description_current = is_native ? DTR(E->value.signals[i].description) : E->value.signals[i].description; + + if (E->value.signals[i].name == p_signal_name) { + description = description_current; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_signal_cache[p_class_name][E->value.signals[i].name] = description_current; + } + } + } + + return description; +} + +String EditorHelpBit::get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const { + if (doc_theme_item_cache.has(p_class_name) && doc_theme_item_cache[p_class_name].has(p_theme_item_name)) { + return doc_theme_item_cache[p_class_name][p_theme_item_name]; + } + + String description; + bool found = false; + DocTools *dd = EditorHelp::get_doc_data(); + HashMap<String, DocData::ClassDoc>::ConstIterator E = dd->class_list.find(p_class_name); + while (E) { + // Non-native theme items shouldn't be cached, nor translated. + bool is_native = ClassDB::class_exists(p_class_name); + + for (int i = 0; i < E->value.theme_properties.size(); i++) { + String description_current = is_native ? DTR(E->value.theme_properties[i].description) : E->value.theme_properties[i].description; + + if (E->value.theme_properties[i].name == p_theme_item_name) { + description = description_current; + found = true; + + if (!is_native) { + break; + } + } + + if (is_native) { + doc_theme_item_cache[p_class_name][E->value.theme_properties[i].name] = description_current; + } + } + + if (found || E->value.inherits.is_empty()) { + break; + } + // Check for inherited theme items. + E = dd->class_list.find(E->value.inherits); + } + + return description; +} + void EditorHelpBit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "text"), &EditorHelpBit::set_text); ADD_SIGNAL(MethodInfo("request_hide")); @@ -2650,7 +2835,73 @@ EditorHelpBit::EditorHelpBit() { set_custom_minimum_size(Size2(0, 50 * EDSCALE)); } -//// FindBar /// +/// EditorHelpTooltip /// + +void EditorHelpTooltip::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: { + if (!tooltip_text.is_empty()) { + parse_tooltip(tooltip_text); + } + } break; + } +} + +// `p_text` is expected to be something like these: +// - `class|Control||`; +// - `property|Control|size|`; +// - `signal|Control|gui_input|(event: InputEvent)` +void EditorHelpTooltip::parse_tooltip(const String &p_text) { + tooltip_text = p_text; + + PackedStringArray slices = p_text.split("|", true, 3); + ERR_FAIL_COND_MSG(slices.size() < 4, "Invalid tooltip formatting. The expect string should be formatted as 'type|class|property|args'."); + + String type = slices[0]; + String class_name = slices[1]; + String property_name = slices[2]; + String property_args = slices[3]; + + String title; + String description; + String formatted_text; + + if (type == "class") { + title = class_name; + description = get_class_description(class_name); + formatted_text = TTR("Class:"); + } else { + title = property_name; + + if (type == "property") { + description = get_property_description(class_name, property_name); + formatted_text = TTR("Property:"); + } else if (type == "method") { + description = get_method_description(class_name, property_name); + formatted_text = TTR("Method:"); + } else if (type == "signal") { + description = get_signal_description(class_name, property_name); + formatted_text = TTR("Signal:"); + } else if (type == "theme_item") { + description = get_theme_item_description(class_name, property_name); + formatted_text = TTR("Theme Item:"); + } else { + ERR_FAIL_MSG("Invalid tooltip type '" + type + "'. Valid types are 'class', 'property', 'method', 'signal', and 'theme_item'."); + } + } + + formatted_text += " [u][b]" + title + "[/b][/u]" + property_args + "\n"; + formatted_text += description.is_empty() ? "[i]" + TTR("No description available.") + "[/i]" : description; + set_text(formatted_text); +} + +EditorHelpTooltip::EditorHelpTooltip(const String &p_text) { + tooltip_text = p_text; + + get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 0)); +} + +/// FindBar /// FindBar::FindBar() { search_text = memnew(LineEdit); diff --git a/editor/editor_help.h b/editor/editor_help.h index 1f1528945b..0ca3942e0b 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -157,7 +157,7 @@ class EditorHelp : public VBoxContainer { //void _button_pressed(int p_idx); void _add_type(const String &p_type, const String &p_enum = String(), bool p_is_bitfield = false); void _add_type_icon(const String &p_type, int p_size = 0, const String &p_fallback = ""); - void _add_method(const DocData::MethodDoc &p_method, bool p_overview = true); + void _add_method(const DocData::MethodDoc &p_method, bool p_overview, bool p_override = true); void _add_bulletpoint(); @@ -177,7 +177,7 @@ class EditorHelp : public VBoxContainer { Error _goto_desc(const String &p_class); //void _update_history_buttons(); - void _update_method_list(const Vector<DocData::MethodDoc> p_methods); + void _update_method_list(const Vector<DocData::MethodDoc> p_methods, MethodType p_method_type); void _update_method_descriptions(const DocData::ClassDoc p_classdoc, const Vector<DocData::MethodDoc> p_methods, MethodType p_method_type); void _update_doc(); @@ -232,6 +232,12 @@ public: class EditorHelpBit : public MarginContainer { GDCLASS(EditorHelpBit, MarginContainer); + inline static HashMap<StringName, String> doc_class_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_property_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_method_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_signal_cache; + inline static HashMap<StringName, HashMap<StringName, String>> doc_theme_item_cache; + RichTextLabel *rich_text = nullptr; void _go_to_help(String p_what); void _meta_clicked(String p_select); @@ -243,9 +249,30 @@ protected: void _notification(int p_what); public: + String get_class_description(const StringName &p_class_name) const; + String get_property_description(const StringName &p_class_name, const StringName &p_property_name) const; + String get_method_description(const StringName &p_class_name, const StringName &p_method_name) const; + String get_signal_description(const StringName &p_class_name, const StringName &p_signal_name) const; + String get_theme_item_description(const StringName &p_class_name, const StringName &p_theme_item_name) const; + RichTextLabel *get_rich_text() { return rich_text; } void set_text(const String &p_text); + EditorHelpBit(); }; +class EditorHelpTooltip : public EditorHelpBit { + GDCLASS(EditorHelpTooltip, EditorHelpBit); + + String tooltip_text; + +protected: + void _notification(int p_what); + +public: + void parse_tooltip(const String &p_text); + + EditorHelpTooltip(const String &p_text = String()); +}; + #endif // EDITOR_HELP_H diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index 382b182e0e..91a3181747 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -905,47 +905,17 @@ void EditorProperty::_update_pin_flags() { } } -static Control *make_help_bit(const String &p_item_type, const String &p_text, const String &p_warning, const Color &p_warn_color) { - // `p_text` is expected to be something like this: - // `item_name|Item description.`. - // Note that the description can be empty or contain `|`. - PackedStringArray slices = p_text.split("|", true, 1); - if (slices.size() < 2) { - return nullptr; // Use default tooltip instead. - } - - String item_name = slices[0].strip_edges(); - String item_descr = slices[1].strip_edges(); - - String text; - if (!p_item_type.is_empty()) { - text = p_item_type + " "; - } - text += "[u][b]" + item_name + "[/b][/u]\n"; - if (item_descr.is_empty()) { - text += "[i]" + TTR("No description.") + "[/i]"; - } else { - text += item_descr; - } - if (!p_warning.is_empty()) { - text += "\n[b][color=" + p_warn_color.to_html(false) + "]" + p_warning + "[/color][/b]"; - } - - EditorHelpBit *help_bit = memnew(EditorHelpBit); - help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1)); - help_bit->set_text(text); - - return help_bit; -} - Control *EditorProperty::make_custom_tooltip(const String &p_text) const { - String warn; - Color warn_color; + EditorHelpTooltip *tooltip = memnew(EditorHelpTooltip(p_text)); + if (object->has_method("_get_property_warning")) { - warn = object->call("_get_property_warning", property); - warn_color = get_theme_color(SNAME("warning_color")); + String warn = object->call("_get_property_warning", property); + if (!warn.is_empty()) { + tooltip->set_text(tooltip->get_rich_text()->get_text() + "\n[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + warn + "[/color][/b]"); + } } - return make_help_bit(TTR("Property:"), p_text, warn, warn_color); + + return tooltip; } void EditorProperty::menu_option(int p_option) { @@ -1178,7 +1148,8 @@ void EditorInspectorCategory::_notification(int p_what) { } Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) const { - return make_help_bit(TTR("Class:"), p_text, String(), Color()); + // Far from perfect solution, as there's nothing that prevents a category from having a name that starts with that. + return p_text.begins_with("class|") ? memnew(EditorHelpTooltip(p_text)) : nullptr; } Size2 EditorInspectorCategory::get_minimum_size() const { @@ -2883,24 +2854,8 @@ void EditorInspector::update_tree() { category->doc_class_name = doc_name; if (use_doc_hints) { - String descr = ""; - // Sets the category tooltip to show documentation. - if (!class_descr_cache.has(doc_name)) { - DocTools *dd = EditorHelp::get_doc_data(); - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(doc_name); - if (E) { - descr = E->value.brief_description; - } - if (ClassDB::class_exists(doc_name)) { - descr = DTR(descr); // Do not translate the class description of scripts. - class_descr_cache[doc_name] = descr; // Do not cache the class description of scripts. - } - } else { - descr = class_descr_cache[doc_name]; - } - - // `|` separator used in `make_help_bit()` for formatting. - category->set_tooltip_text(p.name + "|" + descr); + // `|` separator used in `EditorHelpTooltip` for formatting. + category->set_tooltip_text("class|" + doc_name + "||"); } // Add editors at the start of a category. @@ -3195,13 +3150,12 @@ void EditorInspector::update_tree() { restart_request_props.insert(p.name); } - PropertyDocInfo doc_info; + String doc_path; + String theme_item_name; + StringName classname = doc_name; + // Build the doc hint, to use as tooltip. if (use_doc_hints) { - // Build the doc hint, to use as tooltip. - - // Get the class name. - StringName classname = doc_name; if (!object_class.is_empty()) { classname = object_class; } else if (Object::cast_to<MultiNodeEdit>(object)) { @@ -3231,83 +3185,55 @@ void EditorInspector::update_tree() { classname = get_edited_object()->get_class(); } - // Search for the property description in the cache. - HashMap<StringName, HashMap<StringName, PropertyDocInfo>>::Iterator E = doc_info_cache.find(classname); + // Search for the doc path in the cache. + HashMap<StringName, HashMap<StringName, String>>::Iterator E = doc_path_cache.find(classname); if (E) { - HashMap<StringName, PropertyDocInfo>::Iterator F = E->value.find(propname); + HashMap<StringName, String>::Iterator F = E->value.find(propname); if (F) { found = true; - doc_info = F->value; + doc_path = F->value; } } if (!found) { + DocTools *dd = EditorHelp::get_doc_data(); + // Do not cache the doc path information of scripts. bool is_native_class = ClassDB::class_exists(classname); - // Build the property description String and add it to the cache. - DocTools *dd = EditorHelp::get_doc_data(); HashMap<String, DocData::ClassDoc>::ConstIterator F = dd->class_list.find(classname); - while (F && doc_info.description.is_empty()) { - for (int i = 0; i < F->value.properties.size(); i++) { - if (F->value.properties[i].name == propname.operator String()) { - doc_info.description = F->value.properties[i].description; - if (is_native_class) { - doc_info.description = DTR(doc_info.description); // Do not translate the property description of scripts. - } - - const Vector<String> class_enum = F->value.properties[i].enumeration.split("."); - const String class_name = class_enum[0]; - const String enum_name = class_enum.size() >= 2 ? class_enum[1] : ""; - if (!enum_name.is_empty()) { - HashMap<String, DocData::ClassDoc>::ConstIterator enum_class = dd->class_list.find(class_name); - if (enum_class) { - for (DocData::ConstantDoc val : enum_class->value.constants) { - // Don't display `_MAX` enum value descriptions, as these are never exposed in the inspector. - if (val.enumeration == enum_name && !val.name.ends_with("_MAX")) { - const String enum_value = EditorPropertyNameProcessor::get_singleton()->process_name(val.name, EditorPropertyNameProcessor::STYLE_CAPITALIZED); - // Prettify the enum value display, so that "<ENUM NAME>_<VALUE>" becomes "Value". - String desc = val.description; - if (is_native_class) { - desc = DTR(desc); // Do not translate the enum value description of scripts. - } - desc = desc.trim_prefix("\n"); - doc_info.description += vformat( - "\n[b]%s:[/b] %s", - enum_value.trim_prefix(EditorPropertyNameProcessor::get_singleton()->process_name(enum_name, EditorPropertyNameProcessor::STYLE_CAPITALIZED) + " "), - desc.is_empty() ? ("[i]" + TTR("No description.") + "[/i]") : desc); - } - } - } - } - - doc_info.path = "class_property:" + F->value.name + ":" + F->value.properties[i].name; - break; - } - } - + while (F) { Vector<String> slices = propname.operator String().split("/"); + // Check if it's a theme item first. if (slices.size() == 2 && slices[0].begins_with("theme_override_")) { for (int i = 0; i < F->value.theme_properties.size(); i++) { + String doc_path_current = "class_theme_item:" + F->value.name + ":" + F->value.theme_properties[i].name; if (F->value.theme_properties[i].name == slices[1]) { - doc_info.description = F->value.theme_properties[i].description; - if (is_native_class) { - doc_info.description = DTR(doc_info.description); // Do not translate the theme item description of scripts. - } - doc_info.path = "class_theme_item:" + F->value.name + ":" + F->value.theme_properties[i].name; - break; + doc_path = doc_path_current; + theme_item_name = F->value.theme_properties[i].name; } } - } - if (!F->value.inherits.is_empty()) { - F = dd->class_list.find(F->value.inherits); + if (is_native_class) { + doc_path_cache[classname][propname] = doc_path; + } } else { - break; + for (int i = 0; i < F->value.properties.size(); i++) { + String doc_path_current = "class_property:" + F->value.name + ":" + F->value.properties[i].name; + if (F->value.properties[i].name == propname.operator String()) { + doc_path = doc_path_current; + } + + if (is_native_class) { + doc_path_cache[classname][propname] = doc_path; + } + } } - } - if (is_native_class) { - doc_info_cache[classname][propname] = doc_info; // Do not cache the doc information of scripts. + if (!doc_path.is_empty() || F->value.inherits.is_empty()) { + break; + } + // Couldn't find the doc path in the class itself, try its super class. + F = dd->class_list.find(F->value.inherits); } } } @@ -3346,11 +3272,11 @@ void EditorInspector::update_tree() { if (properties.size()) { if (properties.size() == 1) { - //since it's one, associate: + // Since it's one, associate: ep->property = properties[0]; ep->property_path = property_prefix + properties[0]; ep->property_usage = p.usage; - //and set label? + // And set label? } if (!editors[i].label.is_empty()) { ep->set_label(editors[i].label); @@ -3398,9 +3324,17 @@ void EditorInspector::update_tree() { ep->connect("multiple_properties_changed", callable_mp(this, &EditorInspector::_multiple_properties_changed)); ep->connect("resource_selected", callable_mp(this, &EditorInspector::_resource_selected), CONNECT_DEFERRED); ep->connect("object_id_selected", callable_mp(this, &EditorInspector::_object_id_selected), CONNECT_DEFERRED); - // `|` separator used in `make_help_bit()` for formatting. - ep->set_tooltip_text(property_prefix + p.name + "|" + doc_info.description); - ep->set_doc_path(doc_info.path); + + if (use_doc_hints) { + // `|` separator used in `EditorHelpTooltip` for formatting. + if (theme_item_name.is_empty()) { + ep->set_tooltip_text("property|" + classname + "|" + property_prefix + p.name + "|"); + } else { + ep->set_tooltip_text("theme_item|" + classname + "|" + theme_item_name + "|"); + } + } + + ep->set_doc_path(doc_path); ep->update_property(); ep->_update_pin_flags(); ep->update_editor_property_status(); diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index 4393922f52..b5f0cec80b 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -501,13 +501,7 @@ class EditorInspector : public ScrollContainer { int property_focusable; int update_scroll_request; - struct PropertyDocInfo { - String description; - String path; - }; - - HashMap<StringName, HashMap<StringName, PropertyDocInfo>> doc_info_cache; - HashMap<StringName, String> class_descr_cache; + HashMap<StringName, HashMap<StringName, String>> doc_path_cache; HashSet<StringName> restart_request_props; HashMap<ObjectID, int> scroll_cache; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index e69dcb2278..0037b356d0 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -4473,7 +4473,7 @@ String EditorNode::_get_system_info() const { } if (driver_name == "vulkan") { driver_name = "Vulkan"; - } else if (driver_name == "opengl3") { + } else if (driver_name.begins_with("opengl3")) { driver_name = "GLES3"; } diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 9c6dbd333f..0be23fa3cc 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -47,6 +47,7 @@ #include "editor/plugins/script_editor_plugin.h" #include "editor/project_settings_editor.h" #include "editor/property_selector.h" +#include "editor/scene_tree_dock.h" #include "scene/2d/gpu_particles_2d.h" #include "scene/3d/fog_volume.h" #include "scene/3d/gpu_particles_3d.h" @@ -2769,7 +2770,7 @@ EditorPropertyColor::EditorPropertyColor() { void EditorPropertyNodePath::_set_read_only(bool p_read_only) { assign->set_disabled(p_read_only); - clear->set_disabled(p_read_only); + menu->set_disabled(p_read_only); }; Variant EditorPropertyNodePath::_get_cache_value(const StringName &p_prop, bool &r_valid) const { @@ -2817,9 +2818,79 @@ void EditorPropertyNodePath::_node_assign() { scene_tree->popup_scenetree_dialog(); } -void EditorPropertyNodePath::_node_clear() { - emit_changed(get_edited_property(), Variant()); - update_property(); +void EditorPropertyNodePath::_update_menu() { + const NodePath &np = _get_node_path(); + + menu->get_popup()->set_item_disabled(ACTION_CLEAR, np.is_empty()); + menu->get_popup()->set_item_disabled(ACTION_COPY, np.is_empty()); + + Node *edited_node = Object::cast_to<Node>(get_edited_object()); + menu->get_popup()->set_item_disabled(ACTION_SELECT, !edited_node || !edited_node->has_node(np)); +} + +void EditorPropertyNodePath::_menu_option(int p_idx) { + switch (p_idx) { + case ACTION_CLEAR: { + emit_changed(get_edited_property(), NodePath()); + update_property(); + } break; + + case ACTION_COPY: { + DisplayServer::get_singleton()->clipboard_set(_get_node_path()); + } break; + + case ACTION_EDIT: { + assign->hide(); + menu->hide(); + + const NodePath &np = _get_node_path(); + edit->set_text(np); + edit->show(); + callable_mp((Control *)edit, &Control::grab_focus).call_deferred(); + } break; + + case ACTION_SELECT: { + const Node *edited_node = get_base_node(); + ERR_FAIL_NULL(edited_node); + + const NodePath &np = _get_node_path(); + Node *target_node = edited_node->get_node_or_null(np); + ERR_FAIL_NULL(target_node); + + SceneTreeDock::get_singleton()->set_selected(target_node); + } break; + } +} + +void EditorPropertyNodePath::_accept_text() { + _text_submitted(edit->get_text()); +} + +void EditorPropertyNodePath::_text_submitted(const String &p_text) { + NodePath np = p_text; + emit_changed(get_edited_property(), np); + edit->hide(); + assign->show(); + menu->show(); +} + +const NodePath EditorPropertyNodePath::_get_node_path() const { + const Node *base_node = const_cast<EditorPropertyNodePath *>(this)->get_base_node(); + + Variant val = get_edited_property_value(); + Node *n = Object::cast_to<Node>(val); + if (n) { + if (!n->is_inside_tree()) { + return NodePath(); + } + if (base_node) { + return base_node->get_path_to(n); + } else { + return get_tree()->get_edited_scene_root()->get_path_to(n); + } + } else { + return val; + } } bool EditorPropertyNodePath::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { @@ -2865,26 +2936,11 @@ bool EditorPropertyNodePath::is_drop_valid(const Dictionary &p_drag_data) const } void EditorPropertyNodePath::update_property() { - Node *base_node = get_base_node(); - - NodePath p; - Variant val = get_edited_object()->get(get_edited_property()); - Node *n = Object::cast_to<Node>(val); - if (n) { - if (!n->is_inside_tree()) { - return; - } - if (base_node) { - p = base_node->get_path_to(n); - } else { - p = get_tree()->get_edited_scene_root()->get_path_to(n); - } - } else { - p = get_edited_property_value(); - } - + const Node *base_node = get_base_node(); + const NodePath &p = _get_node_path(); assign->set_tooltip_text(p); - if (p == NodePath()) { + + if (p.is_empty()) { assign->set_icon(Ref<Texture2D>()); assign->set_text(TTR("Assign...")); assign->set_flat(false); @@ -2898,7 +2954,7 @@ void EditorPropertyNodePath::update_property() { return; } - Node *target_node = base_node->get_node(p); + const Node *target_node = base_node->get_node(p); ERR_FAIL_NULL(target_node); if (String(target_node->get_name()).contains("@")) { @@ -2922,14 +2978,15 @@ void EditorPropertyNodePath::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: case NOTIFICATION_THEME_CHANGED: { - Ref<Texture2D> t = get_editor_theme_icon(SNAME("Clear")); - clear->set_icon(t); + menu->set_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); + menu->get_popup()->set_item_icon(ACTION_CLEAR, get_editor_theme_icon(SNAME("Clear"))); + menu->get_popup()->set_item_icon(ACTION_COPY, get_editor_theme_icon(SNAME("ActionCopy"))); + menu->get_popup()->set_item_icon(ACTION_EDIT, get_editor_theme_icon(SNAME("Edit"))); + menu->get_popup()->set_item_icon(ACTION_SELECT, get_editor_theme_icon(SNAME("ExternalLink"))); } break; } } -void EditorPropertyNodePath::_bind_methods() { -} Node *EditorPropertyNodePath::get_base_node() { if (!base_hint.is_empty() && get_tree()->get_root()->has_node(base_hint)) { return get_tree()->get_root()->get_node(base_hint); @@ -2974,12 +3031,23 @@ EditorPropertyNodePath::EditorPropertyNodePath() { SET_DRAG_FORWARDING_CD(assign, EditorPropertyNodePath); hbc->add_child(assign); - clear = memnew(Button); - clear->set_flat(true); - clear->connect("pressed", callable_mp(this, &EditorPropertyNodePath::_node_clear)); - hbc->add_child(clear); - - scene_tree = nullptr; //do not allocate unnecessarily + menu = memnew(MenuButton); + menu->set_flat(true); + menu->connect(SNAME("about_to_popup"), callable_mp(this, &EditorPropertyNodePath::_update_menu)); + hbc->add_child(menu); + + menu->get_popup()->add_item(TTR("Clear"), ACTION_CLEAR); + menu->get_popup()->add_item(TTR("Copy as Text"), ACTION_COPY); + menu->get_popup()->add_item(TTR("Edit"), ACTION_EDIT); + menu->get_popup()->add_item(TTR("Show Node in Tree"), ACTION_SELECT); + menu->get_popup()->connect(SNAME("id_pressed"), callable_mp(this, &EditorPropertyNodePath::_menu_option)); + + edit = memnew(LineEdit); + edit->set_h_size_flags(SIZE_EXPAND_FILL); + edit->hide(); + edit->connect(SNAME("focus_exited"), callable_mp(this, &EditorPropertyNodePath::_accept_text)); + edit->connect(SNAME("text_submitted"), callable_mp(this, &EditorPropertyNodePath::_text_submitted)); + hbc->add_child(edit); } ///////////////////// RID ///////////////////////// diff --git a/editor/editor_properties.h b/editor/editor_properties.h index 5feb40b3d7..ff9d47627a 100644 --- a/editor/editor_properties.h +++ b/editor/editor_properties.h @@ -40,6 +40,7 @@ class EditorFileDialog; class EditorLocaleDialog; class EditorResourcePicker; class EditorSpinSlider; +class MenuButton; class PropertySelector; class SceneTreeDialog; class TextEdit; @@ -649,8 +650,18 @@ public: class EditorPropertyNodePath : public EditorProperty { GDCLASS(EditorPropertyNodePath, EditorProperty); + + enum { + ACTION_CLEAR, + ACTION_COPY, + ACTION_EDIT, + ACTION_SELECT, + }; + Button *assign = nullptr; - Button *clear = nullptr; + MenuButton *menu = nullptr; + LineEdit *edit = nullptr; + SceneTreeDialog *scene_tree = nullptr; NodePath base_hint; bool use_path_from_scene_root = false; @@ -659,8 +670,12 @@ class EditorPropertyNodePath : public EditorProperty { Vector<StringName> valid_types; void _node_selected(const NodePath &p_path); void _node_assign(); - void _node_clear(); Node *get_base_node(); + void _update_menu(); + void _menu_option(int p_idx); + void _accept_text(); + void _text_submitted(const String &p_text); + const NodePath _get_node_path() const; bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const; void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from); @@ -670,7 +685,6 @@ class EditorPropertyNodePath : public EditorProperty { protected: virtual void _set_read_only(bool p_read_only) override; - static void _bind_methods(); void _notification(int p_what); public: diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp index 950a1e1c4d..3fad85c95c 100644 --- a/editor/editor_properties_array_dict.cpp +++ b/editor/editor_properties_array_dict.cpp @@ -255,7 +255,7 @@ void EditorPropertyArray::update_property() { array_type_name = vformat("%s[%s]", array_type_name, type_name); } - if (array.get_type() == Variant::NIL) { + if (!array.is_array()) { edit->set_text(vformat(TTR("(Nil) %s"), array_type_name)); edit->set_pressed(false); if (container) { @@ -287,6 +287,7 @@ void EditorPropertyArray::update_property() { if (!container) { container = memnew(MarginContainer); container->set_theme_type_variation("MarginContainer4px"); + container->set_mouse_filter(MOUSE_FILTER_STOP); add_child(container); set_bottom_editor(container); diff --git a/editor/icons/CodeRegionFoldDownArrow.svg b/editor/icons/CodeRegionFoldDownArrow.svg index 3bc4f3f73b..744ea7197d 100644 --- a/editor/icons/CodeRegionFoldDownArrow.svg +++ b/editor/icons/CodeRegionFoldDownArrow.svg @@ -1 +1 @@ -<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm1 5a1 1 0 0 1 1.414-1.414L6 6.172l1.586-1.586A1 1 0 0 1 9 6L6.707 8.293a1 1 0 0 1-1.414 0Z" fill="#fff"/></svg>
\ No newline at end of file +<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M10 3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1zM3 5.75a1 1 0 0 1 1.414-1.414L6 5.922l1.586-1.586A1 1 0 0 1 9 5.75L6.707 8.043a1 1 0 0 1-1.414 0z" fill="#fff"/></svg> diff --git a/editor/icons/CodeRegionFoldedRightArrow.svg b/editor/icons/CodeRegionFoldedRightArrow.svg index a9b81d54f3..245371b5a1 100644 --- a/editor/icons/CodeRegionFoldedRightArrow.svg +++ b/editor/icons/CodeRegionFoldedRightArrow.svg @@ -1 +1 @@ -<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm3.5 8a1 1 0 0 1-1.414-1.414L5.672 6 4.086 4.414A1 1 0 0 1 5.5 3l2.293 2.293a1 1 0 0 1 0 1.414Z" fill="#fff"/></svg>
\ No newline at end of file +<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M3 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1zm2.75 7a1 1 0 0 1-1.414-1.414L5.922 6 4.336 4.414A1 1 0 0 1 5.75 3l2.293 2.293a1 1 0 0 1 0 1.414z" fill="#fff"/></svg> diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 7b48e6fbe9..75c8ac11d0 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -112,6 +112,7 @@ void AnimationPlayerEditor::_notification(int p_what) { } last_active = player->is_playing(); + updating = false; } break; @@ -942,11 +943,6 @@ void AnimationPlayerEditor::_update_player() { onion_toggle->set_disabled(no_anims_found); onion_skinning->set_disabled(no_anims_found); - if (hack_disable_onion_skinning) { - onion_toggle->set_disabled(true); - onion_skinning->set_disabled(true); - } - _update_animation_list_icons(); updating = false; @@ -1150,33 +1146,33 @@ void AnimationPlayerEditor::forward_force_draw_over_viewport(Control *p_overlay) float alpha_step = 1.0 / (onion.steps + 1); - int cidx = 0; + uint32_t capture_idx = 0; if (onion.past) { - float alpha = 0; + float alpha = 0.0f; do { alpha += alpha_step; - if (onion.captures_valid[cidx]) { + if (onion.captures_valid[capture_idx]) { RS::get_singleton()->canvas_item_add_texture_rect_region( - ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[cidx]), src_rect, Color(1, 1, 1, alpha)); + ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha)); } - cidx++; - } while (cidx < onion.steps); + capture_idx++; + } while (capture_idx < onion.steps); } if (onion.future) { - float alpha = 1; - int base_cidx = cidx; + float alpha = 1.0f; + uint32_t base_cidx = capture_idx; do { alpha -= alpha_step; - if (onion.captures_valid[cidx]) { + if (onion.captures_valid[capture_idx]) { RS::get_singleton()->canvas_item_add_texture_rect_region( - ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[cidx]), src_rect, Color(1, 1, 1, alpha)); + ci, dst_rect, RS::get_singleton()->viewport_get_texture(onion.captures[capture_idx]), src_rect, Color(1, 1, 1, alpha)); } - cidx++; - } while (cidx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it. + capture_idx++; + } while (capture_idx < base_cidx + onion.steps); // In case there's the present capture at the end, skip it. } } @@ -1266,7 +1262,7 @@ void AnimationPlayerEditor::_seek_value_changed(float p_value, bool p_set, bool if (!p_timeline_only) { if (player->is_valid() && !p_set) { - double delta = pos - player->get_current_animation_position(); + double delta = player->get_current_animation_position(); player->seek(pos, true, true); player->seek(pos + delta, true, true); } else { @@ -1394,7 +1390,10 @@ void AnimationPlayerEditor::_onion_skinning_menu(int p_option) { onion.enabled = !onion.enabled; if (onion.enabled) { - _start_onion_skinning(); + if (get_player() && !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) { + EditorNode::get_singleton()->show_warning(TTR("Onion skinning requires a RESET animation.")); + } + _start_onion_skinning(); // It will check for RESET animation anyway. } else { _stop_onion_skinning(); } @@ -1416,7 +1415,7 @@ void AnimationPlayerEditor::_onion_skinning_menu(int p_option) { onion.steps = (p_option - ONION_SKINNING_1_STEP) + 1; int one_frame_idx = menu->get_item_index(ONION_SKINNING_1_STEP); for (int i = 0; i <= ONION_SKINNING_LAST_STEPS_OPTION - ONION_SKINNING_1_STEP; i++) { - menu->set_item_checked(one_frame_idx + i, onion.steps == i + 1); + menu->set_item_checked(one_frame_idx + i, (int)onion.steps == i + 1); } } break; case ONION_SKINNING_DIFFERENCES_ONLY: { @@ -1475,15 +1474,15 @@ void AnimationPlayerEditor::_editor_visibility_changed() { bool AnimationPlayerEditor::_are_onion_layers_valid() { ERR_FAIL_COND_V(!onion.past && !onion.future, false); - Point2 capture_size = get_tree()->get_root()->get_size(); - return onion.captures.size() == onion.get_needed_capture_count() && onion.capture_size == capture_size; + Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID); + return onion.captures.size() == onion.get_capture_count() && onion.capture_size == capture_size; } void AnimationPlayerEditor::_allocate_onion_layers() { _free_onion_layers(); - int captures = onion.get_needed_capture_count(); - Point2 capture_size = get_tree()->get_root()->get_size(); + int captures = onion.get_capture_count(); + Size2 capture_size = DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID); onion.captures.resize(captures); onion.captures_valid.resize(captures); @@ -1492,7 +1491,7 @@ void AnimationPlayerEditor::_allocate_onion_layers() { bool is_present = onion.differences_only && i == captures - 1; // Each capture is a viewport with a canvas item attached that renders a full-size rect with the contents of the main viewport. - onion.captures.write[i] = RS::get_singleton()->viewport_create(); + onion.captures[i] = RS::get_singleton()->viewport_create(); RS::get_singleton()->viewport_set_size(onion.captures[i], capture_size.width, capture_size.height); RS::get_singleton()->viewport_set_update_mode(onion.captures[i], RS::VIEWPORT_UPDATE_ALWAYS); @@ -1502,13 +1501,13 @@ void AnimationPlayerEditor::_allocate_onion_layers() { // Reset the capture canvas item to the current root viewport texture (defensive). RS::get_singleton()->canvas_item_clear(onion.capture.canvas_item); - RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), capture_size), get_tree()->get_root()->get_texture()->get_rid()); + RS::get_singleton()->canvas_item_add_texture_rect(onion.capture.canvas_item, Rect2(Point2(), Point2(capture_size.x, -capture_size.y)), get_tree()->get_root()->get_texture()->get_rid()); onion.capture_size = capture_size; } void AnimationPlayerEditor::_free_onion_layers() { - for (int i = 0; i < onion.captures.size(); i++) { + for (uint32_t i = 0; i < onion.captures.size(); i++) { if (onion.captures[i].is_valid()) { RS::get_singleton()->free(onion.captures[i]); } @@ -1524,7 +1523,7 @@ void AnimationPlayerEditor::_prepare_onion_layers_1() { return; } - if (!onion.enabled || !is_processing() || !is_visible() || !get_player()) { + if (!onion.enabled || !is_visible() || !get_player() || !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) { _stop_onion_skinning(); return; } @@ -1540,14 +1539,10 @@ void AnimationPlayerEditor::_prepare_onion_layers_1() { } // And go to next step afterwards. - call_deferred(SNAME("_prepare_onion_layers_2")); + callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_prolog).call_deferred(); } -void AnimationPlayerEditor::_prepare_onion_layers_1_deferred() { - call_deferred(SNAME("_prepare_onion_layers_1")); -} - -void AnimationPlayerEditor::_prepare_onion_layers_2() { +void AnimationPlayerEditor::_prepare_onion_layers_2_prolog() { Ref<Animation> anim = player->get_animation(player->get_assigned_animation()); if (!anim.is_valid()) { return; @@ -1558,21 +1553,20 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { } // Hide superfluous elements that would make the overlay unnecessary cluttered. - Dictionary canvas_edit_state; - Dictionary spatial_edit_state; if (Node3DEditor::get_singleton()->is_visible()) { // 3D - spatial_edit_state = Node3DEditor::get_singleton()->get_state(); - Dictionary new_state = spatial_edit_state.duplicate(); + onion.temp.spatial_edit_state = Node3DEditor::get_singleton()->get_state(); + Dictionary new_state = onion.temp.spatial_edit_state.duplicate(); new_state["show_grid"] = false; new_state["show_origin"] = false; - Array orig_vp = spatial_edit_state["viewports"]; + Array orig_vp = onion.temp.spatial_edit_state["viewports"]; Array vp; vp.resize(4); for (int i = 0; i < vp.size(); i++) { Dictionary d = ((Dictionary)orig_vp[i]).duplicate(); d["use_environment"] = false; d["doppler"] = false; + d["listener"] = false; d["gizmos"] = onion.include_gizmos ? d["gizmos"] : Variant(false); d["information"] = false; vp[i] = d; @@ -1580,23 +1574,27 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { new_state["viewports"] = vp; // TODO: Save/restore only affected entries. Node3DEditor::get_singleton()->set_state(new_state); - } else { // CanvasItemEditor - // 2D - canvas_edit_state = CanvasItemEditor::get_singleton()->get_state(); - Dictionary new_state = canvas_edit_state.duplicate(); + } else { + // CanvasItemEditor. + onion.temp.canvas_edit_state = CanvasItemEditor::get_singleton()->get_state(); + Dictionary new_state = onion.temp.canvas_edit_state.duplicate(); + new_state["show_origin"] = false; new_state["show_grid"] = false; new_state["show_rulers"] = false; new_state["show_guides"] = false; new_state["show_helpers"] = false; new_state["show_zoom_control"] = false; + new_state["show_edit_locks"] = false; + new_state["grid_visibility"] = 2; // TODO: Expose CanvasItemEditor::GRID_VISIBILITY_HIDE somehow and use it. + new_state["show_transformation_gizmos"] = onion.include_gizmos ? new_state["gizmos"] : Variant(false); // TODO: Save/restore only affected entries. CanvasItemEditor::get_singleton()->set_state(new_state); } // Tweak the root viewport to ensure it's rendered before our target. RID root_vp = get_tree()->get_root()->get_viewport_rid(); - Rect2 root_vp_screen_rect = Rect2(Vector2(), get_tree()->get_root()->get_size()); - RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2()); + onion.temp.screen_rect = Rect2(Vector2(), DisplayServer::get_singleton()->window_get_size(DisplayServer::MAIN_WINDOW_ID)); + RS::get_singleton()->viewport_attach_to_screen(root_vp, Rect2(), DisplayServer::INVALID_WINDOW_ID); RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_ALWAYS); RID present_rid; @@ -1611,8 +1609,8 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { } // Backup current animation state. - Ref<AnimatedValuesBackup> backup_current = player->make_backup(); - float cpos = player->get_current_animation_position(); + onion.temp.anim_values_backup = player->make_backup(); + onion.temp.anim_player_position = player->get_current_animation_position(); // Render every past/future step with the capture shader. @@ -1620,55 +1618,94 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { onion.capture.material->set_shader_parameter("bkg_color", GLOBAL_GET("rendering/environment/defaults/default_clear_color")); onion.capture.material->set_shader_parameter("differences_only", onion.differences_only); onion.capture.material->set_shader_parameter("present", onion.differences_only ? RS::get_singleton()->viewport_get_texture(present_rid) : RID()); - - int step_off_a = onion.past ? -onion.steps : 0; - int step_off_b = onion.future ? onion.steps : 0; - int cidx = 0; onion.capture.material->set_shader_parameter("dir_color", onion.force_white_modulate ? Color(1, 1, 1) : Color(EDITOR_GET("editors/animation/onion_layers_past_color"))); - for (int step_off = step_off_a; step_off <= step_off_b; step_off++) { - if (step_off == 0) { - // Skip present step and switch to the color of future. - if (!onion.force_white_modulate) { - onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color")); - } - continue; - } - float pos = cpos + step_off * anim->get_step(); + uint32_t p_capture_idx = 0; + int first_step_offset = onion.past ? -(int)onion.steps : 0; + _prepare_onion_layers_2_step_prepare(first_step_offset, p_capture_idx); +} + +void AnimationPlayerEditor::_prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx) { + uint32_t next_capture_idx = p_capture_idx; + if (p_step_offset == 0) { + // Skip present step and switch to the color of future. + if (!onion.force_white_modulate) { + onion.capture.material->set_shader_parameter("dir_color", EDITOR_GET("editors/animation/onion_layers_future_color")); + } + } else { + Ref<Animation> anim = player->get_animation(player->get_assigned_animation()); + double pos = onion.temp.anim_player_position + p_step_offset * anim->get_step(); bool valid = anim->get_loop_mode() != Animation::LOOP_NONE || (pos >= 0 && pos <= anim->get_length()); - onion.captures_valid.write[cidx] = valid; + onion.captures_valid[p_capture_idx] = valid; if (valid) { player->seek(pos, true); - get_tree()->flush_transform_notifications(); // Needed for transforms of Node3Ds. - - RS::get_singleton()->viewport_set_active(onion.captures[cidx], true); - RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[cidx]); - RS::get_singleton()->draw(false); - RS::get_singleton()->viewport_set_active(onion.captures[cidx], false); + OS::get_singleton()->get_main_loop()->process(0); + // This is the key: process the frame and let all callbacks/updates/notifications happen + // so everything (transforms, skeletons, etc.) is up-to-date visually. + callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_2_step_capture).bind(p_step_offset, p_capture_idx).call_deferred(); + return; + } else { + next_capture_idx++; } + } + + int last_step_offset = onion.future ? onion.steps : 0; + if (p_step_offset < last_step_offset) { + _prepare_onion_layers_2_step_prepare(p_step_offset + 1, next_capture_idx); + } else { + _prepare_onion_layers_2_epilog(); + } +} + +void AnimationPlayerEditor::_prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx) { + DEV_ASSERT(p_step_offset != 0); + DEV_ASSERT(onion.captures_valid[p_capture_idx]); - cidx++; + RID root_vp = get_tree()->get_root()->get_viewport_rid(); + RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], true); + RS::get_singleton()->viewport_set_parent_viewport(root_vp, onion.captures[p_capture_idx]); + RS::get_singleton()->draw(false); + RS::get_singleton()->viewport_set_active(onion.captures[p_capture_idx], false); + + int last_step_offset = onion.future ? onion.steps : 0; + if (p_step_offset < last_step_offset) { + _prepare_onion_layers_2_step_prepare(p_step_offset + 1, p_capture_idx + 1); + } else { + _prepare_onion_layers_2_epilog(); } +} +void AnimationPlayerEditor::_prepare_onion_layers_2_epilog() { // Restore root viewport. + RID root_vp = get_tree()->get_root()->get_viewport_rid(); RS::get_singleton()->viewport_set_parent_viewport(root_vp, RID()); - RS::get_singleton()->viewport_attach_to_screen(root_vp, root_vp_screen_rect); + RS::get_singleton()->viewport_attach_to_screen(root_vp, onion.temp.screen_rect, DisplayServer::MAIN_WINDOW_ID); RS::get_singleton()->viewport_set_update_mode(root_vp, RS::VIEWPORT_UPDATE_WHEN_VISIBLE); - // Restore animation state - // (Seeking with update=true wouldn't do the trick because the current value of the properties - // may not match their value for the current point in the animation). - player->seek(cpos, false); - player->restore(backup_current); + // Restore animation state. + // Here we're combine the power of seeking back to the original position and + // restoring the values backup. In most cases they will bring the same value back, + // but there are cases handled by one that the other can't. + // Namely: + // - Seeking won't restore any values that may have been modified by the user + // in the node after the last time the AnimationPlayer updated it. + // - Restoring the backup won't account for values that are not directly involved + // in the animation but a consequence of them (e.g., SkeletonModification2DLookAt). + // FIXME: Since backup of values is based on the reset animation, only values + // backed by a proper reset animation will work correctly with onion + // skinning and the possibility to restore the values mentioned in the + // first point above is gone. Still good enough. + player->seek(onion.temp.anim_player_position, true, true); + player->restore(onion.temp.anim_values_backup); // Restore state of main editors. if (Node3DEditor::get_singleton()->is_visible()) { // 3D - Node3DEditor::get_singleton()->set_state(spatial_edit_state); + Node3DEditor::get_singleton()->set_state(onion.temp.spatial_edit_state); } else { // CanvasItemEditor // 2D - CanvasItemEditor::get_singleton()->set_state(canvas_edit_state); + CanvasItemEditor::get_singleton()->set_state(onion.temp.canvas_edit_state); } // Update viewports with skin layers overlaid for the actual engine loop render. @@ -1677,21 +1714,26 @@ void AnimationPlayerEditor::_prepare_onion_layers_2() { } void AnimationPlayerEditor::_start_onion_skinning() { - // FIXME: Using "process_frame" makes onion layers update one frame behind the current. - if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred))) { - get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred)); + if (get_player() && !get_player()->has_animation(SceneStringNames::get_singleton()->RESET)) { + onion.enabled = false; + onion_toggle->set_pressed_no_signal(false); + return; + } + if (!get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) { + get_tree()->connect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1)); } } void AnimationPlayerEditor::_stop_onion_skinning() { - if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred))) { - get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1_deferred)); + if (get_tree()->is_connected(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1))) { + get_tree()->disconnect(SNAME("process_frame"), callable_mp(this, &AnimationPlayerEditor::_prepare_onion_layers_1)); _free_onion_layers(); - // Clean up the overlay. + // Clean up. onion.can_overlay = false; plugin->update_overlays(); + onion.temp = {}; } } @@ -1773,8 +1815,6 @@ void AnimationPlayerEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("_list_changed"), &AnimationPlayerEditor::_list_changed); ClassDB::bind_method(D_METHOD("_animation_duplicate"), &AnimationPlayerEditor::_animation_duplicate); - ClassDB::bind_method(D_METHOD("_prepare_onion_layers_1"), &AnimationPlayerEditor::_prepare_onion_layers_1); - ClassDB::bind_method(D_METHOD("_prepare_onion_layers_2"), &AnimationPlayerEditor::_prepare_onion_layers_2); ClassDB::bind_method(D_METHOD("_start_onion_skinning"), &AnimationPlayerEditor::_start_onion_skinning); ClassDB::bind_method(D_METHOD("_stop_onion_skinning"), &AnimationPlayerEditor::_stop_onion_skinning); @@ -1914,16 +1954,6 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug onion_skinning->get_popup()->add_check_item(TTR("Include Gizmos (3D)"), ONION_SKINNING_INCLUDE_GIZMOS); hb->add_child(onion_skinning); - // FIXME: Onion skinning disabled for now as it's broken and triggers fast - // flickering red/blue modulation (GH-53870). - if (hack_disable_onion_skinning) { - onion_toggle->set_disabled(true); - onion_toggle->set_tooltip_text(TTR("Onion Skinning temporarily disabled due to rendering bug.")); - - onion_skinning->set_disabled(true); - onion_skinning->set_tooltip_text(TTR("Onion Skinning temporarily disabled due to rendering bug.")); - } - hb->add_child(memnew(VSeparator)); pin = memnew(Button); @@ -2013,24 +2043,13 @@ AnimationPlayerEditor::AnimationPlayerEditor(AnimationPlayerEditorPlugin *p_plug track_editor->connect(SNAME("visibility_changed"), callable_mp(this, &AnimationPlayerEditor::_editor_visibility_changed)); - onion.enabled = false; - onion.past = true; - onion.future = false; - onion.steps = 1; - onion.differences_only = false; - onion.force_white_modulate = false; - onion.include_gizmos = false; - - onion.last_frame = 0; - onion.can_overlay = false; - onion.capture_size = Size2(); onion.capture.canvas = RS::get_singleton()->canvas_create(); onion.capture.canvas_item = RS::get_singleton()->canvas_item_create(); RS::get_singleton()->canvas_item_set_parent(onion.capture.canvas_item, onion.capture.canvas); - onion.capture.material = Ref<ShaderMaterial>(memnew(ShaderMaterial)); + onion.capture.material.instantiate(); - onion.capture.shader = Ref<Shader>(memnew(Shader)); + onion.capture.shader.instantiate(); onion.capture.shader->set_code(R"( // Animation editor onion skinning shader. @@ -2047,10 +2066,15 @@ float zero_if_equal(vec4 a, vec4 b) { void fragment() { vec4 capture_samp = texture(TEXTURE, UV); - vec4 present_samp = texture(present, UV); float bkg_mask = zero_if_equal(capture_samp, bkg_color); - float diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color); - diff_mask = min(1.0, diff_mask + float(!differences_only)); + float diff_mask = 1.0; + if (differences_only) { + // FIXME: If Y-flips across render target, canvas item, etc. was handled correctly, + // this would not be as convoluted in the shader. + vec4 capture_samp2 = texture(TEXTURE, vec2(UV.x, 1.0 - UV.y)); + vec4 present_samp = texture(present, vec2(UV.x, 1.0 - UV.y)); + diff_mask = 1.0 - zero_if_equal(present_samp, bkg_color); + } COLOR = vec4(capture_samp.rgb * dir_color.rgb, bkg_mask * diff_mask); } )"); @@ -2061,6 +2085,7 @@ AnimationPlayerEditor::~AnimationPlayerEditor() { _free_onion_layers(); RS::get_singleton()->free(onion.capture.canvas); RS::get_singleton()->free(onion.capture.canvas_item); + onion.capture = {}; } void AnimationPlayerEditorPlugin::_notification(int p_what) { diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index 4763a008fe..6751933839 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -136,20 +136,18 @@ class AnimationPlayerEditor : public VBoxContainer { AnimationTrackEditor *track_editor = nullptr; static AnimationPlayerEditor *singleton; - bool hack_disable_onion_skinning = true; // Temporary hack for GH-53870. - // Onion skinning. struct { // Settings. bool enabled = false; - bool past = false; + bool past = true; bool future = false; - int steps = 0; + uint32_t steps = 1; bool differences_only = false; bool force_white_modulate = false; bool include_gizmos = false; - int get_needed_capture_count() const { + uint32_t get_capture_count() const { // 'Differences only' needs a capture of the present. return (past && future ? 2 * steps : steps) + (differences_only ? 1 : 0); } @@ -158,14 +156,23 @@ class AnimationPlayerEditor : public VBoxContainer { int64_t last_frame = 0; int can_overlay = 0; Size2 capture_size; - Vector<RID> captures; - Vector<bool> captures_valid; + LocalVector<RID> captures; + LocalVector<bool> captures_valid; struct { RID canvas; RID canvas_item; Ref<ShaderMaterial> material; Ref<Shader> shader; } capture; + + // Cross-call state. + struct { + double anim_player_position = 0.0; + Ref<AnimatedValuesBackup> anim_values_backup; + Rect2 screen_rect; + Dictionary canvas_edit_state; + Dictionary spatial_edit_state; + } temp; } onion; void _select_anim_by_name(const String &p_anim); @@ -215,8 +222,10 @@ class AnimationPlayerEditor : public VBoxContainer { void _allocate_onion_layers(); void _free_onion_layers(); void _prepare_onion_layers_1(); - void _prepare_onion_layers_1_deferred(); - void _prepare_onion_layers_2(); + void _prepare_onion_layers_2_prolog(); + void _prepare_onion_layers_2_step_prepare(int p_step_offset, uint32_t p_capture_idx); + void _prepare_onion_layers_2_step_capture(int p_step_offset, uint32_t p_capture_idx); + void _prepare_onion_layers_2_epilog(); void _start_onion_skinning(); void _stop_onion_skinning(); diff --git a/editor/property_selector.cpp b/editor/property_selector.cpp index 5228db03b9..8c0a5b999a 100644 --- a/editor/property_selector.cpp +++ b/editor/property_selector.cpp @@ -370,46 +370,15 @@ void PropertySelector::_item_selected() { class_type = instance->get_class(); } - DocTools *dd = EditorHelp::get_doc_data(); String text; - if (properties) { - while (!class_type.is_empty()) { - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type); - if (E) { - for (int i = 0; i < E->value.properties.size(); i++) { - if (E->value.properties[i].name == name) { - text = DTR(E->value.properties[i].description); - break; - } - } - } - - if (!text.is_empty()) { - break; - } - - // The property may be from a parent class, keep looking. - class_type = ClassDB::get_parent_class(class_type); + while (!class_type.is_empty()) { + text = properties ? help_bit->get_property_description(class_type, name) : help_bit->get_method_description(class_type, name); + if (!text.is_empty()) { + break; } - } else { - while (!class_type.is_empty()) { - HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_type); - if (E) { - for (int i = 0; i < E->value.methods.size(); i++) { - if (E->value.methods[i].name == name) { - text = DTR(E->value.methods[i].description); - break; - } - } - } - if (!text.is_empty()) { - break; - } - - // The method may be from a parent class, keep looking. - class_type = ClassDB::get_parent_class(class_type); - } + // It may be from a parent class, keep looking. + class_type = ClassDB::get_parent_class(class_type); } if (!text.is_empty()) { diff --git a/editor/renames_map_3_to_4.cpp b/editor/renames_map_3_to_4.cpp index 1908e877e7..c44be7e685 100644 --- a/editor/renames_map_3_to_4.cpp +++ b/editor/renames_map_3_to_4.cpp @@ -1071,6 +1071,7 @@ const char *RenamesMap3To4::gdscript_properties_renames[][2] = { // Would need bespoke solution. // { "autowrap", "autowrap_mode" }, // Label -- Changed from bool to enum. + // { "extents", "size" }, // BoxShape3D, LightmapGI, ReflectionProbe // { "frames", "sprite_frames" }, // AnimatedSprite2D, AnimatedSprite3D -- GH-73696 // { "percent_visible, "show_percentage }, // ProgressBar -- Breaks Label and RichTextLabel. // { "pressed", "button_pressed" }, // BaseButton -- Would also rename the signal. @@ -1098,7 +1099,6 @@ const char *RenamesMap3To4::gdscript_properties_renames[][2] = { { "drag_margin_top", "drag_top_margin" }, // Camera2D { "drag_margin_v_enabled", "drag_vertical_enabled" }, // Camera2D { "enabled_focus_mode", "focus_mode" }, // BaseButton - Removed - { "extents", "size" }, // BoxShape3D, LightmapGI, ReflectionProbe { "extra_spacing_bottom", "spacing_bottom" }, // Font { "extra_spacing_top", "spacing_top" }, // Font { "focus_neighbour_bottom", "focus_neighbor_bottom" }, // Control @@ -1198,7 +1198,6 @@ const char *RenamesMap3To4::csharp_properties_renames[][2] = { { "DragMarginTop", "DragTopMargin" }, // Camera2D { "DragMarginVEnabled", "DragVerticalEnabled" }, // Camera2D { "EnabledFocusMode", "FocusMode" }, // BaseButton - Removed - { "Extents", "Size" }, // BoxShape3D, LightmapGI, ReflectionProbe { "ExtraSpacingBottom", "SpacingBottom" }, // Font { "ExtraSpacingTop", "SpacingTop" }, // Font { "FocusNeighbourBottom", "FocusNeighborBottom" }, // Control |