Browse Source

Merge pull request #99700 from hpvb/scene_tree_editor_performance

Improve Scene Tree editor performance
Rémi Verschelde 8 months ago
parent
commit
08508d2e01

+ 1 - 0
core/object/object.h

@@ -89,6 +89,7 @@ enum PropertyHint {
 	PROPERTY_HINT_DICTIONARY_TYPE,
 	PROPERTY_HINT_TOOL_BUTTON,
 	PROPERTY_HINT_ONESHOT, ///< the property will be changed by self after setting, such as AudioStreamPlayer.playing, Particles.emitting.
+	PROPERTY_HINT_NO_NODEPATH, /// < this property will not contain a NodePath, regardless of type (Array, Dictionary, List, etc.). Needed for SceneTreeDock.
 	PROPERTY_HINT_MAX,
 };
 

+ 1 - 1
doc/classes/@GlobalScope.xml

@@ -2943,7 +2943,7 @@
 		<constant name="PROPERTY_HINT_ONESHOT" value="40" enum="PropertyHint">
 			Hints that a property will be changed on its own after setting, such as [member AudioStreamPlayer.playing] or [member GPUParticles3D.emitting].
 		</constant>
-		<constant name="PROPERTY_HINT_MAX" value="41" enum="PropertyHint">
+		<constant name="PROPERTY_HINT_MAX" value="42" enum="PropertyHint">
 			Represents the size of the [enum PropertyHint] enum.
 		</constant>
 		<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags" is_bitfield="true">

+ 5 - 0
doc/classes/Node.xml

@@ -1072,6 +1072,11 @@
 				Emitted when the node's editor description field changed.
 			</description>
 		</signal>
+		<signal name="editor_state_changed">
+			<description>
+				Emitted when an attribute of the node that is relevant to the editor is changed. Only emitted in the editor.
+			</description>
+		</signal>
 		<signal name="ready">
 			<description>
 				Emitted when the node is considered ready, after [method _ready] is called.

+ 6 - 0
doc/classes/TreeItem.xml

@@ -36,6 +36,12 @@
 				Calls the [param method] on the actual TreeItem and its children recursively. Pass parameters as a comma separated list.
 			</description>
 		</method>
+		<method name="clear_buttons">
+			<return type="void" />
+			<description>
+				Removes all buttons from all columns of this item.
+			</description>
+		</method>
 		<method name="clear_custom_bg_color">
 			<return type="void" />
 			<param index="0" name="column" type="int" />

+ 1 - 0
editor/connections_dialog.cpp

@@ -735,6 +735,7 @@ ConnectDialog::ConnectDialog() {
 	from_signal->set_editable(false);
 
 	tree = memnew(SceneTreeEditor(false));
+	tree->set_update_when_invisible(false);
 	tree->set_connecting_signal(true);
 	tree->set_show_enabled_subscene(true);
 	tree->set_v_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND);

+ 575 - 127
editor/gui/scene_tree_editor.cpp

@@ -61,7 +61,7 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
 	}
 
 	if (connect_to_script_mode) {
-		return; //don't do anything in this mode
+		return; // Don't do anything in this mode.
 	}
 
 	TreeItem *item = Object::cast_to<TreeItem>(p_item);
@@ -152,7 +152,8 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
 			const String line = all_warnings.substr(start, end - start);
 			lines.append(line);
 		}
-		all_warnings = String("\n").join(lines).indent("    ").replace(U"    •", U"\n•").substr(2); // We don't want the first two newlines.
+		// 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->popup_centered();
@@ -217,12 +218,35 @@ void SceneTreeEditor::_toggle_visible(Node *p_node) {
 	}
 }
 
-void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
+void SceneTreeEditor::_update_node_path(Node *p_node, bool p_recursive) {
 	if (!p_node) {
 		return;
 	}
 
-	// only owned nodes are editable, since nodes can create their own (manually owned) child nodes,
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node);
+	if (!I) {
+		return;
+	}
+
+	I->value.item->set_metadata(0, p_node->get_path());
+
+	if (!p_recursive) {
+		return;
+	}
+
+	int cc = p_node->get_child_count(false);
+	for (int i = 0; i < cc; i++) {
+		Node *c = p_node->get_child(i, false);
+		_update_node_path(c, p_recursive);
+	}
+}
+
+void SceneTreeEditor::_update_node_subtree(Node *p_node, TreeItem *p_parent, bool p_force) {
+	if (!p_node) {
+		return;
+	}
+
+	// Only owned nodes are editable, since nodes can create their own (manually owned) child nodes,
 	// which the editor needs not to know about.
 
 	bool part_of_subscene = false;
@@ -230,42 +254,152 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 	if (!display_foreign && p_node->get_owner() != get_scene_node() && p_node != get_scene_node()) {
 		if ((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()))) {
 			part_of_subscene = true;
-			//allow
+			// Allow.
 		} else {
+			// Stale node, remove recursively.
+			node_cache.remove(p_node, true);
 			return;
 		}
 	} else {
 		part_of_subscene = p_node != get_scene_node() && get_scene_node()->get_scene_inherited_state().is_valid() && get_scene_node()->get_scene_inherited_state()->find_node_by_path(get_scene_node()->get_path_to(p_node)) >= 0;
 	}
 
-	TreeItem *item = tree->create_item(p_parent);
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node);
+	TreeItem *item = nullptr;
+
+	bool is_new = false;
+
+	if (I) {
+		item = I->value.item;
+		TreeItem *current_parent = item->get_parent();
+
+		// Our parent might be re-created because of a changed type.
+		if (p_parent && p_parent != current_parent) {
+			if (current_parent) {
+				current_parent->remove_child(item);
+			}
+			p_parent->add_child(item);
+			I->value.removed = false;
+			_move_node_item(p_parent, I);
+		}
+
+		if (I->value.has_moved_children) {
+			_move_node_children(I);
+		}
+	} else {
+		int index = -1;
+		// Check to see if there is a root node for us to reuse.
+		if (!p_parent) {
+			item = tree->get_root();
+			if (!item) {
+				item = tree->create_item(nullptr);
+				index = 0;
+			}
+		} else {
+			index = p_node->get_index(false);
+			item = tree->create_item(p_parent, index);
+		}
+
+		I = node_cache.add(p_node, item);
+		I->value.index = index;
+		is_new = true;
+	}
+
+	if (!(p_force || I->value.dirty)) {
+		// Nothing to do.
+		return;
+	}
 
