Skip to content

Commit

Permalink
Allow configuration warnings to refer to a property
Browse files Browse the repository at this point in the history
This is used so that the inspector can show a warning icon on
a specific property.

As an example, Light2D node was updated to use this system.
  • Loading branch information
RedMser committed Nov 8, 2022
1 parent a499f7b commit d6b36cb
Show file tree
Hide file tree
Showing 132 changed files with 460 additions and 341 deletions.
3 changes: 3 additions & 0 deletions doc/classes/EditorProperty.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
<member name="checked" type="bool" setter="set_checked" getter="is_checked" default="false">
Used by the inspector, set to [code]true[/code] when the property is checked.
</member>
<member name="configuration_warning" type="String" setter="set_configuration_warning" getter="get_configuration_warning" default="&quot;&quot;">
Used by the inspector, set to show a configuration warning on the property.
</member>
<member name="deletable" type="bool" setter="set_deletable" getter="is_deletable" default="false">
Used by the inspector, set to [code]true[/code] when the property can be deleted by the user.
</member>
Expand Down
4 changes: 3 additions & 1 deletion doc/classes/Node.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@
</description>
</method>
<method name="_get_configuration_warnings" qualifiers="virtual const">
<return type="PackedStringArray" />
<return type="Dictionary[]" />
<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.
Each element is a dictionary which must contain a key [code]message[/code] of type [String] which is shown in the user interface.
The dictionary may optionally contain a key [code]property[/code] of type [NodePath], which also shows this warning in the inspector on the corresponding property.
Returning an empty array produces no warnings.
Call [method update_configuration_warnings] when the warnings need to be updated for this node.
</description>
Expand Down
85 changes: 85 additions & 0 deletions editor/editor_inspector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,23 @@ void EditorProperty::_notification(int p_what) {
text_size -= close->get_width() + 4 * EDSCALE;
}
}

if (!configuration_warning.is_empty() && !read_only) {
Ref<Texture2D> warning;

// TODO: Warning icon based on count.
warning = get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons"));

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

if (is_layout_rtl()) {
rect.position.x += warning->get_width() + get_theme_constant(SNAME("hseparator"), SNAME("Tree"));
}

if (no_children) {
text_size -= warning->get_width() + 4 * EDSCALE;
}
}
}

//set children
Expand Down Expand Up @@ -393,6 +410,29 @@ void EditorProperty::_notification(int p_what) {
} else {
delete_rect = Rect2();
}

if (!configuration_warning.is_empty() && !read_only) {
Ref<Texture2D> warning;

warning = get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons"));

ofs -= warning->get_width() + get_theme_constant(SNAME("hseparator"), SNAME("Tree"));

Color color2(1, 1, 1);
if (configuration_warning_hover) {
color2.r *= 1.2;
color2.g *= 1.2;
color2.b *= 1.2;
}
configuration_warning_rect = Rect2(ofs, ((size.height - warning->get_height()) / 2), warning->get_width(), warning->get_height());
if (rtl) {
draw_texture(warning, Vector2(size.width - configuration_warning_rect.position.x - warning->get_width(), configuration_warning_rect.position.y), color2);
} else {
draw_texture(warning, configuration_warning_rect.position, color2);
}
} else {
configuration_warning_rect = Rect2();
}
} break;
}
}
Expand Down Expand Up @@ -643,6 +683,12 @@ void EditorProperty::gui_input(const Ref<InputEvent> &p_event) {
check_hover = new_check_hover;
queue_redraw();
}

bool new_configuration_warning_hover = configuration_warning_rect.has_point(mpos) && !button_left;
if (new_configuration_warning_hover != configuration_warning_hover) {
configuration_warning_hover = new_configuration_warning_hover;
queue_redraw();
}
}

Ref<InputEventMouseButton> mb = p_event;
Expand Down Expand Up @@ -699,6 +745,12 @@ void EditorProperty::gui_input(const Ref<InputEvent> &p_event) {
queue_redraw();
emit_signal(SNAME("property_checked"), property, checked);
}

