Browse Source

Improvements to multi-node editing
- Show revert button for properties that are not default for all selected nodes
- Show property documentation tooltips
- Show common class name and icon and number of selected nodes in EditorPath, e.g. "Node2D (4 Selected)"
- Hide metadata for MultiNodeEdit and AnimationMultiTrackKeyEdit
- Hide script for MultiNodeEdit

FireForge 3 years ago
parent
commit
a914dc0c46

+ 8 - 14
editor/animation_track_editor.cpp

@@ -52,13 +52,9 @@ public:
 	bool setting = false;
 	bool animation_read_only = false;
 
-	bool _hide_script_from_inspector() {
-		return true;
-	}
-
-	bool _dont_undo_redo() {
-		return true;
-	}
+	bool _hide_script_from_inspector() { return true; }
+	bool _hide_metadata_from_inspector() { return true; }
+	bool _dont_undo_redo() { return true; }
 
 	bool _is_read_only() {
 		return animation_read_only;
@@ -68,6 +64,7 @@ public:
 		ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationTrackKeyEdit::_update_obj);
 		ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationTrackKeyEdit::_key_ofs_changed);
 		ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationTrackKeyEdit::_hide_script_from_inspector);
+		ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationTrackKeyEdit::_hide_metadata_from_inspector);
 		ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationTrackKeyEdit::get_root_path);
 		ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationTrackKeyEdit::_dont_undo_redo);
 		ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationTrackKeyEdit::_is_read_only);
@@ -719,13 +716,9 @@ public:
 	bool setting = false;
 	bool animation_read_only = false;
 
-	bool _hide_script_from_inspector() {
-		return true;
-	}
-
-	bool _dont_undo_redo() {
-		return true;
-	}
+	bool _hide_script_from_inspector() { return true; }
+	bool _hide_metadata_from_inspector() { return true; }
+	bool _dont_undo_redo() { return true; }
 
 	bool _is_read_only() {
 		return animation_read_only;
@@ -735,6 +728,7 @@ public:
 		ClassDB::bind_method(D_METHOD("_update_obj"), &AnimationMultiTrackKeyEdit::_update_obj);
 		ClassDB::bind_method(D_METHOD("_key_ofs_changed"), &AnimationMultiTrackKeyEdit::_key_ofs_changed);
 		ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_script_from_inspector);
+		ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiTrackKeyEdit::_hide_metadata_from_inspector);
 		ClassDB::bind_method(D_METHOD("get_root_path"), &AnimationMultiTrackKeyEdit::get_root_path);
 		ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiTrackKeyEdit::_dont_undo_redo);
 		ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiTrackKeyEdit::_is_read_only);

+ 13 - 1
editor/editor_inspector.cpp

@@ -2711,6 +2711,11 @@ void EditorInspector::update_tree() {
 				continue;
 			}
 
+			// Hide the "MultiNodeEdit" category for MultiNodeEdit.
+			if (Object::cast_to<MultiNodeEdit>(object) && p.name == "MultiNodeEdit") {
+				continue;
+			}
+
 			// Iterate over remaining properties. If no properties in category, skip the category.
 			List<PropertyInfo>::Element *N = E_property->next();
 			bool valid = true;
@@ -2819,6 +2824,11 @@ void EditorInspector::update_tree() {
 			continue;
 		}
 
+		if (p.name.begins_with("metadata/") && bool(object->call("_hide_metadata_from_inspector"))) {
+			// Hide metadata from inspector if required.
+			continue;
+		}
+
 		// Get the path for property.
 		String path = p.name;
 
@@ -3089,6 +3099,8 @@ void EditorInspector::update_tree() {
 			StringName classname = doc_name == "" ? object->get_class_name() : doc_name;
 			if (!object_class.is_empty()) {
 				classname = object_class;
+			} else if (Object::cast_to<MultiNodeEdit>(object)) {
+				classname = Object::cast_to<MultiNodeEdit>(object)->get_edited_class_name();
 			}
 
 			StringName propname = property_prefix + p.name;
@@ -3242,7 +3254,7 @@ void EditorInspector::update_tree() {
 		}
 	}
 
