Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework the Configuration Warning system #90049

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions core/io/resource.cpp
Original file line number Diff line number Diff line change
@@ -163,12 +163,6 @@ String Resource::get_name() const {
return name;
}

void Resource::update_configuration_warning() {
if (_update_configuration_warning) {
_update_configuration_warning();
}
}

bool Resource::editor_can_reload_from_file() {
return true; //by default yes
}
@@ -488,7 +482,6 @@ void Resource::reset_local_to_scene() {
}

Node *(*Resource::_get_local_scene_func)() = nullptr;
void (*Resource::_update_configuration_warning)() = nullptr;

void Resource::set_as_translation_remapped(bool p_remapped) {
if (remapped_list.in_list() == p_remapped) {
@@ -535,6 +528,32 @@ String Resource::get_id_for_path(const String &p_path) const {
#endif
}

#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> Resource::get_configuration_info() const {
Vector<ConfigurationInfo> ret;

Array info;
if (GDVIRTUAL_CALL(_get_configuration_info, info)) {
ret.resize(info.size());

ConfigurationInfo *ptrw = ret.ptrw();
for (const Variant &variant : info) {
*ptrw++ = ConfigurationInfo::from_variant(variant);
}
}

return ret;
}
#endif

void Resource::update_configuration_info() {
#ifdef TOOLS_ENABLED
if (ConfigurationInfo::configuration_info_changed_func) {
ConfigurationInfo::configuration_info_changed_func(this);
}
#endif
}

void Resource::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_path", "path"), &Resource::_set_path);
ClassDB::bind_method(D_METHOD("take_over_path", "path"), &Resource::_take_over_path);
@@ -557,6 +576,7 @@ void Resource::_bind_methods() {
ClassDB::bind_static_method("Resource", D_METHOD("generate_scene_unique_id"), &Resource::generate_scene_unique_id);
ClassDB::bind_method(D_METHOD("set_scene_unique_id", "id"), &Resource::set_scene_unique_id);
ClassDB::bind_method(D_METHOD("get_scene_unique_id"), &Resource::get_scene_unique_id);
ClassDB::bind_method(D_METHOD("update_configuration_info"), &Resource::update_configuration_info);

ClassDB::bind_method(D_METHOD("emit_changed"), &Resource::emit_changed);

@@ -574,6 +594,7 @@ void Resource::_bind_methods() {
GDVIRTUAL_BIND(_get_rid);
GDVIRTUAL_BIND(_reset_state);
GDVIRTUAL_BIND(_set_path_cache, "path");
GDVIRTUAL_BIND(_get_configuration_info);
}

Resource::Resource() :
10 changes: 8 additions & 2 deletions core/io/resource.h
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@

#include "core/io/resource_uid.h"
#include "core/object/class_db.h"
#include "core/object/configuration_info.h"
#include "core/object/gdvirtual.gen.inc"
#include "core/object/ref_counted.h"
#include "core/templates/safe_refcount.h"
@@ -90,6 +91,7 @@ class Resource : public RefCounted {

virtual void reset_local_to_scene();
GDVIRTUAL0(_setup_local_to_scene);
GDVIRTUAL0RC(Array, _get_configuration_info);

GDVIRTUAL0RC(RID, _get_rid);

@@ -98,9 +100,7 @@ class Resource : public RefCounted {

public:
static Node *(*_get_local_scene_func)(); //used by editor
static void (*_update_configuration_warning)(); //used by editor

void update_configuration_warning();
virtual bool editor_can_reload_from_file();
virtual void reset_state(); //for resources that use variable amount of properties, either via _validate_property or _get_property_list, this function needs to be implemented to correctly clear state
virtual Error copy_from(const Ref<Resource> &p_resource);
@@ -156,6 +156,12 @@ class Resource : public RefCounted {
void set_id_for_path(const String &p_path, const String &p_id);
String get_id_for_path(const String &p_path) const;

#ifdef TOOLS_ENABLED
virtual Vector<ConfigurationInfo> get_configuration_info() const;
#endif

void update_configuration_info();

Resource();
~Resource();
};
112 changes: 112 additions & 0 deletions core/object/configuration_info.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**************************************************************************/
/* configuration_info.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "core/object/callable_method_pointer.h"

#include "configuration_info.h"

#ifdef TOOLS_ENABLED

// Registered in editor, to avoid tight coupling.
void (*ConfigurationInfo::configuration_info_changed_func)(Object *p_object) = nullptr;

void ConfigurationInfo::queue_error_print(const String &p_error) {
if (queued_errors_to_print.is_empty()) {
callable_mp_static(&ConfigurationInfo::_print_errors_from_queue).call_deferred();
}
queued_errors_to_print.insert(p_error);
}

void ConfigurationInfo::_print_errors_from_queue() {
for (const String &error : queued_errors_to_print) {
ERR_PRINT(error);
}
queued_errors_to_print.clear();
}

ConfigurationInfo ConfigurationInfo::from_variant(const Variant &p_variant) {
if (p_variant.get_type() == Variant::STRING) {
return ConfigurationInfo(String(p_variant));
} else if (p_variant.get_type() == Variant::DICTIONARY) {
return ConfigurationInfo(Dictionary(p_variant));
} else {
queue_error_print("Attempted to convert a ConfigurationInfo which is neither a string nor a dictionary, but a " + Variant::get_type_name(p_variant.get_type()));
return ConfigurationInfo();
}
}

ConfigurationInfo::Severity ConfigurationInfo::string_to_severity(const String &p_severity) {
if (p_severity == "error") {
return ConfigurationInfo::Severity::ERROR;
} else if (p_severity == "warning") {
return ConfigurationInfo::Severity::WARNING;
} else if (p_severity == "info") {
return ConfigurationInfo::Severity::INFO;
} else {
queue_error_print("Severity of Configuration Info must be one of \"error\", \"warning\" or \"info\", received \"" + p_severity + "\".");
return ConfigurationInfo::Severity::NONE;
}
}

bool ConfigurationInfo::ensure_valid(Object *p_owner) const {
if (message.is_empty()) {
queue_error_print("Configuration Info may not have an empty message.");
return false;
}

if (p_owner != nullptr && !property_name.is_empty()) {
bool has_property = false;
p_owner->get(property_name, &has_property);
if (!has_property) {
queue_error_print(vformat("Configuration Info on %s refers to property \"%s\" that does not exist.", p_owner->get_class_name(), property_name));
return false;
}
}

return true;
}

ConfigurationInfo::ConfigurationInfo() :
ConfigurationInfo(String()) {
}

ConfigurationInfo::ConfigurationInfo(const Dictionary &p_dict) {
message = p_dict.get("message", "");
property_name = p_dict.get("property", StringName());
severity = string_to_severity(p_dict.get("severity", "warning"));
}

ConfigurationInfo::ConfigurationInfo(const String &p_message, const StringName &p_property_name, Severity p_severity) {
message = p_message;
property_name = p_property_name;
severity = p_severity;
}

#endif // TOOLS_ENABLED
90 changes: 90 additions & 0 deletions core/object/configuration_info.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**************************************************************************/
/* configuration_info.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef CONFIGURATION_INFO_H
#define CONFIGURATION_INFO_H

#ifdef TOOLS_ENABLED

#include "core/templates/hash_set.h"
#include "core/variant/variant.h"

#ifdef ERROR
#undef ERROR // Define from Windows APIs
#endif

#define CONFIG_WARNING(message) infos.push_back(ConfigurationInfo(message, "", ConfigurationInfo::Severity::WARNING));
#define CONFIG_WARNING_P(message, property_name) infos.push_back(ConfigurationInfo(message, property_name, ConfigurationInfo::Severity::WARNING));

class ConfigurationInfo {
public:
enum class Severity {
INFO,
WARNING,
ERROR,
MAX,
NONE = -1,
};

private:
String message;
StringName property_name;
Severity severity;

inline static HashSet<String> queued_errors_to_print;

static void queue_error_print(const String &p_error);
static void _print_errors_from_queue();

public:
static void (*configuration_info_changed_func)(Object *p_object);

static ConfigurationInfo from_variant(const Variant &p_variant);
static Severity string_to_severity(const String &p_severity);

bool ensure_valid(Object *p_owner) const;
String get_message() const { return message; }
StringName get_property_name() const { return property_name; }
Severity get_severity() const { return severity; }

bool operator==(const ConfigurationInfo &p_val) const {
return (message == p_val.message) &&
(property_name == p_val.property_name) &&
(severity == p_val.severity);
}

ConfigurationInfo();
ConfigurationInfo(const Dictionary &p_dict);
ConfigurationInfo(const String &p_message, const StringName &p_property_name = StringName(), Severity p_severity = Severity::WARNING);
};

#endif // TOOLS_ENABLED

#endif // CONFIGURATION_INFO_H
42 changes: 40 additions & 2 deletions doc/classes/Node.xml
Original file line number Diff line number Diff line change
@@ -36,7 +36,39 @@
Corresponds to the [constant NOTIFICATION_EXIT_TREE] notification in [method Object._notification] and signal [signal tree_exiting]. To get notified when the node has already left the active tree, connect to the [signal tree_exited].
</description>
</method>
<method name="_get_configuration_warnings" qualifiers="virtual const">
<method name="_get_configuration_info" qualifiers="virtual const">
<return type="Array" />
<description>
Override this method to inform the user about the configuration of this node.
Call [method update_configuration_info] when the messages need to be updated for this node.
The elements in the array returned from this method are displayed at the top of the Inspector dock, as well as an icon in the Scene dock. The script that overrides it must be a [code]tool[/code] script.
Each array element must either be a [String] or a [Dictionary].
A dictionary element must contain a key [code]message[/code] of type [String] which is shown in the user interface. Following optional keys are supported:
- [code]property[/code] of type [StringName], which adds an icon next to the property with the same name.
- [code]severity[/code] can be set to [code]"error"[/code], [code]"warning"[/code], or [code]"info"[/code] to change the icon and text color. Defaults to warning.
If a string is found in the returned array, it is treated as a warning message with no property set.
Returning an empty array produces no messages.
[codeblock]
@tool
extends Node

@export var energy = 0:
set(value):
energy = value
update_configuration_info()

func _get_configuration_info():
if energy &lt; 0:
return [{
"property": "energy",
"message": "Energy must be 0 or greater."
}]
else:
return []
[/codeblock]
</description>
</method>
<method name="_get_configuration_warnings" qualifiers="virtual const" deprecated="Use [method _get_configuration_info] instead.">
<return type="PackedStringArray" />
<description>
The elements in the array returned from this method are displayed as warnings in the Scene dock if the script that overrides it is a [code]tool[/code] script.
@@ -986,7 +1018,13 @@
This is the default behavior for all nodes. Calling [method Object.set_translation_domain] disables this behavior.
</description>
</method>
<method name="update_configuration_warnings">
<method name="update_configuration_info">
<return type="void" />
<description>
Refreshes the configuration information displayed for this node in the Scene dock and Inspector dock. Use [method _get_configuration_info] to customize the messages to display.
</description>
</method>
<method name="update_configuration_warnings" deprecated="Use [method update_configuration_info] instead.">
<return type="void" />
<description>
Refreshes the warnings displayed for this node in the Scene dock. Use [method _get_configuration_warnings] to customize the warning messages to display.
38 changes: 38 additions & 0 deletions doc/classes/Resource.xml
Original file line number Diff line number Diff line change
@@ -14,6 +14,38 @@
<link title="When and how to avoid using nodes for everything">$DOCS_URL/tutorials/best_practices/node_alternatives.html</link>
</tutorials>
<methods>
<method name="_get_configuration_info" qualifiers="virtual const">
<return type="Array" />
<description>
Override this method to inform the user about the configuration of this resource.
Call [method update_configuration_info] when the messages need to be updated for this resource.
The elements in the array returned from this method are displayed at the top of the Inspector dock. The script that overrides it must be a [code]tool[/code] script.
Each array element must either be a [String] or a [Dictionary].
A dictionary element must contain a key [code]message[/code] of type [String] which is shown in the user interface. Following optional keys are supported:
- [code]property[/code] of type [StringName], which adds an icon next to the property with the same name.
- [code]severity[/code] can be set to [code]"error"[/code], [code]"warning"[/code], or [code]"info"[/code] to change the icon and text color. Defaults to warning.
If a string is found in the returned array, it is treated as a warning message with no property set.
Returning an empty array produces no messages.
[codeblock]
@tool
extends Resource

@export var energy = 0:
set(value):
energy = value
update_configuration_info()

func _get_configuration_info():
if energy &lt; 0:
return [{
"property": "energy",
"message": "Energy must be 0 or greater."
}]
else:
return []
[/codeblock]
</description>
</method>
<method name="_get_rid" qualifiers="virtual const">
<return type="RID" />
<description>
@@ -142,6 +174,12 @@
Sets the [member resource_path] to [param path], potentially overriding an existing cache entry for this path. Further attempts to load an overridden resource by path will instead return this resource.
</description>
</method>
<method name="update_configuration_info">
<return type="void" />
<description>
Refreshes the configuration information displayed for this resource in the Inspector dock. Use [method _get_configuration_info] to customize the messages to display.
</description>
</method>
</methods>
<members>
<member name="resource_local_to_scene" type="bool" setter="set_local_to_scene" getter="is_local_to_scene" default="false">
2 changes: 1 addition & 1 deletion doc/classes/SceneTree.xml
Original file line number Diff line number Diff line change
@@ -284,7 +284,7 @@
Emitted when the [param node] enters this tree.
</description>
</signal>
<signal name="node_configuration_warning_changed">
<signal name="node_configuration_warning_changed" deprecated="This signal is no longer available. Call [code]update_configuration_info[/code] on a [Node] or [Resource] to notify the editor of configuration info changes.">
<param index="0" name="node" type="Node" />
<description>
Emitted when the [param node]'s [method Node.update_configuration_warnings] is called. Only emitted in the editor.
143 changes: 143 additions & 0 deletions editor/editor_configuration_info.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**************************************************************************/
/* editor_configuration_info.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "editor_configuration_info.h"

#include "editor/editor_property_name_processor.h"
#include "scene/main/node.h"
#include "servers/text_server.h"

Vector<ConfigurationInfo> EditorConfigurationInfo::get_configuration_info(Object *p_object) {
Vector<ConfigurationInfo> config_infos;
if (!p_object) {
return config_infos;
}

Node *node = Object::cast_to<Node>(p_object);
if (node) {
config_infos = node->get_configuration_info();

#ifndef DISABLE_DEPRECATED
PackedStringArray warnings = node->get_configuration_warnings();
for (const String &warning : warnings) {
config_infos.push_back(ConfigurationInfo::from_variant(warning));
}
#endif
} else {
Resource *resource = Object::cast_to<Resource>(p_object);
if (resource) {
config_infos = resource->get_configuration_info();
}
}

Vector<ConfigurationInfo> valid_infos;
for (const ConfigurationInfo &config_info : config_infos) {
if (config_info.ensure_valid(p_object)) {
valid_infos.push_back(config_info);
}
}
return valid_infos;
}

ConfigurationInfo::Severity EditorConfigurationInfo::get_max_severity(const Vector<ConfigurationInfo> &p_config_infos) {
ConfigurationInfo::Severity max_severity = ConfigurationInfo::Severity::NONE;

for (const ConfigurationInfo &config_info : p_config_infos) {
ConfigurationInfo::Severity severity = config_info.get_severity();
if (severity > max_severity) {
max_severity = severity;
}
}

return max_severity;
}

StringName EditorConfigurationInfo::get_severity_icon(ConfigurationInfo::Severity p_severity) {
switch (p_severity) {
case ConfigurationInfo::Severity::ERROR:
return SNAME("StatusError");
case ConfigurationInfo::Severity::WARNING:
return SNAME("NodeWarning");
case ConfigurationInfo::Severity::INFO:
return SNAME("NodeInfo");
default:
// Use warning icon as fallback.
return SNAME("NodeWarning");
}
}

Vector<ConfigurationInfo> EditorConfigurationInfo::filter_list_for_property(const Vector<ConfigurationInfo> &p_config_infos, const StringName &p_property_name) {
Vector<ConfigurationInfo> result;
for (const ConfigurationInfo &config_info : p_config_infos) {
if (config_info.get_property_name() == p_property_name) {
result.push_back(config_info);
}
}
return result;
}

String EditorConfigurationInfo::format_as_string(const ConfigurationInfo &p_config_info, bool p_wrap_lines, bool p_prefix_property_name) {
String text;

const StringName &property_name = p_config_info.get_property_name();
if (p_prefix_property_name && !property_name.is_empty()) {
const EditorPropertyNameProcessor::Style style = EditorPropertyNameProcessor::get_default_inspector_style();
const String styled_property_name = EditorPropertyNameProcessor::get_singleton()->process_name(property_name, style, property_name);

text = vformat("[%s] %s", styled_property_name, p_config_info.get_message());
} else {
text = p_config_info.get_message();
}

if (p_wrap_lines) {
// Limit the line width while keeping some padding.
// It is not efficient, but it does not have to be.
const PackedInt32Array boundaries = TS->string_get_word_breaks(text, "", 80);
PackedStringArray lines;
for (int bound = 0; bound < boundaries.size(); bound += 2) {
const int start = boundaries[bound];
const int end = boundaries[bound + 1];
String line = text.substr(start, end - start);
lines.append(line);
}
text = String("\n").join(lines).replace("\n", "\n ");
}
return text;
}

String EditorConfigurationInfo::format_list_as_string(const Vector<ConfigurationInfo> &p_config_infos, bool p_wrap_lines, bool p_prefix_property_name) {
const String bullet_point = U"";
PackedStringArray all_lines;
for (const ConfigurationInfo &config_info : p_config_infos) {
String text = bullet_point + format_as_string(config_info, p_wrap_lines, p_prefix_property_name);
all_lines.append(text);
}
return String("\n").join(all_lines);
}
51 changes: 51 additions & 0 deletions editor/editor_configuration_info.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**************************************************************************/
/* editor_configuration_info.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef EDITOR_CONFIGURATION_INFO_H
#define EDITOR_CONFIGURATION_INFO_H

#include "core/object/configuration_info.h"
#include "core/object/ref_counted.h"
#include "core/string/ustring.h"

class EditorConfigurationInfo {
public:
EditorConfigurationInfo() {}

static Vector<ConfigurationInfo> get_configuration_info(Object *p_object);
static ConfigurationInfo::Severity get_max_severity(const Vector<ConfigurationInfo> &p_config_infos);
static StringName get_severity_icon(ConfigurationInfo::Severity p_severity);

static Vector<ConfigurationInfo> filter_list_for_property(const Vector<ConfigurationInfo> &p_config_infos, const StringName &p_property_name);
static String format_as_string(const ConfigurationInfo &p_config_info, bool p_wrap_lines, bool p_prefix_property_name);
static String format_list_as_string(const Vector<ConfigurationInfo> &p_config_infos, bool p_wrap_lines, bool p_prefix_property_name);
};

#endif // EDITOR_CONFIGURATION_INFO_H
145 changes: 144 additions & 1 deletion editor/editor_inspector.cpp
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@
#include "core/os/keyboard.h"
#include "editor/add_metadata_dialog.h"
#include "editor/doc_tools.h"
#include "editor/editor_configuration_info.h"
#include "editor/editor_feature_profile.h"
#include "editor/editor_main_screen.h"
#include "editor/editor_node.h"
@@ -226,6 +227,13 @@ Size2 EditorProperty::get_minimum_size() const {
ms.width += key->get_width() + get_theme_constant(SNAME("h_separation"), SNAME("Tree"));
}

if (is_config_info_visible()) {
Ref<Texture2D> config_info_icon = get_config_info_icon();

const int icon_width = config_info_icon->get_width() * 1.5f;
ms.width += icon_width + get_theme_constant(SNAME("h_separation"), SNAME("Tree"));
}

if (checkable) {
Ref<Texture2D> check = get_theme_icon(SNAME("checked"), SNAME("CheckBox"));
ms.width += check->get_width() + get_theme_constant(SNAME("h_separation"), SNAME("Tree"));
@@ -338,9 +346,25 @@ void EditorProperty::_notification(int p_what) {
}
}

bool show_config_info_icon = is_config_info_visible();
if (show_config_info_icon) {
Ref<Texture2D> config_info_icon = get_config_info_icon();
const int icon_width = config_info_icon->get_width() * 1.5f;

rect.size.x -= icon_width + get_theme_constant(SNAME("hseparator"), SNAME("Tree"));

if (is_layout_rtl()) {
rect.position.x += icon_width + get_theme_constant(SNAME("hseparator"), SNAME("Tree"));
}

if (no_children) {
text_size -= icon_width + 4 * EDSCALE;
}
}

// Account for the space needed on the outer side
// when any of the icons are visible.
if (keying || deletable) {
if (keying || deletable || show_config_info_icon) {
int separation = get_theme_constant(SNAME("h_separation"), SNAME("Tree"));
rect.size.x -= separation;

@@ -535,6 +559,28 @@ void EditorProperty::_notification(int p_what) {
} else {
delete_rect = Rect2();
}

if (is_config_info_visible()) {
Ref<Texture2D> config_info_icon = get_config_info_icon();
const int icon_width = config_info_icon->get_width() * 1.5f;

ofs -= icon_width + get_theme_constant(SNAME("hseparator"), SNAME("Tree"));

Color color2(1, 1, 1);
if (config_info_hover) {
color2.r *= 1.2;
color2.g *= 1.2;
color2.b *= 1.2;
}
config_info_rect = Rect2(ofs, ((size.height - config_info_icon->get_height()) / 2), icon_width, config_info_icon->get_height());
if (rtl) {
draw_texture(config_info_icon, Vector2(size.width - config_info_rect.position.x - icon_width, config_info_rect.position.y), color2);
} else {
draw_texture(config_info_icon, config_info_rect.position, color2);
}
} else {
config_info_rect = Rect2();
}
} break;
case NOTIFICATION_ENTER_TREE: {
if (has_borders) {
@@ -896,6 +942,12 @@ void EditorProperty::gui_input(const Ref<InputEvent> &p_event) {
check_hover = new_check_hover;
queue_redraw();
}

bool new_config_info_hover = config_info_rect.has_point(mpos) && !button_left;
if (new_config_info_hover != config_info_hover) {
config_info_hover = new_config_info_hover;
queue_redraw();
}
}

Ref<InputEventMouseButton> mb = p_event;
@@ -952,6 +1004,16 @@ void EditorProperty::gui_input(const Ref<InputEvent> &p_event) {
queue_redraw();
emit_signal(SNAME("property_checked"), property, checked);
}

if (config_info_rect.has_point(mpos)) {
if (config_info_dialog == nullptr) {
config_info_dialog = memnew(AcceptDialog);
add_child(config_info_dialog);
config_info_dialog->set_title(TTR("Configuration Info"));
}
config_info_dialog->set_text(EditorConfigurationInfo::format_list_as_string(config_info, true, false));
config_info_dialog->popup_centered();
}
} else if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
accept_event();
_update_popup();
@@ -1073,6 +1135,29 @@ bool EditorProperty::is_selectable() const {
return selectable;
}

void EditorProperty::set_config_info(const Vector<ConfigurationInfo> &p_config_info) {
config_info = p_config_info;
queue_redraw();
queue_sort();
}

Vector<ConfigurationInfo> EditorProperty::get_config_info() const {
return config_info;
}

Ref<Texture2D> EditorProperty::get_config_info_icon() const {
Ref<Texture2D> config_info_icon;

ConfigurationInfo::Severity severity = EditorConfigurationInfo::get_max_severity(config_info);
config_info_icon = get_editor_theme_icon(EditorConfigurationInfo::get_severity_icon(severity));

return config_info_icon;
}

bool EditorProperty::is_config_info_visible() const {
return !config_info.is_empty() && !read_only;
}

void EditorProperty::set_name_split_ratio(float p_ratio) {
split_ratio = p_ratio;
}
@@ -1148,6 +1233,12 @@ void EditorProperty::_update_flags() {
}
}

void EditorProperty::_update_config_info() {
Vector<ConfigurationInfo> new_config_info = EditorConfigurationInfo::get_configuration_info(object);
new_config_info = EditorConfigurationInfo::filter_list_for_property(new_config_info, property_path);
set_config_info(new_config_info);
}

Control *EditorProperty::make_custom_tooltip(const String &p_text) const {
String symbol;
String prologue;
@@ -3735,6 +3826,7 @@ void EditorInspector::update_tree() {
ep->set_checked(checked);
ep->set_keying(keying);
ep->set_read_only(property_read_only || all_read_only);
ep->_update_config_info();
}

if (ep && ep->is_favoritable() && current_favorites.has(p.name)) {
@@ -3959,6 +4051,9 @@ void EditorInspector::edit(Object *p_object) {

object = p_object;

configuration_info_cache.clear();
_update_configuration_info();

if (object) {
update_scroll_request = 0; //reset
if (scroll_cache.has(object->get_instance_id())) { //if exists, set something else
@@ -4639,6 +4734,51 @@ void EditorInspector::_clear_current_favorites() {
update_tree();
}

void EditorInspector::_configuration_info_changed(Object *p_object) {
if (object == p_object) {
// Only update the tree if the list of configuration info has changed.
if (_update_configuration_info()) {
update_tree_pending = true;
}
}

for (int i = 0; i < get_child_count(); i++) {
Array args;
args.append(p_object);

Node *child = get_child(i);
child->propagate_call(StringName("_configuration_info_changed"), args);
}
}

bool EditorInspector::_update_configuration_info() {
bool changed = false;
LocalVector<int> found_indices;
const Vector<ConfigurationInfo> config_info_list = EditorConfigurationInfo::get_configuration_info(object);

// New and changed entries.
for (const ConfigurationInfo &config_info : config_info_list) {
int found_index = configuration_info_cache.find(config_info);
if (found_index < 0) {
found_index = configuration_info_cache.size();
configuration_info_cache.push_back(config_info);
changed = true;
}
found_indices.push_back(found_index);
}

// Removed entries.
for (uint32_t i = 0; i < configuration_info_cache.size(); i++) {
if (!found_indices.has(i)) {
configuration_info_cache.remove_at(i);
i--;
changed = true;
}
}

return changed;
}

void EditorInspector::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
@@ -4659,12 +4799,14 @@ void EditorInspector::_notification(int p_what) {
add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree")));
if (!is_sub_inspector()) {
get_tree()->connect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
EditorNode::get_singleton()->connect(EditorStringName(configuration_info_changed), callable_mp(this, &EditorInspector::_configuration_info_changed));
}
} break;

case NOTIFICATION_PREDELETE: {
if (!is_sub_inspector() && is_inside_tree()) {
get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
EditorNode::get_singleton()->disconnect(EditorStringName(configuration_info_changed), callable_mp(this, &EditorInspector::_configuration_info_changed));
}
edit(nullptr);
} break;
@@ -4849,6 +4991,7 @@ void EditorInspector::_handle_menu_option(int p_option) {
void EditorInspector::_bind_methods() {
ClassDB::bind_method(D_METHOD("edit", "object"), &EditorInspector::edit);
ClassDB::bind_method("_edit_request_change", &EditorInspector::_edit_request_change);
ClassDB::bind_method("_configuration_info_changed", &EditorInspector::_configuration_info_changed);
ClassDB::bind_method("get_selected_path", &EditorInspector::get_selected_path);
ClassDB::bind_method("get_edited_object", &EditorInspector::get_edited_object);

15 changes: 15 additions & 0 deletions editor/editor_inspector.h
Original file line number Diff line number Diff line change
@@ -86,6 +86,7 @@ class EditorProperty : public Container {
String doc_path;
bool internal = false;
bool has_doc_tooltip = false;
AcceptDialog *config_info_dialog = nullptr;

int property_usage;

@@ -110,6 +111,8 @@ class EditorProperty : public Container {
bool check_hover = false;
Rect2 delete_rect;
bool delete_hover = false;
Rect2 config_info_rect;
bool config_info_hover = false;

bool can_revert = false;
bool can_pin = false;
@@ -135,13 +138,15 @@ class EditorProperty : public Container {
Control *label_reference = nullptr;
Control *bottom_editor = nullptr;
PopupMenu *menu = nullptr;
Vector<ConfigurationInfo> config_info;

HashMap<StringName, Variant> cache;

GDVIRTUAL0(_update_property)
GDVIRTUAL1(_set_read_only, bool)

void _update_flags();
void _update_config_info();

protected:
bool has_borders = false;
@@ -210,6 +215,13 @@ class EditorProperty : public Container {

void set_deletable(bool p_enable);
bool is_deletable() const;

void set_config_info(const Vector<ConfigurationInfo> &p_config_info);
Vector<ConfigurationInfo> get_config_info() const;

Ref<Texture2D> get_config_info_icon() const;
bool is_config_info_visible() const;

void add_focusable(Control *p_control);
void grab_focus(int p_focusable = -1);
void select(int p_focusable = -1);
@@ -574,6 +586,7 @@ class EditorInspector : public ScrollContainer {
};

HashMap<StringName, HashMap<StringName, DocCacheInfo>> doc_cache;
Vector<ConfigurationInfo> configuration_info_cache;
HashSet<StringName> restart_request_props;
HashMap<String, String> custom_property_descriptions;

@@ -606,6 +619,8 @@ class EditorInspector : public ScrollContainer {
void _clear_current_favorites();

void _node_removed(Node *p_node);
void _configuration_info_changed(Object *p_object);
bool _update_configuration_info();

HashMap<StringName, int> per_array_page;
void _page_change_request(int p_new_page, const StringName &p_array_prefix);
6 changes: 6 additions & 0 deletions editor/editor_node.cpp
Original file line number Diff line number Diff line change
@@ -6702,12 +6702,17 @@ void EditorNode::_bind_methods() {
ADD_SIGNAL(MethodInfo("scene_saved", PropertyInfo(Variant::STRING, "path")));
ADD_SIGNAL(MethodInfo("scene_changed"));
ADD_SIGNAL(MethodInfo("scene_closed", PropertyInfo(Variant::STRING, "path")));
ADD_SIGNAL(MethodInfo("configuration_info_changed", PropertyInfo(Variant::OBJECT, "object")));
}

static Node *_resource_get_edited_scene() {
return EditorNode::get_singleton()->get_edited_scene();
}

static void _configuration_info_changed(Object *p_object) {
EditorNode::get_singleton()->emit_signal(EditorStringName(configuration_info_changed), p_object);
}

void EditorNode::_print_handler(void *p_this, const String &p_string, bool p_error, bool p_rich) {
callable_mp_static(&EditorNode::_print_handler_impl).call_deferred(p_string, p_error, p_rich);
}
@@ -6799,6 +6804,7 @@ EditorNode::EditorNode() {
set_translation_domain("godot.editor");

Resource::_get_local_scene_func = _resource_get_edited_scene;
ConfigurationInfo::configuration_info_changed_func = _configuration_info_changed;

{
PortableCompressedTexture2D::set_keep_all_compressed_buffers(true);
1 change: 1 addition & 0 deletions editor/editor_string_names.h
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ class EditorStringNames {

_FORCE_INLINE_ static EditorStringNames *get_singleton() { return singleton; }

const StringName configuration_info_changed = StaticCString::create("configuration_info_changed");
const StringName Editor = StaticCString::create("Editor");
const StringName EditorFonts = StaticCString::create("EditorFonts");
const StringName EditorIcons = StaticCString::create("EditorIcons");
93 changes: 20 additions & 73 deletions editor/gui/scene_tree_editor.cpp
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@

#include "core/config/project_settings.h"
#include "core/object/script_language.h"
#include "editor/editor_configuration_info.h"
#include "editor/editor_dock_manager.h"
#include "editor/editor_file_system.h"
#include "editor/editor_node.h"
@@ -56,27 +57,6 @@ Node *SceneTreeEditor::get_scene_node() const {
return get_tree()->get_edited_scene_root();
}

PackedStringArray SceneTreeEditor::_get_node_configuration_warnings(Node *p_node) {
PackedStringArray warnings = p_node->get_configuration_warnings();
if (p_node == get_scene_node()) {
Node2D *node_2d = Object::cast_to<Node2D>(p_node);
if (node_2d) {
// Note: Warn for Node2D but not all CanvasItems, don't warn for Control nodes.
// Control nodes may have reasons to use a transformed root node like anchors.
if (!node_2d->get_transform().is_equal_approx(Transform2D())) {
warnings.append(TTR("The root node of a scene is recommended to not be transformed, since instances of the scene will usually override this. Reset the transform and reload the scene to remove this warning."));
}
}
Node3D *node_3d = Object::cast_to<Node3D>(p_node);
if (node_3d) {
if (!node_3d->get_transform().is_equal_approx(Transform3D())) {
warnings.append(TTR("The root node of a scene is recommended to not be transformed, since instances of the scene will usually override this. Reset the transform and reload the scene to remove this warning."));
}
}
}
return warnings;
}

void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) {
if (p_button != MouseButton::LEFT) {
return;
@@ -151,33 +131,12 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
}
undo_redo->commit_action();
} else if (p_id == BUTTON_WARNING) {
const PackedStringArray warnings = _get_node_configuration_warnings(n);

if (warnings.is_empty()) {
const Vector<ConfigurationInfo> config_infos = EditorConfigurationInfo::get_configuration_info(n);
if (config_infos.is_empty()) {
return;
}

// Improve looks on tooltip, extra spacing on non-bullet point newlines.
const String bullet_point = U"";
String all_warnings;
for (const String &w : warnings) {
all_warnings += "\n" + bullet_point + w;
}

// Limit the line width while keeping some padding.
// It is not efficient, but it does not have to be.
const PackedInt32Array boundaries = TS->string_get_word_breaks(all_warnings, "", 80);
PackedStringArray lines;
for (int i = 0; i < boundaries.size(); i += 2) {
const int start = boundaries[i];
const int end = boundaries[i + 1];
const String line = all_warnings.substr(start, end - start);
lines.append(line);
}
// We don't want the first two newlines.
all_warnings = String("\n").join(lines).indent(" ").replace(U"", U"\n").substr(2);

warning->set_text(all_warnings);
warning->set_text(EditorConfigurationInfo::format_list_as_string(config_infos, true, true));
warning->popup_centered();

} else if (p_id == BUTTON_SIGNALS) {
@@ -473,29 +432,14 @@ void SceneTreeEditor::_update_node(Node *p_node, TreeItem *p_item, bool p_part_o
}

if (can_rename) { // TODO Should be can edit..
const PackedStringArray warnings = _get_node_configuration_warnings(p_node);
const int num_warnings = warnings.size();
if (num_warnings > 0) {
StringName warning_icon;
if (num_warnings == 1) {
warning_icon = SNAME("NodeWarning");
} else if (num_warnings <= 3) {
warning_icon = vformat("NodeWarnings%d", num_warnings);
} else {
warning_icon = SNAME("NodeWarnings4Plus");
}

// Improve looks on tooltip, extra spacing on non-bullet point newlines.
const String bullet_point = U"";
String all_warnings;
for (const String &w : warnings) {
all_warnings += "\n\n" + bullet_point + w.replace("\n", "\n ");
}
if (num_warnings == 1) {
all_warnings.remove_at(0); // With only one warning, two newlines do not look great.
}
const Vector<ConfigurationInfo> config_infos = EditorConfigurationInfo::get_configuration_info(p_node);
if (!config_infos.is_empty()) {
ConfigurationInfo::Severity max_severity = EditorConfigurationInfo::get_max_severity(config_infos);
const StringName config_info_icon = EditorConfigurationInfo::get_severity_icon(max_severity);

p_item->add_button(0, get_editor_theme_icon(warning_icon), BUTTON_WARNING, false, TTR("Node configuration warning:") + all_warnings);
const String config_info_text = EditorConfigurationInfo::format_list_as_string(config_infos, false, true);
p_item->add_button(0, get_editor_theme_icon(config_info_icon), BUTTON_WARNING, false, TTR("Node configuration info:") + "\n\n" + config_info_text);
}

if (p_node->is_unique_name_in_owner()) {
@@ -1295,7 +1239,7 @@ void SceneTreeEditor::_notification(int p_what) {
get_tree()->connect("node_added", callable_mp(this, &SceneTreeEditor::_node_added));
get_tree()->connect("node_removed", callable_mp(this, &SceneTreeEditor::_node_removed));
get_tree()->connect("node_renamed", callable_mp(this, &SceneTreeEditor::_node_renamed));
get_tree()->connect(SceneStringName(node_configuration_warning_changed), callable_mp(this, &SceneTreeEditor::_warning_changed));
EditorNode::get_singleton()->connect(EditorStringName(configuration_info_changed), callable_mp(this, &SceneTreeEditor::_config_info_changed));

tree->connect("item_collapsed", callable_mp(this, &SceneTreeEditor::_cell_collapsed));

@@ -1309,7 +1253,7 @@ void SceneTreeEditor::_notification(int p_what) {
get_tree()->disconnect("node_removed", callable_mp(this, &SceneTreeEditor::_node_removed));
get_tree()->disconnect("node_renamed", callable_mp(this, &SceneTreeEditor::_node_renamed));
tree->disconnect("item_collapsed", callable_mp(this, &SceneTreeEditor::_cell_collapsed));
get_tree()->disconnect(SceneStringName(node_configuration_warning_changed), callable_mp(this, &SceneTreeEditor::_warning_changed));
EditorNode::get_singleton()->disconnect(EditorStringName(configuration_info_changed), callable_mp(this, &SceneTreeEditor::_config_info_changed));
} break;

case NOTIFICATION_THEME_CHANGED: {
@@ -1991,12 +1935,15 @@ void SceneTreeEditor::_rmb_select(const Vector2 &p_pos, MouseButton p_button) {
emit_signal(SNAME("rmb_pressed"), tree->get_screen_position() + p_pos);
}

void SceneTreeEditor::update_warning() {
_warning_changed(nullptr);
void SceneTreeEditor::update_config_info() {
_config_info_changed(nullptr);
}

void SceneTreeEditor::_warning_changed(Node *p_for_node) {
node_cache.mark_dirty(p_for_node);
void SceneTreeEditor::_config_info_changed(Object *p_object) {
Node *node = Object::cast_to<Node>(p_object);
if (node) {
node_cache.mark_dirty(node);
}

// Should use a timer.
update_timer->start();
@@ -2107,7 +2054,7 @@ SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_ope

warning = memnew(AcceptDialog);
add_child(warning);
warning->set_title(TTR("Node Configuration Warning!"));
warning->set_title(TTR("Configuration Info"));
warning->set_flag(Window::FLAG_POPUP, true);

last_hash = 0;
5 changes: 2 additions & 3 deletions editor/gui/scene_tree_editor.h
Original file line number Diff line number Diff line change
@@ -135,7 +135,6 @@ class SceneTreeEditor : public Control {

void _compute_hash(Node *p_node, uint64_t &hash);
void _reset();
PackedStringArray _get_node_configuration_warnings(Node *p_node);

void _update_node_path(Node *p_node, bool p_recursive = true);
void _update_node_subtree(Node *p_node, TreeItem *p_parent, bool p_force = false);
@@ -207,7 +206,7 @@ class SceneTreeEditor : public Control {
void _empty_clicked(const Vector2 &p_pos, MouseButton p_button);
void _rmb_select(const Vector2 &p_pos, MouseButton p_button = MouseButton::RIGHT);

void _warning_changed(Node *p_for_node);
void _config_info_changed(Object *p_object);
void _update_marking_list(const HashSet<Node *> &p_marked);

Timer *update_timer = nullptr;
@@ -254,7 +253,7 @@ class SceneTreeEditor : public Control {

Tree *get_scene_tree() { return tree; }

void update_warning();
void update_config_info();

SceneTreeEditor(bool p_label = true, bool p_can_rename = false, bool p_can_open_instance = false);
~SceneTreeEditor();
1 change: 0 additions & 1 deletion editor/icons/NodeWarnings2.svg

This file was deleted.

1 change: 0 additions & 1 deletion editor/icons/NodeWarnings3.svg

This file was deleted.

1 change: 0 additions & 1 deletion editor/icons/NodeWarnings4Plus.svg

This file was deleted.

263 changes: 263 additions & 0 deletions editor/plugins/configuration_info_editor_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/**************************************************************************/
/* configuration_info_editor_plugin.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "configuration_info_editor_plugin.h"

#include "editor/editor_configuration_info.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "scene/gui/grid_container.h"
#include "scene/gui/label.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/texture_rect.h"

// Inspector controls.

String ConfigurationInfoList::_get_summary_text(const Vector<ConfigurationInfo> &p_config_infos) const {
int errors = 0;
int warnings = 0;
int infos = 0;

for (const ConfigurationInfo &config_info : p_config_infos) {
switch (config_info.get_severity()) {
case ConfigurationInfo::Severity::ERROR: {
errors++;
} break;
case ConfigurationInfo::Severity::WARNING: {
warnings++;
} break;
case ConfigurationInfo::Severity::INFO: {
infos++;
} break;
default:
break;
}
}

PackedStringArray summary_parts;

if (errors > 0) {
summary_parts.append(vformat(TTRN("%d configuration error", "%d configuration errors", errors), errors));
}

if (warnings > 0) {
if (summary_parts.is_empty()) {
summary_parts.append(vformat(TTRN("%d configuration warning", "%d configuration warnings", warnings), warnings));
} else {
summary_parts.append(vformat(TTRN("%d warning", "%d warnings", warnings), warnings));
}
}

if (infos > 0) {
if (summary_parts.is_empty()) {
summary_parts.append(vformat(TTRN("%d configuration info", "%d configuration infos", infos), infos));
} else {
summary_parts.append(vformat(TTRN("%d info", "%d infos", infos), infos));
}
}

return String(", ").join(summary_parts);
}

void ConfigurationInfoList::_update_content() {
if (!object) {
hide();
return;
}

const Vector<ConfigurationInfo> config_infos = EditorConfigurationInfo::get_configuration_info(object);
if (config_infos.is_empty()) {
hide();
return;
}

title_label->set_text(_get_summary_text(config_infos));

const Color &warning_color = get_theme_color("warning_color", EditorStringName(Editor));
const Color &error_color = get_theme_color("error_color", EditorStringName(Editor));

ConfigurationInfo::Severity max_severity = EditorConfigurationInfo::get_max_severity(config_infos);
if (max_severity == ConfigurationInfo::Severity::WARNING) {
title_label->add_theme_color_override(SceneStringName(font_color), warning_color);
} else if (max_severity == ConfigurationInfo::Severity::ERROR) {
title_label->add_theme_color_override(SceneStringName(font_color), error_color);
} else {
title_label->remove_theme_color_override(SceneStringName(font_color));
}

config_info_text->clear();
for (const ConfigurationInfo &config_info : config_infos) {
const String text = EditorConfigurationInfo::format_as_string(config_info, false, true);
ConfigurationInfo::Severity severity = config_info.get_severity();
const StringName icon = EditorConfigurationInfo::get_severity_icon(severity);

config_info_text->push_context();
config_info_text->push_paragraph(HORIZONTAL_ALIGNMENT_LEFT);

if (!icon.is_empty()) {
Ref<Texture2D> image = get_editor_theme_icon(icon);
config_info_text->add_image(image);
}

if (severity == ConfigurationInfo::Severity::WARNING) {
config_info_text->push_color(warning_color);
} else if (severity == ConfigurationInfo::Severity::ERROR) {
config_info_text->push_color(error_color);
}

config_info_text->add_text(" ");
config_info_text->add_text(text);

config_info_text->pop_context();
}

show();
}

void ConfigurationInfoList::_update_toggler() {
Ref<Texture2D> arrow;
if (config_info_text->is_visible()) {
arrow = get_theme_icon("arrow", "Tree");
set_tooltip_text(TTR("Collapse configuration info."));
} else {
if (is_layout_rtl()) {
arrow = get_theme_icon("arrow_collapsed_mirrored", "Tree");
} else {
arrow = get_theme_icon("arrow_collapsed", "Tree");
}
set_tooltip_text(TTR("Expand configuration info."));
}

expand_icon->set_texture(arrow);
}

void ConfigurationInfoList::_update_background(bool p_hovering) {
if (p_hovering) {
if (bg_style_hover.is_valid()) {
bg_panel->add_theme_style_override(SceneStringName(panel), bg_style_hover);
}
} else {
if (bg_style.is_valid()) {
bg_panel->add_theme_style_override(SceneStringName(panel), bg_style);
}
}
}

void ConfigurationInfoList::set_object(Object *p_object) {
object = p_object;
_update_content();
}

void ConfigurationInfoList::gui_input(const Ref<InputEvent> &p_event) {
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
bool state = !config_info_text->is_visible();

config_info_text->set_visible(state);
list_filler_right->set_visible(state);
EditorSettings::get_singleton()->set_project_metadata("editor_metadata", "configuration_info_expanded_in_inspector", state);

_update_toggler();
}
}

void ConfigurationInfoList::_notification(int p_notification) {
switch (p_notification) {
case NOTIFICATION_MOUSE_ENTER: {
_update_background(true);
} break;

case NOTIFICATION_MOUSE_EXIT: {
_update_background(false);
} break;

case NOTIFICATION_THEME_CHANGED: {
bg_style = get_theme_stylebox(CoreStringName(normal), "Button");
bg_style_hover = get_theme_stylebox("hover", "Button");

_update_background(false);
_update_content();
_update_toggler();
} break;
}
}

ConfigurationInfoList::ConfigurationInfoList() {
set_mouse_filter(MOUSE_FILTER_STOP);
hide();

bg_panel = memnew(PanelContainer);
bg_panel->set_mouse_filter(MOUSE_FILTER_IGNORE);
add_child(bg_panel);

grid = memnew(GridContainer);
grid->set_columns(2);
bg_panel->add_child(grid);

expand_icon = memnew(TextureRect);
expand_icon->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED);
grid->add_child(expand_icon);

title_label = memnew(Label);
title_label->set_autowrap_mode(TextServer::AutowrapMode::AUTOWRAP_WORD);
title_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
title_label->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
grid->add_child(title_label);

list_filler_right = memnew(Control);
grid->add_child(list_filler_right);

config_info_text = memnew(RichTextLabel);
config_info_text->add_theme_constant_override(SceneStringName(line_separation), 5);
config_info_text->set_fit_content(true);
config_info_text->set_selection_enabled(true);
bool last_visible = EditorSettings::get_singleton()->get_project_metadata("editor_metadata", "configuration_info_expanded_in_inspector", false);
config_info_text->set_visible(last_visible);
grid->add_child(config_info_text);
}

bool EditorInspectorPluginConfigurationInfo::can_handle(Object *p_object) {
return Object::cast_to<Node>(p_object) != nullptr || Object::cast_to<Resource>(p_object) != nullptr;
}

void EditorInspectorPluginConfigurationInfo::parse_begin(Object *p_object) {
ConfigurationInfoList *config_info_list = memnew(ConfigurationInfoList);
config_info_list->set_object(p_object);
add_custom_control(config_info_list);
}

// Editor plugin.

ConfigurationInfoEditorPlugin::ConfigurationInfoEditorPlugin() {
Ref<EditorInspectorPluginConfigurationInfo> plugin;
plugin.instantiate();
add_inspector_plugin(plugin);
}
92 changes: 92 additions & 0 deletions editor/plugins/configuration_info_editor_plugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**************************************************************************/
/* configuration_info_editor_plugin.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef CONFIGURATION_INFO_EDITOR_PLUGIN_H
#define CONFIGURATION_INFO_EDITOR_PLUGIN_H

#include "editor/plugins/editor_plugin.h"

#include "editor/editor_inspector.h"
#include "editor/plugins/editor_plugin.h"
#include "scene/gui/margin_container.h"

class GridContainer;
class Label;
class PanelContainer;
class RichTextLabel;
class TextureRect;

// Inspector controls.
class ConfigurationInfoList : public MarginContainer {
GDCLASS(ConfigurationInfoList, MarginContainer);

Object *object = nullptr;
Ref<StyleBox> bg_style;
Ref<StyleBox> bg_style_hover;

PanelContainer *bg_panel = nullptr;
GridContainer *grid = nullptr;
TextureRect *expand_icon = nullptr;
Label *title_label = nullptr;
RichTextLabel *config_info_text = nullptr;
Control *list_filler_right = nullptr;

String _get_summary_text(const Vector<ConfigurationInfo> &p_config_infos) const;
void _update_background(bool p_hovering);
void _update_content();
void _update_toggler();
virtual void gui_input(const Ref<InputEvent> &p_event) override;

protected:
void _notification(int p_notification);

public:
void set_object(Object *p_object);

ConfigurationInfoList();
};

class EditorInspectorPluginConfigurationInfo : public EditorInspectorPlugin {
GDCLASS(EditorInspectorPluginConfigurationInfo, EditorInspectorPlugin);

public:
virtual bool can_handle(Object *p_object) override;
virtual void parse_begin(Object *p_object) override;
};

// Editor plugin.
class ConfigurationInfoEditorPlugin : public EditorPlugin {
GDCLASS(ConfigurationInfoEditorPlugin, EditorPlugin);

public:
ConfigurationInfoEditorPlugin();
};

#endif // CONFIGURATION_INFO_EDITOR_PLUGIN_H
2 changes: 2 additions & 0 deletions editor/register_editor_types.cpp
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@
#include "editor/plugins/cast_2d_editor_plugin.h"
#include "editor/plugins/collision_polygon_2d_editor_plugin.h"
#include "editor/plugins/collision_shape_2d_editor_plugin.h"
#include "editor/plugins/configuration_info_editor_plugin.h"
#include "editor/plugins/control_editor_plugin.h"
#include "editor/plugins/curve_editor_plugin.h"
#include "editor/plugins/editor_context_menu_plugin.h"
@@ -211,6 +212,7 @@ void register_editor_types() {
EditorPlugins::add_by_type<BitMapEditorPlugin>();
EditorPlugins::add_by_type<BoneMapEditorPlugin>();
EditorPlugins::add_by_type<Camera3DEditorPlugin>();
EditorPlugins::add_by_type<ConfigurationInfoEditorPlugin>();
EditorPlugins::add_by_type<ControlEditorPlugin>();
EditorPlugins::add_by_type<CPUParticles3DEditorPlugin>();
EditorPlugins::add_by_type<CurveEditorPlugin>();
6 changes: 2 additions & 4 deletions editor/scene_tree_dock.cpp
Original file line number Diff line number Diff line change
@@ -4603,9 +4603,9 @@ void SceneTreeDock::_bind_methods() {

SceneTreeDock *SceneTreeDock::singleton = nullptr;

void SceneTreeDock::_update_configuration_warning() {
void SceneTreeDock::_update_configuration_info() {
if (singleton) {
callable_mp(singleton->scene_tree, &SceneTreeEditor::update_warning).call_deferred();
callable_mp(singleton->scene_tree, &SceneTreeEditor::update_config_info).call_deferred();
}
}

@@ -4853,8 +4853,6 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec
set_process(true);

EDITOR_DEF("_use_favorites_root_selection", false);

Resource::_update_configuration_warning = _update_configuration_warning;
}

SceneTreeDock::~SceneTreeDock() {
2 changes: 1 addition & 1 deletion editor/scene_tree_dock.h
Original file line number Diff line number Diff line change
@@ -291,7 +291,7 @@ class SceneTreeDock : public VBoxContainer {
bool profile_allow_editing = true;
bool profile_allow_script_editing = true;

static void _update_configuration_warning();
static void _update_configuration_info();

bool _update_node_path(Node *p_root_node, NodePath &r_node_path, HashMap<Node *, NodePath> *p_renames) const;
void _check_object_properties_recursive(Node *p_root_node, Object *p_obj, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource = false) const;
12 changes: 7 additions & 5 deletions modules/csg/csg_shape.cpp
Original file line number Diff line number Diff line change
@@ -482,7 +482,7 @@ CSGBrush *CSGShape3D::_get_brush() {
node_aabb = aabb;
brush = n;
dirty = false;
update_configuration_warnings();
update_configuration_info();
return brush;
}

@@ -938,18 +938,20 @@ Array CSGShape3D::get_meshes() const {
return Array();
}

PackedStringArray CSGShape3D::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> CSGShape3D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node::get_configuration_info();
const CSGShape3D *current_shape = this;
while (current_shape) {
if (!current_shape->brush || current_shape->brush->faces.is_empty()) {
warnings.push_back(RTR("The CSGShape3D has an empty shape.\nCSGShape3D empty shapes typically occur because the mesh is not manifold.\nA manifold mesh forms a solid object without gaps, holes, or loose edges.\nEach edge must be a member of exactly two faces."));
CONFIG_WARNING(RTR("The CSGShape3D has an empty shape.\nCSGShape3D empty shapes typically occur because the mesh is not manifold.\nA manifold mesh forms a solid object without gaps, holes, or loose edges.\nEach edge must be a member of exactly two faces."));
break;
}
current_shape = current_shape->parent_shape;
}
return warnings;
return infos;
}
#endif

Ref<TriangleMesh> CSGShape3D::generate_triangle_mesh() const {
if (root_mesh.is_valid()) {
4 changes: 3 additions & 1 deletion modules/csg/csg_shape.h
Original file line number Diff line number Diff line change
@@ -122,7 +122,9 @@ class CSGShape3D : public GeometryInstance3D {
void _notification(int p_what);
virtual CSGBrush *_build_brush() = 0;
void _make_dirty(bool p_parent_removing = false);
PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

static void _bind_methods();

14 changes: 9 additions & 5 deletions modules/multiplayer/multiplayer_spawner.cpp
Original file line number Diff line number Diff line change
@@ -84,14 +84,18 @@ void MultiplayerSpawner::_get_property_list(List<PropertyInfo> *p_list) const {
}
#endif

PackedStringArray MultiplayerSpawner::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> MultiplayerSpawner::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node::get_configuration_info();

if (spawn_path.is_empty() || !has_node(spawn_path)) {
warnings.push_back(RTR("A valid NodePath must be set in the \"Spawn Path\" property in order for MultiplayerSpawner to be able to spawn Nodes."));
CONFIG_WARNING_P(
RTR("A valid NodePath must be set in order for MultiplayerSpawner to be able to spawn Nodes."),
"spawn_path");
}
return warnings;
return infos;
}
#endif

void MultiplayerSpawner::add_spawnable_scene(const String &p_path) {
SpawnableScene sc;
@@ -248,7 +252,7 @@ NodePath MultiplayerSpawner::get_spawn_path() const {
void MultiplayerSpawner::set_spawn_path(const NodePath &p_path) {
spawn_path = p_path;
_update_spawn_node();
update_configuration_warnings();
update_configuration_info();
}

void MultiplayerSpawner::_track(Node *p_node, const Variant &p_argument, int p_scene_id) {
4 changes: 3 additions & 1 deletion modules/multiplayer/multiplayer_spawner.h
Original file line number Diff line number Diff line change
@@ -88,7 +88,9 @@ class MultiplayerSpawner : public Node {
void _get_property_list(List<PropertyInfo> *p_list) const;
#endif
public:
PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

Node *get_spawn_node() const {
return spawn_node.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)) : nullptr;
14 changes: 9 additions & 5 deletions modules/multiplayer/multiplayer_synchronizer.cpp
Original file line number Diff line number Diff line change
@@ -143,15 +143,19 @@ bool MultiplayerSynchronizer::update_inbound_sync_time(uint16_t p_network_time)
return true;
}

PackedStringArray MultiplayerSynchronizer::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> MultiplayerSynchronizer::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node::get_configuration_info();

if (root_path.is_empty() || !has_node(root_path)) {
warnings.push_back(RTR("A valid NodePath must be set in the \"Root Path\" property in order for MultiplayerSynchronizer to be able to synchronize properties."));
CONFIG_WARNING_P(
RTR("A valid NodePath must be set in order for MultiplayerSynchronizer to be able to synchronize properties."),
"root_path");
}

return warnings;
return infos;
}
#endif

Error MultiplayerSynchronizer::get_state(const List<NodePath> &p_properties, Object *p_obj, Vector<Variant> &r_variant, Vector<const Variant *> &r_variant_ptrs) {
ERR_FAIL_NULL_V(p_obj, ERR_INVALID_PARAMETER);
@@ -354,7 +358,7 @@ void MultiplayerSynchronizer::set_root_path(const NodePath &p_path) {
_stop();
root_path = p_path;
_start();
update_configuration_warnings();
update_configuration_info();
}

NodePath MultiplayerSynchronizer::get_root_path() const {
4 changes: 3 additions & 1 deletion modules/multiplayer/multiplayer_synchronizer.h
Original file line number Diff line number Diff line change
@@ -91,7 +91,9 @@ class MultiplayerSynchronizer : public Node {
bool update_outbound_sync_time(uint64_t p_usec);
bool update_inbound_sync_time(uint16_t p_network_time);

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void set_replication_interval(double p_interval);
double get_replication_interval() const;
22 changes: 12 additions & 10 deletions modules/openxr/scene/openxr_composition_layer.cpp
Original file line number Diff line number Diff line change
@@ -303,7 +303,7 @@ void OpenXRCompositionLayer::set_enable_hole_punch(bool p_enable) {
_remove_fallback_node();
}

update_configuration_warnings();
update_configuration_info();
}

bool OpenXRCompositionLayer::get_enable_hole_punch() const {
@@ -312,7 +312,7 @@ bool OpenXRCompositionLayer::get_enable_hole_punch() const {

void OpenXRCompositionLayer::set_sort_order(int p_order) {
openxr_layer_provider->set_sort_order(p_order);
update_configuration_warnings();
update_configuration_info();
}

int OpenXRCompositionLayer::get_sort_order() const {
@@ -408,10 +408,10 @@ void OpenXRCompositionLayer::_notification(int p_what) {
_clear_composition_layer_provider();
}
}
update_configuration_warnings();
update_configuration_info();
} break;
case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
update_configuration_warnings();
update_configuration_info();
} break;
case NOTIFICATION_ENTER_TREE: {
if (layer_viewport && is_viewport_in_use(layer_viewport)) {
@@ -475,23 +475,25 @@ void OpenXRCompositionLayer::_validate_property(PropertyInfo &p_property) const
}
}

PackedStringArray OpenXRCompositionLayer::get_configuration_warnings() const {
PackedStringArray warnings = Node3D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> OpenXRCompositionLayer::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node3D::get_configuration_info();

if (is_visible() && is_inside_tree()) {
XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent());
if (origin == nullptr) {
warnings.push_back(RTR("OpenXR composition layers must have an XROrigin3D node as their parent."));
CONFIG_WARNING(RTR("OpenXR composition layers must have an XROrigin3D node as their parent."));
}
}

if (!get_transform().basis.is_orthonormal()) {
warnings.push_back(RTR("OpenXR composition layers must have orthonormalized transforms (ie. no scale or shearing)."));
CONFIG_WARNING(RTR("OpenXR composition layers must have orthonormalized transforms (ie. no scale or shearing)."));
}

if (enable_hole_punch && get_sort_order() >= 0) {
warnings.push_back(RTR("Hole punching won't work as expected unless the sort order is less than zero."));
CONFIG_WARNING(RTR("Hole punching won't work as expected unless the sort order is less than zero."));
}

return warnings;
return infos;
}
#endif
4 changes: 3 additions & 1 deletion modules/openxr/scene/openxr_composition_layer.h
Original file line number Diff line number Diff line change
@@ -115,7 +115,9 @@ class OpenXRCompositionLayer : public Node3D {
Ref<JavaObject> get_android_surface();
bool is_natively_supported() const;

virtual PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
virtual Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

virtual Vector2 intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const;

10 changes: 6 additions & 4 deletions modules/openxr/scene/openxr_visibility_mask.cpp
Original file line number Diff line number Diff line change
@@ -64,18 +64,20 @@ void OpenXRVisibilityMask::_on_openxr_session_stopping() {
set_base(RID());
}

PackedStringArray OpenXRVisibilityMask::get_configuration_warnings() const {
PackedStringArray warnings = VisualInstance3D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> OpenXRVisibilityMask::get_configuration_info() const {
Vector<ConfigurationInfo> infos = VisualInstance3D::get_configuration_info();

if (is_visible() && is_inside_tree()) {
XRCamera3D *camera = Object::cast_to<XRCamera3D>(get_parent());
if (camera == nullptr) {
warnings.push_back(RTR("OpenXR visibility mask must have an XRCamera3D node as their parent."));
CONFIG_WARNING(RTR("OpenXR visibility mask must have an XRCamera3D node as their parent."));
}
}

return warnings;
return infos;
}
#endif

AABB OpenXRVisibilityMask::get_aabb() const {
AABB ret;
4 changes: 3 additions & 1 deletion modules/openxr/scene/openxr_visibility_mask.h
Original file line number Diff line number Diff line change
@@ -45,7 +45,9 @@ class OpenXRVisibilityMask : public VisualInstance3D {
void _on_openxr_session_stopping();

public:
virtual PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
virtual Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

virtual AABB get_aabb() const override;

14 changes: 9 additions & 5 deletions scene/2d/animated_sprite_2d.cpp
Original file line number Diff line number Diff line change
@@ -316,7 +316,7 @@ void AnimatedSprite2D::set_sprite_frames(const Ref<SpriteFrames> &p_frames) {

notify_property_list_changed();
queue_redraw();
update_configuration_warnings();
update_configuration_info();
emit_signal("sprite_frames_changed");
}

@@ -573,13 +573,17 @@ StringName AnimatedSprite2D::get_animation() const {
return animation;
}

PackedStringArray AnimatedSprite2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> AnimatedSprite2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();
if (frames.is_null()) {
warnings.push_back(RTR("A SpriteFrames resource must be created or set in the \"Sprite Frames\" property in order for AnimatedSprite2D to display frames."));
CONFIG_WARNING_P(
RTR("A SpriteFrames resource must be created or set in order for AnimatedSprite2D to display frames."),
"sprite_frames");
}
return warnings;
return infos;
}
#endif

#ifdef TOOLS_ENABLED
void AnimatedSprite2D::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const {
4 changes: 3 additions & 1 deletion scene/2d/animated_sprite_2d.h
Original file line number Diff line number Diff line change
@@ -128,7 +128,9 @@ class AnimatedSprite2D : public Node2D {
void set_flip_v(bool p_flip);
bool is_flipped_v() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

#ifdef TOOLS_ENABLED
virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;
12 changes: 7 additions & 5 deletions scene/2d/canvas_modulate.cpp
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ void CanvasModulate::_on_in_canvas_visibility_changed(bool p_new_visibility) {
}
}

update_configuration_warnings();
update_configuration_info();
}

void CanvasModulate::_notification(int p_what) {
@@ -113,20 +113,22 @@ Color CanvasModulate::get_color() const {
return color;
}

PackedStringArray CanvasModulate::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> CanvasModulate::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (is_in_canvas && is_visible_in_tree()) {
List<Node *> nodes;
get_tree()->get_nodes_in_group("_canvas_modulate_" + itos(get_canvas().get_id()), &nodes);

if (nodes.size() > 1) {
warnings.push_back(RTR("Only one visible CanvasModulate is allowed per canvas.\nWhen there are more than one, only one of them will be active. Which one is undefined."));
CONFIG_WARNING(RTR("Only one visible CanvasModulate is allowed per canvas.\nWhen there are more than one, only one of them will be active. Which one is undefined."));
}
}

return warnings;
return infos;
}
#endif

CanvasModulate::CanvasModulate() {
}
4 changes: 3 additions & 1 deletion scene/2d/canvas_modulate.h
Original file line number Diff line number Diff line change
@@ -54,7 +54,9 @@ class CanvasModulate : public Node2D {
void set_color(const Color &p_color);
Color get_color() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

CanvasModulate();
~CanvasModulate();
14 changes: 8 additions & 6 deletions scene/2d/cpu_particles_2d.cpp
Original file line number Diff line number Diff line change
@@ -277,20 +277,22 @@ bool CPUParticles2D::get_fractional_delta() const {
return fractional_delta;
}

PackedStringArray CPUParticles2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> CPUParticles2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

CanvasItemMaterial *mat = Object::cast_to<CanvasItemMaterial>(get_material().ptr());

if (get_material().is_null() || (mat && !mat->get_particles_animation())) {
if (get_param_max(PARAM_ANIM_SPEED) != 0.0 || get_param_max(PARAM_ANIM_OFFSET) != 0.0 ||
get_param_curve(PARAM_ANIM_SPEED).is_valid() || get_param_curve(PARAM_ANIM_OFFSET).is_valid()) {
warnings.push_back(RTR("CPUParticles2D animation requires the usage of a CanvasItemMaterial with \"Particles Animation\" enabled."));
CONFIG_WARNING(RTR("CPUParticles2D animation requires the usage of a CanvasItemMaterial with \"Particles Animation\" enabled."));
}
}

return warnings;
return infos;
}
#endif

void CPUParticles2D::restart(bool p_keep_seed) {
time = 0;
@@ -352,7 +354,7 @@ void CPUParticles2D::set_param_max(Parameter p_param, real_t p_value) {
set_param_min(p_param, p_value);
}

update_configuration_warnings();
update_configuration_info();
}

real_t CPUParticles2D::get_param_max(Parameter p_param) const {
@@ -414,7 +416,7 @@ void CPUParticles2D::set_param_curve(Parameter p_param, const Ref<Curve> &p_curv
}
}

update_configuration_warnings();
update_configuration_info();
}

Ref<Curve> CPUParticles2D::get_param_curve(Parameter p_param) const {
4 changes: 3 additions & 1 deletion scene/2d/cpu_particles_2d.h
Original file line number Diff line number Diff line change
@@ -313,7 +313,9 @@ class CPUParticles2D : public Node2D {
void set_gravity(const Vector2 &p_gravity);
Vector2 get_gravity() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void restart(bool p_keep_seed = false);

22 changes: 12 additions & 10 deletions scene/2d/gpu_particles_2d.cpp
Original file line number Diff line number Diff line change
@@ -154,14 +154,14 @@ void GPUParticles2D::set_process_material(const Ref<Material> &p_material) {
}
RS::get_singleton()->particles_set_process_material(particles, material_rid);

update_configuration_warnings();
update_configuration_info();
}

void GPUParticles2D::set_trail_enabled(bool p_enabled) {
trail_enabled = p_enabled;
RS::get_singleton()->particles_set_trails(particles, trail_enabled, trail_lifetime);
queue_redraw();
update_configuration_warnings();
update_configuration_info();

RS::get_singleton()->particles_set_transform_align(particles, p_enabled ? RS::PARTICLES_TRANSFORM_ALIGN_Y_TO_VELOCITY : RS::PARTICLES_TRANSFORM_ALIGN_DISABLED);
}
@@ -349,11 +349,12 @@ void GPUParticles2D::request_particles_process(real_t p_requested_process_time)
RS::get_singleton()->particles_request_process_time(particles, p_requested_process_time);
}

PackedStringArray GPUParticles2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> GPUParticles2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (process_material.is_null()) {
warnings.push_back(RTR("A material to process the particles is not assigned, so no behavior is imprinted."));
CONFIG_WARNING(RTR("A material to process the particles is not assigned, so no behavior is imprinted."));
} else {
CanvasItemMaterial *mat = Object::cast_to<CanvasItemMaterial>(get_material().ptr());

@@ -362,21 +363,22 @@ PackedStringArray GPUParticles2D::get_configuration_warnings() const {
if (process &&
(process->get_param_max(ParticleProcessMaterial::PARAM_ANIM_SPEED) != 0.0 || process->get_param_max(ParticleProcessMaterial::PARAM_ANIM_OFFSET) != 0.0 ||
process->get_param_texture(ParticleProcessMaterial::PARAM_ANIM_SPEED).is_valid() || process->get_param_texture(ParticleProcessMaterial::PARAM_ANIM_OFFSET).is_valid())) {
warnings.push_back(RTR("Particles2D animation requires the usage of a CanvasItemMaterial with \"Particles Animation\" enabled."));
CONFIG_WARNING(RTR("Particles2D animation requires the usage of a CanvasItemMaterial with \"Particles Animation\" enabled."));
}
}
}

if (trail_enabled && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") {
warnings.push_back(RTR("Particle trails are only available when using the Forward+ or Mobile renderers."));
CONFIG_WARNING(RTR("Particle trails are only available when using the Forward+ or Mobile renderers."));
}

if (sub_emitter != NodePath() && OS::get_singleton()->get_current_rendering_method() == "gl_compatibility") {
warnings.push_back(RTR("Particle sub-emitters are not available when using the Compatibility renderer."));
CONFIG_WARNING(RTR("Particle sub-emitters are not available when using the Compatibility renderer."));
}

return warnings;
return infos;
}
#endif

Rect2 GPUParticles2D::capture_rect() const {
AABB aabb = RS::get_singleton()->particles_get_current_aabb(particles);
@@ -453,7 +455,7 @@ void GPUParticles2D::set_sub_emitter(const NodePath &p_path) {
if (is_inside_tree() && sub_emitter != NodePath()) {
_attach_sub_emitter();
}
update_configuration_warnings();
update_configuration_info();
}

NodePath GPUParticles2D::get_sub_emitter() const {
4 changes: 3 additions & 1 deletion scene/2d/gpu_particles_2d.h
Original file line number Diff line number Diff line change
@@ -169,7 +169,9 @@ class GPUParticles2D : public Node2D {
void set_amount_ratio(float p_ratio);
float get_amount_ratio() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void set_sub_emitter(const NodePath &p_path);
NodePath get_sub_emitter() const;
14 changes: 9 additions & 5 deletions scene/2d/light_2d.cpp
Original file line number Diff line number Diff line change
@@ -413,7 +413,7 @@ void PointLight2D::set_texture(const Ref<Texture2D> &p_texture) {
RS::get_singleton()->canvas_light_set_texture(_get_light(), RID());
}

update_configuration_warnings();
update_configuration_info();
}

Ref<Texture2D> PointLight2D::get_texture() const {
@@ -430,15 +430,19 @@ Vector2 PointLight2D::get_texture_offset() const {
return texture_offset;
}

PackedStringArray PointLight2D::get_configuration_warnings() const {
PackedStringArray warnings = Light2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> PointLight2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Light2D::get_configuration_info();

if (texture.is_null()) {
warnings.push_back(RTR("A texture with the shape of the light must be supplied to the \"Texture\" property."));
CONFIG_WARNING_P(
RTR("A texture with the shape of the light must be supplied."),
"texture");
}

return warnings;
return infos;
}
#endif

void PointLight2D::set_texture_scale(real_t p_scale) {
_scale = p_scale;
4 changes: 3 additions & 1 deletion scene/2d/light_2d.h
Original file line number Diff line number Diff line change
@@ -178,7 +178,9 @@ class PointLight2D : public Light2D {
void set_texture_scale(real_t p_scale);
real_t get_texture_scale() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

PointLight2D();
};
14 changes: 8 additions & 6 deletions scene/2d/light_occluder_2d.cpp
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ void OccluderPolygon2D::set_polygon(const Vector<Vector2> &p_polygon) {
rect_cache_dirty = true;
RS::get_singleton()->canvas_occluder_polygon_set_shape(occ_polygon, p_polygon, closed);
emit_changed();
update_configuration_warning();
update_configuration_info();
}

Vector<Vector2> OccluderPolygon2D::get_polygon() const {
@@ -263,19 +263,21 @@ int LightOccluder2D::get_occluder_light_mask() const {
return mask;
}

PackedStringArray LightOccluder2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> LightOccluder2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (occluder_polygon.is_null()) {
warnings.push_back(RTR("An occluder polygon must be set (or drawn) for this occluder to take effect."));
CONFIG_WARNING(RTR("An occluder polygon must be set (or drawn) for this occluder to take effect."));
}

if (occluder_polygon.is_valid() && occluder_polygon->get_polygon().size() == 0) {
warnings.push_back(RTR("The occluder polygon for this occluder is empty. Please draw a polygon."));
CONFIG_WARNING(RTR("The occluder polygon for this occluder is empty. Please draw a polygon."));
}

return warnings;
return infos;
}
#endif

void LightOccluder2D::set_as_sdf_collision(bool p_enable) {
sdf_collision = p_enable;
4 changes: 3 additions & 1 deletion scene/2d/light_occluder_2d.h
Original file line number Diff line number Diff line change
@@ -106,7 +106,9 @@ class LightOccluder2D : public Node2D {
void set_as_sdf_collision(bool p_enable);
bool is_set_as_sdf_collision() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

LightOccluder2D();
~LightOccluder2D();
10 changes: 6 additions & 4 deletions scene/2d/navigation_agent_2d.cpp
Original file line number Diff line number Diff line change
@@ -658,15 +658,17 @@ void NavigationAgent2D::_avoidance_done(Vector3 p_new_velocity) {
emit_signal(SNAME("velocity_computed"), safe_velocity);
}

PackedStringArray NavigationAgent2D::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> NavigationAgent2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node::get_configuration_info();

if (!Object::cast_to<Node2D>(get_parent())) {
warnings.push_back(RTR("The NavigationAgent2D can be used only under a Node2D inheriting parent node."));
CONFIG_WARNING(RTR("The NavigationAgent2D can be used only under a Node2D inheriting parent node."));
}

return warnings;
return infos;
}
#endif

void NavigationAgent2D::_update_navigation() {
if (agent_parent == nullptr) {
4 changes: 3 additions & 1 deletion scene/2d/navigation_agent_2d.h
Original file line number Diff line number Diff line change
@@ -206,7 +206,9 @@ class NavigationAgent2D : public Node {

void _avoidance_done(Vector3 p_new_velocity);

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void set_avoidance_layers(uint32_t p_layers);
uint32_t get_avoidance_layers() const;
14 changes: 8 additions & 6 deletions scene/2d/navigation_link_2d.cpp
Original file line number Diff line number Diff line change
@@ -249,7 +249,7 @@ void NavigationLink2D::set_start_position(Vector2 p_position) {

NavigationServer2D::get_singleton()->link_set_start_position(link, current_global_transform.xform(start_position));

update_configuration_warnings();
update_configuration_info();

#ifdef DEBUG_ENABLED
queue_redraw();
@@ -269,7 +269,7 @@ void NavigationLink2D::set_end_position(Vector2 p_position) {

NavigationServer2D::get_singleton()->link_set_end_position(link, current_global_transform.xform(end_position));

update_configuration_warnings();
update_configuration_info();

#ifdef DEBUG_ENABLED
queue_redraw();
@@ -330,15 +330,17 @@ void NavigationLink2D::set_travel_cost(real_t p_travel_cost) {
NavigationServer2D::get_singleton()->link_set_travel_cost(link, travel_cost);
}

PackedStringArray NavigationLink2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> NavigationLink2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (start_position.is_equal_approx(end_position)) {
warnings.push_back(RTR("NavigationLink2D start position should be different than the end position to be useful."));
CONFIG_WARNING(RTR("NavigationLink2D start position should be different than the end position to be useful."));
}

return warnings;
return infos;
}
#endif

void NavigationLink2D::_link_enter_navigation_map() {
if (!is_inside_tree()) {
4 changes: 3 additions & 1 deletion scene/2d/navigation_link_2d.h
Original file line number Diff line number Diff line change
@@ -101,7 +101,9 @@ class NavigationLink2D : public Node2D {
void set_travel_cost(real_t p_travel_cost);
real_t get_travel_cost() const { return travel_cost; }

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

NavigationLink2D();
~NavigationLink2D();
14 changes: 8 additions & 6 deletions scene/2d/navigation_obstacle_2d.cpp
Original file line number Diff line number Diff line change
@@ -325,24 +325,26 @@ bool NavigationObstacle2D::get_carve_navigation_mesh() const {
return carve_navigation_mesh;
}

PackedStringArray NavigationObstacle2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> NavigationObstacle2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

const Vector2 global_scale = get_global_scale();
if (global_scale.x < 0.001 || global_scale.y < 0.001) {
warnings.push_back(RTR("NavigationObstacle2D does not support negative or zero scaling."));
CONFIG_WARNING_P(RTR("NavigationObstacle2D does not support negative or zero scaling."), "scale");
}

if (radius > 0.0 && !get_global_transform().is_conformal()) {
warnings.push_back(RTR("The agent radius can only be scaled uniformly. The largest value along the two axes of the global scale will be used to scale the radius. This value may change in unexpected ways when the node is rotated."));
CONFIG_WARNING(RTR("The agent radius can only be scaled uniformly. The largest value along the two axes of the global scale will be used to scale the radius. This value may change in unexpected ways when the node is rotated."));
}

if (radius > 0.0 && get_global_skew() != 0.0) {
warnings.push_back(RTR("Skew has no effect on the agent radius."));
CONFIG_WARNING_P(RTR("Skew has no effect on the agent radius."), "skew");
}

return warnings;
return infos;
}
#endif

void NavigationObstacle2D::navmesh_parse_init() {
ERR_FAIL_NULL(NavigationServer2D::get_singleton());
4 changes: 3 additions & 1 deletion scene/2d/navigation_obstacle_2d.h
Original file line number Diff line number Diff line change
@@ -116,7 +116,9 @@ class NavigationObstacle2D : public Node2D {
void set_carve_navigation_mesh(bool p_enabled);
bool get_carve_navigation_mesh() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
virtual Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

private:
static Callable _navmesh_source_geometry_parsing_callback;
14 changes: 9 additions & 5 deletions scene/2d/navigation_region_2d.cpp
Original file line number Diff line number Diff line change
@@ -217,7 +217,7 @@ void NavigationRegion2D::set_navigation_polygon(const Ref<NavigationPolygon> &p_

_navigation_polygon_changed();

update_configuration_warnings();
update_configuration_info();
}

Ref<NavigationPolygon> NavigationRegion2D::get_navigation_polygon() const {
@@ -298,17 +298,21 @@ void NavigationRegion2D::_navigation_debug_changed() {
}
#endif // DEBUG_ENABLED

PackedStringArray NavigationRegion2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> NavigationRegion2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (is_visible_in_tree() && is_inside_tree()) {
if (navigation_polygon.is_null()) {
warnings.push_back(RTR("A NavigationMesh resource must be set or created for this node to work. Please set a property or draw a polygon."));
CONFIG_WARNING_P(
RTR("A NavigationMesh resource must be set or created for this node to work. Please set a property or draw a polygon."),
"navigation_polygon");
}
}

return warnings;
return infos;
}
#endif

void NavigationRegion2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_rid"), &NavigationRegion2D::get_rid);
4 changes: 3 additions & 1 deletion scene/2d/navigation_region_2d.h
Original file line number Diff line number Diff line change
@@ -109,7 +109,9 @@ class NavigationRegion2D : public Node2D {
void set_navigation_polygon(const Ref<NavigationPolygon> &p_navigation_polygon);
Ref<NavigationPolygon> get_navigation_polygon() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void bake_navigation_polygon(bool p_on_thread);
void _bake_finished(Ref<NavigationPolygon> p_navigation_polygon);
16 changes: 16 additions & 0 deletions scene/2d/node_2d.cpp
Original file line number Diff line number Diff line change
@@ -427,6 +427,22 @@ Point2 Node2D::to_global(Point2 p_local) const {
return get_global_transform().xform(p_local);
}

#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> Node2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = CanvasItem::get_configuration_info();

// Note: Warn for Node2D but not all CanvasItems, don't warn for Control nodes.
// Control nodes may have reasons to use a transformed root node like anchors.
if (get_tree()->get_edited_scene_root() == this && !get_transform().is_equal_approx(Transform2D())) {
CONFIG_WARNING_P(
TTR("The root node of a scene is recommended to not be transformed, since instances of the scene will usually override this. Reset the transform to remove this warning."),
"transform");
}

return infos;
}
#endif

void Node2D::_notification(int p_notification) {
switch (p_notification) {
case NOTIFICATION_ENTER_TREE: {
4 changes: 4 additions & 0 deletions scene/2d/node_2d.h
Original file line number Diff line number Diff line change
@@ -117,6 +117,10 @@ class Node2D : public CanvasItem {

Transform2D get_transform() const override;

#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

Node2D() {}
};

10 changes: 6 additions & 4 deletions scene/2d/parallax_layer.cpp
Original file line number Diff line number Diff line change
@@ -132,15 +132,17 @@ void ParallaxLayer::set_base_offset_and_scale(const Point2 &p_offset, real_t p_s
_update_mirroring();
}

PackedStringArray ParallaxLayer::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> ParallaxLayer::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (!Object::cast_to<ParallaxBackground>(get_parent())) {
warnings.push_back(RTR("ParallaxLayer node only works when set as child of a ParallaxBackground node."));
CONFIG_WARNING(RTR("ParallaxLayer node only works when set as child of a ParallaxBackground node."));
}

return warnings;
return infos;
}
#endif

void ParallaxLayer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_motion_scale", "scale"), &ParallaxLayer::set_motion_scale);
4 changes: 3 additions & 1 deletion scene/2d/parallax_layer.h
Original file line number Diff line number Diff line change
@@ -59,7 +59,9 @@ class ParallaxLayer : public Node2D {

void set_base_offset_and_scale(const Point2 &p_offset, real_t p_scale);

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif
ParallaxLayer();
};

10 changes: 6 additions & 4 deletions scene/2d/path_2d.cpp
Original file line number Diff line number Diff line change
@@ -287,17 +287,19 @@ void PathFollow2D::_validate_property(PropertyInfo &p_property) const {
}
}

PackedStringArray PathFollow2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> PathFollow2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (is_visible_in_tree() && is_inside_tree()) {
if (!Object::cast_to<Path2D>(get_parent())) {
warnings.push_back(RTR("PathFollow2D only works when set as a child of a Path2D node."));
CONFIG_WARNING(RTR("PathFollow2D only works when set as a child of a Path2D node."));
}
}

return warnings;
return infos;
}
#endif

void PathFollow2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_progress", "progress"), &PathFollow2D::set_progress);
4 changes: 3 additions & 1 deletion scene/2d/path_2d.h
Original file line number Diff line number Diff line change
@@ -106,7 +106,9 @@ class PathFollow2D : public Node2D {
void set_cubic_interpolation_enabled(bool p_enabled);
bool is_cubic_interpolation_enabled() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

PathFollow2D() {}
};
10 changes: 6 additions & 4 deletions scene/2d/physics/collision_object_2d.cpp
Original file line number Diff line number Diff line change
@@ -581,15 +581,17 @@ void CollisionObject2D::_update_pickable() {
}
}

PackedStringArray CollisionObject2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> CollisionObject2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (shapes.is_empty()) {
warnings.push_back(RTR("This node has no shape, so it can't collide or interact with other objects.\nConsider adding a CollisionShape2D or CollisionPolygon2D as a child to define its shape."));
CONFIG_WARNING(RTR("This node has no shape, so it can't collide or interact with other objects.\nConsider adding a CollisionShape2D or CollisionPolygon2D as a child to define its shape."));
}

return warnings;
return infos;
}
#endif

void CollisionObject2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_rid"), &CollisionObject2D::get_rid);
4 changes: 3 additions & 1 deletion scene/2d/physics/collision_object_2d.h
Original file line number Diff line number Diff line change
@@ -166,7 +166,9 @@ class CollisionObject2D : public Node2D {
void set_pickable(bool p_enabled);
bool is_pickable() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

_FORCE_INLINE_ RID get_rid() const { return rid; }

26 changes: 15 additions & 11 deletions scene/2d/physics/collision_polygon_2d.cpp
Original file line number Diff line number Diff line change
@@ -193,7 +193,7 @@ void CollisionPolygon2D::set_polygon(const Vector<Point2> &p_polygon) {
_update_in_shape_owner();
}
queue_redraw();
update_configuration_warnings();
update_configuration_info();
}

Vector<Point2> CollisionPolygon2D::get_polygon() const {
@@ -208,7 +208,7 @@ void CollisionPolygon2D::set_build_mode(BuildMode p_mode) {
_update_in_shape_owner();
}
queue_redraw();
update_configuration_warnings();
update_configuration_info();
}

CollisionPolygon2D::BuildMode CollisionPolygon2D::get_build_mode() const {
@@ -229,32 +229,36 @@ bool CollisionPolygon2D::_edit_is_selected_on_click(const Point2 &p_point, doubl
}
#endif

PackedStringArray CollisionPolygon2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> CollisionPolygon2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (!Object::cast_to<CollisionObject2D>(get_parent())) {
warnings.push_back(RTR("CollisionPolygon2D only serves to provide a collision shape to a CollisionObject2D derived node. Please only use it as a child of Area2D, StaticBody2D, RigidBody2D, CharacterBody2D, etc. to give them a shape."));
CONFIG_WARNING(RTR("CollisionPolygon2D only serves to provide a collision shape to a CollisionObject2D derived node. Please only use it as a child of Area2D, StaticBody2D, RigidBody2D, CharacterBody2D, etc. to give them a shape."));
}

int polygon_count = polygon.size();
if (polygon_count == 0) {
warnings.push_back(RTR("An empty CollisionPolygon2D has no effect on collision."));
CONFIG_WARNING(RTR("An empty CollisionPolygon2D has no effect on collision."));
} else {
bool solids = build_mode == BUILD_SOLIDS;
if (solids) {
if (polygon_count < 3) {
warnings.push_back(RTR("Invalid polygon. At least 3 points are needed in 'Solids' build mode."));
CONFIG_WARNING(RTR("Invalid polygon. At least 3 points are needed in 'Solids' build mode."));
}
} else if (polygon_count < 2) {
warnings.push_back(RTR("Invalid polygon. At least 2 points are needed in 'Segments' build mode."));
CONFIG_WARNING(RTR("Invalid polygon. At least 2 points are needed in 'Segments' build mode."));
}
}
if (one_way_collision && Object::cast_to<Area2D>(get_parent())) {
warnings.push_back(RTR("The One Way Collision property will be ignored when the collision object is an Area2D."));
CONFIG_WARNING_P(
RTR("One Way Collision will be ignored when the collision object is an Area2D."),
"one_way_collision");
}

return warnings;
return infos;
}
#endif

void CollisionPolygon2D::set_disabled(bool p_disabled) {
disabled = p_disabled;
@@ -274,7 +278,7 @@ void CollisionPolygon2D::set_one_way_collision(bool p_enable) {
if (collision_object) {
collision_object->shape_owner_set_one_way_collision(owner_id, p_enable);
}
update_configuration_warnings();
update_configuration_info();
}

bool CollisionPolygon2D::is_one_way_collision_enabled() const {
4 changes: 3 additions & 1 deletion scene/2d/physics/collision_polygon_2d.h
Original file line number Diff line number Diff line change
@@ -77,7 +77,9 @@ class CollisionPolygon2D : public Node2D {
void set_polygon(const Vector<Point2> &p_polygon);
Vector<Point2> get_polygon() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void set_disabled(bool p_disabled);
bool is_disabled() const;
22 changes: 13 additions & 9 deletions scene/2d/physics/collision_shape_2d.cpp
Original file line number Diff line number Diff line change
@@ -153,7 +153,7 @@ void CollisionShape2D::set_shape(const Ref<Shape2D> &p_shape) {
shape->connect_changed(callable_mp(this, &CollisionShape2D::_shape_changed));
}

update_configuration_warnings();
update_configuration_info();
}

Ref<Shape2D> CollisionShape2D::get_shape() const {
@@ -168,28 +168,32 @@ bool CollisionShape2D::_edit_is_selected_on_click(const Point2 &p_point, double
return shape->_edit_is_selected_on_click(p_point, p_tolerance);
}

PackedStringArray CollisionShape2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> CollisionShape2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

CollisionObject2D *col_object = Object::cast_to<CollisionObject2D>(get_parent());
if (col_object == nullptr) {
warnings.push_back(RTR("CollisionShape2D only serves to provide a collision shape to a CollisionObject2D derived node.\nPlease only use it as a child of Area2D, StaticBody2D, RigidBody2D, CharacterBody2D, etc. to give them a shape."));
CONFIG_WARNING(RTR("CollisionShape2D only serves to provide a collision shape to a CollisionObject2D derived node.\nPlease only use it as a child of Area2D, StaticBody2D, RigidBody2D, CharacterBody2D, etc. to give them a shape."));
}
if (shape.is_null()) {
warnings.push_back(RTR("A shape must be provided for CollisionShape2D to function. Please create a shape resource for it!"));
CONFIG_WARNING_P(RTR("A shape must be provided for CollisionShape2D to function. Please create a shape resource for it!"), "shape");
}
if (one_way_collision && Object::cast_to<Area2D>(col_object)) {
warnings.push_back(RTR("The One Way Collision property will be ignored when the collision object is an Area2D."));
CONFIG_WARNING_P(
RTR("One Way Collision will be ignored when the collision object is an Area2D."),
"one_way_collision");
}

Ref<ConvexPolygonShape2D> convex = shape;
Ref<ConcavePolygonShape2D> concave = shape;
if (convex.is_valid() || concave.is_valid()) {
warnings.push_back(RTR("The CollisionShape2D node has limited editing options for polygon-based shapes. Consider using a CollisionPolygon2D node instead."));
CONFIG_WARNING(RTR("The CollisionShape2D node has limited editing options for polygon-based shapes. Consider using a CollisionPolygon2D node instead."));
}

return warnings;
return infos;
}
#endif

void CollisionShape2D::set_disabled(bool p_disabled) {
disabled = p_disabled;
@@ -209,7 +213,7 @@ void CollisionShape2D::set_one_way_collision(bool p_enable) {
if (collision_object) {
collision_object->shape_owner_set_one_way_collision(owner_id, p_enable);
}
update_configuration_warnings();
update_configuration_info();
}

bool CollisionShape2D::is_one_way_collision_enabled() const {
4 changes: 3 additions & 1 deletion scene/2d/physics/collision_shape_2d.h
Original file line number Diff line number Diff line change
@@ -87,7 +87,9 @@ class CollisionShape2D : public Node2D {
void set_debug_color(const Color &p_color);
Color get_debug_color() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

CollisionShape2D();
};
14 changes: 8 additions & 6 deletions scene/2d/physics/joints/joint_2d.cpp
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ void Joint2D::_disconnect_signals() {
void Joint2D::_body_exit_tree() {
_disconnect_signals();
_update_joint(true);
update_configuration_warnings();
update_configuration_info();
}

void Joint2D::_update_joint(bool p_only_free) {
@@ -90,7 +90,7 @@ void Joint2D::_update_joint(bool p_only_free) {
valid = true;
}

update_configuration_warnings();
update_configuration_info();

if (!valid) {
PhysicsServer2D::get_singleton()->joint_clear(joint);
@@ -215,15 +215,17 @@ bool Joint2D::get_exclude_nodes_from_collision() const {
return exclude_from_collision;
}

PackedStringArray Joint2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> Joint2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (!warning.is_empty()) {
warnings.push_back(warning);
CONFIG_WARNING(warning);
}

return warnings;
return infos;
}
#endif

void Joint2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_node_a", "node"), &Joint2D::set_node_a);
4 changes: 3 additions & 1 deletion scene/2d/physics/joints/joint_2d.h
Original file line number Diff line number Diff line change
@@ -62,7 +62,9 @@ class Joint2D : public Node2D {
_FORCE_INLINE_ bool is_configured() const { return configured; }

public:
virtual PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
virtual Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void set_node_a(const NodePath &p_node_a);
NodePath get_node_a() const;
14 changes: 8 additions & 6 deletions scene/2d/physics/physical_bone_2d.cpp
Original file line number Diff line number Diff line change
@@ -106,24 +106,26 @@ void PhysicalBone2D::_find_joint_child() {
}
}

PackedStringArray PhysicalBone2D::get_configuration_warnings() const {
PackedStringArray warnings = RigidBody2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> PhysicalBone2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = RigidBody2D::get_configuration_info();

if (!parent_skeleton) {
warnings.push_back(RTR("A PhysicalBone2D only works with a Skeleton2D or another PhysicalBone2D as a parent node!"));
CONFIG_WARNING(RTR("A PhysicalBone2D only works with a Skeleton2D or another PhysicalBone2D as a parent node!"));
}
if (parent_skeleton && bone2d_index <= -1) {
warnings.push_back(RTR("A PhysicalBone2D needs to be assigned to a Bone2D node in order to function! Please set a Bone2D node in the inspector."));
CONFIG_WARNING(RTR("A PhysicalBone2D needs to be assigned to a Bone2D node in order to function! Please set a Bone2D node in the inspector."));
}
if (!child_joint) {
PhysicalBone2D *parent_bone = Object::cast_to<PhysicalBone2D>(get_parent());
if (parent_bone) {
warnings.push_back(RTR("A PhysicalBone2D node should have a Joint2D-based child node to keep bones connected! Please add a Joint2D-based node as a child to this node!"));
CONFIG_WARNING(RTR("A PhysicalBone2D node should have a Joint2D-based child node to keep bones connected! Please add a Joint2D-based node as a child to this node!"));
}
}

return warnings;
return infos;
}
#endif

void PhysicalBone2D::_auto_configure_joint() {
if (!auto_configure_joint) {
4 changes: 3 additions & 1 deletion scene/2d/physics/physical_bone_2d.h
Original file line number Diff line number Diff line change
@@ -79,7 +79,9 @@ class PhysicalBone2D : public RigidBody2D {
void set_follow_bone_when_simulating(bool p_follow);
bool get_follow_bone_when_simulating() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

PhysicalBone2D();
~PhysicalBone2D();
12 changes: 7 additions & 5 deletions scene/2d/physics/rigid_body_2d.cpp
Original file line number Diff line number Diff line change
@@ -632,23 +632,25 @@ void RigidBody2D::_notification(int p_what) {
} break;

case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
update_configuration_warnings();
update_configuration_info();
} break;
}
#endif
}

PackedStringArray RigidBody2D::get_configuration_warnings() const {
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> RigidBody2D::get_configuration_info() const {
Transform2D t = get_transform();

PackedStringArray warnings = PhysicsBody2D::get_configuration_warnings();
Vector<ConfigurationInfo> infos = PhysicsBody2D::get_configuration_info();

if (ABS(t.columns[0].length() - 1.0) > 0.05 || ABS(t.columns[1].length() - 1.0) > 0.05) {
warnings.push_back(RTR("Size changes to RigidBody2D will be overridden by the physics engine when running.\nChange the size in children collision shapes instead."));
CONFIG_WARNING(RTR("Size changes to RigidBody2D will be overridden by the physics engine when running.\nChange the size in children collision shapes instead."));
}

return warnings;
return infos;
}
#endif

void RigidBody2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_mass", "mass"), &RigidBody2D::set_mass);
4 changes: 3 additions & 1 deletion scene/2d/physics/rigid_body_2d.h
Original file line number Diff line number Diff line change
@@ -233,7 +233,9 @@ class RigidBody2D : public PhysicsBody2D {

TypedArray<Node2D> get_colliding_bodies() const; //function for script

virtual PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
virtual Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

RigidBody2D();
~RigidBody2D();
12 changes: 7 additions & 5 deletions scene/2d/physics/shape_cast_2d.cpp
Original file line number Diff line number Diff line change
@@ -161,7 +161,7 @@ void ShapeCast2D::set_shape(const Ref<Shape2D> &p_shape) {
shape_rid = shape->get_rid();
}

update_configuration_warnings();
update_configuration_info();
queue_redraw();
}

@@ -401,14 +401,16 @@ Array ShapeCast2D::get_collision_result() const {
return ret;
}

PackedStringArray ShapeCast2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> ShapeCast2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (shape.is_null()) {
warnings.push_back(RTR("This node cannot interact with other objects unless a Shape2D is assigned."));
CONFIG_WARNING(RTR("This node cannot interact with other objects unless a Shape2D is assigned."));
}
return warnings;
return infos;
}
#endif

void ShapeCast2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &ShapeCast2D::set_enabled);
4 changes: 3 additions & 1 deletion scene/2d/physics/shape_cast_2d.h
Original file line number Diff line number Diff line change
@@ -118,7 +118,9 @@ class ShapeCast2D : public Node2D {
void remove_exception(const CollisionObject2D *p_node);
void clear_exceptions();

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

ShapeCast2D();
};
14 changes: 9 additions & 5 deletions scene/2d/remote_transform_2d.cpp
Original file line number Diff line number Diff line change
@@ -148,7 +148,7 @@ void RemoteTransform2D::set_remote_node(const NodePath &p_remote_node) {
_update_remote();
}

update_configuration_warnings();
update_configuration_info();
}

NodePath RemoteTransform2D::get_remote_node() const {
@@ -210,15 +210,19 @@ void RemoteTransform2D::force_update_cache() {
_update_cache();
}

PackedStringArray RemoteTransform2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> RemoteTransform2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();

if (!has_node(remote_node) || !Object::cast_to<Node2D>(get_node(remote_node))) {
warnings.push_back(RTR("Path property must point to a valid Node2D node to work."));
CONFIG_WARNING_P(
RTR("Path must point to a valid Node2D node to work."),
"remote_path");
}

return warnings;
return infos;
}
#endif

void RemoteTransform2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_remote_node", "path"), &RemoteTransform2D::set_remote_node);
4 changes: 3 additions & 1 deletion scene/2d/remote_transform_2d.h
Original file line number Diff line number Diff line change
@@ -70,7 +70,9 @@ class RemoteTransform2D : public Node2D {

void force_update_cache();

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

RemoteTransform2D();
};
16 changes: 9 additions & 7 deletions scene/2d/skeleton_2d.cpp
Original file line number Diff line number Diff line change
@@ -386,7 +386,7 @@ void Bone2D::set_rest(const Transform2D &p_rest) {
skeleton->_make_bone_setup_dirty();
}

update_configuration_warnings();
update_configuration_info();
}

Transform2D Bone2D::get_rest() const {
@@ -411,22 +411,24 @@ int Bone2D::get_index_in_skeleton() const {
return skeleton_index;
}

PackedStringArray Bone2D::get_configuration_warnings() const {
PackedStringArray warnings = Node2D::get_configuration_warnings();
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> Bone2D::get_configuration_info() const {
Vector<ConfigurationInfo> infos = Node2D::get_configuration_info();
if (!skeleton) {
if (parent_bone) {
warnings.push_back(RTR("This Bone2D chain should end at a Skeleton2D node."));
CONFIG_WARNING(RTR("This Bone2D chain should end at a Skeleton2D node."));
} else {
warnings.push_back(RTR("A Bone2D only works with a Skeleton2D or another Bone2D as parent node."));
CONFIG_WARNING(RTR("A Bone2D only works with a Skeleton2D or another Bone2D as parent node."));
}
}

if (rest == Transform2D(0, 0, 0, 0, 0, 0)) {
warnings.push_back(RTR("This bone lacks a proper REST pose. Go to the Skeleton2D node and set one."));
CONFIG_WARNING(RTR("This bone lacks a proper REST pose. Go to the Skeleton2D node and set one."));
}

return warnings;
return infos;
}
#endif

void Bone2D::calculate_length_and_rotation() {
// If there is at least a single child Bone2D node, we can calculate
4 changes: 3 additions & 1 deletion scene/2d/skeleton_2d.h
Original file line number Diff line number Diff line change
@@ -78,7 +78,9 @@ class Bone2D : public Node2D {
void apply_rest();
Transform2D get_skeleton_rest() const;

PackedStringArray get_configuration_warnings() const override;
#ifdef TOOLS_ENABLED
Vector<ConfigurationInfo> get_configuration_info() const override;
#endif

void set_autocalculate_length_and_angle(bool p_autocalculate);
bool get_autocalculate_length_and_angle() const;
Loading