diff --git a/editor/debugger/editor_debugger_inspector.cpp b/editor/debugger/editor_debugger_inspector.cpp index e085e2e4480b..c3d7a5c16610 100644 --- a/editor/debugger/editor_debugger_inspector.cpp +++ b/editor/debugger/editor_debugger_inspector.cpp @@ -33,8 +33,11 @@ #include "core/debugger/debugger_marshalls.h" #include "core/io/marshalls.h" #include "editor/editor_node.h" +#include "editor/inspector_dock.h" #include "scene/debugger/scene_debugger.h" +/// EditorDebuggerRemoteObject + bool EditorDebuggerRemoteObject::_set(const StringName &p_name, const Variant &p_value) { if (!prop_values.has(p_name) || String(p_name).begins_with("Constants/")) { return false; @@ -55,7 +58,7 @@ bool EditorDebuggerRemoteObject::_get(const StringName &p_name, Variant &r_ret) } void EditorDebuggerRemoteObject::_get_property_list(List *p_list) const { - p_list->clear(); // Sorry, no want category. + 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. @@ -84,11 +87,84 @@ 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); ADD_SIGNAL(MethodInfo("value_edited", PropertyInfo(Variant::INT, "object_id"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value"))); } +/// EditorDebuggerMultiRemoteObject + +bool EditorDebuggerMultiRemoteObject::_set(const StringName &p_name, const Variant &p_value) { + return _set_impl(p_name, p_value, ""); +} + +bool EditorDebuggerMultiRemoteObject::_set_impl(const StringName &p_name, const Variant &p_value, const String &p_field) { + String name = p_name; + + if (!prop_values.has(name) || String(name).begins_with("Constants/")) { + return false; + } + if (name.begins_with("Metadata/")) { + name = name.replace_first("Metadata/", "metadata/"); + } + + emit_signal(SNAME("value_edited"), remote_object_ids, name, p_value, p_field); + return true; +} + +bool EditorDebuggerMultiRemoteObject::_get(const StringName &p_name, Variant &r_ret) const { + String name = p_name; + + if (!prop_values.has(name)) { + return false; + } + if (name.begins_with("Metadata/")) { + name = name.replace_first("Metadata/", "metadata/"); + } + + r_ret = prop_values[name]; + return true; +} + +void EditorDebuggerMultiRemoteObject::_get_property_list(List *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. + continue; + } + + p_list->push_back(prop); + } +} + +void EditorDebuggerMultiRemoteObject::set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field) { + _set_impl(p_property, p_value, p_field); +} + +String EditorDebuggerMultiRemoteObject::get_title() { + if (remote_object_ids.size() && ((ObjectID)remote_object_ids[0]).is_valid()) { + return vformat(TTR("Remote %s (%d Selected)"), type_name, remote_object_ids.size()); + } else { + return ""; + } +} + +Variant EditorDebuggerMultiRemoteObject::get_variant(const StringName &p_name) { + Variant var; + _get(p_name, var); + return var; +} + +void EditorDebuggerMultiRemoteObject::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_title"), &EditorDebuggerMultiRemoteObject::get_title); + ClassDB::bind_method(D_METHOD("get_variant"), &EditorDebuggerMultiRemoteObject::get_variant); + ClassDB::bind_method(D_METHOD("clear"), &EditorDebuggerMultiRemoteObject::clear); + + ADD_SIGNAL(MethodInfo("value_edited", PropertyInfo(Variant::PACKED_INT64_ARRAY, "object_ids"), PropertyInfo(Variant::STRING, "property"), PropertyInfo("value"), PropertyInfo(Variant::STRING, "field"))); +} + +/// EditorDebuggerInspector + EditorDebuggerInspector::EditorDebuggerInspector() { variables = memnew(EditorDebuggerRemoteObject); } @@ -101,6 +177,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("multi_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"))); } @@ -120,17 +197,20 @@ void EditorDebuggerInspector::_object_edited(ObjectID p_id, const String &p_prop emit_signal(SNAME("object_edited"), p_id, p_prop, p_value); } +void EditorDebuggerInspector::_multi_objects_edited(const TypedArray &p_ids, const String &p_prop, const Variant &p_value, const String &p_field) { + emit_signal(SNAME("multi_objects_edited"), p_ids, p_prop, p_value, 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; - SceneDebuggerObject obj; obj.deserialize(p_arr); ERR_FAIL_COND_V(obj.id.is_null(), ObjectID()); + EditorDebuggerRemoteObject *debug_obj = nullptr; if (remote_objects.has(obj.id)) { debug_obj = remote_objects[obj.id]; } else { @@ -154,7 +234,7 @@ ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) { if (var.is_string()) { String path = var; if (path.contains("::")) { - // built-in resource + // Built-in resource. String base_path = path.get_slice("::", 0); Ref dependency = ResourceLoader::load(base_path); if (dependency.is_valid()) { @@ -178,40 +258,212 @@ ObjectID EditorDebuggerInspector::add_object(const Array &p_arr) { } } - //always add the property, since props may have been added or removed + // Always add the property, since props may have been added or removed. debug_obj->prop_list.push_back(pinfo); if (!debug_obj->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, debug_obj->prop_values[pinfo.name], var))) { + debug_obj->prop_values[pinfo.name] = var; + changed.insert(pinfo.name); } } 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 + // 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); } } else { - //full update, because props were added or removed + // Full update, because props were added or removed. debug_obj->update(); } return obj.id; } +EditorDebuggerMultiRemoteObject *EditorDebuggerInspector::get_multi_objects(const Array &p_arr) { + ERR_FAIL_COND_V(p_arr.is_empty(), nullptr); + + if (!multi_remote_obj) { + multi_remote_obj = memnew(EditorDebuggerMultiRemoteObject); + multi_remote_obj->connect("value_edited", callable_mp(this, &EditorDebuggerInspector::_multi_objects_edited)); + } else { + multi_remote_obj->remote_object_ids.clear(); + } + + LocalVector objects; + for (const Array arr : p_arr) { + SceneDebuggerObject obj; + obj.deserialize(arr); + if (obj.id.is_valid()) { + multi_remote_obj->remote_object_ids.push_front((uint64_t)obj.id); + objects.push_back(obj); + } + } + ERR_FAIL_COND_V(multi_remote_obj->remote_object_ids.is_empty(), nullptr); + + 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; + } + } + } + multi_remote_obj->type_name = class_name; + + // Search for properties that are present in all selected objects. + HashMap> usage; + LocalVector *> data_list; + LocalVector matching_props; + 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 EditorDebuggerMultiRemoteObject. + } + + if (!usage.has(pinfo.name)) { + Pair pld; + pld.first = 0; + pld.second = prop; + pld.second.first.name = pinfo.name; + usage[pinfo.name] = pld; + data_list.push_back(usage.getptr(pinfo.name)); + } + + // Make sure only properties with the same exact PropertyInfo data will appear. + if (usage[pinfo.name].second.first == pinfo) { + usage[pinfo.name].first++; + } + } + + nc++; + } + for (const Pair *pld : data_list) { + if (nc == pld->first) { + matching_props.push_back(pld->second); + } + } + + int old_prop_size = multi_remote_obj->prop_list.size(); + + multi_remote_obj->prop_list.clear(); + int new_props_added = 0; + HashSet changed; + for (SceneDebuggerObject::SceneDebuggerProperty &prop : matching_props) { + PropertyInfo &pinfo = prop.first; + Variant &var = prop.second; + + if (pinfo.type == Variant::OBJECT) { + if (var.is_string()) { + String path = var; + if (path.contains("::")) { + // Built-in resource. + String base_path = path.get_slice("::", 0); + Ref dependency = ResourceLoader::load(base_path); + if (dependency.is_valid()) { + remote_dependencies.insert(dependency); + } + } + var = ResourceLoader::load(path); + + if (pinfo.hint_string == "Script") { + if (multi_remote_obj->get_script() != var) { + multi_remote_obj->set_script(Ref()); + Ref