-	item->set_text(0, p_node->get_name());
-	item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING);
-	if (can_rename && !part_of_subscene) {
-		item->set_editable(0, true);
+	_update_node(p_node, item, part_of_subscene);
+	I->value.dirty = false;
+	I->value.can_process = p_node->can_process();
+
+	// Force update all our children if we are new or if we were forced to update.
+	bool force_update_children = p_force || is_new;
+	// Update all our children.
+	for (int i = 0; i < p_node->get_child_count(false); i++) {
+		_update_node_subtree(p_node->get_child(i, false), item, force_update_children);
+	}
+
+	if (valid_types.size()) {
+		bool valid = false;
+		for (const StringName &E : valid_types) {
+			if (p_node->is_class(E) ||
+					EditorNode::get_singleton()->is_object_of_custom_type(p_node, E)) {
+				valid = true;
+				break;
+			} else {
+				Ref<Script> node_script = p_node->get_script();
+				while (node_script.is_valid()) {
+					if (node_script->get_path() == E) {
+						valid = true;
+						break;
+					}
+					node_script = node_script->get_base_script();
+				}
+				if (valid) {
+					break;
+				}
+			}
+		}
+
+		if (!valid) {
+			_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+			item->set_selectable(0, false);
+			item->deselect(0);
+			if (selected == p_node) {
+				set_selected(nullptr, false);
+			}
+		}
+	}
+}
+
+void SceneTreeEditor::_update_node(Node *p_node, TreeItem *p_item, bool p_part_of_subscene) {
+	// Reset item properties that are not explicitly set in the default case.
+	p_item->clear_buttons();
+	p_item->remove_meta(SNAME("custom_color"));
+	p_item->clear_custom_color(0);
+	p_item->set_selectable(0, true);
+
+	p_item->set_text(0, p_node->get_name());
+	p_item->set_text_overrun_behavior(0, TextServer::OVERRUN_NO_TRIMMING);
+	if (can_rename && !p_part_of_subscene) {
+		p_item->set_editable(0, true);
 	}
 
-	item->set_selectable(0, true);
 	if (can_rename) {
 		bool collapsed = p_node->is_displayed_folded();
 		if (collapsed) {
-			item->set_collapsed(true);
+			p_item->set_collapsed(true);
 		}
 	}
 
 	Ref<Texture2D> icon = EditorNode::get_singleton()->get_object_icon(p_node, "Node");
-	item->set_icon(0, icon);
-	item->set_metadata(0, p_node->get_path());
+	p_item->set_icon(0, icon);
+	p_item->set_metadata(0, p_node->get_path());
+
+	if (!p_node->is_connected("child_order_changed", callable_mp(this, &SceneTreeEditor::_node_child_order_changed))) {
+		p_node->connect("child_order_changed", callable_mp(this, &SceneTreeEditor::_node_child_order_changed).bind(p_node));
+	}
+
+	if (!p_node->is_connected("editor_state_changed", callable_mp(this, &SceneTreeEditor::_node_editor_state_changed))) {
+		p_node->connect("editor_state_changed", callable_mp(this, &SceneTreeEditor::_node_editor_state_changed).bind(p_node));
+	}
+
+	if (connecting_signal || (can_open_instance && is_scene_tree_dock)) {
+		if (!p_node->is_connected(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
+			p_node->connect(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed).bind(p_node));
+		}
+	}
 
 	if (connecting_signal) {
 		// Add script icons for all scripted nodes.
 		Ref<Script> scr = p_node->get_script();
 		if (scr.is_valid()) {
-			item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT);
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT);
 			if (EditorNode::get_singleton()->get_object_custom_type_base(p_node) == scr) {
 				// Disable button on custom scripts (pure visual cue).
-				item->set_button_disabled(0, item->get_button_count(0) - 1, true);
+				p_item->set_button_disabled(0, p_item->get_button_count(0) - 1, true);
 			}
 		}
 	}
@@ -276,8 +410,8 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 		Ref<Script> scr = p_node->get_script();
 		bool has_custom_script = scr.is_valid() && EditorNode::get_singleton()->get_object_custom_type_base(p_node) == scr;
 		if (scr.is_null() || has_custom_script) {
-			_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
-			item->set_selectable(0, false);
+			_set_item_custom_color(p_item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+			p_item->set_selectable(0, false);
 
 			accent.a *= 0.7;
 		}
@@ -287,36 +421,36 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 			if (connecting_signal) {
 				node_name += " " + TTR("(Connecting From)");
 			}
-			item->set_text(0, node_name);
-			_set_item_custom_color(item, accent);
+			p_item->set_text(0, node_name);
+			_set_item_custom_color(p_item, accent);
 		}