-	if (!hide_metadata) {
+	if (!hide_metadata && !object->call("_hide_metadata_from_inspector")) {
 		// Add 4px of spacing between the "Add Metadata" button and the content above it.
 		Control *spacer = memnew(Control);
 		spacer->set_custom_minimum_size(Size2(0, 4) * EDSCALE);

+ 17 - 9
editor/editor_path.cpp

@@ -33,6 +33,7 @@
 #include "editor/editor_data.h"
 #include "editor/editor_node.h"
 #include "editor/editor_scale.h"
+#include "editor/multi_node_edit.h"
 
 void EditorPath::_add_children_to_popup(Object *p_obj, int p_depth) {
 	if (p_depth > 8) {
@@ -121,14 +122,22 @@ void EditorPath::update_path() {
 			continue;
 		}
 
-		Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(obj);
+		Ref<Texture2D> icon;
+		if (Object::cast_to<MultiNodeEdit>(obj)) {
+			icon = EditorNode::get_singleton()->get_class_icon(Object::cast_to<MultiNodeEdit>(obj)->get_edited_class_name());
+		} else {
+			icon = EditorNode::get_singleton()->get_object_icon(obj);
+		}
+
 		if (icon.is_valid()) {
 			current_object_icon->set_texture(icon);
 		}
 
 		if (i == history->get_path_size() - 1) {
 			String name;
-			if (Object::cast_to<Resource>(obj)) {
+			if (obj->has_method("_get_editor_name")) {
+				name = obj->call("_get_editor_name");
+			} else if (Object::cast_to<Resource>(obj)) {
 				Resource *r = Object::cast_to<Resource>(obj);
 				if (r->get_path().is_resource_file()) {
 					name = r->get_path().get_file();
@@ -149,7 +158,7 @@ void EditorPath::update_path() {
 				name = obj->get_class();
 			}
 
-			current_object_label->set_text(" " + name); // An extra space so the text is not too close of the icon.
+			current_object_label->set_text(name);
 			set_tooltip_text(obj->get_class());
 		}
 	}
@@ -161,12 +170,12 @@ void EditorPath::clear_path() {
 
 	current_object_label->set_text("");
 	current_object_icon->set_texture(nullptr);
-	sub_objects_icon->set_visible(false);
+	sub_objects_icon->hide();
 }
 
 void EditorPath::enable_path() {
 	set_disabled(false);
-	sub_objects_icon->set_visible(true);
+	sub_objects_icon->show();
 }
 
 void EditorPath::_id_pressed(int p_idx) {
@@ -186,7 +195,7 @@ void EditorPath::_notification(int p_what) {
 		case NOTIFICATION_THEME_CHANGED: {
 			update_path();
 
-			sub_objects_icon->set_texture(get_theme_icon(SNAME("select_arrow"), SNAME("Tree")));
+			sub_objects_icon->set_texture(get_theme_icon(SNAME("arrow"), SNAME("OptionButton")));
 			current_object_label->add_theme_font_override("font", get_theme_font(SNAME("main"), SNAME("EditorFonts")));
 		} break;
 
@@ -216,13 +225,12 @@ EditorPath::EditorPath(EditorSelectionHistory *p_history) {
 	main_hb->add_child(current_object_icon);
 
 	current_object_label = memnew(Label);
-	current_object_label->set_clip_text(true);
-	current_object_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT);
+	current_object_label->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS);
 	current_object_label->set_h_size_flags(SIZE_EXPAND_FILL);
 	main_hb->add_child(current_object_label);
 
 	sub_objects_icon = memnew(TextureRect);
-	sub_objects_icon->set_visible(false);
+	sub_objects_icon->hide();
 	sub_objects_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
 	main_hb->add_child(sub_objects_icon);
 

+ 6 - 8
editor/inspector_dock.cpp

@@ -311,7 +311,6 @@ void InspectorDock::_prepare_history() {
 
 	history_menu->get_popup()->clear();
 
-	Ref<Texture2D> base_icon = get_theme_icon(SNAME("Object"), SNAME("EditorIcons"));
 	HashSet<ObjectID> already;
 	for (int i = editor_history->get_history_len() - 1; i >= history_to; i--) {
 		ObjectID id = editor_history->get_history_obj(i);
@@ -325,13 +324,12 @@ void InspectorDock::_prepare_history() {
 
 		already.insert(id);
 
-		Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(obj, "");
-		if (icon.is_null()) {
-			icon = base_icon;
-		}
+		Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(obj, "Object");
 
 		String text;
-		if (Object::cast_to<Resource>(obj)) {
+		if (obj->has_method("_get_editor_name")) {
+			text = obj->call("_get_editor_name");
+		} else if (Object::cast_to<Resource>(obj)) {
 			Resource *r = Object::cast_to<Resource>(obj);
 			if (r->get_path().is_resource_file()) {
 				text = r->get_path().get_file();
@@ -349,14 +347,14 @@ void InspectorDock::_prepare_history() {
 		}
 
 		if (i == editor_history->get_history_pos() && current) {
-			text = "[" + text + "]";
+			text += " " + TTR("(Current)");
 		}
 		history_menu->get_popup()->add_icon_item(icon, text, i);
 	}
 }
 
 void InspectorDock::_select_history(int p_idx) {
-	//push it to the top, it is not correct, but it's more useful
+	// Push it to the top, it is not correct, but it's more useful.
 	ObjectID id = EditorNode::get_singleton()->get_editor_selection_history()->get_history_obj(p_idx);
 	Object *obj = ObjectDB::get_instance(id);
 	if (!obj) {

+ 128 - 20
editor/multi_node_edit.cpp

@@ -46,7 +46,7 @@ bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value,
 
 	String name = p_name;
 
-	if (name == "scripts") { // script set is intercepted at object level (check Variant Object::get() ) ,so use a different name
+	if (name == "scripts") { // Script set is intercepted at object level (check Variant Object::get()), so use a different name.
 		name = "script";
 	}
 
@@ -57,13 +57,9 @@ bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value,
 
 	Ref<EditorUndoRedoManager> &ur = EditorNode::get_undo_redo();
 
-	ur->create_action(TTR("MultiNode Set") + " " + String(name), UndoRedo::MERGE_ENDS);
+	ur->create_action(vformat(TTR("Set %s on %d nodes"), name, get_node_count()), UndoRedo::MERGE_ENDS);
 	for (const NodePath &E : nodes) {
-		if (!es->has_node(E)) {
-			continue;
-		}
-
-		Node *n = es->get_node(E);
+		Node *n = es->get_node_or_null(E);
 		if (!n) {
 			continue;
 		}
@@ -100,16 +96,12 @@ bool MultiNodeEdit::_get(const StringName &p_name, Variant &r_ret) const {
 	}
 
 	String name = p_name;
-	if (name == "scripts") { // script set is intercepted at object level (check Variant Object::get() ) ,so use a different name
+	if (name == "scripts") { // Script set is intercepted at object level (check Variant Object::get()), so use a different name.
 		name = "script";
 	}
 
 	for (const NodePath &E : nodes) {
-		if (!es->has_node(E)) {
-			continue;
-		}
-
-		const Node *n = es->get_node(E);
+		const Node *n = es->get_node_or_null(E);
 		if (!n) {
 			continue;
 		}
@@ -137,11 +129,7 @@ void MultiNodeEdit::_get_property_list(List<PropertyInfo> *p_list) const {
 	List<PLData *> data_list;
 
 	for (const NodePath &E : nodes) {
-		if (!es->has_node(E)) {
-			continue;
-		}
-
-		Node *n = es->get_node(E);
+		Node *n = es->get_node_or_null(E);
 		if (!n) {
 			continue;
 		}
@@ -151,7 +139,7 @@ void MultiNodeEdit::_get_property_list(List<PropertyInfo> *p_list) const {
 
 		for (const PropertyInfo &F : plist) {
 			if (F.name == "script") {
-				continue; //added later manually, since this is intercepted before being set (check Variant Object::get() )
+				continue; // Added later manually, since this is intercepted before being set (check Variant Object::get()).
 			}
 			if (!usage.has(F.name)) {
 				PLData pld;
@@ -161,7 +149,7 @@ void MultiNodeEdit::_get_property_list(List<PropertyInfo> *p_list) const {
 				data_list.push_back(usage.getptr(F.name));
 			}
 
-			// Make sure only properties with the same exact PropertyInfo data will appear
+			// Make sure only properties with the same exact PropertyInfo data will appear.
 			if (usage[F.name].info == F) {
 				usage[F.name].uses++;
 			}
@@ -179,6 +167,66 @@ void MultiNodeEdit::_get_property_list(List<PropertyInfo> *p_list) const {
 	p_list->push_back(PropertyInfo(Variant::OBJECT, "scripts", PROPERTY_HINT_RESOURCE_TYPE, "Script"));
 }
 
+String MultiNodeEdit::_get_editor_name() const {
+	return vformat(TTR("%s (%d Selected)"), get_edited_class_name(), get_node_count());
+}
+
+bool MultiNodeEdit::_property_can_revert(const StringName &p_name) const {
+	Node *es = EditorNode::get_singleton()->get_edited_scene();
+	if (!es) {
+		return false;
+	}
+
+	if (ClassDB::has_property(get_edited_class_name(), p_name)) {
+		StringName class_name;
+		for (const NodePath &E : nodes) {
+			Node *node = es->get_node_or_null(E);
+			if (!node) {
+				continue;
+			}
+
+			class_name = node->get_class_name();
+		}
+
+		Variant default_value = ClassDB::class_get_default_property_value(class_name, p_name);
+		for (const NodePath &E : nodes) {
+			Node *node = es->get_node_or_null(E);
+			if (!node) {
+				continue;
+			}
+
+			if (node->get(p_name) != default_value) {
+				// A node that doesn't have the default value has been found, so show the revert button.
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	// Don't show the revert button if the edited class doesn't have the property.
+	return false;
+}
+
+bool MultiNodeEdit::_property_get_revert(const StringName &p_name, Variant &r_property) const {
+	Node *es = EditorNode::get_singleton()->get_edited_scene();
+	if (!es) {
+		return false;
+	}
+
+	for (const NodePath &E : nodes) {
+		Node *node = es->get_node_or_null(E);
+		if (!node) {
+			continue;
+		}
+
+		r_property = ClassDB::class_get_default_property_value(node->get_class_name(), p_name);
+		return true;
+	}
+
+	return false;
+}
+
 void MultiNodeEdit::add_node(const NodePath &p_node) {
 	nodes.push_back(p_node);
 }
@@ -192,9 +240,69 @@ NodePath MultiNodeEdit::get_node(int p_index) const {
 	return nodes[p_index];
 }
 
+StringName MultiNodeEdit::get_edited_class_name() const {
+	Node *es = EditorNode::get_singleton()->get_edited_scene();
+	if (!es) {
+		return SNAME("Node");
+	}
+
+	// Get the class name of the first node.
+	StringName class_name;
+	for (const NodePath &E : nodes) {
+		Node *node = es->get_node_or_null(E);
+		if (!node) {
+			continue;
+		}
+
+		class_name = node->get_class_name();
+		break;
+	}
+
+	if (class_name == StringName()) {
+		return SNAME("Node");
+	}
+
+	bool check_again = true;
+	while (check_again) {
+		check_again = false;
+
+		if (class_name == SNAME("Node") || class_name == StringName()) {
+			// All nodes inherit from Node, so no need to continue checking.
+			return SNAME("Node");
+		}
+
+		// Check that all nodes inherit from class_name.
+		for (const NodePath &E : nodes) {
+			Node *node = es->get_node_or_null(E);
+			if (!node) {
+				continue;
+			}
+
+			const StringName node_class_name = node->get_class_name();
+			if (class_name == node_class_name || ClassDB::is_parent_class(node_class_name, class_name)) {
+				// class_name is the same or a parent of the node's class.
+				continue;
+			}
+
+			// class_name is not a parent of the node's class, so check again with the parent class.
+			class_name = ClassDB::get_parent_class(class_name);
+			check_again = true;
+			break;
+		}
+	}
+
+	return class_name;
+}
+
 void MultiNodeEdit::set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field) {
 	_set_impl(p_property, p_value, p_field);
 }
 
+void MultiNodeEdit::_bind_methods() {
+	ClassDB::bind_method("_hide_script_from_inspector", &MultiNodeEdit::_hide_script_from_inspector);
+	ClassDB::bind_method("_hide_metadata_from_inspector", &MultiNodeEdit::_hide_metadata_from_inspector);
+	ClassDB::bind_method("_get_editor_name", &MultiNodeEdit::_get_editor_name);
+}
+
 MultiNodeEdit::MultiNodeEdit() {
 }

+ 10 - 0
editor/multi_node_edit.h

@@ -45,15 +45,25 @@ class MultiNodeEdit : public RefCounted {
 	bool _set_impl(const StringName &p_name, const Variant &p_value, const String &p_field);
 
 protected:
+	static void _bind_methods();
+
 	bool _set(const StringName &p_name, const Variant &p_value);
 	bool _get(const StringName &p_name, Variant &r_ret) const;
 	void _get_property_list(List<PropertyInfo> *p_list) const;
 
 public:
+	bool _hide_script_from_inspector() { return true; }
+	bool _hide_metadata_from_inspector() { return true; }
+
+	bool _property_can_revert(const StringName &p_name) const;
+	bool _property_get_revert(const StringName &p_name, Variant &r_property) const;
+	String _get_editor_name() const;
+
 	void add_node(const NodePath &p_node);
 
 	int get_node_count() const;
 	NodePath get_node(int p_index) const;
+	StringName get_edited_class_name() const;
 
 	void set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field);