if (configuration_warning_rect.has_point(mpos)) {
String warning_text = configuration_warning.word_wrap(80);
warning_dialog->set_text(warning_text);
warning_dialog->popup_centered();
}
} else if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
_update_popup();
menu->set_position(get_screen_position() + get_local_mouse_position());
Expand Down Expand Up @@ -823,6 +875,16 @@ float EditorProperty::get_name_split_ratio() const {
return split_ratio;
}

void EditorProperty::set_configuration_warning(const String &p_configuration_warning) {
configuration_warning = p_configuration_warning;
queue_redraw();
queue_sort();
}

String EditorProperty::get_configuration_warning() const {
return configuration_warning;
}

void EditorProperty::set_object_and_property(Object *p_object, const StringName &p_property) {
object = p_object;
property = p_property;
Expand Down Expand Up @@ -879,6 +941,12 @@ void EditorProperty::_update_pin_flags() {
}
}

void EditorProperty::_update_configuration_warnings() {
if (Node *node = Object::cast_to<Node>(object)) {
set_configuration_warning(node->get_configuration_warnings_as_string(property_path));
}
}

static Control *make_help_bit(const String &p_text, bool p_property) {
EditorHelpBit *help_bit = memnew(EditorHelpBit);
help_bit->get_rich_text()->set_fixed_size_to_width(360 * EDSCALE);
Expand Down Expand Up @@ -958,6 +1026,9 @@ void EditorProperty::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_deletable", "deletable"), &EditorProperty::set_deletable);
ClassDB::bind_method(D_METHOD("is_deletable"), &EditorProperty::is_deletable);

ClassDB::bind_method(D_METHOD("set_configuration_warning", "configuration_warning"), &EditorProperty::set_configuration_warning);
ClassDB::bind_method(D_METHOD("get_configuration_warning"), &EditorProperty::get_configuration_warning);

ClassDB::bind_method(D_METHOD("get_edited_property"), &EditorProperty::get_edited_property);
ClassDB::bind_method(D_METHOD("get_edited_object"), &EditorProperty::get_edited_object);

Expand All @@ -975,6 +1046,7 @@ void EditorProperty::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_warning"), "set_draw_warning", "is_draw_warning");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "keying"), "set_keying", "is_keying");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deletable"), "set_deletable", "is_deletable");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "configuration_warning"), "set_configuration_warning", "get_configuration_warning");

ADD_SIGNAL(MethodInfo("property_changed", PropertyInfo(Variant::STRING_NAME, "property"), PropertyInfo(Variant::NIL, "value", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT), PropertyInfo(Variant::STRING_NAME, "field"), PropertyInfo(Variant::BOOL, "changing")));
ADD_SIGNAL(MethodInfo("multiple_properties_changed", PropertyInfo(Variant::PACKED_STRING_ARRAY, "properties"), PropertyInfo(Variant::ARRAY, "value")));
Expand Down Expand Up @@ -1004,6 +1076,10 @@ EditorProperty::EditorProperty() {
bottom_editor = nullptr;
menu = nullptr;
set_process_shortcut_input(true);

warning_dialog = memnew(AcceptDialog);
add_child(warning_dialog);
warning_dialog->set_title(TTR("Node Configuration Warning!"));
}

void EditorProperty::_update_popup() {
Expand Down Expand Up @@ -3238,6 +3314,7 @@ void EditorInspector::update_tree() {
ep->set_keying(keying);
ep->set_read_only(property_read_only || all_read_only);
ep->set_deletable(deletable_properties || p.name.begins_with("metadata/"));
ep->_update_configuration_warnings();
}

current_vbox->add_child(editors[i].property_editor);
Expand Down Expand Up @@ -3834,6 +3911,12 @@ void EditorInspector::_node_removed(Node *p_node) {
}
}

void EditorInspector::_warning_changed(Node *p_node) {
if (p_node == object) {
update_tree_pending = true;
}
}