-	} else if (part_of_subscene) {
+	} else if (p_part_of_subscene) {
 		if (valid_types.size() == 0) {
-			_set_item_custom_color(item, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
+			_set_item_custom_color(p_item, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
 		}
 	} else if (marked.has(p_node)) {
 		String node_name = p_node->get_name();
 		if (connecting_signal) {
 			node_name += " " + TTR("(Connecting From)");
 		}
-		item->set_text(0, node_name);
-		item->set_selectable(0, marked_selectable);
-		_set_item_custom_color(item, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
+		p_item->set_text(0, node_name);
+		p_item->set_selectable(0, marked_selectable);
+		_set_item_custom_color(p_item, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
 	} else if (!p_node->can_process()) {
-		_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
+		_set_item_custom_color(p_item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
 	} else if (!marked_selectable && !marked_children_selectable) {
 		Node *node = p_node;
 		while (node) {
 			if (marked.has(node)) {
-				item->set_selectable(0, false);
-				_set_item_custom_color(item, get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
+				p_item->set_selectable(0, false);
+				_set_item_custom_color(p_item, get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
 				break;
 			}
 			node = node->get_parent();
 		}
 	}
 
-	if (can_rename) { //should be can edit..
+	if (can_rename) { // TODO Should be can edit..
 
 		const PackedStringArray warnings = p_node->get_configuration_warnings();
 		const int num_warnings = warnings.size();
@@ -340,7 +474,7 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 				all_warnings.remove_at(0); // With only one warning, two newlines do not look great.
 			}
 
-			item->add_button(0, get_editor_theme_icon(warning_icon), BUTTON_WARNING, false, TTR("Node configuration warning:") + all_warnings);
+			p_item->add_button(0, get_editor_theme_icon(warning_icon), BUTTON_WARNING, false, TTR("Node configuration warning:") + all_warnings);
 		}
 
 		if (p_node->is_unique_name_in_owner()) {
@@ -349,7 +483,7 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 			if (!disabled) {
 				button_text += "\n" + TTR("Click to disable this.");
 			}
-			item->add_button(0, get_editor_theme_icon(SNAME("SceneUniqueName")), BUTTON_UNIQUE, disabled, button_text);
+			p_item->add_button(0, get_editor_theme_icon(SNAME("SceneUniqueName")), BUTTON_UNIQUE, disabled, button_text);
 		}
 
 		int num_connections = p_node->get_persistent_signal_connection_count();
@@ -394,24 +528,21 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 
 		if (num_connections >= 1 || num_groups >= 1) {
 			msg_temp += msg_temp_end;
-			item->add_button(0, icon_temp, signal_temp, false, msg_temp);
+			p_item->add_button(0, icon_temp, signal_temp, false, msg_temp);
 		}
 	}
 
 	{
-		_update_node_tooltip(p_node, item);
+		_update_node_tooltip(p_node, p_item);
 		Callable delay_update_tooltip = callable_mp(this, &SceneTreeEditor::_queue_update_node_tooltip);
 		if (p_node->is_connected("editor_description_changed", delay_update_tooltip)) {
 			p_node->disconnect("editor_description_changed", delay_update_tooltip);
 		}
-		p_node->connect("editor_description_changed", delay_update_tooltip.bind(item));
+		p_node->connect("editor_description_changed", delay_update_tooltip.bind(p_item));
 	}
 
-	if (can_open_instance && is_scene_tree_dock) { // Show buttons only when necessary (SceneTreeDock) to avoid crashes.
-		if (!p_node->is_connected(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
-			p_node->connect(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed).bind(p_node));
-		}
-
+	// Show buttons only when necessary (SceneTreeDock) to avoid crashes.
+	if (can_open_instance && is_scene_tree_dock) {
 		Ref<Script> scr = p_node->get_script();
 		if (!scr.is_null()) {
 			String additional_notes;
@@ -425,84 +556,61 @@ void SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 				additional_notes += "\n" + TTR("This script is a custom type.");
 				button_color.a = 0.5;
 			}
-			item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT, false, TTR("Open Script:") + " " + scr->get_path() + additional_notes);
-			item->set_button_color(0, item->get_button_count(0) - 1, button_color);
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Script")), BUTTON_SCRIPT, false, TTR("Open Script:") + " " + scr->get_path() + additional_notes);
+			p_item->set_button_color(0, p_item->get_button_count(0) - 1, button_color);
 		}
 
 		if (p_node->has_meta("_edit_lock_")) {
-			item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it."));
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Lock")), BUTTON_LOCK, false, TTR("Node is locked.\nClick to unlock it."));
 		}
 		if (p_node->has_meta("_edit_group_")) {
-			item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable."));
+			p_item->add_button(0, get_editor_theme_icon(SNAME("Group")), BUTTON_GROUP, false, TTR("Children are not selectable.\nClick to make them selectable."));
 		}
 
 		if (p_node->has_method("is_visible") && p_node->has_method("set_visible") && p_node->has_signal(SceneStringName(visibility_changed))) {
 			bool is_visible = p_node->call("is_visible");
 			if (is_visible) {
-				item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+				p_item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityVisible")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
 			} else {
-				item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
+				p_item->add_button(0, get_editor_theme_icon(SNAME("GuiVisibilityHidden")), BUTTON_VISIBILITY, false, TTR("Toggle Visibility"));
 			}
 			const Callable vis_changed = callable_mp(this, &SceneTreeEditor::_node_visibility_changed);
 			if (!p_node->is_connected(SceneStringName(visibility_changed), vis_changed)) {
 				p_node->connect(SceneStringName(visibility_changed), vis_changed.bind(p_node));
 			}
-			_update_visibility_color(p_node, item);
+			_update_visibility_color(p_node, p_item);
 		}
 
 		if (p_node->is_class("AnimationMixer")) {
 			bool is_pinned = AnimationPlayerEditor::get_singleton()->get_editing_node() == p_node && AnimationPlayerEditor::get_singleton()->is_pinned();
 
 			if (is_pinned) {
-				item->add_button(0, get_editor_theme_icon(SNAME("Pin")), BUTTON_PIN, false, TTR("AnimationPlayer is pinned.\nClick to unpin."));
+				p_item->add_button(0, get_editor_theme_icon(SNAME("Pin")), BUTTON_PIN, false, TTR("AnimationPlayer is pinned.\nClick to unpin."));
 			}
 		}
 	}
 
 	if (editor_selection) {
 		if (editor_selection->is_selected(p_node)) {
-			item->select(0);
+			p_item->select(0);
 		}
 	}
 
 	if (selected == p_node) {
 		if (!editor_selection) {
-			item->select(0);
+			p_item->select(0);
 		}
-		item->set_as_cursor(0);
+		p_item->set_as_cursor(0);
 	}
+}
 
-	for (int i = 0; i < p_node->get_child_count(); i++) {
-		_add_nodes(p_node->get_child(i), item);
+void SceneTreeEditor::_update_if_clean() {
+	if (tree_dirty) {
+		return;
 	}
 
-	if (valid_types.size()) {
-		bool valid = false;
-		for (const StringName &E : valid_types) {
-			if (p_node->is_class(E) ||
-					EditorNode::get_singleton()->is_object_of_custom_type(p_node, E)) {
-				valid = true;
-				break;
-			} else {
-				Ref<Script> node_script = p_node->get_script();
-				while (node_script.is_valid()) {
-					if (node_script->get_path() == E) {
-						valid = true;
-						break;
-					}
-					node_script = node_script->get_base_script();
-				}
-				if (valid) {
-					break;
-				}
-			}
-		}
-
-		if (!valid) {
-			_set_item_custom_color(item, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
-			item->set_selectable(0, false);
-		}
-	}
+	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
+	tree_dirty = true;
 }
 
 void SceneTreeEditor::_queue_update_node_tooltip(Node *p_node, TreeItem *p_item) {
@@ -550,6 +658,13 @@ void SceneTreeEditor::_update_node_tooltip(Node *p_node, TreeItem *p_item) {
 }
 
 void SceneTreeEditor::_node_visibility_changed(Node *p_node) {
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (!I) {
+		// We leave these signals connected when switching tabs.
+		// If the node is not in cache it was for a different tab.
+		return;
+	}
+
 	if (!p_node || (p_node != get_scene_node() && !p_node->get_owner())) {
 		return;
 	}
@@ -599,45 +714,155 @@ void SceneTreeEditor::_set_item_custom_color(TreeItem *p_item, Color p_color) {
 }
 
 void SceneTreeEditor::_node_script_changed(Node *p_node) {
-	if (tree_dirty) {
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (!I) {
+		// We leave these signals connected when switching tabs.
+		// If the node is not in cache it was for a different tab.
 		return;
 	}
 
-	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
-	tree_dirty = true;
+	node_cache.mark_dirty(p_node);
+
+	_update_if_clean();
+}
+
+void SceneTreeEditor::_move_node_children(HashMap<Node *, CachedNode>::Iterator &p_I) {
+	TreeItem *item = p_I->value.item;
+	Node *node = p_I->key;
+	int cc = node->get_child_count(false);
+
+	for (int i = 0; i < cc; i++) {
+		HashMap<Node *, CachedNode>::Iterator CI = node_cache.get(node->get_child(i, false));
+		if (CI) {
+			_move_node_item(item, CI);
+		}
+	}
+
+	p_I->value.has_moved_children = false;
+}
+
+void SceneTreeEditor::_move_node_item(TreeItem *p_parent, HashMap<Node *, CachedNode>::Iterator &p_I) {
+	if (!p_parent) {
+		return;
+	}
+
+	Node *node = p_I->key;
+
+	int current_node_index = node->get_index(false);
+	int current_item_index = -1;
+	TreeItem *item = p_I->value.item;
+
+	if (item->get_parent() != p_parent) {
+		TreeItem *p = item->get_parent();
+		if (p) {
+			item->get_parent()->remove_child(item);
+		}
+		p_parent->add_child(item);
+		p_I->value.removed = false;
+		current_item_index = p_parent->get_child_count() - 1;
+		p_I->value.index = current_item_index;
+	}
+
+	if (p_I->value.index != current_node_index) {
+		// If we just re-parented we know our index.
+		if (current_item_index == -1) {
+			current_item_index = item->get_index();
+		}
+
+		// Are we already in the right place?
+		if (current_node_index == current_item_index) {
+			p_I->value.index = current_node_index;
+			return;
+		}
+
+		// Are we the first node?
+		if (current_node_index == 0) {
+			// There has to be at least 1 other node, otherwise we would not have gotten here.
+			TreeItem *neighbor_item = p_parent->get_child(0);
+			item->move_before(neighbor_item);
+		} else {
+			TreeItem *neighbor_item = p_parent->get_child(CLAMP(current_node_index - 1, 0, p_parent->get_child_count() - 1));
+			item->move_after(neighbor_item);
+		}
+
+		p_I->value.index = current_node_index;
+	}
+}
+
+void SceneTreeEditor::_node_child_order_changed(Node *p_node) {
+	// Do not try to change children on nodes currently marked for removal.
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (I) {
+		node_cache.mark_dirty(I->key);
+		I->value.has_moved_children = true;
+	}
+
+	_update_if_clean();
+}
+
+void SceneTreeEditor::_node_editor_state_changed(Node *p_node) {
+	node_cache.mark_dirty(p_node);
+	HashMap<Node *, CachedNode>::Iterator I = node_cache.get(p_node, false);
+	if (I) {
+		if (p_node->is_inside_tree() && p_node->can_process() != I->value.can_process) {
+			// All our children also change process mode.
+			node_cache.mark_children_dirty(p_node, true);
+		}
+	}
+
+	_update_if_clean();
+}
+
+void SceneTreeEditor::_node_added(Node *p_node) {
+	if (!get_scene_node()) {
+		return;
+	}
+
+	if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
+		return;
+	}
+
+	node_cache.mark_dirty(p_node);
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_node_removed(Node *p_node) {
 	if (EditorNode::get_singleton()->is_exiting()) {
-		return; //speed up exit
+		return; // Speed up exit.
 	}
 
-	if (p_node->is_connected(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed))) {
-		p_node->disconnect(CoreStringName(script_changed), callable_mp(this, &SceneTreeEditor::_node_script_changed));
+	if (EditorNode::get_singleton()->is_changing_scene()) {
+		return; // Switching tabs we will be destroying node cache anyway.
 	}
 
-	if (p_node->has_signal(SceneStringName(visibility_changed))) {
-		if (p_node->is_connected(SceneStringName(visibility_changed), callable_mp(this, &SceneTreeEditor::_node_visibility_changed))) {
-			p_node->disconnect(SceneStringName(visibility_changed), callable_mp(this, &SceneTreeEditor::_node_visibility_changed));
-		}
+	if (!get_scene_node()) {
+		return;
 	}
 
-	if (p_node == selected) {
-		selected = nullptr;
+	if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
+		return;
 	}
+
+	node_cache.remove(p_node);
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_node_renamed(Node *p_node) {
+	if (!get_scene_node()) {
+		return;
+	}
+
 	if (p_node != get_scene_node() && !get_scene_node()->is_ancestor_of(p_node)) {
 		return;
 	}
 
+	node_cache.mark_dirty(p_node);
+	// Recursively update child node paths.
+	_update_node_path(p_node, true);
+
 	emit_signal(SNAME("node_renamed"));
 
-	if (!tree_dirty) {
-		callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
-		tree_dirty = true;
-	}
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) {
@@ -646,19 +871,49 @@ void SceneTreeEditor::_update_tree(bool p_scroll_to_selected) {
 		return;
 	}
 
+	if (!update_when_invisible && !is_visible_in_tree()) {
+		return;
+	}
+
 	if (tree->is_editing()) {
 		return;
 	}
 
 	updating_tree = true;
-	tree->clear();
+
 	last_hash = hash_djb2_one_64(0);
-	if (get_scene_node()) {
-		_add_nodes(get_scene_node(), nullptr);
+	Node *scene_node = get_scene_node();
+
+	if (node_cache.current_scene_node != scene_node) {
+		_reset();
+		node_cache.current_scene_node = scene_node;
+		node_cache.force_update = true;
+	}
+
+	if (node_cache.current_scene_node) {
+		// Handle pinning/unpinning the animation player only do this once per iteration.
+		Node *pinned_node = AnimationPlayerEditor::get_singleton()->get_editing_node();
+		// If pinned state changed, update the currently pinned node.
+		if (AnimationPlayerEditor::get_singleton()->is_pinned() != node_cache.current_has_pin) {
+			node_cache.current_has_pin = AnimationPlayerEditor::get_singleton()->is_pinned();
+			node_cache.mark_dirty(pinned_node);
+		}
+		// If the current pinned node changed update both the old and new node.
+		if (node_cache.current_pinned_node != pinned_node) {
+			node_cache.mark_dirty(pinned_node);
+			node_cache.mark_dirty(node_cache.current_pinned_node);
+			node_cache.current_pinned_node = pinned_node;
+		}
+
+		_update_node_subtree(get_scene_node(), nullptr, node_cache.force_update);
 		_compute_hash(get_scene_node(), last_hash);
+
+		node_cache.delete_pending();
 	}
+
 	updating_tree = false;
 	tree_dirty = false;
+	node_cache.force_update = false;
 
 	if (!filter.strip_edges().is_empty() || !show_all_nodes) {
 		_update_filter(nullptr, p_scroll_to_selected);
@@ -822,16 +1077,19 @@ bool SceneTreeEditor::_item_matches_all_terms(TreeItem *p_item, const PackedStri
 }
 
 void SceneTreeEditor::_compute_hash(Node *p_node, uint64_t &hash) {
-	hash = hash_djb2_one_64(p_node->get_instance_id(), hash);
-	if (p_node->get_parent()) {
-		hash = hash_djb2_one_64(p_node->get_parent()->get_instance_id(), hash); //so a reparent still produces a different hash
-	}
-
+	// Nodes are added and removed by Node* pointers.
+	hash = hash_djb2_one_64((ptrdiff_t)p_node, hash);
+	// This hash is non-commutative: if the node order changes so will the hash.
 	for (int i = 0; i < p_node->get_child_count(); i++) {
 		_compute_hash(p_node->get_child(i), hash);
 	}
 }
 
+void SceneTreeEditor::_reset() {
+	tree->clear();
+	node_cache.clear();
+}
+
 void SceneTreeEditor::_test_update_tree() {
 	pending_test_update = false;
 
@@ -840,20 +1098,20 @@ void SceneTreeEditor::_test_update_tree() {
 	}
 
 	if (tree_dirty) {
-		return; // don't even bother
+		return; // Don't even bother.
 	}
 
 	uint64_t hash = hash_djb2_one_64(0);
 	if (get_scene_node()) {
 		_compute_hash(get_scene_node(), hash);
 	}
-	//test hash
+
+	// Test hash.
 	if (hash == last_hash) {
-		return; // did not change
+		return; // Did not change.
 	}
 
-	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
-	tree_dirty = true;
+	_update_if_clean();
 }
 
 void SceneTreeEditor::_tree_process_mode_changed() {
@@ -863,11 +1121,13 @@ void SceneTreeEditor::_tree_process_mode_changed() {
 
 void SceneTreeEditor::_tree_changed() {
 	if (EditorNode::get_singleton()->is_exiting()) {
-		return; //speed up exit
+		return; // Speed up exit.
 	}
+
 	if (pending_test_update) {
 		return;
 	}
+
 	if (tree_dirty) {
 		return;
 	}
@@ -887,7 +1147,7 @@ void SceneTreeEditor::_selected_changed() {
 		return;
 	}
 
-	selected = get_node(np);
+	selected = n;
 
 	blocked++;
 	emit_signal(SNAME("node_selected"));
@@ -929,7 +1189,7 @@ void SceneTreeEditor::_cell_multi_selected(Object *p_object, int p_cell, bool p_
 		editor_selection->remove_node(n);
 	}
 
-	// Emitted "selected" in _selected_changed() when select single node, so select multiple node emit "changed"
+	// Emitted "selected" in _selected_changed() when select single node, so select multiple node emit "changed".
 	if (editor_selection->get_selected_nodes().size() > 1) {
 		emit_signal(SNAME("node_changed"));
 	}
@@ -948,6 +1208,7 @@ void SceneTreeEditor::_notification(int p_what) {
 		case NOTIFICATION_ENTER_TREE: {
 			get_tree()->connect("tree_changed", callable_mp(this, &SceneTreeEditor::_tree_changed));
 			get_tree()->connect("tree_process_mode_changed", callable_mp(this, &SceneTreeEditor::_tree_process_mode_changed));
+			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));
@@ -960,6 +1221,7 @@ void SceneTreeEditor::_notification(int p_what) {
 		case NOTIFICATION_EXIT_TREE: {
 			get_tree()->disconnect("tree_changed", callable_mp(this, &SceneTreeEditor::_tree_changed));
 			get_tree()->disconnect("tree_process_mode_changed", callable_mp(this, &SceneTreeEditor::_tree_process_mode_changed));
+			get_tree()->disconnect("node_added", callable_mp(this, &SceneTreeEditor::_node_added));
 			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));
@@ -969,6 +1231,9 @@ void SceneTreeEditor::_notification(int p_what) {
 		case NOTIFICATION_THEME_CHANGED: {
 			tree->add_theme_constant_override("icon_max_width", get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)));
 
+			// When we change theme we need to re-do everything.
+			_reset();
+
 			_update_tree();
 		} break;
 
@@ -986,10 +1251,15 @@ void SceneTreeEditor::_notification(int p_what) {
 					}
 				}
 
-				if (item) {
-					// Must wait until tree is properly sized before scrolling.
-					ObjectID item_id = item->get_instance_id();
-					callable_mp(this, &SceneTreeEditor::_tree_scroll_to_item).call_deferred(item_id);
+				bool has_item = item;
+
+				if (update_when_invisible) {
+					if (has_item) {
+						ObjectID item_id = item->get_instance_id();
+						callable_mp(this, &SceneTreeEditor::_tree_scroll_to_item).call_deferred(item_id);
+					}
+				} else {
+					callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(has_item);
 				}
 			}
 		} break;
@@ -1024,6 +1294,7 @@ void SceneTreeEditor::set_selected(Node *p_node, bool p_emit_selected) {
 	if (pending_test_update) {
 		_test_update_tree();
 	}
+
 	if (tree_dirty) {
 		_update_tree();
 	}
@@ -1101,7 +1372,8 @@ void SceneTreeEditor::rename_node(Node *p_node, const String &p_name, TreeItem *
 		}
 	}
 
-	// Trim leading/trailing whitespace to prevent node names from containing accidental whitespace, which would make it more difficult to get the node via `get_node()`.
+	// Trim leading/trailing whitespace to prevent node names from containing accidental whitespace,
+	// which would make it more difficult to get the node via `get_node()`.
 	new_name = new_name.strip_edges();
 	if (new_name.is_empty() && p_node->get_owner() != nullptr && !p_node->get_scene_file_path().is_empty()) {
 		// If name is empty and node is root of an instance, revert to the original name.
@@ -1129,7 +1401,8 @@ void SceneTreeEditor::rename_node(Node *p_node, const String &p_name, TreeItem *
 		return;
 	}
 
-	// We previously made sure name is not the same as current name so that it won't complain about already used unique name when not changing name.
+	// We previously made sure name is not the same as current name
+	// so that it won't complain about already used unique name when not changing name.
 	if (p_node->is_unique_name_in_owner() && get_tree()->get_edited_scene_root()->get_node_or_null("%" + new_name)) {
 		String text = vformat(TTR("A node with the unique name %s already exists in this scene."), new_name);
 		if (error->is_visible()) {
@@ -1207,11 +1480,24 @@ Node *SceneTreeEditor::get_selected() {
 	return selected;
 }
 
-void SceneTreeEditor::set_marked(const HashSet<Node *> &p_marked, bool p_selectable, bool p_children_selectable) {
-	if (tree_dirty) {
-		_update_tree();
+void SceneTreeEditor::_update_marking_list(const HashSet<Node *> &p_marked) {
+	for (Node *N : p_marked) {
+		HashMap<Node *, CachedNode>::Iterator I = node_cache.get(N);
+		if (I) {
+			node_cache.mark_dirty(N);
+			node_cache.mark_children_dirty(N, true);
+		}
 	}
+}
+
+void SceneTreeEditor::set_marked(const HashSet<Node *> &p_marked, bool p_selectable, bool p_children_selectable) {
+	_update_if_clean();
+
+	_update_marking_list(marked);
+	_update_marking_list(p_marked);
+
 	marked = p_marked;
+
 	marked_selectable = p_selectable;
 	marked_children_selectable = p_children_selectable;
 	_update_tree();
@@ -1254,6 +1540,9 @@ void SceneTreeEditor::set_display_foreign_nodes(bool p_display) {
 
 void SceneTreeEditor::set_valid_types(const Vector<StringName> &p_valid) {
 	valid_types = p_valid;
+	node_cache.force_update = true;
+	callable_mp(this, &SceneTreeEditor::_update_tree).call_deferred(false);
+	tree_dirty = true;
 }
 
 void SceneTreeEditor::set_editor_selection(EditorSelection *p_selection) {
@@ -1286,7 +1575,9 @@ void SceneTreeEditor::_update_selection(TreeItem *item) {
 		if (item->is_selected(0)) {
 			TreeItem *previous_cursor_item = tree->get_selected();
 			item->deselect(0);
-			previous_cursor_item->set_as_cursor(0);
+			if (previous_cursor_item) {
+				previous_cursor_item->set_as_cursor(0);
+			}
 		}
 	}
 
@@ -1336,11 +1627,11 @@ void SceneTreeEditor::_cell_collapsed(Object *p_obj) {
 
 Variant SceneTreeEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
 	if (!can_rename) {
-		return Variant(); //not editable tree
+		return Variant(); // Not editable tree.
 	}
 
 	if (tree->get_button_id_at_position(p_point) != -1) {
-		return Variant(); //dragging from button
+		return Variant(); // Dragging from button.
 	}
 
 	Vector<Node *> selected_nodes;
@@ -1404,7 +1695,7 @@ bool SceneTreeEditor::_is_script_type(const StringName &p_type) const {
 
 bool SceneTreeEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const {
 	if (!can_rename) {
-		return false; //not editable tree
+		return false; // Not editable tree.
 	}
 
 	Dictionary d = p_data;
@@ -1426,7 +1717,7 @@ bool SceneTreeEditor::can_drop_data_fw(const Point2 &p_point, const Variant &p_d
 		Vector<String> files = d["files"];
 
 		if (files.size() == 0) {
-			return false; //weird
+			return false; // TODO Weird?
 		}
 
 		if (_is_script_type(EditorFileSystem::get_singleton()->get_file_type(files[0]))) {
@@ -1564,7 +1855,9 @@ void SceneTreeEditor::update_warning() {
 }
 
 void SceneTreeEditor::_warning_changed(Node *p_for_node) {
-	//should use a timer
+	node_cache.mark_dirty(p_for_node);
+
+	// Should use a timer.
 	update_timer->start();
 }
 
@@ -1585,6 +1878,11 @@ void SceneTreeEditor::set_connecting_signal(bool p_enable) {
 	update_tree();
 }
 
+void SceneTreeEditor::set_update_when_invisible(bool p_enable) {
+	update_when_invisible = p_enable;
+	update_tree();
+}
+
 void SceneTreeEditor::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_update_tree"), &SceneTreeEditor::_update_tree, DEFVAL(false)); // Still used by UndoRedo.
 
@@ -1604,7 +1902,8 @@ void SceneTreeEditor::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("open_script"));
 }
 
-SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_open_instance) {
+SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_open_instance) :
+		node_cache(this) {
 	selected = nullptr;
 
 	can_rename = p_can_rename;
@@ -1861,6 +2160,7 @@ SceneTreeDialog::SceneTreeDialog() {
 	filter_hbc->add_child(show_all_nodes);
 
 	tree = memnew(SceneTreeEditor(false, false, true));
+	tree->set_update_when_invisible(false);
 	tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	tree->get_scene_tree()->connect("item_activated", callable_mp(this, &SceneTreeDialog::_select));
 	// Initialize button state, must be done after the tree has been created to update its 'show_all_nodes' flag.
@@ -1875,3 +2175,151 @@ SceneTreeDialog::SceneTreeDialog() {
 
 SceneTreeDialog::~SceneTreeDialog() {
 }
+
+/******** CACHE *********/
+
+HashMap<Node *, SceneTreeEditor::CachedNode>::Iterator SceneTreeEditor::NodeCache::add(Node *p_node, TreeItem *p_item) {
+	if (!p_node) {
+		return HashMap<Node *, CachedNode>::Iterator();
+	}
+
+	return cache.insert(p_node, CachedNode(p_node, p_item));
+}
+
+HashMap<Node *, SceneTreeEditor::CachedNode>::Iterator SceneTreeEditor::NodeCache::get(Node *p_node, bool p_deleted_ok) {
+	if (!p_node) {
+		return HashMap<Node *, CachedNode>::Iterator();
+	}
+
+	HashMap<Node *, CachedNode>::Iterator I = cache.find(p_node);
+	if (I) {
+		if (I->value.delete_serial != UINT16_MAX) {
+			// Don't give us a node marked for deletion.
+			if (!p_deleted_ok) {
+				return HashMap<Node *, CachedNode>::Iterator();
+			}
+
+			to_delete.erase(&I->value);
+			I->value.delete_serial = UINT16_MAX;
+
+			// If we were resurrected from near-death we might have been renamed.
+			// Make sure that we are updated properly.
+			mark_dirty(p_node);
+			mark_children_dirty(p_node, true);
+		}
+	}
+
+	return I;
+}
+
+void SceneTreeEditor::NodeCache::remove(Node *p_node, bool p_recursive) {
+	if (!p_node) {
+		return;
+	}
+
+	if (p_node == editor->selected) {
+		editor->selected = nullptr;
+	}
+
+	editor->marked.erase(p_node);
+
+	HashMap<Node *, CachedNode>::Iterator I = cache.find(p_node);
+	if (I) {
+		if (p_recursive) {
+			int cc = p_node->get_child_count(false);
+
+			for (int i = 0; i < cc; i++) {
+				remove(p_node->get_child(i, false), p_recursive);
+			}
+		}
+
+		if (current_scene_node != p_node) {
+			// Do not remove from the Tree control here. See delete_pending below.
+			I->value.item->deselect(0);
+			I->value.delete_serial = delete_serial;
+			I->value.index = -1;
+			I->value.cache_iterator = I;
+			to_delete.insert(&I->value);
+		} else {
+			// If it is the root node, we leave the TreeItem and reuse it later.
+			cache.remove(I);
+		}
+	}
+}
+
+void SceneTreeEditor::NodeCache::mark_dirty(Node *p_node, bool p_parents) {
+	Node *node = p_node;
+	while (node) {
+		HashMap<Node *, CachedNode>::Iterator I = cache.find(node);
+		if (I) {
+			I->value.dirty = true;
+		}
+
+		if (!p_parents) {
+			break;
+		}
+		node = node->get_parent();
+	}
+}
+
+void SceneTreeEditor::NodeCache::mark_children_dirty(Node *p_node, bool p_recursive) {
+	if (!p_node) {
+		return;
+	}
+
+	int cc = p_node->get_child_count(false);
+	for (int i = 0; i < cc; i++) {
+		Node *c = p_node->get_child(i, false);
+		HashMap<Node *, CachedNode>::Iterator IC = cache.find(c);
+
+		if (IC) {
+			IC->value.dirty = true;
+
+			if (p_recursive) {
+				mark_children_dirty(c, p_recursive);
+			}
+		}
+	}
+}
+
+void SceneTreeEditor::NodeCache::delete_pending() {
+	HashSet<CachedNode *>::Iterator I = to_delete.begin();
+	while (I) {
+		// We want to keep TreeItems around just long enough for a Node removal,
+		// and immediate reinsertion. This is what happens with moves and
+		// type changes.
+		if (Math::abs((*I)->delete_serial - delete_serial) >= 2) {
+			memdelete((*I)->item);
+			cache.remove((*I)->cache_iterator);
+			to_delete.remove(I);
+		} else if (!(*I)->removed) {
+			// We don't remove from the tree until now because if the node got
+			// deleted from a @tool script the SceneTreeEditor might have had it
+			// marked or selected before the node was removed. If we immediately
+			// remove from the Tree control then we end up trying to scroll to an
+			// Item without a parent.
+			//
+			// We might already be removed (and thus not have a parent) by rapid
+			// undo/redo.
+			if (!(*I)->removed) {
+				TreeItem *parent = (*I)->item->get_parent();
+				parent->remove_child((*I)->item);
+			}
+			(*I)->removed = true;
+		}
+		++I;
+	}
+
+	++delete_serial;
+}
+
+void SceneTreeEditor::NodeCache::clear() {
+	for (CachedNode *E : to_delete) {
+		// Only removed entries won't be automatically cleaned up by Tree::clear().
+		if (E->removed) {
+			memdelete(E->item);
+		}
+	}
+	cache.clear();
+	to_delete.clear();
+}

+ 67 - 1
editor/gui/scene_tree_editor.h

@@ -58,6 +58,57 @@ class SceneTreeEditor : public Control {
 		BUTTON_UNIQUE = 9,
 	};
 
+	struct CachedNode {
+		Node *node = nullptr;
+		TreeItem *item = nullptr;
+		int index = -1;
+		bool dirty = true;
+		bool has_moved_children = false;
+		bool removed = false;
+
+		// Store the iterator for faster removal. This is safe as
+		// HashMap never moves elements.
+		HashMap<Node *, CachedNode>::Iterator cache_iterator;
+		// This is safe because it gets compared to a uint8_t.
+		uint16_t delete_serial = UINT16_MAX;
+
+		// To know whether to update children or not.
+		bool can_process = false;
+
+		CachedNode() = delete; // Always an error.
+		CachedNode(Node *p_node, TreeItem *p_item) :
+				node(p_node), item(p_item) {}
+	};
+
+	struct NodeCache {
+		~NodeCache() {
+			clear();
+		}
+
+		NodeCache(SceneTreeEditor *p_editor) :
+				editor(p_editor) {}
+
+		HashMap<Node *, CachedNode>::Iterator add(Node *p_node, TreeItem *p_item);
+		HashMap<Node *, CachedNode>::Iterator get(Node *p_node, bool p_deleted_ok = true);
+		void remove(Node *p_node, bool p_recursive = false);
+		void mark_dirty(Node *p_node, bool p_parents = true);
+		void mark_children_dirty(Node *p_node, bool p_recursive = false);
+
+		void delete_pending();
+		void clear();
+
+		SceneTreeEditor *editor;
+		HashMap<Node *, CachedNode> cache;
+		HashSet<CachedNode *> to_delete;
+		Node *current_scene_node = nullptr;
+		Node *current_pinned_node = nullptr;
+		bool current_has_pin = false;
+		bool force_update = false;
+		uint8_t delete_serial = 0;
+	};
+
+	NodeCache node_cache;
+
 	Tree *tree = nullptr;
 	Node *selected = nullptr;
 	ObjectID instance_node;
@@ -77,17 +128,30 @@ class SceneTreeEditor : public Control {
 	bool auto_expand_selected = true;
 	bool connect_to_script_mode = false;
 	bool connecting_signal = false;
+	bool update_when_invisible = true;
 
 	int blocked;
 
 	void _compute_hash(Node *p_node, uint64_t &hash);
+	void _reset();
+
+	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);
+	void _update_node(Node *p_node, TreeItem *p_item, bool p_part_of_subscene);
+	void _update_if_clean();
 
-	void _add_nodes(Node *p_node, TreeItem *p_parent);
 	void _test_update_tree();
 	bool _update_filter(TreeItem *p_parent = nullptr, bool p_scroll_to_selected = false);
 	bool _item_matches_all_terms(TreeItem *p_item, const PackedStringArray &p_terms);
 	void _tree_changed();
 	void _tree_process_mode_changed();
+
+	void _move_node_children(HashMap<Node *, CachedNode>::Iterator &p_I);
+	void _move_node_item(TreeItem *p_parent, HashMap<Node *, CachedNode>::Iterator &p_I);
+
+	void _node_child_order_changed(Node *p_node);
+	void _node_editor_state_changed(Node *p_node);
+	void _node_added(Node *p_node);
 	void _node_removed(Node *p_node);
 	void _node_renamed(Node *p_node);
 
@@ -142,6 +206,7 @@ class SceneTreeEditor : public Control {
 	void _rmb_select(const Vector2 &p_pos, MouseButton p_button = MouseButton::RIGHT);
 
 	void _warning_changed(Node *p_for_node);
+	void _update_marking_list(const HashSet<Node *> &p_marked);
 
 	Timer *update_timer = nullptr;
 
@@ -182,6 +247,7 @@ public:
 	void set_auto_expand_selected(bool p_auto, bool p_update_settings);
 	void set_connect_to_script_mode(bool p_enable);
 	void set_connecting_signal(bool p_enable);
+	void set_update_when_invisible(bool p_enable);
 
 	Tree *get_scene_tree() { return tree; }
 

+ 1 - 0
editor/reparent_dialog.cpp

@@ -76,6 +76,7 @@ ReparentDialog::ReparentDialog() {
 	add_child(vbc);
 
 	tree = memnew(SceneTreeEditor(false));
+	tree->set_update_when_invisible(false);
 	tree->set_show_enabled_subscene(true);
 	tree->get_scene_tree()->connect("item_activated", callable_mp(this, &ReparentDialog::_reparent));
 	vbc->add_margin_child(TTR("Select new parent:"), tree, true);

+ 71 - 38
editor/scene_tree_dock.cpp

@@ -78,17 +78,31 @@ void SceneTreeDock::_quick_open(const String &p_file_path) {
 	instantiate_scenes({ p_file_path }, scene_tree->get_selected());
 }
 
+static void _restore_treeitem_custom_color(TreeItem *p_item) {
+	if (!p_item) {
+		return;
+	}
+	Color custom_color = p_item->get_meta(SNAME("custom_color"), Color(0, 0, 0, 0));
+	if (custom_color != Color(0, 0, 0, 0)) {
+		p_item->set_custom_color(0, custom_color);
+	} else {
+		p_item->clear_custom_color(0);
+	}
+}
+
 void SceneTreeDock::_inspect_hovered_node() {
 	select_node_hovered_at_end_of_drag = true;
 	Tree *tree = scene_tree->get_scene_tree();
 	TreeItem *item = tree->get_item_with_metadata(node_hovered_now->get_path());
+
+	_restore_treeitem_custom_color(tree_item_inspected);
+	tree_item_inspected = item;
+
 	if (item) {
-		if (tree_item_inspected) {
-			tree_item_inspected->clear_custom_color(0);
-		}
-		tree_item_inspected = item;
-		tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
+		Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
+		tree_item_inspected->set_custom_color(0, accent_color);
 	}
+
 	EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();
 	editor_history->add_object(node_hovered_now->get_instance_id());
 	InspectorDock::get_inspector_singleton()->edit(node_hovered_now);
@@ -1716,7 +1730,7 @@ void SceneTreeDock::_notification(int p_what) {
 		case NOTIFICATION_DRAG_END: {
 			_reset_hovering_timer();
 			if (tree_item_inspected) {
-				tree_item_inspected->clear_custom_color(0);
+				_restore_treeitem_custom_color(tree_item_inspected);
 				tree_item_inspected = nullptr;
 			} else {
 				return;
@@ -1963,6 +1977,49 @@ bool SceneTreeDock::_update_node_path(Node *p_root_node, NodePath &r_node_path,
 	return false;
 }
 
+_ALWAYS_INLINE_ static bool _recurse_into_property(const PropertyInfo &p_property) {
+	// Only check these types for NodePaths.
+	static const Variant::Type property_type_check[] = { Variant::OBJECT, Variant::NODE_PATH, Variant::ARRAY, Variant::DICTIONARY };
+
+	if (!(p_property.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
+		return false;
+	}
+
+	// Avoid otherwise acceptable types if we marked them as irrelevant.
+	if (p_property.hint == PROPERTY_HINT_NO_NODEPATH) {
+		return false;
+	}
+
+	for (Variant::Type type : property_type_check) {
+		if (p_property.type == type) {
+			return true;
+		}
+	}
+	return false;
+}
+
+void SceneTreeDock::_check_object_properties_recursive(Node *p_root_node, Object *p_obj, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource) const {
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+
+	List<PropertyInfo> properties;
+	p_obj->get_property_list(&properties);
+
+	for (const PropertyInfo &E : properties) {
+		if (!_recurse_into_property(E)) {
+			continue;
+		}
+
+		StringName propertyname = E.name;
+
+		Variant old_variant = p_obj->get(propertyname);
+		Variant updated_variant = old_variant;
+		if (_check_node_path_recursive(p_root_node, updated_variant, p_renames, p_inside_resource)) {
+			undo_redo->add_do_property(p_obj, propertyname, updated_variant);
+			undo_redo->add_undo_property(p_obj, propertyname, old_variant);
+		}
+	}
+}
+
 bool SceneTreeDock::_check_node_path_recursive(Node *p_root_node, Variant &r_variant, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource) const {
 	switch (r_variant.get_type()) {
 		case Variant::NODE_PATH: {
@@ -2027,27 +2084,18 @@ bool SceneTreeDock::_check_node_path_recursive(Node *p_root_node, Variant &r_var
 				break;
 			}
 
+			if (Object::cast_to<Material>(resource)) {
+				// For performance reasons, assume that Materials don't have NodePaths in them.
+				// TODO This check could be removed when String performance has improved.
+				break;
+			}
+
 			if (!resource->is_built_in()) {
 				// For performance reasons, assume that scene paths are no concern for external resources.
 				break;
 			}
 
-			List<PropertyInfo> properties;
-			resource->get_property_list(&properties);
-
-			for (const PropertyInfo &E : properties) {
-				if (!(E.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
-					continue;
-				}
-				String propertyname = E.name;
-				Variant old_variant = resource->get(propertyname);
-				Variant updated_variant = old_variant;
-				if (_check_node_path_recursive(p_root_node, updated_variant, p_renames, true)) {
-					EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
-					undo_redo->add_do_property(resource, propertyname, updated_variant);
-					undo_redo->add_undo_property(resource, propertyname, old_variant);
-				}
-			}
+			_check_object_properties_recursive(p_root_node, resource, p_renames, true);
 		} break;
 
 		default: {
@@ -2173,22 +2221,7 @@ void SceneTreeDock::perform_node_renames(Node *p_base, HashMap<Node *, NodePath>
 	}
 
 	// Renaming node paths used in node properties.
-	List<PropertyInfo> properties;
-	p_base->get_property_list(&properties);
-
-	for (const PropertyInfo &E : properties) {
-		if (!(E.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
-			continue;
-		}
-		String propertyname = E.name;
-		Variant old_variant = p_base->get(propertyname);
-		Variant updated_variant = old_variant;
-		if (_check_node_path_recursive(p_base, updated_variant, p_renames)) {
-			EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
-			undo_redo->add_do_property(p_base, propertyname, updated_variant);
-			undo_redo->add_undo_property(p_base, propertyname, old_variant);
-		}
-	}
+	_check_object_properties_recursive(p_base, p_base, p_renames);
 
 	for (int i = 0; i < p_base->get_child_count(); i++) {
 		perform_node_renames(p_base->get_child(i), p_renames, r_rem_anims);

+ 1 - 0
editor/scene_tree_dock.h

@@ -302,6 +302,7 @@ class SceneTreeDock : public VBoxContainer {
 	static void _update_configuration_warning();
 
 	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;
 	bool _check_node_path_recursive(Node *p_root_node, Variant &r_variant, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource = false) const;
 	bool _check_node_recursive(Variant &r_variant, Node *p_node, Node *p_by_node, const String type_hint, String &r_warn_message);
 	void _replace_node(Node *p_node, Node *p_by_node, bool p_keep_properties = true, bool p_remove_old = true);

+ 14 - 1
scene/gui/tree.cpp

@@ -1258,6 +1258,18 @@ void TreeItem::deselect(int p_column) {
 	_cell_deselected(p_column);
 }
 
+void TreeItem::clear_buttons() {
+	int i = 0;
+	for (Cell &cell : cells) {
+		if (!cell.buttons.is_empty()) {
+			cell.buttons.clear();
+			cell.cached_minimum_size_dirty = true;
+			_changed_notify(i);
+		}
+		++i;
+	}
+}
+
 void TreeItem::add_button(int p_column, const Ref<Texture2D> &p_button, int p_id, bool p_disabled, const String &p_tooltip) {
 	ERR_FAIL_INDEX(p_column, cells.size());
 	ERR_FAIL_COND(!p_button.is_valid());
@@ -1768,6 +1780,7 @@ void TreeItem::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_custom_as_button", "column", "enable"), &TreeItem::set_custom_as_button);
 	ClassDB::bind_method(D_METHOD("is_custom_set_as_button", "column"), &TreeItem::is_custom_set_as_button);
 
+	ClassDB::bind_method(D_METHOD("clear_buttons"), &TreeItem::clear_buttons);
 	ClassDB::bind_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip_text"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL(""));
 	ClassDB::bind_method(D_METHOD("get_button_count", "column"), &TreeItem::get_button_count);
 	ClassDB::bind_method(D_METHOD("get_button_tooltip_text", "column", "button_index"), &TreeItem::get_button_tooltip_text);
@@ -3726,7 +3739,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
 		_determine_hovered_item();
 
 		bool rtl = is_layout_rtl();
-		if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
+		if (pressing_for_editor && popup_pressing_edited_item && !popup_pressing_edited_item->cells.is_empty() && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
 			/* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */
 			popup_edited_item = popup_pressing_edited_item;
 			popup_edited_item_col = popup_pressing_edited_item_column;

+ 1 - 0
scene/gui/tree.h

@@ -277,6 +277,7 @@ public:
 	void set_icon_max_width(int p_column, int p_max);
 	int get_icon_max_width(int p_column) const;
 
+	void clear_buttons();
 	void add_button(int p_column, const Ref<Texture2D> &p_button, int p_id = -1, bool p_disabled = false, const String &p_tooltip = "");
 	int get_button_count(int p_column) const;
 	String get_button_tooltip_text(int p_column, int p_index) const;

+ 55 - 1
scene/main/node.cpp

@@ -678,6 +678,8 @@ void Node::set_process_mode(ProcessMode p_mode) {
 	if (Engine::get_singleton()->is_editor_hint()) {
 		get_tree()->emit_signal(SNAME("tree_process_mode_changed"));
 	}
+
+	_emit_editor_state_changed();
 #endif
 }
 
@@ -2163,6 +2165,7 @@ void Node::set_unique_name_in_owner(bool p_enabled) {
 	}
 
 	update_configuration_warnings();
+	_emit_editor_state_changed();
 }
 
 bool Node::is_unique_name_in_owner() const {
@@ -2200,6 +2203,8 @@ void Node::set_owner(Node *p_owner) {
 	if (data.unique_name_in_owner) {
 		_acquire_unique_name_in_owner();
 	}
+
+	_emit_editor_state_changed();
 }
 
 Node *Node::get_owner() const {
@@ -2383,6 +2388,9 @@ void Node::add_to_group(const StringName &p_identifier, bool p_persistent) {
 	gd.persistent = p_persistent;
 
 	data.grouped[p_identifier] = gd;
+	if (p_persistent) {
+		_emit_editor_state_changed();
+	}
 }
 
 void Node::remove_from_group(const StringName &p_identifier) {
@@ -2393,11 +2401,21 @@ void Node::remove_from_group(const StringName &p_identifier) {
 		return;
 	}
 
+#ifdef TOOLS_ENABLED
+	bool persistent = E->value.persistent;
+#endif
+
 	if (data.tree) {
 		data.tree->remove_from_group(E->key, this);
 	}
 
 	data.grouped.remove(E);
+
+#ifdef TOOLS_ENABLED
+	if (persistent) {
+		_emit_editor_state_changed();
+	}
+#endif
 }
 
 TypedArray<StringName> Node::_get_groups() const {
@@ -2560,6 +2578,7 @@ Ref<Tween> Node::create_tween() {
 void Node::set_scene_file_path(const String &p_scene_file_path) {
 	ERR_THREAD_GUARD
 	data.scene_file_path = p_scene_file_path;
+	_emit_editor_state_changed();
 }
 
 String Node::get_scene_file_path() const {
@@ -2592,6 +2611,8 @@ void Node::set_editable_instance(Node *p_node, bool p_editable) {
 	} else {
 		p_node->data.editable_instance = true;
 	}
+
+	p_node->_emit_editor_state_changed();
 }
 
 bool Node::is_editable_instance(const Node *p_node) const {
@@ -2702,6 +2723,7 @@ Ref<SceneState> Node::get_scene_instance_state() const {
 void Node::set_scene_inherited_state(const Ref<SceneState> &p_state) {
 	ERR_THREAD_GUARD
 	data.inherited_state = p_state;
+	_emit_editor_state_changed();
 }
 
 Ref<SceneState> Node::get_scene_inherited_state() const {
@@ -2950,6 +2972,14 @@ void Node::remap_nested_resources(Ref<Resource> p_resource, const HashMap<Ref<Re
 		}
 	}
 }
+
+void Node::_emit_editor_state_changed() {
+	// This is required for the SceneTreeEditor to properly keep track of when an update is needed.
+	// This signal might be expensive and not needed for anything outside of the editor.
+	if (Engine::get_singleton()->is_editor_hint()) {
+		emit_signal(SNAME("editor_state_changed"));
+	}
+}
 #endif
 
 // Duplicate node's properties.
@@ -3843,6 +3873,7 @@ void Node::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("child_order_changed"));
 	ADD_SIGNAL(MethodInfo("replacing_by", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
 	ADD_SIGNAL(MethodInfo("editor_description_changed", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
+	ADD_SIGNAL(MethodInfo("editor_state_changed"));
 
 	ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_name", "get_name");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "unique_name_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_unique_name_in_owner", "is_unique_name_in_owner");
@@ -3966,11 +3997,13 @@ bool Node::has_meta(const StringName &p_name) const {
 void Node::set_meta(const StringName &p_name, const Variant &p_value) {
 	ERR_THREAD_GUARD;
 	Object::set_meta(p_name, p_value);
+	_emit_editor_state_changed();
 }
 
 void Node::remove_meta(const StringName &p_name) {
 	ERR_THREAD_GUARD;
 	Object::remove_meta(p_name);
+	_emit_editor_state_changed();
 }
 
 Variant Node::get_meta(const StringName &p_name, const Variant &p_default) const {
@@ -4020,12 +4053,33 @@ void Node::get_signals_connected_to_this(List<Connection> *p_connections) const
 
 Error Node::connect(const StringName &p_signal, const Callable &p_callable, uint32_t p_flags) {
 	ERR_THREAD_GUARD_V(ERR_INVALID_PARAMETER);
-	return Object::connect(p_signal, p_callable, p_flags);
+
+	Error retval = Object::connect(p_signal, p_callable, p_flags);
+#ifdef TOOLS_ENABLED
+	if (p_flags & CONNECT_PERSIST) {
+		_emit_editor_state_changed();
+	}
+#endif
+
+	return retval;
 }
 
 void Node::disconnect(const StringName &p_signal, const Callable &p_callable) {
 	ERR_THREAD_GUARD;
+
+#ifdef TOOLS_ENABLED
+	// Already under thread guard, don't check again.
+	int old_connection_count = Object::get_persistent_signal_connection_count();
+#endif
+
 	Object::disconnect(p_signal, p_callable);
+
+#ifdef TOOLS_ENABLED
+	int new_connection_count = Object::get_persistent_signal_connection_count();
+	if (old_connection_count != new_connection_count) {
+		_emit_editor_state_changed();
+	}
+#endif
 }
 
 bool Node::is_connected(const StringName &p_signal, const Callable &p_callable) const {

+ 7 - 0
scene/main/node.h

@@ -331,6 +331,13 @@ private:
 	Variant _call_deferred_thread_group_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
 	Variant _call_thread_safe_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
 
+	// Editor only signal to keep the SceneTreeEditor in sync.
+#ifdef TOOLS_ENABLED
+	void _emit_editor_state_changed();
+#else
+	void _emit_editor_state_changed() {}
+#endif
+
 protected:
 	void _block() { data.blocked++; }
 	void _unblock() { data.blocked--; }

+ 4 - 4
scene/resources/mesh.cpp

@@ -1740,7 +1740,7 @@ void ArrayMesh::_get_property_list(List<PropertyInfo> *p_list) const {
 	}
 
 	for (int i = 0; i < surfaces.size(); i++) {
-		p_list->push_back(PropertyInfo(Variant::STRING, "surface_" + itos(i) + "/name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR));
+		p_list->push_back(PropertyInfo(Variant::STRING, "surface_" + itos(i) + "/name", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_EDITOR));
 		if (surfaces[i].is_2d) {
 			p_list->push_back(PropertyInfo(Variant::OBJECT, "surface_" + itos(i) + "/material", PROPERTY_HINT_RESOURCE_TYPE, "CanvasItemMaterial,ShaderMaterial", PROPERTY_USAGE_EDITOR));
 		} else {
@@ -2308,10 +2308,10 @@ void ArrayMesh::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("_set_surfaces", "surfaces"), &ArrayMesh::_set_surfaces);
 	ClassDB::bind_method(D_METHOD("_get_surfaces"), &ArrayMesh::_get_surfaces);
 
-	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_blend_shape_names", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_blend_shape_names", "_get_blend_shape_names");
-	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "_surfaces", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_surfaces", "_get_surfaces");
+	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_blend_shape_names", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_blend_shape_names", "_get_blend_shape_names");
+	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "_surfaces", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_surfaces", "_get_surfaces");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_shape_mode", PROPERTY_HINT_ENUM, "Normalized,Relative"), "set_blend_shape_mode", "get_blend_shape_mode");
-	ADD_PROPERTY(PropertyInfo(Variant::AABB, "custom_aabb", PROPERTY_HINT_NONE, "suffix:m"), "set_custom_aabb", "get_custom_aabb");
+	ADD_PROPERTY(PropertyInfo(Variant::AABB, "custom_aabb", PROPERTY_HINT_NO_NODEPATH, "suffix:m"), "set_custom_aabb", "get_custom_aabb");
 	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shadow_mesh", PROPERTY_HINT_RESOURCE_TYPE, "ArrayMesh"), "set_shadow_mesh", "get_shadow_mesh");
 }