summaryrefslogtreecommitdiffstats
path: root/editor
diff options
context:
space:
mode:
Diffstat (limited to 'editor')
-rw-r--r--editor/connections_dialog.cpp85
-rw-r--r--editor/connections_dialog.h2
-rw-r--r--editor/create_dialog.cpp5
-rw-r--r--editor/editor_build_profile.cpp29
-rw-r--r--editor/editor_feature_profile.cpp25
-rw-r--r--editor/editor_help.cpp295
-rw-r--r--editor/editor_help.h31
-rw-r--r--editor/editor_inspector.cpp182
-rw-r--r--editor/editor_inspector.h8
-rw-r--r--editor/editor_node.cpp2
-rw-r--r--editor/editor_properties.cpp136
-rw-r--r--editor/editor_properties.h20
-rw-r--r--editor/editor_properties_array_dict.cpp3
-rw-r--r--editor/icons/CodeRegionFoldDownArrow.svg2
-rw-r--r--editor/icons/CodeRegionFoldedRightArrow.svg2
-rw-r--r--editor/plugins/animation_player_editor_plugin.cpp245
-rw-r--r--editor/plugins/animation_player_editor_plugin.h27
-rw-r--r--editor/property_selector.cpp43
-rw-r--r--editor/renames_map_3_to_4.cpp3
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