Просмотр исходного кода

Allow to select multiple remote nodes at runtime

Michael Alexsander 9 месяцев назад
Родитель
Сommit
5c66129e62

+ 2 - 2
core/math/math_fieldwise.cpp

@@ -28,7 +28,7 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
 /**************************************************************************/
 
-#ifdef TOOLS_ENABLED
+#ifdef DEBUG_ENABLED
 
 #include "math_fieldwise.h"
 
@@ -242,4 +242,4 @@ Variant fieldwise_assign(const Variant &p_target, const Variant &p_source, const
 	/* clang-format on */
 }
 
-#endif // TOOLS_ENABLED
+#endif // DEBUG_ENABLED

+ 2 - 2
core/math/math_fieldwise.h

@@ -30,10 +30,10 @@
 
 #pragma once
 
-#ifdef TOOLS_ENABLED
+#ifdef DEBUG_ENABLED
 
 #include "core/variant/variant.h"
 
 Variant fieldwise_assign(const Variant &p_target, const Variant &p_source, const String &p_field);
 
-#endif // TOOLS_ENABLED
+#endif // DEBUG_ENABLED

+ 4 - 0
doc/classes/EditorSettings.xml

@@ -191,6 +191,10 @@
 		<member name="debugger/auto_switch_to_stack_trace" type="bool" setter="" getter="">
 			If [code]true[/code], automatically switches to the [b]Stack Trace[/b] panel when the debugger hits a breakpoint or steps.
 		</member>
+		<member name="debugger/max_node_selection" type="int" setter="" getter="">
+			The limit of how many remote nodes can be selected at once.
+			[b]Warning:[/b] Increasing this value is not recommended, as selecting too many can make the editing and inspection of remote properties unreliable.
+		</member>
 		<member name="debugger/profile_native_calls" type="bool" setter="" getter="">
 			If [code]true[/code], enables collection of profiling data from non-GDScript Godot functions, such as engine class methods. Enabling this slows execution while profiling further.
 		</member>

+ 3 - 1
editor/debugger/debug_adapter/debug_adapter_protocol.cpp

@@ -843,7 +843,9 @@ bool DebugAdapterProtocol::request_remote_object(const ObjectID &p_object_id) {
 		return false;
 	}
 
-	EditorDebuggerNode::get_singleton()->get_default_debugger()->request_remote_object(p_object_id);
+	TypedArray<uint64_t> arr;
+	arr.append(p_object_id);
+	EditorDebuggerNode::get_singleton()->get_default_debugger()->request_remote_objects(arr);
 	object_pending_set.insert(p_object_id);
 
 	return true;

+ 219 - 76
editor/debugger/editor_debugger_inspector.cpp

@@ -33,29 +33,57 @@
 #include "core/debugger/debugger_marshalls.h"
 #include "core/io/marshalls.h"
 #include "editor/editor_node.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "editor/inspector_dock.h"
 #include "scene/debugger/scene_debugger.h"
 