void EditorInspector::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
Expand All @@ -3845,6 +3928,7 @@ void EditorInspector::_notification(int p_what) {
case NOTIFICATION_ENTER_TREE: {
if (!sub_inspector) {
get_tree()->connect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
get_tree()->connect("node_configuration_warning_changed", callable_mp(this, &EditorInspector::_warning_changed));
}
} break;

Expand All @@ -3855,6 +3939,7 @@ void EditorInspector::_notification(int p_what) {
case NOTIFICATION_EXIT_TREE: {
if (!sub_inspector) {
get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed));
get_tree()->disconnect("node_configuration_warning_changed", callable_mp(this, &EditorInspector::_warning_changed));
}
edit(nullptr);
} break;
Expand Down
10 changes: 10 additions & 0 deletions editor/editor_inspector.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class EditorProperty : public Container {
StringName property;
String property_path;
String doc_path;
AcceptDialog *warning_dialog = nullptr;

int property_usage;

Expand All @@ -93,6 +94,8 @@ class EditorProperty : public Container {
bool check_hover = false;
Rect2 delete_rect;
bool delete_hover = false;
Rect2 configuration_warning_rect;
bool configuration_warning_hover = false;

bool can_revert = false;
bool can_pin = false;
Expand All @@ -116,12 +119,15 @@ class EditorProperty : public Container {
Control *bottom_editor = nullptr;
PopupMenu *menu = nullptr;

String configuration_warning;

HashMap<StringName, Variant> cache;

GDVIRTUAL0(_update_property)
GDVIRTUAL1(_set_read_only, bool)

void _update_pin_flags();
void _update_configuration_warnings();

protected:
void _notification(int p_what);
Expand Down Expand Up @@ -195,6 +201,9 @@ class EditorProperty : public Container {
void set_name_split_ratio(float p_ratio);
float get_name_split_ratio() const;

void set_configuration_warning(const String &p_configuration_warning);
String get_configuration_warning() const;

void set_object_and_property(Object *p_object, const StringName &p_property);
virtual Control *make_custom_tooltip(const String &p_text) const override;

Expand Down Expand Up @@ -510,6 +519,7 @@ class EditorInspector : public ScrollContainer {
void _object_id_selected(const String &p_path, ObjectID p_id);

void _node_removed(Node *p_node);
void _warning_changed(Node *p_node);

HashMap<StringName, int> per_array_page;
void _page_change_request(int p_new_page, const StringName &p_array_prefix);
Expand Down
10 changes: 5 additions & 5 deletions modules/multiplayer/multiplayer_spawner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@ void MultiplayerSpawner::_get_property_list(List<PropertyInfo> *p_list) const {
}
#endif

PackedStringArray MultiplayerSpawner::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
TypedArray<Dictionary> MultiplayerSpawner::get_configuration_warnings() const {
TypedArray<Dictionary> warnings = Node::get_configuration_warnings();

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."));
warnings.push_back(CONFIG_WARNING(RTR("A valid NodePath must be set in the \"Spawn Path\" property in order for MultiplayerSpawner to be able to spawn Nodes.")));
}
bool has_scenes = get_spawnable_scene_count() > 0;
// Can't check if method is overridden in placeholder scripts.
bool has_placeholder_script = get_script_instance() && get_script_instance()->is_placeholder();
if (!has_scenes && !GDVIRTUAL_IS_OVERRIDDEN(_spawn_custom) && !has_placeholder_script) {
warnings.push_back(RTR("A list of PackedScenes must be set in the \"Auto Spawn List\" property in order for MultiplayerSpawner to automatically spawn them remotely when added as child of \"spawn_path\"."));
warnings.push_back(RTR("Alternatively, a Script implementing the function \"_spawn_custom\" must be set for this MultiplayerSpawner, and \"spawn\" must be called explicitly in code."));
warnings.push_back(CONFIG_WARNING(RTR("A list of PackedScenes must be set in the \"Auto Spawn List\" property in order for MultiplayerSpawner to automatically spawn them remotely when added as child of \"spawn_path\".")));
warnings.push_back(CONFIG_WARNING(RTR("Alternatively, a Script implementing the function \"_spawn_custom\" must be set for this MultiplayerSpawner, and \"spawn\" must be called explicitly in code.")));
}
return warnings;
}
Expand Down
2 changes: 1 addition & 1 deletion modules/multiplayer/multiplayer_spawner.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class MultiplayerSpawner : public Node {
void _get_property_list(List<PropertyInfo> *p_list) const;
#endif
public:
PackedStringArray get_configuration_warnings() const override;
TypedArray<Dictionary> get_configuration_warnings() const override;

Node *get_spawn_node() const {
return spawn_node.is_valid() ? Object::cast_to<Node>(ObjectDB::get_instance(spawn_node)) : nullptr;
Expand Down
6 changes: 3 additions & 3 deletions modules/multiplayer/multiplayer_synchronizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ 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();
TypedArray<Dictionary> MultiplayerSynchronizer::get_configuration_warnings() const {
TypedArray<Dictionary> warnings = Node::get_configuration_warnings();

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."));
warnings.push_back(CONFIG_WARNING(RTR("A valid NodePath must be set in the \"Root Path\" property in order for MultiplayerSynchronizer to be able to synchronize properties.")));
}

return warnings;
Expand Down
2 changes: 1 addition & 1 deletion modules/multiplayer/multiplayer_synchronizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class MultiplayerSynchronizer : public Node {
bool update_outbound_sync_time(uint64_t p_msec);
bool update_inbound_sync_time(uint16_t p_network_time);

PackedStringArray get_configuration_warnings() const override;
TypedArray<Dictionary> get_configuration_warnings() const override;

void set_replication_interval(double p_interval);
double get_replication_interval() const;
Expand Down
6 changes: 3 additions & 3 deletions scene/2d/animated_sprite_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,11 @@ StringName AnimatedSprite2D::get_animation() const {
return animation;
}

PackedStringArray AnimatedSprite2D::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
TypedArray<Dictionary> AnimatedSprite2D::get_configuration_warnings() const {
TypedArray<Dictionary> warnings = Node::get_configuration_warnings();

if (frames.is_null()) {
warnings.push_back(RTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite2D to display frames."));
warnings.push_back(CONFIG_WARNING(RTR("A SpriteFrames resource must be created or set in the \"Frames\" property in order for AnimatedSprite2D to display frames.")));
}

return warnings;
Expand Down
2 changes: 1 addition & 1 deletion scene/2d/animated_sprite_2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class AnimatedSprite2D : public Node2D {
void set_flip_v(bool p_flip);
bool is_flipped_v() const;

PackedStringArray get_configuration_warnings() const override;
TypedArray<Dictionary> get_configuration_warnings() const override;
virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override;

AnimatedSprite2D();
Expand Down
6 changes: 3 additions & 3 deletions scene/2d/canvas_modulate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ Color CanvasModulate::get_color() const {
return color;
}

PackedStringArray CanvasModulate::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
TypedArray<Dictionary> CanvasModulate::get_configuration_warnings() const {
TypedArray<Dictionary> warnings = Node::get_configuration_warnings();

if (is_visible_in_tree() && is_inside_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 scene (or set of instantiated scenes). The first created one will work, while the rest will be ignored."));
warnings.push_back(CONFIG_WARNING(RTR("Only one visible CanvasModulate is allowed per scene (or set of instantiated scenes). The first created one will work, while the rest will be ignored.")));
}
}

Expand Down
2 changes: 1 addition & 1 deletion scene/2d/canvas_modulate.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class CanvasModulate : public Node2D {
void set_color(const Color &p_color);
Color get_color() const;

PackedStringArray get_configuration_warnings() const override;
TypedArray<Dictionary> get_configuration_warnings() const override;

CanvasModulate();
~CanvasModulate();
Expand Down
6 changes: 3 additions & 3 deletions scene/2d/collision_object_2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -565,11 +565,11 @@ void CollisionObject2D::_update_pickable() {
}
}

PackedStringArray CollisionObject2D::get_configuration_warnings() const {
PackedStringArray warnings = Node::get_configuration_warnings();
TypedArray<Dictionary> CollisionObject2D::get_configuration_warnings() const {
TypedArray<Dictionary> warnings = Node::get_configuration_warnings();

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."));
warnings.push_back(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;
Expand Down
2 changes: 1 addition & 1 deletion scene/2d/collision_object_2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class CollisionObject2D : public Node2D {
void set_pickable(bool p_enabled);
bool is_pickable() const;

PackedStringArray get_configuration_warnings() const override;
TypedArray<Dictionary> get_configuration_warnings() const override;

_FORCE_INLINE_ RID get_rid() const { return rid; }

Expand Down

0 comments on commit d6b36cb

Please sign in to comment.