diff --git a/core/math/math_fieldwise.cpp b/core/math/math_fieldwise.cpp index 4064e6aaa6b..8e81e4cc346 100644 --- a/core/math/math_fieldwise.cpp +++ b/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 diff --git a/core/math/math_fieldwise.h b/core/math/math_fieldwise.h index 8519e9d4308..2628beb4cfe 100644 --- a/core/math/math_fieldwise.h +++ b/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 diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 5c1660d318f..de1126023c3 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -191,6 +191,10 @@ If [code]true[/code], automatically switches to the [b]Stack Trace[/b] panel when the debugger hits a breakpoint or steps. + + 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. + 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. diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index ba4c078d7ed..3ee4899be23 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/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 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; diff --git a/editor/debugger/editor_debugger_inspector.cpp b/editor/debugger/editor_debugger_inspector.cpp index 7cfd7c11b76..7e9e963bebc 100644 --- a/editor/debugger/editor_debugger_inspector.cpp +++ b/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 *p_list) const { - p_list->clear(); // Sorry, no want category. +void EditorDebuggerRemoteObjects::_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. @@ -66,31 +94,35 @@ void EditorDebuggerRemoteObject::_get_property_list(List *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 ""; - } +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); } -Variant EditorDebuggerRemoteObject::get_variant(const StringName &p_name) { +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 ""; +} + +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 &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); - SceneDebuggerObject obj; - obj.deserialize(p_arr); - ERR_FAIL_COND_V(obj.id.is_null(), ObjectID()); + TypedArray ids; + LocalVector 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); - 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)); + // Sorting is necessary, as selected nodes in the remote tree are ordered by index. + ids.sort(); + + EditorDebuggerRemoteObjects *remote_objects = nullptr; + for (EditorDebuggerRemoteObjects *robjs : remote_objects_list) { + if (robjs->remote_object_ids == ids) { + remote_objects = robjs; + break; + } } - int old_prop_size = debug_obj->prop_list.size(); + 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); + } - debug_obj->prop_list.clear(); + 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 values; + }; + HashMap 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::Iterator E = usage.begin(); E;) { + HashMap::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); + } + + E = next; + } + + int old_prop_size = remote_objects->prop_list.size(); + + remote_objects->prop_list.clear(); int new_props_added = 0; HashSet changed; - for (SceneDebuggerObject::SceneDebuggerProperty &property : obj.properties) { - PropertyInfo &pinfo = property.first; - Variant &var = property.second; + for (const KeyValue &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 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()); + if (remote_objects->get_script() != var) { + remote_objects->set_script(Ref()); Ref