-bool EditorDebuggerRemoteObject::_set(const StringName &p_name, const Variant &p_value) {
-	if (!prop_values.has(p_name) || String(p_name).begins_with("Constants/")) {
+bool EditorDebuggerRemoteObjects::_set(const StringName &p_name, const Variant &p_value) {
+	return _set_impl(p_name, p_value, "");
+}
+
+bool EditorDebuggerRemoteObjects::_set_impl(const StringName &p_name, const Variant &p_value, const String &p_field) {
+	String name = p_name;
+
+	if (name.begins_with("Metadata/")) {
+		name = name.replace_first("Metadata/", "metadata/");
+	}
+	if (!prop_values.has(name) || String(name).begins_with("Constants/")) {
 		return false;
 	}
 
-	prop_values[p_name] = p_value;
-	emit_signal(SNAME("value_edited"), remote_object_id, p_name, p_value);
+	Dictionary &values = prop_values[p_name];
+	Dictionary old_values = values.duplicate();
+	for (const uint64_t key : values.keys()) {
+		values.set(key, p_value);
+	}
+
+	EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
+	const int size = remote_object_ids.size();
+	ur->create_action(size == 1 ? vformat(TTR("Set %s"), name) : vformat(TTR("Set %s on %d objects"), name, size), UndoRedo::MERGE_ENDS);
+
+	ur->add_do_method(this, SNAME("emit_signal"), SNAME("values_edited"), name, values, p_field);
+	ur->add_undo_method(this, SNAME("emit_signal"), SNAME("values_edited"), name, old_values, p_field);
+	ur->commit_action();
+
 	return true;
 }
 
-bool EditorDebuggerRemoteObject::_get(const StringName &p_name, Variant &r_ret) const {
-	if (!prop_values.has(p_name)) {
+bool EditorDebuggerRemoteObjects::_get(const StringName &p_name, Variant &r_ret) const {
+	String name = p_name;
+
+	if (name.begins_with("Metadata/")) {
+		name = name.replace_first("Metadata/", "metadata/");
+	}
+	if (!prop_values.has(name)) {
 		return false;
 	}
 
-	r_ret = prop_values[p_name];
+	r_ret = prop_values[p_name][remote_object_ids[0]];
 	return true;
 }
 
-void EditorDebuggerRemoteObject::_get_property_list(List<PropertyInfo> *p_list) const {
-	p_list->clear(); // Sorry, no want category.
+void EditorDebuggerRemoteObjects::_get_property_list(List<PropertyInfo> *p_list) const {
+	p_list->clear(); // Sorry, don't want any categories.
 	for (const PropertyInfo &prop : prop_list) {
 		if (prop.name == "script") {
 			// Skip the script property, it's always added by the non-virtual method.
@@ -66,31 +94,35 @@ void EditorDebuggerRemoteObject::_get_property_list(List<PropertyInfo> *p_list)
 	}
 }
 
-String EditorDebuggerRemoteObject::get_title() {
-	if (remote_object_id.is_valid()) {
-		return vformat(TTR("Remote %s:"), String(type_name)) + " " + itos(remote_object_id);
-	} else {
-		return "<null>";
+void EditorDebuggerRemoteObjects::set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field) {
+	_set_impl(p_property, p_value, p_field);
+}
+
+String EditorDebuggerRemoteObjects::get_title() {
+	if (!remote_object_ids.is_empty() && ObjectID(remote_object_ids[0].operator uint64_t()).is_valid()) {
+		const int size = remote_object_ids.size();
+		return size == 1 ? vformat(TTR("Remote %s: %d"), type_name, remote_object_ids[0]) : vformat(TTR("Remote %s (%d Selected)"), type_name, size);
 	}
+
+	return "<null>";
 }
 
-Variant EditorDebuggerRemoteObject::get_variant(const StringName &p_name) {
+Variant EditorDebuggerRemoteObjects::get_variant(const StringName &p_name) {
 	Variant var;
 	_get(p_name, var);
 	return var;
 }
 
-void EditorDebuggerRemoteObject::_bind_methods() {
-	ClassDB::bind_method(D_METHOD("get_title"), &EditorDebuggerRemoteObject::get_title);
-	ClassDB::bind_method(D_METHOD("get_variant"), &EditorDebuggerRemoteObject::get_variant);
-	ClassDB::bind_method(D_METHOD("clear"), &EditorDebuggerRemoteObject::clear);
-	ClassDB::bind_method(D_METHOD("get_remote_object_id"), &EditorDebuggerRemoteObject::get_remote_object_id);
+void EditorDebuggerRemoteObjects::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_title"), &EditorDebuggerRemoteObjects::get_title);
 
-	ADD_SIGNAL(MethodInfo("value_edited", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value")));
+	ADD_SIGNAL(MethodInfo("values_edited", PropertyInfo(Variant::STRING, "property"), PropertyInfo(Variant::DICTIONARY, "values", PROPERTY_HINT_DICTIONARY_TYPE, "uint64_t:Variant"), PropertyInfo(Variant::STRING, "field")));
 }
 
+/// EditorDebuggerInspector
+
 EditorDebuggerInspector::EditorDebuggerInspector() {
-	variables = memnew(EditorDebuggerRemoteObject);
+	variables = memnew(EditorDebuggerRemoteObjects);
 }
 
 EditorDebuggerInspector::~EditorDebuggerInspector() {
@@ -100,7 +132,7 @@ EditorDebuggerInspector::~EditorDebuggerInspector() {
 
 void EditorDebuggerInspector::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("object_selected", PropertyInfo(Variant::INT, "id")));
-	ADD_SIGNAL(MethodInfo("object_edited", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value")));
+	ADD_SIGNAL(MethodInfo("objects_edited", PropertyInfo(Variant::ARRAY, "ids"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value"), PropertyInfo(Variant::STRING, "field")));
 	ADD_SIGNAL(MethodInfo("object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property")));
 }
 
@@ -111,50 +143,143 @@ void EditorDebuggerInspector::_notification(int p_what) {
 		} break;
 
 		case NOTIFICATION_ENTER_TREE: {
+			variables->remote_object_ids.append(0);
 			edit(variables);
 		} break;
 	}
 }
 
-void EditorDebuggerInspector::_object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value) {
-	emit_signal(SNAME("object_edited"), p_id, p_prop, p_value);
+void EditorDebuggerInspector::_objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field) {
+	emit_signal(SNAME("objects_edited"), p_prop, p_values, p_field);
 }
 
 void EditorDebuggerInspector::_object_selected(ObjectID p_object) {
 	emit_signal(SNAME("object_selected"), p_object);
 }
 
-ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) {
-	EditorDebuggerRemoteObject *debug_obj = nullptr;
+EditorDebuggerRemoteObjects *EditorDebuggerInspector::set_objects(const Array &p_arr) {
+	ERR_FAIL_COND_V(p_arr.is_empty(), nullptr);
+
+	TypedArray<uint64_t> ids;
+	LocalVector<SceneDebuggerObject> objects;
+	for (const Array arr : p_arr) {
+		SceneDebuggerObject obj;
+		obj.deserialize(arr);
+		if (obj.id.is_valid()) {
+			ids.push_back((uint64_t)obj.id);
+			objects.push_back(obj);
+		}
+	}
+	ERR_FAIL_COND_V(ids.is_empty(), nullptr);
 
-	SceneDebuggerObject obj;
-	obj.deserialize(p_arr);
-	ERR_FAIL_COND_V(obj.id.is_null(), ObjectID());
+	// Sorting is necessary, as selected nodes in the remote tree are ordered by index.
+	ids.sort();
 
-	if (remote_objects.has(obj.id)) {
-		debug_obj = remote_objects[obj.id];
-	} else {
-		debug_obj = memnew(EditorDebuggerRemoteObject);
-		debug_obj->remote_object_id = obj.id;
-		debug_obj->type_name = obj.class_name;
-		remote_objects[obj.id] = debug_obj;
-		debug_obj->connect("value_edited", callable_mp(this, &EditorDebuggerInspector::_object_edited));
+	EditorDebuggerRemoteObjects *remote_objects = nullptr;
+	for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) {
+		if (robjs->remote_object_ids == ids) {
+			remote_objects = robjs;
+			break;
+		}
+	}
+
+	if (!remote_objects) {
+		remote_objects = memnew(EditorDebuggerRemoteObjects);
+		remote_objects->remote_object_ids = ids;
+		remote_objects->remote_object_ids.make_read_only();
+		remote_objects->connect("values_edited", callable_mp(this, &EditorDebuggerInspector::_objects_edited));
+		remote_objects_list.push_back(remote_objects);
+	}
+
+	StringName class_name = objects[0].class_name;
+	if (class_name != SNAME("Object")) {
+		// Search for the common class between all selected objects.
+		bool check_type_again = true;
+		while (check_type_again) {
+			check_type_again = false;
+
+			if (class_name == SNAME("Object") || class_name == StringName()) {
+				// All objects inherit from Object, so no need to continue checking.
+				class_name = SNAME("Object");
+				break;
+			}
+
+			// Check that all objects inherit from type_name.
+			for (const SceneDebuggerObject &obj : objects) {
+				if (obj.class_name == class_name || ClassDB::is_parent_class(obj.class_name, class_name)) {
+					continue; // class_name is the same or a parent of the object's class.
+				}
+
+				// 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_type_again = true;
+				break;
+			}
+		}
+	}
+	remote_objects->type_name = class_name;
+
+	// Search for properties that are present in all selected objects.
+	struct UsageData {
+		int qty = 0;
+		SceneDebuggerObject::SceneDebuggerProperty prop;
+		TypedDictionary<uint64_t, Variant> values;
+	};
+	HashMap<String, UsageData> usage;
+	int nc = 0;
+	for (const SceneDebuggerObject &obj : objects) {
+		for (const SceneDebuggerObject::SceneDebuggerProperty &prop : obj.properties) {
+			PropertyInfo pinfo = prop.first;
+			if (pinfo.name == "script") {
+				continue; // Added later manually, since this is intercepted before being set (check Variant Object::get()).
+			} else if (pinfo.name.begins_with("metadata/")) {
+				pinfo.name = pinfo.name.replace_first("metadata/", "Metadata/"); // Trick to not get actual metadata edited from EditorDebuggerRemoteObjects.
+			}
+
+			if (!usage.has(pinfo.name)) {
+				UsageData usage_dt;
+				usage_dt.prop = prop;
+				usage_dt.prop.first.name = pinfo.name;
+				usage_dt.values[obj.id] = prop.second;
+				usage[pinfo.name] = usage_dt;
+			}
+
+			// Make sure only properties with the same exact PropertyInfo data will appear.
+			if (usage[pinfo.name].prop.first == pinfo) {
+				usage[pinfo.name].qty++;
+				usage[pinfo.name].values[obj.id] = prop.second;
+			}
+		}
+
+		nc++;
 	}
+	for (HashMap<String, UsageData>::Iterator E = usage.begin(); E;) {
+		HashMap<String, UsageData>::Iterator next = E;
+		++next;
+
+		UsageData usage_dt = E->value;
+		if (nc != usage_dt.qty) {
+			// Doesn't appear on all of them, remove it.
+			usage.erase(E->key);
+		}
 
-	int old_prop_size = debug_obj->prop_list.size();
+		E = next;
+	}
+
+	int old_prop_size = remote_objects->prop_list.size();
 
-	debug_obj->prop_list.clear();
+	remote_objects->prop_list.clear();
 	int new_props_added = 0;
 	HashSet<String> changed;
-	for (SceneDebuggerObject::SceneDebuggerProperty &property : obj.properties) {
-		PropertyInfo &pinfo = property.first;
-		Variant &var = property.second;
+	for (const KeyValue<String, UsageData> &KV : usage) {
+		const PropertyInfo &pinfo = KV.value.prop.first;
+		Variant var = KV.value.values[remote_objects->remote_object_ids[0]];
 
 		if (pinfo.type == Variant::OBJECT) {
 			if (var.is_string()) {
 				String path = var;
 				if (path.contains("::")) {
-					// built-in resource
+					// Built-in resource.
 					String base_path = path.get_slice("::", 0);
 					Ref<Resource> dependency = ResourceLoader::load(base_path);
 					if (dependency.is_valid()) {
@@ -164,13 +289,13 @@ ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) {
 				var = ResourceLoader::load(path);
 
 				if (pinfo.hint_string == "Script") {
-					if (debug_obj->get_script() != var) {
-						debug_obj->set_script(Ref<RefCounted>());
+					if (remote_objects->get_script() != var) {
+						remote_objects->set_script(Ref<RefCounted>());
 						Ref<Script> scr(var);
 						if (scr.is_valid()) {
-							ScriptInstance *scr_instance = scr->placeholder_instance_create(debug_obj);
+							ScriptInstance *scr_instance = scr->placeholder_instance_create(remote_objects);
 							if (scr_instance) {
-								debug_obj->set_script_and_instance(var, scr_instance);
+								remote_objects->set_script_and_instance(var, scr_instance);
 							}
 						}
 					}
@@ -178,49 +303,67 @@ ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) {
 			}
 		}
 
-		//always add the property, since props may have been added or removed
-		debug_obj->prop_list.push_back(pinfo);
+		// Always add the property, since props may have been added or removed.
+		remote_objects->prop_list.push_back(pinfo);
 
-		if (!debug_obj->prop_values.has(pinfo.name)) {
+		if (!remote_objects->prop_values.has(pinfo.name)) {
 			new_props_added++;
-			debug_obj->prop_values[pinfo.name] = var;
-		} else {
-			if (bool(Variant::evaluate(Variant::OP_NOT_EQUAL, debug_obj->prop_values[pinfo.name], var))) {
-				debug_obj->prop_values[pinfo.name] = var;
-				changed.insert(pinfo.name);
-			}
+		} else if (bool(Variant::evaluate(Variant::OP_NOT_EQUAL, remote_objects->prop_values[pinfo.name], var))) {
+			changed.insert(pinfo.name);
 		}
+
+		remote_objects->prop_values[pinfo.name] = KV.value.values;
 	}
 
-	if (old_prop_size == debug_obj->prop_list.size() && new_props_added == 0) {
-		//only some may have changed, if so, then update those, if exist
+	if (old_prop_size == remote_objects->prop_list.size() && new_props_added == 0) {
+		// Only some may have changed, if so, then update those, if they exist.
 		for (const String &E : changed) {
-			emit_signal(SNAME("object_property_updated"), debug_obj->remote_object_id, E);
+			emit_signal(SNAME("object_property_updated"), remote_objects->get_instance_id(), E);
 		}
 	} else {
-		//full update, because props were added or removed
-		debug_obj->update();
+		// Full update, because props were added or removed.
+		remote_objects->update();
+	}
+
+	return remote_objects;
+}
+
+void EditorDebuggerInspector::clear_remote_inspector() {
+	if (remote_objects_list.is_empty()) {
+		return;
+	}
+
+	const Object *obj = InspectorDock::get_inspector_singleton()->get_edited_object();
+	// Check if the inspector holds remote items, and take it out if so.
+	if (Object::cast_to<EditorDebuggerRemoteObjects>(obj)) {
+		EditorNode::get_singleton()->push_item(nullptr);
 	}
-	return obj.id;
 }
 
 void EditorDebuggerInspector::clear_cache() {
-	for (const KeyValue<ObjectID, EditorDebuggerRemoteObject *> &E : remote_objects) {
-		EditorNode *editor = EditorNode::get_singleton();
-		if (editor->get_editor_selection_history()->get_current() == E.value->get_instance_id()) {
-			editor->push_item(nullptr);
-		}
-		memdelete(E.value);
+	clear_remote_inspector();
+
+	for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) {
+		memdelete(robjs);
 	}
-	remote_objects.clear();
+	remote_objects_list.clear();
+
 	remote_dependencies.clear();
 }
 
-Object *EditorDebuggerInspector::get_object(ObjectID p_id) {
-	if (remote_objects.has(p_id)) {
-		return remote_objects[p_id];
+void EditorDebuggerInspector::invalidate_selection_from_cache(const TypedArray<uint64_t> &p_ids) {
+	for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) {
+		if (robjs->remote_object_ids == p_ids) {
+			const Object *obj = InspectorDock::get_inspector_singleton()->get_edited_object();
+			if (obj == robjs) {
+				EditorNode::get_singleton()->push_item(nullptr);
+			}
+
+			remote_objects_list.erase(robjs);
+			memdelete(robjs);
+			break;
+		}
 	}
-	return nullptr;
 }
 
 void EditorDebuggerInspector::add_stack_variable(const Array &p_array, int p_offset) {
@@ -270,7 +413,7 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array, int p_off
 		}
 		variables->prop_list.insert_before(current, pinfo);
 	}
-	variables->prop_values[type + n] = v;
+	variables->prop_values[type + n][0] = v;
 	variables->update();
 	edit(variables);
 
@@ -288,7 +431,7 @@ void EditorDebuggerInspector::clear_stack_variables() {
 }
 
 String EditorDebuggerInspector::get_stack_variable(const String &p_var) {
-	for (KeyValue<StringName, Variant> &E : variables->prop_values) {
+	for (KeyValue<StringName, TypedDictionary<uint64_t, Variant>> &E : variables->prop_values) {
 		String v = E.key.operator String();
 		if (v.get_slicec('/', 1) == p_var) {
 			return variables->get_variant(v);

+ 18 - 13
editor/debugger/editor_debugger_inspector.h

@@ -30,10 +30,16 @@
 
 #pragma once
 
+#include "core/variant/typed_dictionary.h"
 #include "editor/editor_inspector.h"
 
-class EditorDebuggerRemoteObject : public Object {
-	GDCLASS(EditorDebuggerRemoteObject, Object);
+class SceneDebuggerObject;
+
+class EditorDebuggerRemoteObjects : public Object {
+	GDCLASS(EditorDebuggerRemoteObjects, Object);
+
+private:
+	bool _set_impl(const StringName &p_name, const Variant &p_value, const String &p_field);
 
 protected:
 	bool _set(const StringName &p_name, const Variant &p_value);
@@ -42,14 +48,13 @@ protected:
 	static void _bind_methods();
 
 public:
-	ObjectID remote_object_id;
+	TypedArray<uint64_t> remote_object_ids;
 	String type_name;
 	List<PropertyInfo> prop_list;
-	HashMap<StringName, Variant> prop_values;
+	HashMap<StringName, TypedDictionary<uint64_t, Variant>> prop_values;
 
-	ObjectID get_remote_object_id() { return remote_object_id; }
+	void set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field);
 	String get_title();
-
 	Variant get_variant(const StringName &p_name);
 
 	void clear() {
@@ -59,20 +64,19 @@ public:
 
 	void update() { notify_property_list_changed(); }
 
-	EditorDebuggerRemoteObject() {}
+	EditorDebuggerRemoteObjects() {}
 };
 
 class EditorDebuggerInspector : public EditorInspector {
 	GDCLASS(EditorDebuggerInspector, EditorInspector);
 
 private:
-	ObjectID inspected_object_id;
-	HashMap<ObjectID, EditorDebuggerRemoteObject *> remote_objects;
+	LocalVector<EditorDebuggerRemoteObjects *> remote_objects_list;
 	HashSet<Ref<Resource>> remote_dependencies;
-	EditorDebuggerRemoteObject *variables = nullptr;
+	EditorDebuggerRemoteObjects *variables = nullptr;
 
 	void _object_selected(ObjectID p_object);
-	void _object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value);
+	void _objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field);
 
 protected:
 	void _notification(int p_what);
@@ -83,9 +87,10 @@ public:
 	~EditorDebuggerInspector();
 
 	// Remote Object cache
-	ObjectID add_object(const Array &p_arr);
-	Object *get_object(ObjectID p_id);
+	EditorDebuggerRemoteObjects *set_objects(const Array &p_array);
+	void clear_remote_inspector();
 	void clear_cache();
+	void invalidate_selection_from_cache(const TypedArray<uint64_t> &p_ids);
 
 	// Stack Dump variables
 	String get_stack_variable(const String &p_var);

+ 104 - 46
editor/debugger/editor_debugger_node.cpp

@@ -82,7 +82,8 @@ EditorDebuggerNode::EditorDebuggerNode() {
 
 	// Remote scene tree
 	remote_scene_tree = memnew(EditorDebuggerTree);
-	remote_scene_tree->connect("object_selected", callable_mp(this, &EditorDebuggerNode::_remote_object_requested));
+	remote_scene_tree->connect("objects_selected", callable_mp(this, &EditorDebuggerNode::_remote_objects_requested));
+	remote_scene_tree->connect("selection_cleared", callable_mp(this, &EditorDebuggerNode::_remote_selection_cleared));
 	remote_scene_tree->connect("save_node", callable_mp(this, &EditorDebuggerNode::_save_node_requested));
 	remote_scene_tree->connect("button_clicked", callable_mp(this, &EditorDebuggerNode::_remote_tree_button_pressed));
 	SceneTreeDock::get_singleton()->add_remote_tree_editor(remote_scene_tree);
@@ -109,11 +110,12 @@ ScriptEditorDebugger *EditorDebuggerNode::_add_debugger() {
 	node->connect("breakpoint_selected", callable_mp(this, &EditorDebuggerNode::_error_selected).bind(id));
 	node->connect("clear_execution", callable_mp(this, &EditorDebuggerNode::_clear_execution));
 	node->connect("breaked", callable_mp(this, &EditorDebuggerNode::_breaked).bind(id));
+	node->connect("debug_data", callable_mp(this, &EditorDebuggerNode::_debug_data).bind(id));
 	node->connect("remote_tree_select_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_select_requested).bind(id));
+	node->connect("remote_tree_clear_selection_requested", callable_mp(this, &EditorDebuggerNode::_remote_tree_clear_selection_requested).bind(id));
 	node->connect("remote_tree_updated", callable_mp(this, &EditorDebuggerNode::_remote_tree_updated).bind(id));
-	node->connect("remote_object_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_updated).bind(id));
+	node->connect("remote_objects_updated", callable_mp(this, &EditorDebuggerNode::_remote_objects_updated).bind(id));
 	node->connect("remote_object_property_updated", callable_mp(this, &EditorDebuggerNode::_remote_object_property_updated).bind(id));
-	node->connect("remote_object_requested", callable_mp(this, &EditorDebuggerNode::_remote_object_requested).bind(id));
 	node->connect("set_breakpoint", callable_mp(this, &EditorDebuggerNode::_breakpoint_set_in_tree).bind(id));
 	node->connect("clear_breakpoints", callable_mp(this, &EditorDebuggerNode::_breakpoints_cleared_in_tree).bind(id));
 	node->connect("errors_cleared", callable_mp(this, &EditorDebuggerNode::_update_errors));
@@ -222,10 +224,6 @@ void EditorDebuggerNode::register_undo_redo(UndoRedo *p_undo_redo) {
 	p_undo_redo->set_property_notify_callback(_properties_changed, this);
 }
 
-EditorDebuggerRemoteObject *EditorDebuggerNode::get_inspected_remote_object() {
-	return Object::cast_to<EditorDebuggerRemoteObject>(ObjectDB::get_instance(EditorNode::get_singleton()->get_editor_selection_history()->get_current()));
-}
-
 ScriptEditorDebugger *EditorDebuggerNode::get_debugger(int p_id) const {
 	return Object::cast_to<ScriptEditorDebugger>(tabs->get_tab_control(p_id));
 }
@@ -292,6 +290,10 @@ void EditorDebuggerNode::stop(bool p_force) {
 	if (keep_open && !p_force) {
 		return;
 	}
+
+	remote_scene_tree_wait = false;
+	inspect_edited_object_wait = false;
+
 	current_uri.clear();
 	if (server.is_valid()) {
 		server->stop();
@@ -304,6 +306,7 @@ void EditorDebuggerNode::stop(bool p_force) {
 
 		server.unref();
 	}
+
 	// Also close all debugging sessions.
 	_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
 		if (dbg->is_session_active()) {
@@ -351,21 +354,29 @@ void EditorDebuggerNode::_notification(int p_what) {
 
 			_update_errors();
 
-			// Remote scene tree update
-			remote_scene_tree_timeout -= get_process_delta_time();
-			if (remote_scene_tree_timeout < 0) {
-				remote_scene_tree_timeout = EDITOR_GET("debugger/remote_scene_tree_refresh_interval");
-				if (remote_scene_tree->is_visible_in_tree()) {
-					get_current_debugger()->request_remote_tree();
+			// Remote scene tree update.
+			if (!remote_scene_tree_wait) {
+				remote_scene_tree_timeout -= get_process_delta_time();
+				if (remote_scene_tree_timeout < 0) {
+					remote_scene_tree_timeout = EDITOR_GET("debugger/remote_scene_tree_refresh_interval");
+
+					if (remote_scene_tree->is_visible_in_tree()) {
+						remote_scene_tree_wait = true;
+						get_current_debugger()->request_remote_tree();
+					}
 				}
 			}
 
-			// Remote inspector update
-			inspect_edited_object_timeout -= get_process_delta_time();
-			if (inspect_edited_object_timeout < 0) {
-				inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
-				if (EditorDebuggerRemoteObject *obj = get_inspected_remote_object()) {
-					get_current_debugger()->request_remote_object(obj->remote_object_id);
+			// Remote inspector update.
+			if (!inspect_edited_object_wait) {
+				inspect_edited_object_timeout -= get_process_delta_time();
+				if (inspect_edited_object_timeout < 0) {
+					inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
+
+					if (EditorDebuggerRemoteObjects *robjs = Object::cast_to<EditorDebuggerRemoteObjects>(InspectorDock::get_inspector_singleton()->get_edited_object())) {
+						inspect_edited_object_wait = true;
+						get_current_debugger()->request_remote_objects(robjs->remote_object_ids, false);
+					}
 				}
 			}
 
@@ -467,21 +478,26 @@ void EditorDebuggerNode::_debugger_stopped(int p_id) {
 
 void EditorDebuggerNode::_debugger_wants_stop(int p_id) {
 	// Ask editor to kill PID.
-	int pid = get_debugger(p_id)->get_remote_pid();
-	if (pid) {
+	if (int pid = get_debugger(p_id)->get_remote_pid()) {
 		callable_mp(EditorNode::get_singleton(), &EditorNode::stop_child_process).call_deferred(pid);
 	}
 }
 
 void EditorDebuggerNode::_debugger_changed(int p_tab) {
-	if (get_inspected_remote_object()) {
-		// Clear inspected object, you can only inspect objects in selected debugger.
-		// Hopefully, in the future, we will have one inspector per debugger.
-		EditorNode::get_singleton()->push_item(nullptr);
+	remote_scene_tree_wait = false;
+	inspect_edited_object_wait = false;
+
+	if (Object *robjs = InspectorDock::get_inspector_singleton()->get_edited_object()) {
+		if (Object::cast_to<EditorDebuggerRemoteObjects>(robjs)) {
+			// Clear inspected object, you can only inspect objects in selected debugger.
+			// Hopefully, in the future, we will have one inspector per debugger.
+			EditorNode::get_singleton()->push_item(nullptr);
+		}
 	}
 
-	if (get_previous_debugger()) {
-		_text_editor_stack_clear(get_previous_debugger());
+	if (ScriptEditorDebugger *prev_debug = get_previous_debugger()) {
+		prev_debug->clear_inspector();
+		_text_editor_stack_clear(prev_debug);
 	}
 	if (remote_scene_tree->is_visible_in_tree()) {
 		get_current_debugger()->request_remote_tree();
@@ -493,6 +509,18 @@ void EditorDebuggerNode::_debugger_changed(int p_tab) {
 	_break_state_changed();
 }
 
+void EditorDebuggerNode::_debug_data(const String &p_msg, const Array &p_data, int p_debugger) {
+	if (p_debugger != tabs->get_current_tab()) {
+		return;
+	}
+
+	if (p_msg == "scene:scene_tree") {
+		remote_scene_tree_wait = false;
+	} else if (p_msg == "scene:inspect_objects") {
+		inspect_edited_object_wait = false;
+	}
+}
+
 void EditorDebuggerNode::set_script_debug_button(MenuButton *p_button) {
 	script_menu = p_button;
 	script_menu->set_text(TTR("Debug"));
@@ -646,11 +674,41 @@ void EditorDebuggerNode::request_remote_tree() {
 	get_current_debugger()->request_remote_tree();
 }
 
-void EditorDebuggerNode::_remote_tree_select_requested(ObjectID p_id, int p_debugger) {
+void EditorDebuggerNode::set_remote_selection(const TypedArray<int64_t> &p_ids) {
+	remote_scene_tree->select_nodes(p_ids);
+
+	stop_waiting_inspection();
+	get_current_debugger()->request_remote_objects(p_ids);
+}
+
+void EditorDebuggerNode::clear_remote_tree_selection() {
+	remote_scene_tree->clear_selection();
+	get_current_debugger()->clear_inspector(remote_scene_tree_clear_msg);
+}
+
+void EditorDebuggerNode::stop_waiting_inspection() {
+	inspect_edited_object_timeout = EDITOR_GET("debugger/remote_inspect_refresh_interval");
+	inspect_edited_object_wait = false;
+}
+
+bool EditorDebuggerNode::match_remote_selection(const TypedArray<uint64_t> &p_ids) const {
+	return p_ids == remote_scene_tree->get_selection();
+}
+
+void EditorDebuggerNode::_remote_tree_select_requested(const TypedArray<int64_t> &p_ids, int p_debugger) {
+	if (p_debugger == tabs->get_current_tab()) {
+		remote_scene_tree->select_nodes(p_ids);
+	}
+}
+
+void EditorDebuggerNode::_remote_tree_clear_selection_requested(int p_debugger) {
 	if (p_debugger != tabs->get_current_tab()) {
 		return;
 	}
-	remote_scene_tree->select_node(p_id);
+	remote_scene_tree->clear_selection();
+	remote_scene_tree_clear_msg = false;
+	get_current_debugger()->clear_inspector(false);
+	remote_scene_tree_clear_msg = true;
 }
 
 void EditorDebuggerNode::_remote_tree_updated(int p_debugger) {
@@ -679,37 +737,37 @@ void EditorDebuggerNode::_remote_tree_button_pressed(Object *p_item, int p_colum
 	}
 }
 
-void EditorDebuggerNode::_remote_object_updated(ObjectID p_id, int p_debugger) {
-	if (p_debugger != tabs->get_current_tab()) {
-		return;
+void EditorDebuggerNode::_remote_objects_updated(EditorDebuggerRemoteObjects *p_objs, int p_debugger) {
+	if (p_debugger == tabs->get_current_tab() && p_objs != InspectorDock::get_inspector_singleton()->get_edited_object()) {
+		EditorNode::get_singleton()->push_item(p_objs);
 	}
-	if (EditorDebuggerRemoteObject *obj = get_inspected_remote_object()) {
-		if (obj->remote_object_id == p_id) {
-			return; // Already being edited
-		}
-	}
-
-	EditorNode::get_singleton()->push_item(get_current_debugger()->get_remote_object(p_id));
 }
 
 void EditorDebuggerNode::_remote_object_property_updated(ObjectID p_id, const String &p_property, int p_debugger) {
 	if (p_debugger != tabs->get_current_tab()) {
 		return;
 	}
-	if (EditorDebuggerRemoteObject *obj = get_inspected_remote_object()) {
-		if (obj->remote_object_id != p_id) {
-			return;
-		}
+
+	Object *obj = InspectorDock::get_inspector_singleton()->get_edited_object();
+	if (obj && obj->get_instance_id() == p_id) {
 		InspectorDock::get_inspector_singleton()->update_property(p_property);
 	}
 }
 
-void EditorDebuggerNode::_remote_object_requested(ObjectID p_id, int p_debugger) {
+void EditorDebuggerNode::_remote_objects_requested(const TypedArray<uint64_t> &p_ids, int p_debugger) {
+	if (p_debugger != tabs->get_current_tab()) {
+		return;
+	}
+	stop_waiting_inspection();
+	get_current_debugger()->request_remote_objects(p_ids);
+}
+
+void EditorDebuggerNode::_remote_selection_cleared(int p_debugger) {
 	if (p_debugger != tabs->get_current_tab()) {
 		return;
 	}
-	inspect_edited_object_timeout = 0.7; // Temporarily disable timeout to avoid multiple requests.
-	get_current_debugger()->request_remote_object(p_id);
+	stop_waiting_inspection();
+	get_current_debugger()->clear_inspector();
 }
 
 void EditorDebuggerNode::_save_node_requested(ObjectID p_id, const String &p_file, int p_debugger) {

+ 14 - 5
editor/debugger/editor_debugger_node.h

@@ -38,7 +38,7 @@ class Button;
 class DebugAdapterParser;
 class EditorDebuggerPlugin;
 class EditorDebuggerTree;
-class EditorDebuggerRemoteObject;
+class EditorDebuggerRemoteObjects;
 class MenuButton;
 class ScriptEditorDebugger;
 class TabContainer;
@@ -102,9 +102,12 @@ private:
 	int last_error_count = 0;
 	int last_warning_count = 0;
 
+	bool inspect_edited_object_wait = false;
 	float inspect_edited_object_timeout = 0;
 	EditorDebuggerTree *remote_scene_tree = nullptr;
+	bool remote_scene_tree_wait = false;
 	float remote_scene_tree_timeout = 0.0;
+	bool remote_scene_tree_clear_msg = true;
 	bool auto_switch_remote_scene_tree = false;
 	bool debug_with_external_editor = false;
 	bool keep_open = false;
@@ -116,7 +119,6 @@ private:
 	HashSet<Ref<EditorDebuggerPlugin>> debugger_plugins;
 
 	ScriptEditorDebugger *_add_debugger();
-	EditorDebuggerRemoteObject *get_inspected_remote_object();
 	void _update_errors();
 
 	friend class DebuggerEditorPlugin;
@@ -128,12 +130,15 @@ protected:
 	void _debugger_stopped(int p_id);
 	void _debugger_wants_stop(int p_id);
 	void _debugger_changed(int p_tab);
-	void _remote_tree_select_requested(ObjectID p_id, int p_debugger);
+	void _debug_data(const String &p_msg, const Array &p_data, int p_debugger);
+	void _remote_tree_select_requested(const TypedArray<int64_t> &p_ids, int p_debugger);
+	void _remote_tree_clear_selection_requested(int p_debugger);
 	void _remote_tree_updated(int p_debugger);
 	void _remote_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button);
-	void _remote_object_updated(ObjectID p_id, int p_debugger);
+	void _remote_objects_updated(EditorDebuggerRemoteObjects *p_objs, int p_debugger);
 	void _remote_object_property_updated(ObjectID p_id, const String &p_property, int p_debugger);
-	void _remote_object_requested(ObjectID p_id, int p_debugger);
+	void _remote_objects_requested(const TypedArray<uint64_t> &p_ids, int p_debugger);
+	void _remote_selection_cleared(int p_debugger);
 	void _save_node_requested(ObjectID p_id, const String &p_file, int p_debugger);
 
 	void _breakpoint_set_in_tree(Ref<RefCounted> p_script, int p_line, bool p_enabled, int p_debugger);
@@ -190,6 +195,10 @@ public:
 
 	// Remote inspector/edit.
 	void request_remote_tree();
+	void set_remote_selection(const TypedArray<int64_t> &p_ids);
+	void clear_remote_tree_selection();
+	void stop_waiting_inspection();
+	bool match_remote_selection(const TypedArray<uint64_t> &p_ids) const;
 	static void _methods_changed(void *p_ud, Object *p_base, const StringName &p_name, const Variant **p_args, int p_argcount);
 	static void _properties_changed(void *p_ud, Object *p_base, const StringName &p_property, const Variant &p_value);
 

+ 86 - 29
editor/debugger/editor_debugger_tree.cpp

@@ -35,6 +35,7 @@
 #include "editor/editor_settings.h"
 #include "editor/editor_string_names.h"
 #include "editor/gui/editor_file_dialog.h"
+#include "editor/gui/editor_toaster.h"
 #include "editor/scene_tree_dock.h"
 #include "scene/debugger/scene_debugger.h"
 #include "scene/gui/texture_rect.h"
@@ -44,6 +45,7 @@
 EditorDebuggerTree::EditorDebuggerTree() {
 	set_v_size_flags(SIZE_EXPAND_FILL);
 	set_allow_rmb_select(true);
+	set_select_mode(SELECT_MULTI);
 
 	// Popup
 	item_menu = memnew(PopupMenu);
@@ -54,6 +56,9 @@ EditorDebuggerTree::EditorDebuggerTree() {
 	file_dialog = memnew(EditorFileDialog);
 	file_dialog->connect("file_selected", callable_mp(this, &EditorDebuggerTree::_file_selected));
 	add_child(file_dialog);
+
+	accept = memnew(AcceptDialog);
+	add_child(accept);
 }
 
 void EditorDebuggerTree::_notification(int p_what) {
@@ -61,7 +66,8 @@ void EditorDebuggerTree::_notification(int p_what) {
 		case NOTIFICATION_POSTINITIALIZE: {
 			set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
 
-			connect("cell_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_selected));
+			connect("multi_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_selection_changed));
+			connect("nothing_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_nothing_selected));
 			connect("item_collapsed", callable_mp(this, &EditorDebuggerTree::_scene_tree_folded));
 			connect("item_mouse_selected", callable_mp(this, &EditorDebuggerTree::_scene_tree_rmb_selected));
 		} break;
@@ -73,24 +79,57 @@ void EditorDebuggerTree::_notification(int p_what) {
 }
 
 void EditorDebuggerTree::_bind_methods() {
-	ADD_SIGNAL(MethodInfo("object_selected", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::INT, "debugger")));
+	ADD_SIGNAL(MethodInfo("objects_selected", PropertyInfo(Variant::ARRAY, "object_ids"), PropertyInfo(Variant::INT, "debugger")));
+	ADD_SIGNAL(MethodInfo("selection_cleared", PropertyInfo(Variant::INT, "debugger")));
 	ADD_SIGNAL(MethodInfo("save_node", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "filename"), PropertyInfo(Variant::INT, "debugger")));
 	ADD_SIGNAL(MethodInfo("open"));
 }
 
-void EditorDebuggerTree::_scene_tree_selected() {
-	if (updating_scene_tree) {
+void EditorDebuggerTree::_scene_tree_selection_changed(TreeItem *p_item, int p_column, bool p_selected) {
+	if (updating_scene_tree || !p_item) {
 		return;
 	}
 
-	TreeItem *item = get_selected();
-	if (!item) {
-		return;
+	uint64_t id = uint64_t(p_item->get_metadata(0));
+	if (p_selected) {
+		if (inspected_object_ids.size() == (int)EDITOR_GET("debugger/max_node_selection")) {
+			selection_surpassed_limit = true;
+			p_item->deselect(0);
+			return;
+		}
+
+		if (!inspected_object_ids.has(id)) {
+			inspected_object_ids.append(id);
+		}
+	} else if (inspected_object_ids.has(id)) {
+		inspected_object_ids.erase(id);
+	}
+
+	if (!notify_selection_queued) {
+		callable_mp(this, &EditorDebuggerTree::_notify_selection_changed).call_deferred();
+		notify_selection_queued = true;
 	}
+}
+
+void EditorDebuggerTree::_scene_tree_nothing_selected() {
+	deselect_all();
+	inspected_object_ids.clear();
+	emit_signal(SNAME("selection_cleared"), debugger_id);
+}
 
-	inspected_object_id = uint64_t(item->get_metadata(0));
+void EditorDebuggerTree::_notify_selection_changed() {
+	notify_selection_queued = false;
 
-	emit_signal(SNAME("object_selected"), inspected_object_id, debugger_id);
+	if (inspected_object_ids.is_empty()) {
+		emit_signal(SNAME("selection_cleared"), debugger_id);
+	} else {
+		emit_signal(SNAME("objects_selected"), inspected_object_ids.duplicate(), debugger_id);
+	}
+
+	if (selection_surpassed_limit) {
+		selection_surpassed_limit = false;
+		EditorToaster::get_singleton()->popup_str(vformat(TTR("Some remote nodes were not selected, as the configured maximum selection is %d. This can be changed at \"debugger/max_node_selection\" in the Editor Settings."), EDITOR_GET("debugger/max_node_selection")), EditorToaster::SEVERITY_WARNING);
+	}
 }
 
 void EditorDebuggerTree::_scene_tree_folded(Object *p_obj) {
@@ -124,7 +163,7 @@ void EditorDebuggerTree::_scene_tree_rmb_selected(const Vector2 &p_position, Mou
 	item->select(0);
 
 	item_menu->clear();
-	item_menu->add_icon_item(get_editor_theme_icon(SNAME("CreateNewSceneFrom")), TTR("Save Branch as Scene"), ITEM_MENU_SAVE_REMOTE_NODE);
+	item_menu->add_icon_item(get_editor_theme_icon(SNAME("CreateNewSceneFrom")), TTR("Save Branch as Scene..."), ITEM_MENU_SAVE_REMOTE_NODE);
 	item_menu->add_icon_item(get_editor_theme_icon(SNAME("CopyNodePath")), TTR("Copy Node Path"), ITEM_MENU_COPY_NODE_PATH);
 	item_menu->add_icon_item(get_editor_theme_icon(SNAME("Collapse")), TTR("Expand/Collapse Branch"), ITEM_MENU_EXPAND_COLLAPSE);
 	item_menu->set_position(get_screen_position() + get_local_mouse_position());
@@ -152,12 +191,13 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 	updating_scene_tree = true;
 	const String last_path = get_selected_path();
 	const String filter = SceneTreeDock::get_singleton()->get_filter();
-	TreeItem *select_item = nullptr;
+	LocalVector<TreeItem *> select_items;
 	bool hide_filtered_out_parents = EDITOR_GET("docks/scene_tree/hide_filtered_out_parents");
 
 	bool should_scroll = scrolling_to_item || filter != last_filter;
 	scrolling_to_item = false;
 	TreeItem *scroll_item = nullptr;
+	TypedArray<uint64_t> ids_present;
 
 	// Nodes are in a flatten list, depth first. Use a stack of parents, avoid recursion.
 	List<ParentItem> parents;
@@ -216,9 +256,11 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 		}
 		item->set_meta("node_path", current_path + "/" + item->get_text(0));
 
-		// Select previously selected node.
+		// Select previously selected nodes.
 		if (debugger_id == p_debugger) { // Can use remote id.
-			if (node.id == inspected_object_id) {
+			if (inspected_object_ids.has(uint64_t(node.id))) {
+				ids_present.append(node.id);
+
 				if (selection_uncollapse_all) {
 					selection_uncollapse_all = false;
 
@@ -228,14 +270,14 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 					updating_scene_tree = true;
 				}
 
-				select_item = item;
+				select_items.push_back(item);
 				if (should_scroll) {
 					scroll_item = item;
 				}
 			}
 		} else if (last_path == (String)item->get_meta("node_path")) { // Must use path.
-			updating_scene_tree = false; // Force emission of new selection.
-			select_item = item;
+			updating_scene_tree = false; // Force emission of new selections.
+			select_items.push_back(item);
 			if (should_scroll) {
 				scroll_item = item;
 			}
@@ -280,12 +322,12 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 					break; // Filter matches, must survive.
 				}
 
-				parent->remove_child(item);
-				memdelete(item);
-				if (select_item == item || scroll_item == item) {
-					select_item = nullptr;
+				if (select_items.has(item) || scroll_item == item) {
+					select_items.resize(select_items.size() - 1);
 					scroll_item = nullptr;
 				}
+				parent->remove_child(item);
+				memdelete(item);
 
 				if (had_siblings) {
 					break; // Parent must survive.
@@ -316,18 +358,20 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 
 				from->get_parent()->remove_child(from);
 				memdelete(from);
-				if (select_item == from || scroll_item == from) {
-					select_item = nullptr;
+				if (select_items.has(from) || scroll_item == from) {
+					select_items.erase(from);
 					scroll_item = nullptr;
 				}
 			}
 		}
 	}
 
+	inspected_object_ids = ids_present;
+
 	debugger_id = p_debugger; // Needed by hook, could be avoided if every debugger had its own tree.
 
-	if (select_item) {
-		select_item->select(0);
+	for (TreeItem *item : select_items) {
+		item->select(0);
 	}
 	if (scroll_item) {
 		scroll_to_item(scroll_item, false);
@@ -337,12 +381,22 @@ void EditorDebuggerTree::update_scene_tree(const SceneDebuggerTree *p_tree, int
 	updating_scene_tree = false;
 }
 
-void EditorDebuggerTree::select_node(ObjectID p_id) {
+void EditorDebuggerTree::select_nodes(const TypedArray<int64_t> &p_ids) {
 	// Manually select, as the tree control may be out-of-date for some reason (e.g. not shown yet).
 	selection_uncollapse_all = true;
-	inspected_object_id = uint64_t(p_id);
+	inspected_object_ids = p_ids;
 	scrolling_to_item = true;
-	emit_signal(SNAME("object_selected"), inspected_object_id, debugger_id);
+
+	if (!updating_scene_tree) {
+		// Request a tree refresh.
+		EditorDebuggerNode::get_singleton()->request_remote_tree();
+	}
+	// Set the value immediately, so no update flooding happens and causes a crash.
+	updating_scene_tree = true;
+}
+
+void EditorDebuggerTree::clear_selection() {
+	inspected_object_ids.clear();
 
 	if (!updating_scene_tree) {
 		// Request a tree refresh.
@@ -453,8 +507,11 @@ void EditorDebuggerTree::_item_menu_id_pressed(int p_option) {
 }
 
 void EditorDebuggerTree::_file_selected(const String &p_file) {
-	if (inspected_object_id.is_null()) {
+	if (inspected_object_ids.size() != 1) {
+		accept->set_text(vformat(TTR("Saving the branch as a scene requires selecting only one node, but you have selected %d nodes."), inspected_object_ids.size()));
+		accept->popup_centered();
 		return;
 	}
-	emit_signal(SNAME("save_node"), inspected_object_id, p_file, debugger_id);
+
+	emit_signal(SNAME("save_node"), inspected_object_ids[0], p_file, debugger_id);
 }

+ 12 - 3
editor/debugger/editor_debugger_tree.h

@@ -32,6 +32,7 @@
 
 #include "scene/gui/tree.h"
 
+class AcceptDialog;
 class SceneDebuggerTree;
 class EditorFileDialog;
 
@@ -57,18 +58,23 @@ private:
 		ITEM_MENU_EXPAND_COLLAPSE,
 	};
 
-	ObjectID inspected_object_id;
+	TypedArray<uint64_t> inspected_object_ids;
 	int debugger_id = 0;
 	bool updating_scene_tree = false;
 	bool scrolling_to_item = false;
+	bool notify_selection_queued = false;
+	bool selection_surpassed_limit = false;
 	bool selection_uncollapse_all = false;
 	HashSet<ObjectID> unfold_cache;
 	PopupMenu *item_menu = nullptr;
 	EditorFileDialog *file_dialog = nullptr;
+	AcceptDialog *accept = nullptr;
 	String last_filter;
 
 	void _scene_tree_folded(Object *p_obj);
-	void _scene_tree_selected();
+	void _scene_tree_selection_changed(TreeItem *p_item, int p_column, bool p_selected);
+	void _scene_tree_nothing_selected();
+	void _notify_selection_changed();
 	void _scene_tree_rmb_selected(const Vector2 &p_position, MouseButton p_button);
 	void _item_menu_id_pressed(int p_option);
 	void _file_selected(const String &p_file);
@@ -89,7 +95,10 @@ public:
 	String get_selected_path();
 	ObjectID get_selected_object();
 	int get_current_debugger(); // Would love to have one tree for every debugger.
+	inline TypedArray<uint64_t> get_selection() const { return inspected_object_ids.duplicate(); }
 	void update_scene_tree(const SceneDebuggerTree *p_tree, int p_debugger);
-	void select_node(ObjectID p_id);
+	void select_nodes(const TypedArray<int64_t> &p_ids);
+	void clear_selection();
+
 	EditorDebuggerTree();
 };

+ 62 - 30
editor/debugger/script_editor_debugger.cpp

@@ -33,10 +33,8 @@
 #include "core/config/project_settings.h"
 #include "core/debugger/debugger_marshalls.h"
 #include "core/debugger/remote_debugger.h"
-#include "core/io/marshalls.h"
 #include "core/string/ustring.h"
 #include "core/version.h"
-#include "editor/debugger/debug_adapter/debug_adapter_protocol.h"
 #include "editor/debugger/editor_expression_evaluator.h"
 #include "editor/debugger/editor_performance_profiler.h"
 #include "editor/debugger/editor_profiler.h"
@@ -48,6 +46,7 @@
 #include "editor/editor_settings.h"
 #include "editor/editor_string_names.h"
 #include "editor/gui/editor_file_dialog.h"
+#include "editor/gui/editor_toaster.h"
 #include "editor/inspector_dock.h"
 #include "editor/plugins/canvas_item_editor_plugin.h"
 #include "editor/plugins/editor_debugger_plugin.h"
@@ -77,7 +76,8 @@ void ScriptEditorDebugger::_put_msg(const String &p_message, const Array &p_data
 		msg.push_back(p_message);
 		msg.push_back(p_thread_id);
 		msg.push_back(p_data);
-		peer->put_message(msg);
+		Error err = peer->put_message(msg);
+		ERR_FAIL_COND_MSG(err != OK, vformat("Failed to send message %d", err));
 	}
 }
 
@@ -257,32 +257,44 @@ void ScriptEditorDebugger::request_remote_evaluate(const String &p_expression, i
 	_put_msg("evaluate", msg);
 }
 
-void ScriptEditorDebugger::update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value) {
+void ScriptEditorDebugger::update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value, const String &p_field) {
 	Array msg;
 	msg.push_back(p_obj_id);
 	msg.push_back(p_prop);
 	msg.push_back(p_value);
-	_put_msg("scene:set_object_property", msg);
+	if (p_field.is_empty()) {
+		_put_msg("scene:set_object_property", msg);
+	} else {
+		msg.push_back(p_field);
+		_put_msg("scene:set_object_property_field", msg);
+	}
 }
 
-void ScriptEditorDebugger::request_remote_object(ObjectID p_obj_id) {
-	ERR_FAIL_COND(p_obj_id.is_null());
+void ScriptEditorDebugger::request_remote_objects(const TypedArray<uint64_t> &p_obj_ids, bool p_update_selection) {
+	ERR_FAIL_COND(p_obj_ids.is_empty());
 	Array msg;
-	msg.push_back(p_obj_id);
-	_put_msg("scene:inspect_object", msg);
+	msg.push_back(p_obj_ids.duplicate());
+	msg.push_back(p_update_selection);
+	_put_msg("scene:inspect_objects", msg);
 }
 
-Object *ScriptEditorDebugger::get_remote_object(ObjectID p_id) {
-	return inspector->get_object(p_id);
+void ScriptEditorDebugger::clear_inspector(bool p_send_msg) {
+	inspector->clear_remote_inspector();
+	if (p_send_msg) {
+		_put_msg("scene:clear_selection", Array());
+	}
 }
 
 void ScriptEditorDebugger::_remote_object_selected(ObjectID p_id) {
 	emit_signal(SNAME("remote_object_requested"), p_id);
 }
 
-void ScriptEditorDebugger::_remote_object_edited(ObjectID p_id, const String &p_prop, const Variant &p_value) {
-	update_remote_object(p_id, p_prop, p_value);
-	request_remote_object(p_id);
+void ScriptEditorDebugger::_remote_objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field) {
+	const Array &ids = p_values.keys();
+	for (uint64_t id : ids) {
+		update_remote_object(ObjectID(id), p_prop, p_values[id], p_field);
+	}
+	request_remote_objects(p_values.keys(), false);
 }
 
 void ScriptEditorDebugger::_remote_object_property_updated(ObjectID p_id, const String &p_property) {
@@ -405,10 +417,19 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread
 		scene_tree->deserialize(p_data);
 		emit_signal(SNAME("remote_tree_updated"));
 		_update_buttons_state();
-	} else if (p_msg == "scene:inspect_object") {
-		ObjectID id = inspector->add_object(p_data);
-		if (id.is_valid()) {
-			emit_signal(SNAME("remote_object_updated"), id);
+	} else if (p_msg == "scene:inspect_objects") {
+		ERR_FAIL_COND(p_data.is_empty());
+
+		TypedArray<uint64_t> ids;
+		for (const Array arr : p_data) {
+			ERR_FAIL_COND(arr.is_empty());
+			ERR_FAIL_COND(arr[0].get_type() != Variant::INT);
+			ids.append(arr[0]);
+		}
+
+		if (EditorDebuggerNode::get_singleton()->match_remote_selection(ids)) {
+			EditorDebuggerRemoteObjects *objs = inspector->set_objects(p_data);
+			emit_signal(SNAME("remote_objects_updated"), objs);
 		}
 	} else if (p_msg == "servers:memory_usage") {
 		vmem_tree->clear();
@@ -809,10 +830,24 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread
 	} else if (p_msg == "request_quit") {
 		emit_signal(SNAME("stop_requested"));
 		_stop_and_notify();
-	} else if (p_msg == "remote_node_clicked") {
-		if (!p_data.is_empty()) {
-			emit_signal(SNAME("remote_tree_select_requested"), p_data[0]);
+	} else if (p_msg == "remote_nodes_clicked") {
+		ERR_FAIL_COND(p_data.is_empty());
+		EditorDebuggerRemoteObjects *objs = inspector->set_objects(p_data);
+		if (objs) {
+			EditorDebuggerNode::get_singleton()->stop_waiting_inspection();
+
+			emit_signal(SNAME("remote_objects_updated"), objs);
+			emit_signal(SNAME("remote_tree_select_requested"), objs->remote_object_ids.duplicate());
 		}
+	} else if (p_msg == "remote_nothing_clicked") {
+		EditorDebuggerNode::get_singleton()->stop_waiting_inspection();
+
+		emit_signal(SNAME("remote_tree_clear_selection_requested"));
+	} else if (p_msg == "remote_selection_invalidated") {
+		ERR_FAIL_COND(p_data.is_empty());
+		inspector->invalidate_selection_from_cache(p_data[0]);
+	} else if (p_msg == "show_selection_limit_warning") {
+		EditorToaster::get_singleton()->popup_str(vformat(TTR("Some remote nodes were not selected, as the configured maximum selection is %d. This can be changed at \"debugger/max_node_selection\" in the Editor Settings."), EDITOR_GET("debugger/max_node_selection")), EditorToaster::SEVERITY_WARNING);
 	} else if (p_msg == "performance:profile_names") {
 		Vector<StringName> monitors;
 		monitors.resize(p_data.size());
@@ -1772,8 +1807,7 @@ void ScriptEditorDebugger::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("live_debug_restore_node"), &ScriptEditorDebugger::live_debug_restore_node);
 	ClassDB::bind_method(D_METHOD("live_debug_duplicate_node"), &ScriptEditorDebugger::live_debug_duplicate_node);
 	ClassDB::bind_method(D_METHOD("live_debug_reparent_node"), &ScriptEditorDebugger::live_debug_reparent_node);
-	ClassDB::bind_method(D_METHOD("request_remote_object", "id"), &ScriptEditorDebugger::request_remote_object);
-	ClassDB::bind_method(D_METHOD("update_remote_object", "id", "property", "value"), &ScriptEditorDebugger::update_remote_object);
+	ClassDB::bind_method(D_METHOD("update_remote_object", "id", "property", "value", "field"), &ScriptEditorDebugger::update_remote_object);
 
 	ADD_SIGNAL(MethodInfo("started"));
 	ADD_SIGNAL(MethodInfo("stopped"));
@@ -1784,12 +1818,12 @@ void ScriptEditorDebugger::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("set_execution", PropertyInfo("script"), PropertyInfo(Variant::INT, "line")));
 	ADD_SIGNAL(MethodInfo("clear_execution", PropertyInfo("script")));
 	ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "reallydid"), PropertyInfo(Variant::BOOL, "can_debug"), PropertyInfo(Variant::STRING, "reason"), PropertyInfo(Variant::BOOL, "has_stackdump")));
-	ADD_SIGNAL(MethodInfo("remote_object_requested", PropertyInfo(Variant::INT, "id")));
-	ADD_SIGNAL(MethodInfo("remote_object_updated", PropertyInfo(Variant::INT, "id")));
+	ADD_SIGNAL(MethodInfo("remote_objects_updated", PropertyInfo(Variant::OBJECT, "remote_objects")));
 	ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property")));
-	ADD_SIGNAL(MethodInfo("remote_tree_updated"));
-	ADD_SIGNAL(MethodInfo("remote_tree_select_requested", PropertyInfo(Variant::NODE_PATH, "path")));
 	ADD_SIGNAL(MethodInfo("remote_window_title_changed", PropertyInfo(Variant::STRING, "title")));
+	ADD_SIGNAL(MethodInfo("remote_tree_updated"));
+	ADD_SIGNAL(MethodInfo("remote_tree_select_requested", PropertyInfo(Variant::ARRAY, "ids")));
+	ADD_SIGNAL(MethodInfo("remote_tree_clear_selection_requested"));
 	ADD_SIGNAL(MethodInfo("output", PropertyInfo(Variant::STRING, "msg"), PropertyInfo(Variant::INT, "level")));
 	ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump")));
 	ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars")));
@@ -1955,7 +1989,7 @@ ScriptEditorDebugger::ScriptEditorDebugger() {
 		inspector->set_property_name_style(EditorPropertyNameProcessor::STYLE_RAW);
 		inspector->set_read_only(true);
 		inspector->connect("object_selected", callable_mp(this, &ScriptEditorDebugger::_remote_object_selected));
-		inspector->connect("object_edited", callable_mp(this, &ScriptEditorDebugger::_remote_object_edited));
+		inspector->connect("objects_edited", callable_mp(this, &ScriptEditorDebugger::_remote_objects_edited));
 		inspector->connect("object_property_updated", callable_mp(this, &ScriptEditorDebugger::_remote_object_property_updated));
 		inspector->register_text_enter(search);
 		inspector->set_use_filter(true);
@@ -2169,9 +2203,7 @@ ScriptEditorDebugger::ScriptEditorDebugger() {
 	msgdialog = memnew(AcceptDialog);
 	add_child(msgdialog);
 
-	live_debug = true;
 	camera_override = CameraOverride::OVERRIDE_NONE;
-	last_path_id = false;
 	error_count = 0;
 	warning_count = 0;
 	_update_buttons_state();

+ 7 - 7
editor/debugger/script_editor_debugger.h

@@ -34,7 +34,6 @@
 #include "core/os/os.h"
 #include "editor/debugger/editor_debugger_inspector.h"
 #include "editor/debugger/editor_debugger_node.h"
-#include "editor/debugger/editor_debugger_server.h"
 #include "scene/gui/button.h"
 #include "scene/gui/margin_container.h"
 
@@ -146,7 +145,7 @@ private:
 	Ref<RemoteDebuggerPeer> peer;
 
 	HashMap<NodePath, int> node_path_cache;
-	int last_path_id;
+	int last_path_id = 0;
 	HashMap<String, int> res_path_cache;
 
 	EditorProfiler *profiler = nullptr;
@@ -158,7 +157,7 @@ private:
 	bool move_to_foreground = true;
 	bool can_request_idle_draw = false;
 
-	bool live_debug;
+	bool live_debug = true;
 
 	uint64_t debugging_thread_id = Thread::UNASSIGNED_ID;
 
@@ -191,7 +190,7 @@ private:
 	void _set_reason_text(const String &p_reason, MessageType p_type);
 	void _update_buttons_state();
 	void _remote_object_selected(ObjectID p_object);
-	void _remote_object_edited(ObjectID, const String &p_prop, const Variant &p_value);
+	void _remote_objects_edited(const String &p_prop, const TypedDictionary<uint64_t, Variant> &p_values, const String &p_field);
 	void _remote_object_property_updated(ObjectID p_id, const String &p_property);
 
 	void _video_mem_request();
@@ -245,9 +244,10 @@ protected:
 	static void _bind_methods();
 
 public:
-	void request_remote_object(ObjectID p_obj_id);
-	void update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value);
-	Object *get_remote_object(ObjectID p_id);
+	void request_remote_objects(const TypedArray<uint64_t> &p_obj_ids, bool p_update_selection = true);
+	void update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value, const String &p_field = "");
+
+	void clear_inspector(bool p_send_msg = true);
 
 	// Needed by _live_edit_set, buttons state.
 	void set_editor_remote_tree(const Tree *p_tree) { editor_remote_tree = p_tree; }

+ 5 - 0
editor/editor_inspector.cpp

@@ -33,6 +33,7 @@
 
 #include "core/os/keyboard.h"
 #include "editor/add_metadata_dialog.h"
+#include "editor/debugger/editor_debugger_inspector.h"
 #include "editor/doc_tools.h"
 #include "editor/editor_feature_profile.h"
 #include "editor/editor_main_screen.h"
@@ -4282,6 +4283,10 @@ void EditorInspector::_edit_set(const String &p_name, const Variant &p_value, bo
 		Object::cast_to<MultiNodeEdit>(object)->set_property_field(p_name, p_value, p_changed_field);
 		_edit_request_change(object, p_name);
 		emit_signal(_prop_edited, p_name);
+	} else if (Object::cast_to<EditorDebuggerRemoteObjects>(object)) {
+		Object::cast_to<EditorDebuggerRemoteObjects>(object)->set_property_field(p_name, p_value, p_changed_field);
+		_edit_request_change(object, p_name);
+		emit_signal(_prop_edited, p_name);
 	} else {
 		undo_redo->create_action(vformat(TTR("Set %s"), p_name), UndoRedo::MERGE_ENDS);
 		undo_redo->add_do_property(object, p_name, p_value);

+ 16 - 4
editor/editor_node.cpp

@@ -2533,12 +2533,13 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 		InspectorDock::get_inspector_singleton()->edit(nullptr);
 		NodeDock::get_singleton()->set_node(nullptr);
 		InspectorDock::get_singleton()->update(nullptr);
+		EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
 		hide_unused_editors();
 		return;
 	}
 
 	// Update the use folding setting and state.
-	bool disable_folding = bool(EDITOR_GET("interface/inspector/disable_folding")) || current_obj->is_class("EditorDebuggerRemoteObject");
+	bool disable_folding = bool(EDITOR_GET("interface/inspector/disable_folding")) || current_obj->is_class("EditorDebuggerRemoteObjects");
 	if (InspectorDock::get_inspector_singleton()->is_using_folding() == disable_folding) {
 		InspectorDock::get_inspector_singleton()->set_use_folding(!disable_folding, false);
 	}
@@ -2566,6 +2567,7 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 			SceneTreeDock::get_singleton()->set_selected(nullptr);
 			NodeDock::get_singleton()->set_node(nullptr);
 			InspectorDock::get_singleton()->update(nullptr);
+			EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
 			ImportDock::get_singleton()->set_edit_path(current_res->get_path());
 		}
 
@@ -2605,6 +2607,7 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 			SceneTreeDock::get_singleton()->set_selected(nullptr);
 			InspectorDock::get_singleton()->update(nullptr);
 		}
+		EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
 
 		if (get_edited_scene() && !get_edited_scene()->get_scene_file_path().is_empty()) {
 			String source_scene = get_edited_scene()->get_scene_file_path();
@@ -2613,7 +2616,6 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 				info_is_warning = true;
 			}
 		}
-
 	} else {
 		Node *selected_node = nullptr;
 
@@ -2639,6 +2641,10 @@ void EditorNode::_edit_current(bool p_skip_foreign, bool p_skip_inspector_update
 			}
 		}
 
+		if (!current_obj->is_class("EditorDebuggerRemoteObjects")) {
+			EditorDebuggerNode::get_singleton()->clear_remote_tree_selection();
+		}
+
 		InspectorDock::get_inspector_singleton()->edit(current_obj);
 		NodeDock::get_singleton()->set_node(nullptr);
 		SceneTreeDock::get_singleton()->set_selected(selected_node);
@@ -4932,7 +4938,8 @@ Ref<Texture2D> EditorNode::get_object_icon(const Object *p_object, const String
 
 	Ref<Script> scr = p_object->get_script();
 
-	if (Object::cast_to<EditorDebuggerRemoteObject>(p_object)) {
+	const EditorDebuggerRemoteObjects *robjs = Object::cast_to<EditorDebuggerRemoteObjects>(p_object);
+	if (robjs) {
 		String class_name;
 		if (scr.is_valid()) {
 			class_name = scr->get_global_name();
@@ -4942,7 +4949,12 @@ Ref<Texture2D> EditorNode::get_object_icon(const Object *p_object, const String
 				class_name = scr->get_path();
 			}
 		}
-		return get_class_icon(class_name.is_empty() ? Object::cast_to<EditorDebuggerRemoteObject>(p_object)->type_name : class_name, p_fallback);
+
+		if (class_name.is_empty()) {
+			return get_class_icon(robjs->type_name, p_fallback);
+		}
+
+		return get_class_icon(class_name, p_fallback);
 	}
 
 	if (scr.is_null() && p_object->is_class("Script")) {

+ 1 - 0
editor/editor_settings.cpp

@@ -993,6 +993,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 
 	EDITOR_SETTING_BASIC(Variant::BOOL, PROPERTY_HINT_NONE, "debugger/auto_switch_to_remote_scene_tree", false, "")
 	EDITOR_SETTING_BASIC(Variant::BOOL, PROPERTY_HINT_NONE, "debugger/auto_switch_to_stack_trace", true, "")
+	EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "debugger/max_node_selection", 20, "1,100,1")
 	EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "debugger/profiler_frame_history_size", 3600, "60,10000,1")
 	EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "debugger/profiler_frame_max_functions", 64, "16,512,1")
 	EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "debugger/profiler_target_fps", 60, "1,1000,1")

+ 1 - 1
editor/editor_undo_redo_manager.cpp

@@ -61,7 +61,7 @@ UndoRedo *EditorUndoRedoManager::get_history_undo_redo(int p_idx) const {
 int EditorUndoRedoManager::get_history_id_for_object(Object *p_object) const {
 	int history_id = INVALID_HISTORY;
 
-	if (Object::cast_to<EditorDebuggerRemoteObject>(p_object)) {
+	if (Object::cast_to<EditorDebuggerRemoteObjects>(p_object)) {
 		return REMOTE_HISTORY;
 	}
 

+ 1 - 2
editor/gui/editor_object_selector.cpp

@@ -128,7 +128,6 @@ void EditorObjectSelector::update_path() {
 		}
 
 		Ref<Texture2D> obj_icon = EditorNode::get_singleton()->get_object_icon(obj);
-
 		if (obj_icon.is_valid()) {
 			current_object_icon->set_texture(obj_icon);
 		}
@@ -148,7 +147,7 @@ void EditorObjectSelector::update_path() {
 				if (name.is_empty()) {
 					name = r->get_class();
 				}
-			} else if (obj->is_class("EditorDebuggerRemoteObject")) {
+			} else if (obj->is_class("EditorDebuggerRemoteObjects")) {
 				name = obj->call("get_title");
 			} else if (Object::cast_to<Node>(obj)) {
 				name = Object::cast_to<Node>(obj)->get_name();

+ 15 - 1
editor/inspector_dock.cpp

@@ -30,6 +30,8 @@
 
 #include "inspector_dock.h"
 
+#include "editor/debugger/editor_debugger_inspector.h"
+#include "editor/debugger/editor_debugger_node.h"
 #include "editor/editor_main_screen.h"
 #include "editor/editor_node.h"
 #include "editor/editor_settings.h"
@@ -351,7 +353,7 @@ void InspectorDock::_prepare_history() {
 			}
 		} else if (Object::cast_to<Node>(obj)) {
 			text = Object::cast_to<Node>(obj)->get_name();
-		} else if (obj->is_class("EditorDebuggerRemoteObject")) {
+		} else if (obj->is_class("EditorDebuggerRemoteObjects")) {
 			text = obj->call("get_title");
 		} else {
 			text = obj->get_class();
@@ -372,6 +374,10 @@ void InspectorDock::_select_history(int p_idx) {
 		return;
 	}
 	EditorNode::get_singleton()->push_item(obj);
+
+	if (const EditorDebuggerRemoteObjects *robjs = Object::cast_to<EditorDebuggerRemoteObjects>(obj)) {
+		EditorDebuggerNode::get_singleton()->set_remote_selection(robjs->remote_object_ids.duplicate());
+	}
 }
 
 void InspectorDock::_resource_created() {
@@ -396,6 +402,10 @@ void InspectorDock::_resource_selected(const Ref<Resource> &p_res, const String
 void InspectorDock::_edit_forward() {
 	if (EditorNode::get_singleton()->get_editor_selection_history()->next()) {
 		EditorNode::get_singleton()->edit_current();
+
+		if (const EditorDebuggerRemoteObjects *robjs = Object::cast_to<EditorDebuggerRemoteObjects>(current)) {
+			EditorDebuggerNode::get_singleton()->set_remote_selection(robjs->remote_object_ids.duplicate());
+		}
 	}
 }
 
@@ -403,6 +413,10 @@ void InspectorDock::_edit_back() {
 	EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();
 	if ((current && editor_history->previous()) || editor_history->get_path_size() == 1) {
 		EditorNode::get_singleton()->edit_current();
+
+		if (const EditorDebuggerRemoteObjects *robjs = Object::cast_to<EditorDebuggerRemoteObjects>(current)) {
+			EditorDebuggerNode::get_singleton()->set_remote_selection(robjs->remote_object_ids.duplicate());
+		}
 	}
 }
 

+ 27 - 56
editor/plugins/game_view_plugin.cpp

@@ -35,7 +35,6 @@
 #include "core/string/translation_server.h"
 #include "editor/debugger/editor_debugger_node.h"
 #include "editor/debugger/script_editor_debugger.h"
-#include "editor/editor_command_palette.h"
 #include "editor/editor_feature_profile.h"
 #include "editor/editor_interface.h"
 #include "editor/editor_main_screen.h"
@@ -58,13 +57,16 @@ void GameViewDebugger::_session_started(Ref<EditorDebuggerSession> p_session) {
 		return;
 	}
 
-	Array setup_data;
 	Dictionary settings;
+	settings["debugger/max_node_selection"] = EDITOR_GET("debugger/max_node_selection");
 	settings["editors/panning/2d_editor_panning_scheme"] = EDITOR_GET("editors/panning/2d_editor_panning_scheme");
 	settings["editors/panning/simple_panning"] = EDITOR_GET("editors/panning/simple_panning");
 	settings["editors/panning/warped_mouse_panning"] = EDITOR_GET("editors/panning/warped_mouse_panning");
 	settings["editors/panning/2d_editor_pan_speed"] = EDITOR_GET("editors/panning/2d_editor_pan_speed");
+	settings["editors/polygon_editor/point_grab_radius"] = EDITOR_GET("editors/polygon_editor/point_grab_radius");
 	settings["canvas_item_editor/pan_view"] = DebuggerMarshalls::serialize_key_shortcut(ED_GET_SHORTCUT("canvas_item_editor/pan_view"));
+	settings["box_selection_fill_color"] = EditorNode::get_singleton()->get_editor_theme()->get_color(SNAME("box_selection_fill_color"), EditorStringName(Editor));
+	settings["box_selection_stroke_color"] = EditorNode::get_singleton()->get_editor_theme()->get_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor));
 #ifndef _3D_DISABLED
 	settings["editors/3d/default_fov"] = EDITOR_GET("editors/3d/default_fov");
 	settings["editors/3d/default_z_near"] = EDITOR_GET("editors/3d/default_z_near");
@@ -76,7 +78,11 @@ void GameViewDebugger::_session_started(Ref<EditorDebuggerSession> p_session) {
 	settings["editors/3d/freelook/freelook_sensitivity"] = EDITOR_GET("editors/3d/freelook/freelook_sensitivity");
 	settings["editors/3d/navigation_feel/orbit_sensitivity"] = EDITOR_GET("editors/3d/navigation_feel/orbit_sensitivity");
 	settings["editors/3d/navigation_feel/translation_sensitivity"] = EDITOR_GET("editors/3d/navigation_feel/translation_sensitivity");
+	settings["editors/3d/selection_box_color"] = EDITOR_GET("editors/3d/selection_box_color");
+	settings["editors/3d/freelook/freelook_base_speed"] = EDITOR_GET("editors/3d/freelook/freelook_base_speed");
 #endif // _3D_DISABLED
+
+	Array setup_data;
 	setup_data.append(settings);
 	p_session->send_message("scene:runtime_node_select_setup", setup_data);
 
@@ -422,6 +428,8 @@ void GameView::_select_mode_pressed(int p_option) {
 	}
 
 	debugger->set_select_mode(mode);
+
+	EditorSettings::get_singleton()->set_project_metadata("game_view", "select_mode", mode);
 }
 
 void GameView::_embed_options_menu_menu_id_pressed(int p_id) {
@@ -581,6 +589,8 @@ void GameView::_hide_selection_toggled(bool p_pressed) {
 	hide_selection->set_button_icon(get_editor_theme_icon(p_pressed ? SNAME("GuiVisibilityHidden") : SNAME("GuiVisibilityVisible")));
 
 	debugger->set_selection_visible(!p_pressed);
+
+	EditorSettings::get_singleton()->set_project_metadata("game_view", "hide_selection", p_pressed);
 }
 
 void GameView::_camera_override_button_toggled(bool p_pressed) {
@@ -609,12 +619,16 @@ void GameView::_camera_override_menu_id_pressed(int p_id) {
 			menu->set_item_checked(menu->get_item_index(p_id), true);
 
 			_update_debugger_buttons();
+
+			EditorSettings::get_singleton()->set_project_metadata("game_view", "camera_override_mode", p_id);
 		} break;
 		case CAMERA_MODE_EDITORS: {
 			debugger->set_camera_manipulate_mode(EditorDebuggerNode::OVERRIDE_EDITORS);
 			menu->set_item_checked(menu->get_item_index(p_id), true);
 
 			_update_debugger_buttons();
+
+			EditorSettings::get_singleton()->set_project_metadata("game_view", "camera_override_mode", p_id);
 		} break;
 	}
 }
@@ -700,41 +714,6 @@ void GameView::set_is_feature_enabled(bool p_enabled) {
 	is_feature_enabled = p_enabled;
 }
 
-void GameView::set_state(const Dictionary &p_state) {
-	if (p_state.has("hide_selection")) {
-		hide_selection->set_pressed(p_state["hide_selection"]);
-		_hide_selection_toggled(hide_selection->is_pressed());
-	}
-	if (p_state.has("select_mode")) {
-		_select_mode_pressed(p_state["select_mode"]);
-	}
-	if (p_state.has("camera_override_mode")) {
-		_camera_override_menu_id_pressed(p_state["camera_override_mode"]);
-	}
-}
-
-Dictionary GameView::get_state() const {
-	Dictionary d;
-	d["hide_selection"] = hide_selection->is_pressed();
-
-	for (int i = 0; i < RuntimeNodeSelect::SELECT_MODE_MAX; i++) {
-		if (select_mode_button[i]->is_pressed()) {
-			d["select_mode"] = i;
-			break;
-		}
-	}
-
-	PopupMenu *menu = camera_override_menu->get_popup();
-	for (int i = CAMERA_MODE_INGAME; i < CAMERA_MODE_EDITORS + 1; i++) {
-		if (menu->is_item_checked(menu->get_item_index(i))) {
-			d["camera_override_mode"] = i;
-			break;
-		}
-	}
-
-	return d;
-}
-
 void GameView::set_window_layout(Ref<ConfigFile> p_layout) {
 	floating_window_rect = p_layout->get_value("GameView", "floating_window_rect", Rect2i());
 	floating_window_screen = p_layout->get_value("GameView", "floating_window_screen", -1);
@@ -959,6 +938,7 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	hide_selection->set_theme_type_variation(SceneStringName(FlatButton));
 	hide_selection->connect(SceneStringName(toggled), callable_mp(this, &GameView::_hide_selection_toggled));
 	hide_selection->set_tooltip_text(TTR("Toggle Selection Visibility"));
+	hide_selection->set_pressed(EditorSettings::get_singleton()->get_project_metadata("game_view", "hide_selection", false));
 
 	main_menu_hbox->add_child(memnew(VSeparator));
 
@@ -979,14 +959,16 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->connect(SceneStringName(pressed), callable_mp(this, &GameView::_select_mode_pressed).bind(RuntimeNodeSelect::SELECT_MODE_LIST));
 	select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_tooltip_text(TTR("Show list of selectable nodes at position clicked."));
 
+	_select_mode_pressed(EditorSettings::get_singleton()->get_project_metadata("game_view", "select_mode", 0));
+
 	main_menu_hbox->add_child(memnew(VSeparator));
 
 	camera_override_button = memnew(Button);
 	main_menu_hbox->add_child(camera_override_button);
 	camera_override_button->set_toggle_mode(true);
 	camera_override_button->set_theme_type_variation(SceneStringName(FlatButton));
-	camera_override_button->connect(SceneStringName(toggled), callable_mp(this, &GameView::_camera_override_button_toggled));
 	camera_override_button->set_tooltip_text(TTR("Override the in-game camera."));
+	camera_override_button->connect(SceneStringName(toggled), callable_mp(this, &GameView::_camera_override_button_toggled));
 
 	camera_override_menu = memnew(MenuButton);
 	main_menu_hbox->add_child(camera_override_menu);
@@ -994,6 +976,7 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	camera_override_menu->set_theme_type_variation("FlatMenuButton");
 	camera_override_menu->set_h_size_flags(SIZE_SHRINK_END);
 	camera_override_menu->set_tooltip_text(TTR("Camera Override Options"));
+	_camera_override_menu_id_pressed(EditorSettings::get_singleton()->get_project_metadata("game_view", "camera_override_mode", 0));
 
 	PopupMenu *menu = camera_override_menu->get_popup();
 	menu->connect(SceneStringName(id_pressed), callable_mp(this, &GameView::_camera_override_menu_id_pressed));
@@ -1098,7 +1081,7 @@ void GameViewPlugin::selected_notify() {
 		notify_main_screen_changed(get_plugin_name());
 #else
 		window_wrapper->grab_window_focus();
-#endif
+#endif // ANDROID_ENABLED
 		_focus_another_editor();
 	}
 }
@@ -1119,19 +1102,7 @@ void GameViewPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
 void GameViewPlugin::get_window_layout(Ref<ConfigFile> p_layout) {
 	game_view->get_window_layout(p_layout);
 }
-
-void GameViewPlugin::set_state(const Dictionary &p_state) {
-	game_view->set_state(p_state);
-}
-
-Dictionary GameViewPlugin::get_state() const {
-	return game_view->get_state();
-}
-
-void GameViewPlugin::_window_visibility_changed(bool p_visible) {
-	_focus_another_editor();
-}
-#endif
+#endif // ANDROID_ENABLED
 
 void GameViewPlugin::_notification(int p_what) {
 	switch (p_what) {
@@ -1161,7 +1132,7 @@ void GameViewPlugin::_feature_profile_changed() {
 	if (game_view) {
 		game_view->set_is_feature_enabled(is_feature_enabled);
 	}
-#endif
+#endif // ANDROID_ENABLED
 }
 
 void GameViewPlugin::_save_last_editor(const String &p_editor) {
@@ -1185,7 +1156,7 @@ bool GameViewPlugin::_is_window_wrapper_enabled() const {
 	return true;
 #else
 	return window_wrapper->get_window_enabled();
-#endif
+#endif // ANDROID_ENABLED
 }
 
 GameViewPlugin::GameViewPlugin() {
@@ -1204,8 +1175,8 @@ GameViewPlugin::GameViewPlugin() {
 	EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(window_wrapper);
 	window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	window_wrapper->hide();
-	window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_window_visibility_changed));
-#endif
+	window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_focus_another_editor).unbind(1));
+#endif // ANDROID_ENABLED
 
 	EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &GameViewPlugin::_feature_profile_changed));
 }

+ 3 - 6
editor/plugins/game_view_plugin.h

@@ -212,7 +212,7 @@ class GameViewPlugin : public EditorPlugin {
 #ifndef ANDROID_ENABLED
 	GameView *game_view = nullptr;
 	WindowWrapper *window_wrapper = nullptr;
-#endif
+#endif // ANDROID_ENABLED
 
 	Ref<GameViewDebugger> debugger;
 
@@ -221,7 +221,7 @@ class GameViewPlugin : public EditorPlugin {
 	void _feature_profile_changed();
 #ifndef ANDROID_ENABLED
 	void _window_visibility_changed(bool p_visible);
-#endif
+#endif // ANDROID_ENABLED
 	void _save_last_editor(const String &p_editor);
 	void _focus_another_editor();
 	bool _is_window_wrapper_enabled() const;
@@ -243,10 +243,7 @@ public:
 
 	virtual void set_window_layout(Ref<ConfigFile> p_layout) override;
 	virtual void get_window_layout(Ref<ConfigFile> p_layout) override;
-
-	virtual void set_state(const Dictionary &p_state) override;
-	virtual Dictionary get_state() const override;
-#endif
+#endif // ANDROID_ENABLED
 
 	GameViewPlugin();
 	~GameViewPlugin();

Разница между файлами не показана из-за своего большого размера
+ 577 - 195
scene/debugger/scene_debugger.cpp


+ 69 - 26
scene/debugger/scene_debugger.h

@@ -36,11 +36,16 @@
 #include "core/templates/pair.h"
 #include "core/variant/array.h"
 #include "scene/gui/view_panner.h"
+#ifndef _3D_DISABLED
 #include "scene/resources/mesh.h"
+#endif // _3D_DISABLED
 
+class CanvasItem;
 class PopupMenu;
 class Script;
-class Node;
+#ifndef _3D_DISABLED
+class Node3D;
+#endif // _3D_DISABLED
 
 class SceneDebugger {
 private:
@@ -60,8 +65,8 @@ private:
 
 	static void _save_node(ObjectID id, const String &p_path);
 	static void _set_node_owner_recursive(Node *p_node, Node *p_owner);
-	static void _set_object_property(ObjectID p_id, const String &p_property, const Variant &p_value);
-	static void _send_object_id(ObjectID p_id, int p_max_size = 1 << 20);
+	static void _set_object_property(ObjectID p_id, const String &p_property, const Variant &p_value, const String &p_field = "");
+	static void _send_object_ids(const Vector<ObjectID> &p_ids, bool p_update_selection);
 	static void _next_frame();
 
 public:
@@ -179,30 +184,49 @@ public:
 		NODE_TYPE_NONE,
 		NODE_TYPE_2D,
 		NODE_TYPE_3D,
-		NODE_TYPE_MAX
+		NODE_TYPE_MAX,
 	};
 
 	enum SelectMode {
 		SELECT_MODE_SINGLE,
 		SELECT_MODE_LIST,
-		SELECT_MODE_MAX
+		SELECT_MODE_MAX,
 	};
 
 private:
 	friend class SceneDebugger;
 
+	NodeType node_select_type = NODE_TYPE_2D;
+	SelectMode node_select_mode = SELECT_MODE_SINGLE;
+
 	struct SelectResult {
 		Node *item = nullptr;
 		real_t order = 0;
 		_FORCE_INLINE_ bool operator<(const SelectResult &p_rr) const { return p_rr.order < order; }
 	};
 
+	const int SELECTION_MIN_AREA = 8 * 8;
+	enum SelectionDragState {
+		SELECTION_DRAG_NONE,
+		SELECTION_DRAG_MOVE,
+		SELECTION_DRAG_END,
+	};
+	SelectionDragState selection_drag_state = SELECTION_DRAG_NONE;
+
 	bool has_selection = false;
-	Node *selected_node = nullptr;
+	int max_selection = 1;
+	Point2 selection_position = Point2(INFINITY, INFINITY);
+	Rect2 selection_drag_area;
 	PopupMenu *selection_list = nullptr;
+	Color selection_area_fill;
+	Color selection_area_outline;
 	bool selection_visible = true;
 	bool selection_update_queued = false;
-	bool warped_panning = false;
+
+	bool multi_shortcut_pressed = false;
+	bool list_shortcut_pressed = false;
+	RID draw_canvas;
+	RID sel_drag_ci;
 
 	bool camera_override = false;
 
@@ -213,11 +237,12 @@ private:
 	Ref<ViewPanner> panner;
 	Vector2 view_2d_offset;
 	real_t view_2d_zoom = 1.0;
+	bool warped_panning = false;
+
+	LocalVector<ObjectID> selected_ci_nodes;
+	real_t sel_2d_grab_dist = 0;
 
-	RID sbox_2d_canvas;
 	RID sbox_2d_ci;
-	Transform2D sbox_2d_xform;
-	Rect2 sbox_2d_rect;
 
 #ifndef _3D_DISABLED
 	struct Cursor {
@@ -264,22 +289,35 @@ private:
 
 	Vector2 previous_mouse_position;
 
+	struct SelectionBox3D : public RefCounted {
+		RID instance;
+		RID instance_ofs;
+		RID instance_xray;
+		RID instance_xray_ofs;
+
+		Transform3D transform;
+		AABB bounds;
+
+		~SelectionBox3D() {
+			if (instance.is_valid()) {
+				RS::get_singleton()->free(instance);
+				RS::get_singleton()->free(instance_ofs);
+				RS::get_singleton()->free(instance_xray);
+				RS::get_singleton()->free(instance_xray_ofs);
+			}
+		}
+	};
+	HashMap<ObjectID, Ref<SelectionBox3D>> selected_3d_nodes;
+
+	Color sbox_3d_color;
 	Ref<ArrayMesh> sbox_3d_mesh;
 	Ref<ArrayMesh> sbox_3d_mesh_xray;
-	RID sbox_3d_instance;
-	RID sbox_3d_instance_ofs;
-	RID sbox_3d_instance_xray;
-	RID sbox_3d_instance_xray_ofs;
-	Transform3D sbox_3d_xform;
-	AABB sbox_3d_bounds;
+	RID sbox_3d;
+	RID sbox_3d_ofs;
+	RID sbox_3d_xray;
+	RID sbox_3d_xray_ofs;
 #endif // _3D_DISABLED
 
-	Point2 selection_position = Point2(INFINITY, INFINITY);
-	bool list_shortcut_pressed = false;
-
-	NodeType node_select_type = NODE_TYPE_2D;
-	SelectMode node_select_mode = SELECT_MODE_SINGLE;
-
 	void _setup(const Dictionary &p_settings);
 
 	void _node_set_type(NodeType p_type);
@@ -294,17 +332,19 @@ private:
 	void _process_frame();
 	void _physics_frame();
 
-	void _click_point();
-	void _select_node(Node *p_node);
+	void _send_ids(const Vector<Node *> &p_picked_nodes, bool p_invert_new_selections = true);
+	void _set_selected_nodes(const Vector<Node *> &p_nodes);
 	void _queue_selection_update();
 	void _update_selection();
 	void _clear_selection();
+	void _update_selection_drag(const Point2 &p_end_pos = Point2());
 	void _set_selection_visible(bool p_visible);
 
 	void _open_selection_list(const Vector<SelectResult> &p_items, const Point2 &p_pos);
 	void _close_selection_list();
 
 	void _find_canvas_items_at_pos(const Point2 &p_pos, Node *p_node, Vector<SelectResult> &r_items, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D());
+	void _find_canvas_items_at_rect(const Rect2 &p_rect, Node *p_node, Vector<SelectResult> &r_items, const Transform2D &p_parent_xform = Transform2D(), const Transform2D &p_canvas_xform = Transform2D());
 	void _pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event);
 	void _zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event);
 	void _reset_camera_2d();
@@ -312,6 +352,9 @@ private:
 
 #ifndef _3D_DISABLED
 	void _find_3d_items_at_pos(const Point2 &p_pos, Vector<SelectResult> &r_items);
+	void _find_3d_items_at_rect(const Rect2 &p_rect, Vector<SelectResult> &r_items);
+	Vector3 _get_screen_to_space(const Vector3 &p_vector3);
+
 	bool _handle_3d_input(const Ref<InputEvent> &p_event);
 	void _set_camera_freelook_enabled(bool p_enabled);
 	void _cursor_scale_distance(real_t p_scale);
@@ -322,7 +365,7 @@ private:
 	Point2 _get_warped_mouse_motion(const Ref<InputEventMouseMotion> &p_event, Rect2 p_border) const;
 	Transform3D _get_cursor_transform();
 	void _reset_camera_3d();
-#endif
+#endif // _3D_DISABLED
 
 	RuntimeNodeSelect() { singleton = this; }
 
@@ -333,4 +376,4 @@ public:
 
 	~RuntimeNodeSelect();
 };
-#endif
+#endif // DEBUG_ENABLED

Некоторые файлы не были показаны из-за большого количества измененных файлов