diff options
157 files changed, 6368 insertions, 1327 deletions
diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 7951ee9edd..562bde978e 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1398,7 +1398,7 @@ void ProjectSettings::_add_builtin_input_map() { } Dictionary action; - action["deadzone"] = Variant(0.5f); + action["deadzone"] = Variant(0.2f); action["events"] = events; String action_name = "input/" + E.key; diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp index e2ed7245a2..fc1b7b74f9 100644 --- a/core/debugger/remote_debugger.cpp +++ b/core/debugger/remote_debugger.cpp @@ -37,6 +37,7 @@ #include "core/debugger/script_debugger.h" #include "core/input/input.h" #include "core/io/resource_loader.h" +#include "core/math/expression.h" #include "core/object/script_language.h" #include "core/os/os.h" #include "servers/display_server.h" @@ -529,6 +530,41 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } else if (command == "set_skip_breakpoints") { ERR_FAIL_COND(data.is_empty()); script_debugger->set_skip_breakpoints(data[0]); + } else if (command == "evaluate") { + String expression_str = data[0]; + int frame = data[1]; + + ScriptInstance *breaked_instance = script_debugger->get_break_language()->debug_get_stack_level_instance(frame); + if (!breaked_instance) { + break; + } + + List<String> locals; + List<Variant> local_vals; + + script_debugger->get_break_language()->debug_get_stack_level_locals(frame, &locals, &local_vals); + ERR_FAIL_COND(locals.size() != local_vals.size()); + + PackedStringArray locals_vector; + for (const String &S : locals) { + locals_vector.append(S); + } + + Array local_vals_array; + for (const Variant &V : local_vals) { + local_vals_array.append(V); + } + + Expression expression; + expression.parse(expression_str, locals_vector); + const Variant return_val = expression.execute(local_vals_array, breaked_instance->get_owner()); + + DebuggerMarshalls::ScriptStackVariable stvar; + stvar.name = expression_str; + stvar.value = return_val; + stvar.type = 3; + + send_message("evaluation_return", stvar.serialize()); } else { bool captured = false; ERR_CONTINUE(_try_capture(command, data, captured) != OK); diff --git a/core/extension/gdextension_library_loader.cpp b/core/extension/gdextension_library_loader.cpp index 5ba4933c35..d5f2eb668f 100644 --- a/core/extension/gdextension_library_loader.cpp +++ b/core/extension/gdextension_library_loader.cpp @@ -259,6 +259,10 @@ bool GDExtensionLibraryLoader::has_library_changed() const { return false; } +bool GDExtensionLibraryLoader::library_exists() const { + return FileAccess::exists(resource_path); +} + Error GDExtensionLibraryLoader::parse_gdextension_file(const String &p_path) { resource_path = p_path; diff --git a/core/extension/gdextension_library_loader.h b/core/extension/gdextension_library_loader.h index f4372a75d4..f781611b30 100644 --- a/core/extension/gdextension_library_loader.h +++ b/core/extension/gdextension_library_loader.h @@ -77,6 +77,7 @@ public: virtual void close_library() override; virtual bool is_library_open() const override; virtual bool has_library_changed() const override; + virtual bool library_exists() const override; Error parse_gdextension_file(const String &p_path); }; diff --git a/core/extension/gdextension_loader.h b/core/extension/gdextension_loader.h index 7d779858b7..2289550329 100644 --- a/core/extension/gdextension_loader.h +++ b/core/extension/gdextension_loader.h @@ -42,6 +42,7 @@ public: virtual void close_library() = 0; virtual bool is_library_open() const = 0; virtual bool has_library_changed() const = 0; + virtual bool library_exists() const = 0; }; #endif // GDEXTENSION_LOADER_H diff --git a/core/extension/gdextension_manager.cpp b/core/extension/gdextension_manager.cpp index 01efe0d96e..fff938858f 100644 --- a/core/extension/gdextension_manager.cpp +++ b/core/extension/gdextension_manager.cpp @@ -302,7 +302,8 @@ bool GDExtensionManager::ensure_extensions_loaded(const HashSet<String> &p_exten for (const String &loaded_extension : loaded_extensions) { if (!p_extensions.has(loaded_extension)) { // The extension may not have a .gdextension file. - if (!FileAccess::exists(loaded_extension)) { + const Ref<GDExtension> extension = GDExtensionManager::get_singleton()->get_extension(loaded_extension); + if (!extension->get_loader()->library_exists()) { extensions_removed.push_back(loaded_extension); } } diff --git a/core/input/input_map.compat.inc b/core/input/input_map.compat.inc new file mode 100644 index 0000000000..da4bd962b6 --- /dev/null +++ b/core/input/input_map.compat.inc @@ -0,0 +1,41 @@ +/**************************************************************************/ +/* input_map.compat.inc */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef DISABLE_DEPRECATED + +void InputMap::_add_action_bind_compat_97281(const StringName &p_action, float p_deadzone) { + add_action(p_action, p_deadzone); +} + +void InputMap::_bind_compatibility_methods() { + ClassDB::bind_compatibility_method(D_METHOD("add_action", "action", "deadzone"), &InputMap::_add_action_bind_compat_97281, DEFVAL(0.5f)); +} + +#endif // DISABLE_DEPRECATED diff --git a/core/input/input_map.cpp b/core/input/input_map.cpp index 9a772c87c9..27a50c79f6 100644 --- a/core/input/input_map.cpp +++ b/core/input/input_map.cpp @@ -29,6 +29,7 @@ /**************************************************************************/ #include "input_map.h" +#include "input_map.compat.inc" #include "core/config/project_settings.h" #include "core/input/input.h" @@ -43,7 +44,7 @@ int InputMap::ALL_DEVICES = -1; void InputMap::_bind_methods() { ClassDB::bind_method(D_METHOD("has_action", "action"), &InputMap::has_action); ClassDB::bind_method(D_METHOD("get_actions"), &InputMap::_get_actions); - ClassDB::bind_method(D_METHOD("add_action", "action", "deadzone"), &InputMap::add_action, DEFVAL(0.5f)); + ClassDB::bind_method(D_METHOD("add_action", "action", "deadzone"), &InputMap::add_action, DEFVAL(0.2f)); ClassDB::bind_method(D_METHOD("erase_action", "action"), &InputMap::erase_action); ClassDB::bind_method(D_METHOD("action_set_deadzone", "action", "deadzone"), &InputMap::action_set_deadzone); @@ -306,7 +307,7 @@ void InputMap::load_from_project_settings() { String name = pi.name.substr(pi.name.find("/") + 1, pi.name.length()); Dictionary action = GLOBAL_GET(pi.name); - float deadzone = action.has("deadzone") ? (float)action["deadzone"] : 0.5f; + float deadzone = action.has("deadzone") ? (float)action["deadzone"] : 0.2f; Array events = action["events"]; add_action(name, deadzone); diff --git a/core/input/input_map.h b/core/input/input_map.h index 3774a131e6..b29687d144 100644 --- a/core/input/input_map.h +++ b/core/input/input_map.h @@ -69,12 +69,17 @@ private: protected: static void _bind_methods(); +#ifndef DISABLE_DEPRECATED + void _add_action_bind_compat_97281(const StringName &p_action, float p_deadzone = 0.5); + static void _bind_compatibility_methods(); +#endif // DISABLE_DEPRECATED + public: static _FORCE_INLINE_ InputMap *get_singleton() { return singleton; } bool has_action(const StringName &p_action) const; List<StringName> get_actions() const; - void add_action(const StringName &p_action, float p_deadzone = 0.5); + void add_action(const StringName &p_action, float p_deadzone = 0.2); void erase_action(const StringName &p_action); float action_get_deadzone(const StringName &p_action); diff --git a/core/object/ref_counted.h b/core/object/ref_counted.h index f0706b4d08..22eb5a7a3f 100644 --- a/core/object/ref_counted.h +++ b/core/object/ref_counted.h @@ -57,24 +57,30 @@ template <typename T> class Ref { T *reference = nullptr; - void ref(const Ref &p_from) { - if (p_from.reference == reference) { + _FORCE_INLINE_ void ref(const Ref &p_from) { + ref_pointer<false>(p_from.reference); + } + + template <bool Init> + _FORCE_INLINE_ void ref_pointer(T *p_refcounted) { + if (p_refcounted == reference) { return; } - unref(); - - reference = p_from.reference; + // This will go out of scope and get unref'd. + Ref cleanup_ref; + cleanup_ref.reference = reference; + reference = p_refcounted; if (reference) { - reference->reference(); - } - } - - void ref_pointer(T *p_ref) { - ERR_FAIL_NULL(p_ref); - - if (p_ref->init_ref()) { - reference = p_ref; + if constexpr (Init) { + if (!reference->init_ref()) { + reference = nullptr; + } + } else { + if (!reference->reference()) { + reference = nullptr; + } + } } } @@ -124,15 +130,11 @@ public: template <typename T_Other> void operator=(const Ref<T_Other> &p_from) { - RefCounted *refb = const_cast<RefCounted *>(static_cast<const RefCounted *>(p_from.ptr())); - if (!refb) { - unref(); - return; - } - Ref r; - r.reference = Object::cast_to<T>(refb); - ref(r); - r.reference = nullptr; + ref_pointer<false>(Object::cast_to<T>(p_from.ptr())); + } + + void operator=(T *p_from) { + ref_pointer<true>(p_from); } void operator=(const Variant &p_variant) { @@ -142,16 +144,7 @@ public: return; } - unref(); - - if (!object) { - return; - } - - T *r = Object::cast_to<T>(object); - if (r && r->reference()) { - reference = r; - } + ref_pointer<false>(Object::cast_to<T>(object)); } template <typename T_Other> @@ -159,48 +152,25 @@ public: if (reference == p_ptr) { return; } - unref(); - T *r = Object::cast_to<T>(p_ptr); - if (r) { - ref_pointer(r); - } + ref_pointer<true>(Object::cast_to<T>(p_ptr)); } Ref(const Ref &p_from) { - ref(p_from); + this->operator=(p_from); } template <typename T_Other> Ref(const Ref<T_Other> &p_from) { - RefCounted *refb = const_cast<RefCounted *>(static_cast<const RefCounted *>(p_from.ptr())); - if (!refb) { - unref(); - return; - } - Ref r; - r.reference = Object::cast_to<T>(refb); - ref(r); - r.reference = nullptr; + this->operator=(p_from); } - Ref(T *p_reference) { - if (p_reference) { - ref_pointer(p_reference); - } + Ref(T *p_from) { + this->operator=(p_from); } - Ref(const Variant &p_variant) { - Object *object = p_variant.get_validated_object(); - - if (!object) { - return; - } - - T *r = Object::cast_to<T>(object); - if (r && r->reference()) { - reference = r; - } + Ref(const Variant &p_from) { + this->operator=(p_from); } inline bool is_valid() const { return reference != nullptr; } @@ -222,7 +192,7 @@ public: ref(memnew(T(p_params...))); } - Ref() {} + Ref() = default; ~Ref() { unref(); @@ -299,13 +269,13 @@ struct GetTypeInfo<const Ref<T> &> { template <typename T> struct VariantInternalAccessor<Ref<T>> { static _FORCE_INLINE_ Ref<T> get(const Variant *v) { return Ref<T>(*VariantInternal::get_object(v)); } - static _FORCE_INLINE_ void set(Variant *v, const Ref<T> &p_ref) { VariantInternal::refcounted_object_assign(v, p_ref.ptr()); } + static _FORCE_INLINE_ void set(Variant *v, const Ref<T> &p_ref) { VariantInternal::object_assign(v, p_ref); } }; template <typename T> struct VariantInternalAccessor<const Ref<T> &> { static _FORCE_INLINE_ Ref<T> get(const Variant *v) { return Ref<T>(*VariantInternal::get_object(v)); } - static _FORCE_INLINE_ void set(Variant *v, const Ref<T> &p_ref) { VariantInternal::refcounted_object_assign(v, p_ref.ptr()); } + static _FORCE_INLINE_ void set(Variant *v, const Ref<T> &p_ref) { VariantInternal::object_assign(v, p_ref); } }; #endif // REF_COUNTED_H diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 391a203d5b..e6f7492a18 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -221,18 +221,35 @@ void CharString::copy_from(const char *p_cstr) { /* String */ /*************************************************************************/ -Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const { - // Splits the URL into scheme, host, port, path. Strip credentials when present. +Error String::parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const { + // Splits the URL into scheme, host, port, path, fragment. Strip credentials when present. String base = *this; r_scheme = ""; r_host = ""; r_port = 0; r_path = ""; + r_fragment = ""; + int pos = base.find("://"); // Scheme if (pos != -1) { - r_scheme = base.substr(0, pos + 3).to_lower(); - base = base.substr(pos + 3, base.length() - pos - 3); + bool is_scheme_valid = true; + for (int i = 0; i < pos; i++) { + if (!is_ascii_alphanumeric_char(base[i]) && base[i] != '+' && base[i] != '-' && base[i] != '.') { + is_scheme_valid = false; + break; + } + } + if (is_scheme_valid) { + r_scheme = base.substr(0, pos + 3).to_lower(); + base = base.substr(pos + 3, base.length() - pos - 3); + } + } + pos = base.find("#"); + // Fragment + if (pos != -1) { + r_fragment = base.substr(pos + 1); + base = base.substr(0, pos); } pos = base.find("/"); // Path diff --git a/core/string/ustring.h b/core/string/ustring.h index 5d4b209c25..aa62c9cb18 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -452,7 +452,7 @@ public: String c_escape_multiline() const; String c_unescape() const; String json_escape() const; - Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path) const; + Error parse_url(String &r_scheme, String &r_host, int &r_port, String &r_path, String &r_fragment) const; String property_name_encode() const; diff --git a/core/templates/hashfuncs.h b/core/templates/hashfuncs.h index fc7a78bcf5..21eef10297 100644 --- a/core/templates/hashfuncs.h +++ b/core/templates/hashfuncs.h @@ -32,10 +32,17 @@ #define HASHFUNCS_H #include "core/math/aabb.h" +#include "core/math/basis.h" +#include "core/math/color.h" #include "core/math/math_defs.h" #include "core/math/math_funcs.h" +#include "core/math/plane.h" +#include "core/math/projection.h" +#include "core/math/quaternion.h" #include "core/math/rect2.h" #include "core/math/rect2i.h" +#include "core/math/transform_2d.h" +#include "core/math/transform_3d.h" #include "core/math/vector2.h" #include "core/math/vector2i.h" #include "core/math/vector3.h" @@ -414,6 +421,13 @@ struct HashMapComparatorDefault<double> { }; template <> +struct HashMapComparatorDefault<Color> { + static bool compare(const Color &p_lhs, const Color &p_rhs) { + return ((p_lhs.r == p_rhs.r) || (Math::is_nan(p_lhs.r) && Math::is_nan(p_rhs.r))) && ((p_lhs.g == p_rhs.g) || (Math::is_nan(p_lhs.g) && Math::is_nan(p_rhs.g))) && ((p_lhs.b == p_rhs.b) || (Math::is_nan(p_lhs.b) && Math::is_nan(p_rhs.b))) && ((p_lhs.a == p_rhs.a) || (Math::is_nan(p_lhs.a) && Math::is_nan(p_rhs.a))); + } +}; + +template <> struct HashMapComparatorDefault<Vector2> { static bool compare(const Vector2 &p_lhs, const Vector2 &p_rhs) { return ((p_lhs.x == p_rhs.x) || (Math::is_nan(p_lhs.x) && Math::is_nan(p_rhs.x))) && ((p_lhs.y == p_rhs.y) || (Math::is_nan(p_lhs.y) && Math::is_nan(p_rhs.y))); @@ -427,6 +441,87 @@ struct HashMapComparatorDefault<Vector3> { } }; +template <> +struct HashMapComparatorDefault<Vector4> { + static bool compare(const Vector4 &p_lhs, const Vector4 &p_rhs) { + return ((p_lhs.x == p_rhs.x) || (Math::is_nan(p_lhs.x) && Math::is_nan(p_rhs.x))) && ((p_lhs.y == p_rhs.y) || (Math::is_nan(p_lhs.y) && Math::is_nan(p_rhs.y))) && ((p_lhs.z == p_rhs.z) || (Math::is_nan(p_lhs.z) && Math::is_nan(p_rhs.z))) && ((p_lhs.w == p_rhs.w) || (Math::is_nan(p_lhs.w) && Math::is_nan(p_rhs.w))); + } +}; + +template <> +struct HashMapComparatorDefault<Rect2> { + static bool compare(const Rect2 &p_lhs, const Rect2 &p_rhs) { + return HashMapComparatorDefault<Vector2>().compare(p_lhs.position, p_rhs.position) && HashMapComparatorDefault<Vector2>().compare(p_lhs.size, p_rhs.size); + } +}; + +template <> +struct HashMapComparatorDefault<AABB> { + static bool compare(const AABB &p_lhs, const AABB &p_rhs) { + return HashMapComparatorDefault<Vector3>().compare(p_lhs.position, p_rhs.position) && HashMapComparatorDefault<Vector3>().compare(p_lhs.size, p_rhs.size); + } +}; + +template <> +struct HashMapComparatorDefault<Plane> { + static bool compare(const Plane &p_lhs, const Plane &p_rhs) { + return HashMapComparatorDefault<Vector3>().compare(p_lhs.normal, p_rhs.normal) && ((p_lhs.d == p_rhs.d) || (Math::is_nan(p_lhs.d) && Math::is_nan(p_rhs.d))); + } +}; + +template <> +struct HashMapComparatorDefault<Transform2D> { + static bool compare(const Transform2D &p_lhs, const Transform2D &p_rhs) { + for (int i = 0; i < 3; ++i) { + if (!HashMapComparatorDefault<Vector2>().compare(p_lhs.columns[i], p_rhs.columns[i])) { + return false; + } + } + + return true; + } +}; + +template <> +struct HashMapComparatorDefault<Basis> { + static bool compare(const Basis &p_lhs, const Basis &p_rhs) { + for (int i = 0; i < 3; ++i) { + if (!HashMapComparatorDefault<Vector3>().compare(p_lhs.rows[i], p_rhs.rows[i])) { + return false; + } + } + + return true; + } +}; + +template <> +struct HashMapComparatorDefault<Transform3D> { + static bool compare(const Transform3D &p_lhs, const Transform3D &p_rhs) { + return HashMapComparatorDefault<Basis>().compare(p_lhs.basis, p_rhs.basis) && HashMapComparatorDefault<Vector3>().compare(p_lhs.origin, p_rhs.origin); + } +}; + +template <> +struct HashMapComparatorDefault<Projection> { + static bool compare(const Projection &p_lhs, const Projection &p_rhs) { + for (int i = 0; i < 4; ++i) { + if (!HashMapComparatorDefault<Vector4>().compare(p_lhs.columns[i], p_rhs.columns[i])) { + return false; + } + } + + return true; + } +}; + +template <> +struct HashMapComparatorDefault<Quaternion> { + static bool compare(const Quaternion &p_lhs, const Quaternion &p_rhs) { + return ((p_lhs.x == p_rhs.x) || (Math::is_nan(p_lhs.x) && Math::is_nan(p_rhs.x))) && ((p_lhs.y == p_rhs.y) || (Math::is_nan(p_lhs.y) && Math::is_nan(p_rhs.y))) && ((p_lhs.z == p_rhs.z) || (Math::is_nan(p_lhs.z) && Math::is_nan(p_rhs.z))) && ((p_lhs.w == p_rhs.w) || (Math::is_nan(p_lhs.w) && Math::is_nan(p_rhs.w))); + } +}; + constexpr uint32_t HASH_TABLE_SIZE_MAX = 29; inline constexpr uint32_t hash_table_size_primes[HASH_TABLE_SIZE_MAX] = { diff --git a/core/variant/callable.cpp b/core/variant/callable.cpp index bb2d0313f6..5ce90cd8ff 100644 --- a/core/variant/callable.cpp +++ b/core/variant/callable.cpp @@ -315,31 +315,32 @@ bool Callable::operator<(const Callable &p_callable) const { } void Callable::operator=(const Callable &p_callable) { + CallableCustom *cleanup_ref = nullptr; if (is_custom()) { if (p_callable.is_custom()) { if (custom == p_callable.custom) { return; } } - - if (custom->ref_count.unref()) { - memdelete(custom); - custom = nullptr; - } + cleanup_ref = custom; + custom = nullptr; } if (p_callable.is_custom()) { method = StringName(); - if (!p_callable.custom->ref_count.ref()) { - object = 0; - } else { - object = 0; + object = 0; + if (p_callable.custom->ref_count.ref()) { custom = p_callable.custom; } } else { method = p_callable.method; object = p_callable.object; } + + if (cleanup_ref != nullptr && cleanup_ref->ref_count.unref()) { + memdelete(cleanup_ref); + } + cleanup_ref = nullptr; } Callable::operator String() const { diff --git a/core/variant/variant.cpp b/core/variant/variant.cpp index 186643b024..e2865a06be 100644 --- a/core/variant/variant.cpp +++ b/core/variant/variant.cpp @@ -1072,17 +1072,69 @@ bool Variant::is_null() const { } } +void Variant::ObjData::ref(const ObjData &p_from) { + // Mirrors Ref::ref in refcounted.h + if (p_from.id == id) { + return; + } + + ObjData cleanup_ref = *this; + + *this = p_from; + if (id.is_ref_counted()) { + RefCounted *reference = static_cast<RefCounted *>(obj); + // Assuming reference is not null because id.is_ref_counted() was true. + if (!reference->reference()) { + *this = ObjData(); + } + } + + cleanup_ref.unref(); +} + +void Variant::ObjData::ref_pointer(Object *p_object) { + // Mirrors Ref::ref_pointer in refcounted.h + if (p_object == obj) { + return; + } + + ObjData cleanup_ref = *this; + + if (p_object) { + *this = ObjData{ p_object->get_instance_id(), p_object }; + if (p_object->is_ref_counted()) { + RefCounted *reference = static_cast<RefCounted *>(p_object); + if (!reference->init_ref()) { + *this = ObjData(); + } + } + } else { + *this = ObjData(); + } + + cleanup_ref.unref(); +} + +void Variant::ObjData::unref() { + // Mirrors Ref::unref in refcounted.h + if (id.is_ref_counted()) { + RefCounted *reference = static_cast<RefCounted *>(obj); + // Assuming reference is not null because id.is_ref_counted() was true. + if (reference->unreference()) { + memdelete(reference); + } + } + *this = ObjData(); +} + void Variant::reference(const Variant &p_variant) { - switch (type) { - case NIL: - case BOOL: - case INT: - case FLOAT: - break; - default: - clear(); + if (type == OBJECT && p_variant.type == OBJECT) { + _get_obj().ref(p_variant._get_obj()); + return; } + clear(); + type = p_variant.type; switch (p_variant.type) { @@ -1165,18 +1217,7 @@ void Variant::reference(const Variant &p_variant) { } break; case OBJECT: { memnew_placement(_data._mem, ObjData); - - if (p_variant._get_obj().obj && p_variant._get_obj().id.is_ref_counted()) { - RefCounted *ref_counted = static_cast<RefCounted *>(p_variant._get_obj().obj); - if (!ref_counted->reference()) { - _get_obj().obj = nullptr; - _get_obj().id = ObjectID(); - break; - } - } - - _get_obj().obj = const_cast<Object *>(p_variant._get_obj().obj); - _get_obj().id = p_variant._get_obj().id; + _get_obj().ref(p_variant._get_obj()); } break; case CALLABLE: { memnew_placement(_data._mem, Callable(*reinterpret_cast<const Callable *>(p_variant._data._mem))); @@ -1375,15 +1416,7 @@ void Variant::_clear_internal() { reinterpret_cast<NodePath *>(_data._mem)->~NodePath(); } break; case OBJECT: { - if (_get_obj().id.is_ref_counted()) { - // We are safe that there is a reference here. - RefCounted *ref_counted = static_cast<RefCounted *>(_get_obj().obj); - if (ref_counted->unreference()) { - memdelete(ref_counted); - } - } - _get_obj().obj = nullptr; - _get_obj().id = ObjectID(); + _get_obj().unref(); } break; case RID: { // Not much need probably. @@ -2589,24 +2622,8 @@ Variant::Variant(const ::RID &p_rid) : Variant::Variant(const Object *p_object) : type(OBJECT) { - memnew_placement(_data._mem, ObjData); - - if (p_object) { - if (p_object->is_ref_counted()) { - RefCounted *ref_counted = const_cast<RefCounted *>(static_cast<const RefCounted *>(p_object)); - if (!ref_counted->init_ref()) { - _get_obj().obj = nullptr; - _get_obj().id = ObjectID(); - return; - } - } - - _get_obj().obj = const_cast<Object *>(p_object); - _get_obj().id = p_object->get_instance_id(); - } else { - _get_obj().obj = nullptr; - _get_obj().id = ObjectID(); - } + _get_obj() = ObjData(); + _get_obj().ref_pointer(const_cast<Object *>(p_object)); } Variant::Variant(const Callable &p_callable) : @@ -2828,26 +2845,7 @@ void Variant::operator=(const Variant &p_variant) { *reinterpret_cast<::RID *>(_data._mem) = *reinterpret_cast<const ::RID *>(p_variant._data._mem); } break; case OBJECT: { - if (_get_obj().id.is_ref_counted()) { - //we are safe that there is a reference here - RefCounted *ref_counted = static_cast<RefCounted *>(_get_obj().obj); - if (ref_counted->unreference()) { - memdelete(ref_counted); - } - } - - if (p_variant._get_obj().obj && p_variant._get_obj().id.is_ref_counted()) { - RefCounted *ref_counted = static_cast<RefCounted *>(p_variant._get_obj().obj); - if (!ref_counted->reference()) { - _get_obj().obj = nullptr; - _get_obj().id = ObjectID(); - break; - } - } - - _get_obj().obj = const_cast<Object *>(p_variant._get_obj().obj); - _get_obj().id = p_variant._get_obj().id; - + _get_obj().ref(p_variant._get_obj()); } break; case CALLABLE: { *reinterpret_cast<Callable *>(_data._mem) = *reinterpret_cast<const Callable *>(p_variant._data._mem); diff --git a/core/variant/variant.h b/core/variant/variant.h index d4e4b330cd..c76b849abd 100644 --- a/core/variant/variant.h +++ b/core/variant/variant.h @@ -62,6 +62,10 @@ #include "core/variant/dictionary.h" class Object; +class RefCounted; + +template <typename T> +class Ref; struct PropertyInfo; struct MethodInfo; @@ -175,6 +179,20 @@ private: struct ObjData { ObjectID id; Object *obj = nullptr; + + void ref(const ObjData &p_from); + void ref_pointer(Object *p_object); + void ref_pointer(RefCounted *p_object); + void unref(); + + template <typename T> + _ALWAYS_INLINE_ void ref(const Ref<T> &p_from) { + if (p_from.is_valid()) { + ref(ObjData{ p_from->get_instance_id(), p_from.ptr() }); + } else { + unref(); + } + } }; /* array helpers */ diff --git a/core/variant/variant_construct.cpp b/core/variant/variant_construct.cpp index fb75a874e7..6c37d5e4b7 100644 --- a/core/variant/variant_construct.cpp +++ b/core/variant/variant_construct.cpp @@ -323,36 +323,6 @@ String Variant::get_constructor_argument_name(Variant::Type p_type, int p_constr return construct_data[p_type][p_constructor].arg_names[p_argument]; } -void VariantInternal::refcounted_object_assign(Variant *v, const RefCounted *rc) { - if (!rc || !const_cast<RefCounted *>(rc)->init_ref()) { - v->_get_obj().obj = nullptr; - v->_get_obj().id = ObjectID(); - return; - } - - v->_get_obj().obj = const_cast<RefCounted *>(rc); - v->_get_obj().id = rc->get_instance_id(); -} - -void VariantInternal::object_assign(Variant *v, const Object *o) { - if (o) { - if (o->is_ref_counted()) { - RefCounted *ref_counted = const_cast<RefCounted *>(static_cast<const RefCounted *>(o)); - if (!ref_counted->init_ref()) { - v->_get_obj().obj = nullptr; - v->_get_obj().id = ObjectID(); - return; - } - } - - v->_get_obj().obj = const_cast<Object *>(o); - v->_get_obj().id = o->get_instance_id(); - } else { - v->_get_obj().obj = nullptr; - v->_get_obj().id = ObjectID(); - } -} - void Variant::get_constructor_list(Type p_type, List<MethodInfo> *r_list) { ERR_FAIL_INDEX(p_type, Variant::VARIANT_MAX); diff --git a/core/variant/variant_construct.h b/core/variant/variant_construct.h index 68210a9451..f625419da7 100644 --- a/core/variant/variant_construct.h +++ b/core/variant/variant_construct.h @@ -156,14 +156,14 @@ public: if (p_args[0]->get_type() == Variant::NIL) { VariantInternal::clear(&r_ret); VariantTypeChanger<Object *>::change(&r_ret); - VariantInternal::object_assign_null(&r_ret); + VariantInternal::object_reset_data(&r_ret); r_error.error = Callable::CallError::CALL_OK; } else if (p_args[0]->get_type() == Variant::OBJECT) { - VariantInternal::clear(&r_ret); VariantTypeChanger<Object *>::change(&r_ret); VariantInternal::object_assign(&r_ret, p_args[0]); r_error.error = Callable::CallError::CALL_OK; } else { + VariantInternal::clear(&r_ret); r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; r_error.argument = 0; r_error.expected = Variant::OBJECT; @@ -171,7 +171,6 @@ public: } static inline void validated_construct(Variant *r_ret, const Variant **p_args) { - VariantInternal::clear(r_ret); VariantTypeChanger<Object *>::change(r_ret); VariantInternal::object_assign(r_ret, p_args[0]); } @@ -203,13 +202,13 @@ public: VariantInternal::clear(&r_ret); VariantTypeChanger<Object *>::change(&r_ret); - VariantInternal::object_assign_null(&r_ret); + VariantInternal::object_reset_data(&r_ret); } static inline void validated_construct(Variant *r_ret, const Variant **p_args) { VariantInternal::clear(r_ret); VariantTypeChanger<Object *>::change(r_ret); - VariantInternal::object_assign_null(r_ret); + VariantInternal::object_reset_data(r_ret); } static void ptr_construct(void *base, const void **p_args) { PtrConstruct<Object *>::construct(nullptr, base); diff --git a/core/variant/variant_internal.h b/core/variant/variant_internal.h index 58a45c0a1f..58652d26e0 100644 --- a/core/variant/variant_internal.h +++ b/core/variant/variant_internal.h @@ -220,7 +220,7 @@ public: // Should be in the same order as Variant::Type for consistency. // Those primitive and vector types don't need an `init_` method: // Nil, bool, float, Vector2/i, Rect2/i, Vector3/i, Plane, Quat, RID. - // Object is a special case, handled via `object_assign_null`. + // Object is a special case, handled via `object_reset_data`. _FORCE_INLINE_ static void init_string(Variant *v) { memnew_placement(v->_data._mem, String); v->type = Variant::STRING; @@ -319,7 +319,7 @@ public: v->type = Variant::PACKED_VECTOR4_ARRAY; } _FORCE_INLINE_ static void init_object(Variant *v) { - object_assign_null(v); + object_reset_data(v); v->type = Variant::OBJECT; } @@ -327,19 +327,28 @@ public: v->clear(); } - static void object_assign(Variant *v, const Object *o); // Needs RefCounted, so it's implemented elsewhere. - static void refcounted_object_assign(Variant *v, const RefCounted *rc); + _FORCE_INLINE_ static void object_assign(Variant *v, const Variant *vo) { + v->_get_obj().ref(vo->_get_obj()); + } + + _FORCE_INLINE_ static void object_assign(Variant *v, Object *o) { + v->_get_obj().ref_pointer(o); + } - _FORCE_INLINE_ static void object_assign(Variant *v, const Variant *o) { - object_assign(v, o->_get_obj().obj); + _FORCE_INLINE_ static void object_assign(Variant *v, const Object *o) { + v->_get_obj().ref_pointer(const_cast<Object *>(o)); + } + + template <typename T> + _FORCE_INLINE_ static void object_assign(Variant *v, const Ref<T> &r) { + v->_get_obj().ref(r); } - _FORCE_INLINE_ static void object_assign_null(Variant *v) { - v->_get_obj().obj = nullptr; - v->_get_obj().id = ObjectID(); + _FORCE_INLINE_ static void object_reset_data(Variant *v) { + v->_get_obj() = Variant::ObjData(); } - static void update_object_id(Variant *v) { + _FORCE_INLINE_ static void update_object_id(Variant *v) { const Object *o = v->_get_obj().obj; if (o) { v->_get_obj().id = o->get_instance_id(); diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 63d20242d6..55d00b6cf9 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -1209,7 +1209,7 @@ <return type="int" /> <param index="0" name="x" type="int" /> <description> - Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if if [param x] is zero. + Returns [code]-1[/code] if [param x] is negative, [code]1[/code] if [param x] is positive, and [code]0[/code] if [param x] is zero. [codeblock] signi(-6) # Returns -1 signi(0) # Returns 0 diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 887e9cda81..609d7eff39 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -34,6 +34,14 @@ <link title="Animation documentation index">$DOCS_URL/tutorials/animation/index.html</link> </tutorials> <methods> + <method name="add_marker"> + <return type="void" /> + <param index="0" name="name" type="StringName" /> + <param index="1" name="time" type="float" /> + <description> + Adds a marker to this Animation. + </description> + </method> <method name="add_track"> <return type="int" /> <param index="0" name="type" type="int" enum="Animation.TrackType" /> @@ -271,12 +279,60 @@ Returns the index of the specified track. If the track is not found, return -1. </description> </method> + <method name="get_marker_at_time" qualifiers="const"> + <return type="StringName" /> + <param index="0" name="time" type="float" /> + <description> + Returns the name of the marker located at the given time. + </description> + </method> + <method name="get_marker_color" qualifiers="const"> + <return type="Color" /> + <param index="0" name="name" type="StringName" /> + <description> + Returns the given marker's color. + </description> + </method> + <method name="get_marker_names" qualifiers="const"> + <return type="PackedStringArray" /> + <description> + Returns every marker in this Animation, sorted ascending by time. + </description> + </method> + <method name="get_marker_time" qualifiers="const"> + <return type="float" /> + <param index="0" name="name" type="StringName" /> + <description> + Returns the given marker's time. + </description> + </method> + <method name="get_next_marker" qualifiers="const"> + <return type="StringName" /> + <param index="0" name="time" type="float" /> + <description> + Returns the closest marker that comes after the given time. If no such marker exists, an empty string is returned. + </description> + </method> + <method name="get_prev_marker" qualifiers="const"> + <return type="StringName" /> + <param index="0" name="time" type="float" /> + <description> + Returns the closest marker that comes before the given time. If no such marker exists, an empty string is returned. + </description> + </method> <method name="get_track_count" qualifiers="const"> <return type="int" /> <description> Returns the amount of tracks in the animation. </description> </method> + <method name="has_marker" qualifiers="const"> + <return type="bool" /> + <param index="0" name="name" type="StringName" /> + <description> + Returns [code]true[/code] if this Animation contains a marker with the given name. + </description> + </method> <method name="method_track_get_name" qualifiers="const"> <return type="StringName" /> <param index="0" name="track_idx" type="int" /> @@ -320,6 +376,13 @@ Returns the interpolated position value at the given time (in seconds). The [param track_idx] must be the index of a 3D position track. </description> </method> + <method name="remove_marker"> + <return type="void" /> + <param index="0" name="name" type="StringName" /> + <description> + Removes the marker with the given name from this Animation. + </description> + </method> <method name="remove_track"> <return type="void" /> <param index="0" name="track_idx" type="int" /> @@ -363,6 +426,14 @@ Returns the interpolated scale value at the given time (in seconds). The [param track_idx] must be the index of a 3D scale track. </description> </method> + <method name="set_marker_color"> + <return type="void" /> + <param index="0" name="name" type="StringName" /> + <param index="1" name="color" type="Color" /> + <description> + Sets the given marker's color. + </description> + </method> <method name="track_find_key" qualifiers="const"> <return type="int" /> <param index="0" name="track_idx" type="int" /> diff --git a/doc/classes/AnimationPlayer.xml b/doc/classes/AnimationPlayer.xml index 1ca8ac2fa5..9aeb4b7162 100644 --- a/doc/classes/AnimationPlayer.xml +++ b/doc/classes/AnimationPlayer.xml @@ -75,6 +75,24 @@ Returns the node which node path references will travel from. </description> </method> + <method name="get_section_end_time" qualifiers="const"> + <return type="float" /> + <description> + Returns the end time of the section currently being played. + </description> + </method> + <method name="get_section_start_time" qualifiers="const"> + <return type="float" /> + <description> + Returns the start time of the section currently being played. + </description> + </method> + <method name="has_section" qualifiers="const"> + <return type="bool" /> + <description> + Returns [code]true[/code] if an animation is currently playing with section. + </description> + </method> <method name="is_playing" qualifiers="const"> <return type="bool" /> <description> @@ -110,6 +128,54 @@ This method is a shorthand for [method play] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], so see its description for more information. </description> </method> + <method name="play_section"> + <return type="void" /> + <param index="0" name="name" type="StringName" default="&""" /> + <param index="1" name="start_time" type="float" default="-1" /> + <param index="2" name="end_time" type="float" default="-1" /> + <param index="3" name="custom_blend" type="float" default="-1" /> + <param index="4" name="custom_speed" type="float" default="1.0" /> + <param index="5" name="from_end" type="bool" default="false" /> + <description> + Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time]. See also [method play]. + Setting [param start_time] to a value outside the range of the animation means the start of the animation will be used instead, and setting [param end_time] to a value outside the range of the animation means the end of the animation will be used instead. [param start_time] cannot be equal to [param end_time]. + </description> + </method> + <method name="play_section_backwards"> + <return type="void" /> + <param index="0" name="name" type="StringName" default="&""" /> + <param index="1" name="start_time" type="float" default="-1" /> + <param index="2" name="end_time" type="float" default="-1" /> + <param index="3" name="custom_blend" type="float" default="-1" /> + <description> + Plays the animation with key [param name] and the section starting from [param start_time] and ending on [param end_time] in reverse. + This method is a shorthand for [method play_section] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information. + </description> + </method> + <method name="play_section_with_markers"> + <return type="void" /> + <param index="0" name="name" type="StringName" default="&""" /> + <param index="1" name="start_marker" type="StringName" default="&""" /> + <param index="2" name="end_marker" type="StringName" default="&""" /> + <param index="3" name="custom_blend" type="float" default="-1" /> + <param index="4" name="custom_speed" type="float" default="1.0" /> + <param index="5" name="from_end" type="bool" default="false" /> + <description> + Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker]. + If the start marker is empty, the section starts from the beginning of the animation. If the end marker is empty, the section ends on the end of the animation. See also [method play]. + </description> + </method> + <method name="play_section_with_markers_backwards"> + <return type="void" /> + <param index="0" name="name" type="StringName" default="&""" /> + <param index="1" name="start_marker" type="StringName" default="&""" /> + <param index="2" name="end_marker" type="StringName" default="&""" /> + <param index="3" name="custom_blend" type="float" default="-1" /> + <description> + Plays the animation with key [param name] and the section starting from [param start_marker] and ending on [param end_marker] in reverse. + This method is a shorthand for [method play_section_with_markers] with [code]custom_speed = -1.0[/code] and [code]from_end = true[/code], see its description for more information. + </description> + </method> <method name="play_with_capture"> <return type="void" /> <param index="0" name="name" type="StringName" default="&""" /> @@ -139,6 +205,12 @@ [b]Note:[/b] If a looped animation is currently playing, the queued animation will never play unless the looped animation is stopped somehow. </description> </method> + <method name="reset_section"> + <return type="void" /> + <description> + Resets the current section if section is set. + </description> + </method> <method name="seek"> <return type="void" /> <param index="0" name="seconds" type="float" /> @@ -180,6 +252,23 @@ Sets the node which node path references will travel from. </description> </method> + <method name="set_section"> + <return type="void" /> + <param index="0" name="start_time" type="float" default="-1" /> + <param index="1" name="end_time" type="float" default="-1" /> + <description> + Changes the start and end times of the section being played. The current playback position will be clamped within the new section. See also [method play_section]. + </description> + </method> + <method name="set_section_with_markers"> + <return type="void" /> + <param index="0" name="start_marker" type="StringName" default="&""" /> + <param index="1" name="end_marker" type="StringName" default="&""" /> + <description> + Changes the start and end markers of the section being played. The current playback position will be clamped within the new section. See also [method play_section_with_markers]. + If the argument is empty, the section uses the beginning or end of the animation. If both are empty, it means that the section is not set. + </description> + </method> <method name="stop"> <return type="void" /> <param index="0" name="keep_state" type="bool" default="false" /> diff --git a/doc/classes/CameraAttributesPhysical.xml b/doc/classes/CameraAttributesPhysical.xml index faedfee712..e2036162c7 100644 --- a/doc/classes/CameraAttributesPhysical.xml +++ b/doc/classes/CameraAttributesPhysical.xml @@ -25,7 +25,7 @@ The maximum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing below a certain brightness, resulting in a cut off point where the scene will remain bright. </member> <member name="auto_exposure_min_exposure_value" type="float" setter="set_auto_exposure_min_exposure_value" getter="get_auto_exposure_min_exposure_value" default="-8.0"> - The minimum luminance luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark. + The minimum luminance (in EV100) used when calculating auto exposure. When calculating scene average luminance, color values will be clamped to at least this value. This limits the auto-exposure from exposing above a certain brightness, resulting in a cut off point where the scene will remain dark. </member> <member name="exposure_aperture" type="float" setter="set_aperture" getter="get_aperture" default="16.0"> Size of the aperture of the camera, measured in f-stops. An f-stop is a unitless ratio between the focal length of the camera and the diameter of the aperture. A high aperture setting will result in a smaller aperture which leads to a dimmer image and sharper focus. A low aperture results in a wide aperture which lets in more light resulting in a brighter, less-focused image. Default is appropriate for outdoors at daytime (i.e. for use with a default [DirectionalLight3D]), for indoor lighting, a value between 2 and 4 is more appropriate. diff --git a/doc/classes/EditorExportPlugin.xml b/doc/classes/EditorExportPlugin.xml index 42e1968eb0..aa8e4b3d56 100644 --- a/doc/classes/EditorExportPlugin.xml +++ b/doc/classes/EditorExportPlugin.xml @@ -159,6 +159,15 @@ Return a [PackedStringArray] of additional features this preset, for the given [param platform], should have. </description> </method> + <method name="_get_export_option_visibility" qualifiers="virtual const"> + <return type="bool" /> + <param index="0" name="platform" type="EditorExportPlatform" /> + <param index="1" name="option" type="String" /> + <description> + [b]Optional.[/b] + Validates [param option] and returns the visibility for the specified [param platform]. The default implementation returns [code]true[/code] for all options. + </description> + </method> <method name="_get_export_option_warning" qualifiers="virtual const"> <return type="String" /> <param index="0" name="platform" type="EditorExportPlatform" /> diff --git a/doc/classes/EditorInspector.xml b/doc/classes/EditorInspector.xml index cfdc172fd1..6b25be490e 100644 --- a/doc/classes/EditorInspector.xml +++ b/doc/classes/EditorInspector.xml @@ -28,6 +28,7 @@ </method> </methods> <members> + <member name="follow_focus" type="bool" setter="set_follow_focus" getter="is_following_focus" overrides="ScrollContainer" default="true" /> <member name="horizontal_scroll_mode" type="int" setter="set_horizontal_scroll_mode" getter="get_horizontal_scroll_mode" overrides="ScrollContainer" enum="ScrollContainer.ScrollMode" default="0" /> </members> <signals> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 5eb8ac6199..a5097521dc 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -714,6 +714,12 @@ If [code]true[/code], when saving a file, the editor will rename the old file to a different name, save a new file, then only remove the old file once the new file has been saved. This makes loss of data less likely to happen if the editor or operating system exits unexpectedly while saving (e.g. due to a crash or power outage). [b]Note:[/b] On Windows, this feature can interact negatively with certain antivirus programs. In this case, you may have to set this to [code]false[/code] to prevent file locking issues. </member> + <member name="filesystem/quick_open_dialog/default_display_mode" type="int" setter="" getter=""> + If set to [code]Adaptive[/code], the dialog opens in list view or grid view depending on the requested type. If set to [code]Last Used[/code], the display mode will always open the way you last used it. + </member> + <member name="filesystem/quick_open_dialog/include_addons" type="bool" setter="" getter=""> + If [code]true[/code], results will include files located in the [code]addons[/code] folder. + </member> <member name="filesystem/tools/oidn/oidn_denoise_path" type="String" setter="" getter=""> The path to the directory containing the Open Image Denoise (OIDN) executable, used optionally for denoising lightmaps. It can be downloaded from [url=https://www.openimagedenoise.org/downloads.html]openimagedenoise.org[/url]. To enable this feature for your specific project, use [member ProjectSettings.rendering/lightmapping/denoising/denoiser]. @@ -772,7 +778,7 @@ Translations are provided by the community. If you spot a mistake, [url=$DOCS_URL/contributing/documentation/editor_and_docs_localization.html]contribute to editor translations on Weblate![/url] </member> <member name="interface/editor/editor_screen" type="int" setter="" getter=""> - The preferred monitor to display the editor. + The preferred monitor to display the editor. If [b]Auto[/b], the editor will remember the last screen it was displayed on across restarts. </member> <member name="interface/editor/expand_to_title" type="bool" setter="" getter=""> Expanding main editor window content to the title, if supported by [DisplayServer]. See [constant DisplayServer.WINDOW_FLAG_EXTEND_TO_TITLE]. @@ -826,9 +832,6 @@ <member name="interface/editor/project_manager_screen" type="int" setter="" getter=""> The preferred monitor to display the project manager. </member> - <member name="interface/editor/remember_window_size_and_position" type="bool" setter="" getter=""> - If [code]true[/code], the editor window will remember its size, position, and which screen it was displayed on across restarts. - </member> <member name="interface/editor/save_each_scene_on_quit" type="bool" setter="" getter=""> If [code]false[/code], the editor will save all scenes when confirming the [b]Save[/b] action when quitting the editor or quitting to the project list. If [code]true[/code], the editor will ask to save each scene individually. </member> diff --git a/doc/classes/InputMap.xml b/doc/classes/InputMap.xml index ff04040826..0abd7c6974 100644 --- a/doc/classes/InputMap.xml +++ b/doc/classes/InputMap.xml @@ -67,7 +67,7 @@ <method name="add_action"> <return type="void" /> <param index="0" name="action" type="StringName" /> - <param index="1" name="deadzone" type="float" default="0.5" /> + <param index="1" name="deadzone" type="float" default="0.2" /> <description> Adds an empty action to the [InputMap] with a configurable [param deadzone]. An [InputEvent] can then be added to this action with [method action_add_event]. diff --git a/doc/classes/PopupMenu.xml b/doc/classes/PopupMenu.xml index 004bbe2286..d73cda7460 100644 --- a/doc/classes/PopupMenu.xml +++ b/doc/classes/PopupMenu.xml @@ -776,7 +776,7 @@ [StyleBox] for the right side of labeled separator. See [method add_separator]. </theme_item> <theme_item name="panel" data_type="style" type="StyleBox"> - [StyleBox] for the the background panel. + [StyleBox] for the background panel. </theme_item> <theme_item name="separator" data_type="style" type="StyleBox"> [StyleBox] used for the separators. See [method add_separator]. diff --git a/doc/classes/PopupPanel.xml b/doc/classes/PopupPanel.xml index 399e285402..b581f32686 100644 --- a/doc/classes/PopupPanel.xml +++ b/doc/classes/PopupPanel.xml @@ -10,7 +10,7 @@ </tutorials> <theme_items> <theme_item name="panel" data_type="style" type="StyleBox"> - [StyleBox] for the the background panel. + [StyleBox] for the background panel. </theme_item> </theme_items> </class> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index af5ec41b60..4266bab2a1 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1369,7 +1369,7 @@ macOS specific override for the shortcut to select the word currently under the caret. </member> <member name="input/ui_text_skip_selection_for_next_occurrence" type="Dictionary" setter="" getter=""> - If no selection is currently active with the last caret in text fields, searches for the next occurrence of the the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret. + If no selection is currently active with the last caret in text fields, searches for the next occurrence of the word currently under the caret and moves the caret to the next occurrence. The action can be performed sequentially for other occurrences of the word under the last caret. If a selection is currently active with the last caret in text fields, searches for the next occurrence of the selection, adds a caret, selects the next occurrence then deselects the previous selection and its associated caret. The action can be performed sequentially for other occurrences of the selection of the last caret. The viewport is adjusted to the latest newly added caret. [b]Note:[/b] Default [code]ui_*[/code] actions cannot be removed as they are necessary for the internal logic of several [Control]s. The events assigned to the action can however be modified. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index a57f6adec8..91af70b565 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -913,7 +913,7 @@ <param index="1" name="transform" type="Transform2D" /> <description> Transforms both the current and previous stored transform for a canvas light. - This allows transforming a light without creating a "glitch" in the interpolation, which is is particularly useful for large worlds utilizing a shifting origin. + This allows transforming a light without creating a "glitch" in the interpolation, which is particularly useful for large worlds utilizing a shifting origin. </description> </method> <method name="canvas_occluder_polygon_create"> diff --git a/doc/classes/Sprite2D.xml b/doc/classes/Sprite2D.xml index d73cb02d94..239c4dcf09 100644 --- a/doc/classes/Sprite2D.xml +++ b/doc/classes/Sprite2D.xml @@ -76,7 +76,7 @@ If [code]true[/code], texture is cut from a larger atlas texture. See [member region_rect]. </member> <member name="region_filter_clip_enabled" type="bool" setter="set_region_filter_clip_enabled" getter="is_region_filter_clip_enabled" default="false"> - If [code]true[/code], the outermost pixels get blurred out. [member region_enabled] must be [code]true[/code]. + If [code]true[/code], the area outside of the [member region_rect] is clipped to avoid bleeding of the surrounding texture pixels. [member region_enabled] must be [code]true[/code]. </member> <member name="region_rect" type="Rect2" setter="set_region_rect" getter="get_region_rect" default="Rect2(0, 0, 0, 0)"> The region of the atlas texture to display. [member region_enabled] must be [code]true[/code]. diff --git a/doc/classes/TreeItem.xml b/doc/classes/TreeItem.xml index 861a53aaad..132ecc3f92 100644 --- a/doc/classes/TreeItem.xml +++ b/doc/classes/TreeItem.xml @@ -73,6 +73,13 @@ Removes the button at index [param button_index] in column [param column]. </description> </method> + <method name="get_auto_translate_mode" qualifiers="const"> + <return type="int" enum="Node.AutoTranslateMode" /> + <param index="0" name="column" type="int" /> + <description> + Returns the column's auto translate mode. + </description> + </method> <method name="get_autowrap_mode" qualifiers="const"> <return type="int" enum="TextServer.AutowrapMode" /> <param index="0" name="column" type="int" /> @@ -493,6 +500,15 @@ Selects the given [param column]. </description> </method> + <method name="set_auto_translate_mode"> + <return type="void" /> + <param index="0" name="column" type="int" /> + <param index="1" name="mode" type="int" enum="Node.AutoTranslateMode" /> + <description> + Sets the given column's auto translate mode to [param mode]. + All columns use [constant Node.AUTO_TRANSLATE_MODE_INHERIT] by default, which uses the same auto translate mode as the [Tree] itself. + </description> + </method> <method name="set_autowrap_mode"> <return type="void" /> <param index="0" name="column" type="int" /> diff --git a/doc/classes/Vector4i.xml b/doc/classes/Vector4i.xml index 8f54b767e0..b351f2ccb6 100644 --- a/doc/classes/Vector4i.xml +++ b/doc/classes/Vector4i.xml @@ -216,7 +216,7 @@ <return type="Vector4i" /> <param index="0" name="right" type="int" /> <description> - Gets the remainder of each component of the [Vector4i] with the the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers. + Gets the remainder of each component of the [Vector4i] with the given [int]. This operation uses truncated division, which is often not desired as it does not work well with negative numbers. Consider using [method @GlobalScope.posmod] instead if you want to handle negative numbers. [codeblock] print(Vector4i(10, -20, 30, -40) % 7) # Prints "(3, -6, 2, -5)" [/codeblock] diff --git a/drivers/gles3/shaders/canvas.glsl b/drivers/gles3/shaders/canvas.glsl index 76881c8032..5e7fb3b338 100644 --- a/drivers/gles3/shaders/canvas.glsl +++ b/drivers/gles3/shaders/canvas.glsl @@ -579,7 +579,8 @@ void main() { #endif if (bool(read_draw_data_flags & FLAGS_CLIP_RECT_UV)) { - uv = clamp(uv, read_draw_data_src_rect.xy, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw)); + vec2 half_texpixel = read_draw_data_color_texture_pixel_size * 0.5; + uv = clamp(uv, read_draw_data_src_rect.xy + half_texpixel, read_draw_data_src_rect.xy + abs(read_draw_data_src_rect.zw) - half_texpixel); } #endif diff --git a/drivers/vulkan/godot_vulkan.h b/drivers/vulkan/godot_vulkan.h new file mode 100644 index 0000000000..f911c5520a --- /dev/null +++ b/drivers/vulkan/godot_vulkan.h @@ -0,0 +1,42 @@ +/**************************************************************************/ +/* godot_vulkan.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef GODOT_VULKAN_H +#define GODOT_VULKAN_H + +#ifdef USE_VOLK +#include <volk.h> +#else +#include <stdint.h> +#define VK_NO_STDINT_H +#include <vulkan/vulkan.h> +#endif + +#endif // GODOT_VULKAN_H diff --git a/drivers/vulkan/rendering_context_driver_vulkan.h b/drivers/vulkan/rendering_context_driver_vulkan.h index f9352d617b..26de386206 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.h +++ b/drivers/vulkan/rendering_context_driver_vulkan.h @@ -40,11 +40,7 @@ #define VK_TRACK_DEVICE_MEMORY #endif -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" class RenderingContextDriverVulkan : public RenderingContextDriver { public: diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h index 81f4256941..cc15c0a0fe 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.h +++ b/drivers/vulkan/rendering_device_driver_vulkan.h @@ -43,11 +43,7 @@ #endif #include "thirdparty/vulkan/vk_mem_alloc.h" -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" // Design principles: // - Vulkan structs are zero-initialized and fields not requiring a non-zero value are omitted (except in cases where expresivity reasons apply). diff --git a/drivers/vulkan/vulkan_hooks.h b/drivers/vulkan/vulkan_hooks.h index bb30b29cec..82bcc9a064 100644 --- a/drivers/vulkan/vulkan_hooks.h +++ b/drivers/vulkan/vulkan_hooks.h @@ -31,11 +31,7 @@ #ifndef VULKAN_HOOKS_H #define VULKAN_HOOKS_H -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" class VulkanHooks { private: diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index d277ba2f6d..63f86607e5 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -39,6 +39,7 @@ #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_spin_slider.h" +#include "editor/gui/editor_validation_panel.h" #include "editor/gui/scene_tree_editor.h" #include "editor/inspector_dock.h" #include "editor/multi_node_edit.h" @@ -48,6 +49,7 @@ #include "scene/animation/animation_player.h" #include "scene/animation/tween.h" #include "scene/gui/check_box.h" +#include "scene/gui/color_picker.h" #include "scene/gui/grid_container.h" #include "scene/gui/option_button.h" #include "scene/gui/panel_container.h" @@ -1467,7 +1469,7 @@ void AnimationTimelineEdit::_notification(int p_what) { case NOTIFICATION_DRAW: { int key_range = get_size().width - get_buttons_width() - get_name_limit(); - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -1522,6 +1524,18 @@ void AnimationTimelineEdit::_notification(int p_what) { } } + PackedStringArray markers = animation->get_marker_names(); + if (markers.size() > 0) { + float min_marker = animation->get_marker_time(markers[0]); + float max_marker = animation->get_marker_time(markers[markers.size() - 1]); + if (min_marker < time_min) { + time_min = min_marker; + } + if (max_marker > time_max) { + time_max = max_marker; + } + } + float extra = (zoomw / scale) * 0.5; time_max += extra; @@ -1701,7 +1715,7 @@ void AnimationTimelineEdit::set_zoom(Range *p_zoom) { } void AnimationTimelineEdit::auto_fit() { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -1780,7 +1794,7 @@ void AnimationTimelineEdit::update_play_position() { } void AnimationTimelineEdit::update_values() { - if (!animation.is_valid() || editing) { + if (animation.is_null() || editing) { return; } @@ -1792,6 +1806,7 @@ void AnimationTimelineEdit::update_values() { time_icon->set_tooltip_text(TTR("Animation length (frames)")); if (track_edit) { track_edit->editor->_update_key_edit(); + track_edit->editor->marker_edit->_update_key_edit(); } } else { length->set_value(animation->get_length()); @@ -1821,7 +1836,7 @@ void AnimationTimelineEdit::update_values() { } void AnimationTimelineEdit::_play_position_draw() { - if (!animation.is_valid() || play_position_pos < 0) { + if (animation.is_null() || play_position_pos < 0) { return; } @@ -1972,6 +1987,7 @@ AnimationTimelineEdit::AnimationTimelineEdit() { Control *expander = memnew(Control); expander->set_h_size_flags(SIZE_EXPAND_FILL); + expander->set_mouse_filter(MOUSE_FILTER_IGNORE); len_hb->add_child(expander); time_icon = memnew(TextureRect); time_icon->set_v_size_flags(SIZE_SHRINK_CENTER); @@ -2124,6 +2140,62 @@ void AnimationTrackEdit::_notification(int p_what) { draw_line(Point2(limit, 0), Point2(limit, get_size().height), h_line_color, Math::round(EDSCALE)); } + // Marker sections. + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = editor->get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = animation->get_marker_time(start_marker); + double end_time = animation->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { + start_time += editor->get_marker_moving_selection_offset(); + end_time += editor->get_marker_moving_selection_offset(); + } + + if (start_time < animation->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // Marker overlays. + + { + float scale = timeline->get_zoom_scale(); + PackedStringArray markers = animation->get_marker_names(); + for (const StringName marker : markers) { + double time = animation->get_marker_time(marker); + if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { + time += editor->get_marker_moving_selection_offset(); + } + if (time >= 0) { + float offset = time - timeline->get_value(); + offset = offset * scale + limit; + Color marker_color = animation->get_marker_color(marker); + marker_color.a = 0.2; + draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color); + } + } + } + // Keyframes. draw_bg(limit, get_size().width - timeline->get_buttons_width() - outer_margin); @@ -2352,7 +2424,7 @@ void AnimationTrackEdit::_notification(int p_what) { } int AnimationTrackEdit::get_key_height() const { - if (!animation.is_valid()) { + if (animation.is_null()) { return 0; } @@ -2360,7 +2432,7 @@ int AnimationTrackEdit::get_key_height() const { } Rect2 AnimationTrackEdit::get_key_rect(int p_index, float p_pixels_sec) { - if (!animation.is_valid()) { + if (animation.is_null()) { return Rect2(); } Rect2 rect = Rect2(-type_icon->get_width() / 2, 0, type_icon->get_width(), get_size().height); @@ -2399,7 +2471,7 @@ void AnimationTrackEdit::draw_key_link(int p_index, float p_pixels_sec, int p_x, } void AnimationTrackEdit::draw_key(int p_index, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -2573,7 +2645,7 @@ void AnimationTrackEdit::set_editor(AnimationTrackEditor *p_editor) { } void AnimationTrackEdit::_play_position_draw() { - if (!animation.is_valid() || play_position_pos < 0) { + if (animation.is_null() || play_position_pos < 0) { return; } @@ -3522,6 +3594,64 @@ void AnimationTrackEditGroup::_notification(int p_what) { draw_style_box(stylebox_header, Rect2(Point2(), get_size())); + int limit = timeline->get_name_limit(); + + // Section preview. + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = editor->get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = editor->get_current_animation()->get_marker_time(start_marker); + double end_time = editor->get_current_animation()->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (editor->is_marker_moving_selection() && !(player && player->is_playing())) { + start_time += editor->get_marker_moving_selection_offset(); + end_time += editor->get_marker_moving_selection_offset(); + } + + if (start_time < editor->get_current_animation()->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(editor->get_current_animation()->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // Marker overlays. + + { + float scale = timeline->get_zoom_scale(); + PackedStringArray markers = editor->get_current_animation()->get_marker_names(); + for (const StringName marker : markers) { + double time = editor->get_current_animation()->get_marker_time(marker); + if (editor->is_marker_selected(marker) && editor->is_marker_moving_selection()) { + time += editor->get_marker_moving_selection_offset(); + } + if (time >= 0) { + float offset = time - timeline->get_value(); + offset = offset * scale + limit; + Color marker_color = editor->get_current_animation()->get_marker_color(marker); + marker_color.a = 0.2; + draw_line(Point2(offset, 0), Point2(offset, get_size().height), marker_color); + } + } + } + draw_line(Point2(), Point2(get_size().width, 0), h_line_color, Math::round(EDSCALE)); draw_line(Point2(timeline->get_name_limit(), 0), Point2(timeline->get_name_limit(), get_size().height), v_line_color, Math::round(EDSCALE)); draw_line(Point2(get_size().width - timeline->get_buttons_width() - outer_margin, 0), Point2(get_size().width - timeline->get_buttons_width() - outer_margin, get_size().height), v_line_color, Math::round(EDSCALE)); @@ -3590,6 +3720,10 @@ void AnimationTrackEditGroup::set_root(Node *p_root) { queue_redraw(); } +void AnimationTrackEditGroup::set_editor(AnimationTrackEditor *p_editor) { + editor = p_editor; +} + void AnimationTrackEditGroup::_zoom_changed() { queue_redraw(); } @@ -3623,6 +3757,9 @@ void AnimationTrackEditor::set_animation(const Ref<Animation> &p_anim, bool p_re read_only = p_read_only; timeline->set_animation(p_anim, read_only); + marker_edit->set_animation(p_anim, read_only); + marker_edit->set_play_position(timeline->get_play_position()); + _cancel_bezier_edit(); _update_tracks(); @@ -3873,6 +4010,7 @@ void AnimationTrackEditor::_track_grab_focus(int p_track) { void AnimationTrackEditor::set_anim_pos(float p_pos) { timeline->set_play_position(p_pos); + marker_edit->set_play_position(p_pos); for (int i = 0; i < track_edits.size(); i++) { track_edits[i]->set_play_position(p_pos); } @@ -4043,7 +4181,7 @@ void AnimationTrackEditor::insert_transform_key(Node3D *p_node, const String &p_ if (!keying) { return; } - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -4083,7 +4221,7 @@ bool AnimationTrackEditor::has_track(Node3D *p_node, const String &p_sub, const if (!keying) { return false; } - if (!animation.is_valid()) { + if (animation.is_null()) { return false; } @@ -4230,6 +4368,22 @@ void AnimationTrackEditor::insert_node_value_key(Node *p_node, const String &p_p _query_insert(id); } +PackedStringArray AnimationTrackEditor::get_selected_section() const { + return marker_edit->get_selected_section(); +} + +bool AnimationTrackEditor::is_marker_selected(const StringName &p_marker) const { + return marker_edit->is_marker_selected(p_marker); +} + +bool AnimationTrackEditor::is_marker_moving_selection() const { + return marker_edit->is_moving_selection(); +} + +float AnimationTrackEditor::get_marker_moving_selection_offset() const { + return marker_edit->get_moving_selection_offset(); +} + void AnimationTrackEditor::insert_value_key(const String &p_property, bool p_advance) { EditorSelectionHistory *history = EditorNode::get_singleton()->get_editor_selection_history(); @@ -4316,7 +4470,7 @@ void AnimationTrackEditor::_confirm_insert_list() { PropertyInfo AnimationTrackEditor::_find_hint_for_track(int p_idx, NodePath &r_base_path, Variant *r_current_val) { r_base_path = NodePath(); - ERR_FAIL_COND_V(!animation.is_valid(), PropertyInfo()); + ERR_FAIL_COND_V(animation.is_null(), PropertyInfo()); ERR_FAIL_INDEX_V(p_idx, animation->get_track_count(), PropertyInfo()); if (!root) { @@ -4769,6 +4923,7 @@ void AnimationTrackEditor::_update_tracks() { g->set_root(root); g->set_tooltip_text(tooltip); g->set_timeline(timeline); + g->set_editor(this); groups.push_back(g); VBoxContainer *vb = memnew(VBoxContainer); vb->add_theme_constant_override("separation", 0); @@ -4860,12 +5015,13 @@ void AnimationTrackEditor::_snap_mode_changed(int p_mode) { if (key_edit) { key_edit->set_use_fps(use_fps); } + marker_edit->set_use_fps(use_fps); step->set_step(use_fps ? FPS_DECIMAL : SECOND_DECIMAL); _update_step_spinbox(); } void AnimationTrackEditor::_update_step_spinbox() { - if (!animation.is_valid()) { + if (animation.is_null()) { return; } step->set_block_signals(true); @@ -4978,6 +5134,7 @@ void AnimationTrackEditor::_notification(int p_what) { void AnimationTrackEditor::_update_scroll(double) { _redraw_tracks(); _redraw_groups(); + marker_edit->queue_redraw(); } void AnimationTrackEditor::_update_step(double p_new_step) { @@ -5253,6 +5410,8 @@ void AnimationTrackEditor::_timeline_value_changed(double) { bezier_edit->queue_redraw(); bezier_edit->update_play_position(); + + marker_edit->update_play_position(); } int AnimationTrackEditor::_get_track_selected() { @@ -5445,6 +5604,8 @@ void AnimationTrackEditor::_key_selected(int p_key, bool p_single, int p_track) _redraw_tracks(); _update_key_edit(); + + marker_edit->_clear_selection(marker_edit->is_selection_active()); } void AnimationTrackEditor::_key_deselected(int p_key, int p_track) { @@ -5513,7 +5674,7 @@ void AnimationTrackEditor::_clear_selection(bool p_update) { void AnimationTrackEditor::_update_key_edit() { _clear_key_edit(); - if (!animation.is_valid()) { + if (animation.is_null()) { return; } @@ -5600,6 +5761,8 @@ void AnimationTrackEditor::_select_at_anim(const Ref<Animation> &p_anim, int p_t selection.insert(sk, ki); _update_key_edit(); + + marker_edit->_clear_selection(marker_edit->is_selection_active()); } void AnimationTrackEditor::_move_selection_commit() { @@ -7311,6 +7474,15 @@ AnimationTrackEditor::AnimationTrackEditor() { box_selection_container->set_clip_contents(true); timeline_vbox->add_child(box_selection_container); + marker_edit = memnew(AnimationMarkerEdit); + timeline->get_child(0)->add_child(marker_edit); + marker_edit->set_editor(this); + marker_edit->set_timeline(timeline); + marker_edit->set_h_size_flags(SIZE_EXPAND_FILL); + marker_edit->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_groups)); + marker_edit->connect(SceneStringName(draw), callable_mp(this, &AnimationTrackEditor::_redraw_tracks)); + scroll = memnew(ScrollContainer); box_selection_container->add_child(scroll); scroll->set_anchors_and_offsets_preset(PRESET_FULL_RECT); @@ -7826,3 +7998,1203 @@ AnimationTrackKeyEditEditor::AnimationTrackKeyEditEditor(Ref<Animation> p_animat AnimationTrackKeyEditEditor::~AnimationTrackKeyEditEditor() { } + +void AnimationMarkerEdit::_zoom_changed() { + queue_redraw(); + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::_menu_selected(int p_index) { + switch (p_index) { + case MENU_KEY_INSERT: { + _insert_marker(insert_at_pos); + } break; + case MENU_KEY_RENAME: { + if (selection.size() > 0) { + _rename_marker(*selection.last()); + } + } break; + case MENU_KEY_DELETE: { + _delete_selected_markers(); + } break; + case MENU_KEY_TOGGLE_MARKER_NAMES: { + should_show_all_marker_names = !should_show_all_marker_names; + queue_redraw(); + } break; + } +} + +void AnimationMarkerEdit::_play_position_draw() { + if (animation.is_null() || play_position_pos < 0) { + return; + } + + float scale = timeline->get_zoom_scale(); + int h = get_size().height; + + int px = (play_position_pos - timeline->get_value()) * scale + timeline->get_name_limit(); + + if (px >= timeline->get_name_limit() && px < (get_size().width - timeline->get_buttons_width())) { + Color color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor)); + play_position->draw_line(Point2(px, 0), Point2(px, h), color, Math::round(2 * EDSCALE)); + } +} + +bool AnimationMarkerEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + PackedStringArray names = animation->get_marker_names(); + for (int i = 0; i < names.size(); i++) { + Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale()); + float offset = animation->get_marker_time(names[i]) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + limit; + rect.position.x += offset; + if (rect.has_point(p_pos)) { + if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) { + float distance = Math::abs(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + break; + } + } + } + + if (key_idx != -1) { + if (p_aggregate) { + StringName name = names[key_idx]; + if (selection.has(name)) { + if (p_deselectable) { + call_deferred("_deselect_key", name); + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; + } + } else { + call_deferred("_select_key", name, false); + moving_selection_attempt = true; + moving_selection_effective = false; + select_single_attempt = StringName(); + moving_selection_pivot = animation->get_marker_time(name); + moving_selection_mouse_begin_x = p_pos.x; + } + + } else { + StringName name = names[key_idx]; + if (!selection.has(name)) { + call_deferred("_select_key", name, true); + select_single_attempt = StringName(); + } else { + select_single_attempt = name; + } + + moving_selection_attempt = true; + moving_selection_effective = false; + moving_selection_pivot = animation->get_marker_time(name); + moving_selection_mouse_begin_x = p_pos.x; + } + + if (read_only) { + moving_selection_attempt = false; + moving_selection_pivot = 0.0f; + moving_selection_mouse_begin_x = 0.0f; + } + return true; + } + } + + return false; +} + +bool AnimationMarkerEdit::_is_ui_pos_in_current_section(const Point2 &p_pos) { + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + if (p_pos.x >= limit && p_pos.x <= limit_end) { + PackedStringArray section = get_selected_section(); + if (!section.is_empty()) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + float start_offset = (animation->get_marker_time(start_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit; + float end_offset = (animation->get_marker_time(end_marker) - timeline->get_value()) * timeline->get_zoom_scale() + limit; + return p_pos.x >= start_offset && p_pos.x <= end_offset; + } + } + + return false; +} + +HBoxContainer *AnimationMarkerEdit::_create_hbox_labeled_control(const String &p_text, Control *p_control) const { + HBoxContainer *hbox = memnew(HBoxContainer); + Label *label = memnew(Label); + label->set_text(p_text); + hbox->add_child(label); + hbox->add_child(p_control); + hbox->set_h_size_flags(SIZE_EXPAND_FILL); + label->set_h_size_flags(SIZE_EXPAND_FILL); + label->set_stretch_ratio(1.0); + p_control->set_h_size_flags(SIZE_EXPAND_FILL); + p_control->set_stretch_ratio(1.0); + return hbox; +} + +void AnimationMarkerEdit::_update_key_edit() { + _clear_key_edit(); + if (animation.is_null()) { + return; + } + + if (selection.size() == 1) { + key_edit = memnew(AnimationMarkerKeyEdit); + key_edit->animation = animation; + key_edit->animation_read_only = read_only; + key_edit->marker_name = *selection.begin(); + key_edit->use_fps = timeline->is_using_fps(); + key_edit->marker_edit = this; + + EditorNode::get_singleton()->push_item(key_edit); + + InspectorDock::get_singleton()->set_info(TTR("Marker name is read-only in the inspector."), TTR("A marker's name can only be changed by right-clicking it in the animation editor and selecting \"Rename Marker\", in order to make sure that marker names are all unique."), true); + } else if (selection.size() > 1) { + multi_key_edit = memnew(AnimationMultiMarkerKeyEdit); + multi_key_edit->animation = animation; + multi_key_edit->animation_read_only = read_only; + multi_key_edit->marker_edit = this; + for (const StringName &name : selection) { + multi_key_edit->marker_names.push_back(name); + } + + EditorNode::get_singleton()->push_item(multi_key_edit); + } +} + +void AnimationMarkerEdit::_clear_key_edit() { + if (key_edit) { + // If key edit is the object being inspected, remove it first. + if (InspectorDock::get_inspector_singleton()->get_edited_object() == key_edit) { + EditorNode::get_singleton()->push_item(nullptr); + } + + // Then actually delete it. + memdelete(key_edit); + key_edit = nullptr; + } + + if (multi_key_edit) { + if (InspectorDock::get_inspector_singleton()->get_edited_object() == multi_key_edit) { + EditorNode::get_singleton()->push_item(nullptr); + } + + memdelete(multi_key_edit); + multi_key_edit = nullptr; + } +} + +void AnimationMarkerEdit::_bind_methods() { + ClassDB::bind_method("_clear_selection_for_anim", &AnimationMarkerEdit::_clear_selection_for_anim); + ClassDB::bind_method("_select_key", &AnimationMarkerEdit::_select_key); + ClassDB::bind_method("_deselect_key", &AnimationMarkerEdit::_deselect_key); +} + +void AnimationMarkerEdit::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + if (animation.is_null()) { + return; + } + + type_icon = get_editor_theme_icon(SNAME("Marker")); + selected_icon = get_editor_theme_icon(SNAME("MarkerSelected")); + } break; + + case NOTIFICATION_DRAW: { + if (animation.is_null()) { + return; + } + + int limit = timeline->get_name_limit(); + + Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label")); + Color color = get_theme_color(SceneStringName(font_color), SNAME("Label")); + int hsep = get_theme_constant(SNAME("h_separation"), SNAME("ItemList")); + Color linecolor = color; + linecolor.a = 0.2; + + // SECTION PREVIEW // + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray section = get_selected_section(); + if (section.size() == 2) { + StringName start_marker = section[0]; + StringName end_marker = section[1]; + double start_time = animation->get_marker_time(start_marker); + double end_time = animation->get_marker_time(end_marker); + + // When AnimationPlayer is playing, don't move the preview rect, so it still indicates the playback section. + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (moving_selection && !(player && player->is_playing())) { + start_time += moving_selection_offset; + end_time += moving_selection_offset; + } + + if (start_time < animation->get_length() && end_time >= 0) { + float start_ofs = MAX(0, start_time) - timeline->get_value(); + float end_ofs = MIN(animation->get_length(), end_time) - timeline->get_value(); + start_ofs = start_ofs * scale + limit; + end_ofs = end_ofs * scale + limit; + start_ofs = MAX(start_ofs, limit); + end_ofs = MIN(end_ofs, limit_end); + Rect2 rect; + rect.set_position(Vector2(start_ofs, 0)); + rect.set_size(Vector2(end_ofs - start_ofs, get_size().height)); + + draw_rect(rect, Color(1, 0.1, 0.1, 0.2)); + } + } + } + + // KEYFRAMES // + + draw_bg(limit, get_size().width - timeline->get_buttons_width()); + + { + float scale = timeline->get_zoom_scale(); + int limit_end = get_size().width - timeline->get_buttons_width(); + + PackedStringArray names = animation->get_marker_names(); + for (int i = 0; i < names.size(); i++) { + StringName name = names[i]; + bool is_selected = selection.has(name); + float offset = animation->get_marker_time(name) - timeline->get_value(); + if (is_selected && moving_selection) { + offset += moving_selection_offset; + } + + offset = offset * scale + limit; + + draw_key(name, scale, int(offset), is_selected, limit, limit_end); + + const int font_size = 16; + Size2 string_size = font->get_string_size(name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size); + if (int(offset) <= limit_end && int(offset) >= limit && should_show_all_marker_names) { + float bottom = get_size().height + string_size.y - font->get_descent(font_size); + float extrusion = MAX(0, offset + string_size.x - limit_end); // How much the string would extrude outside limit_end if unadjusted. + Color marker_color = animation->get_marker_color(name); + draw_string(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, marker_color); + draw_string_outline(font, Point2(offset - extrusion, bottom), name, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, 1, color); + } + } + } + + draw_fg(limit, get_size().width - timeline->get_buttons_width()); + + // BUTTONS // + + { + int ofs = get_size().width - timeline->get_buttons_width(); + + draw_line(Point2(ofs, 0), Point2(ofs, get_size().height), linecolor, Math::round(EDSCALE)); + + ofs += hsep; + } + + draw_line(Vector2(0, get_size().height), get_size(), linecolor, Math::round(EDSCALE)); + } break; + + case NOTIFICATION_MOUSE_ENTER: + hovered = true; + queue_redraw(); + break; + case NOTIFICATION_MOUSE_EXIT: + hovered = false; + // When the mouse cursor exits the track, we're no longer hovering any keyframe. + hovering_marker = StringName(); + queue_redraw(); + break; + } +} + +void AnimationMarkerEdit::gui_input(const Ref<InputEvent> &p_event) { + ERR_FAIL_COND(p_event.is_null()); + + if (animation.is_null()) { + return; + } + + if (p_event->is_pressed()) { + if (ED_IS_SHORTCUT("animation_marker_edit/rename_marker", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_RENAME); + } + } + + if (ED_IS_SHORTCUT("animation_marker_edit/delete_selection", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_DELETE); + } + } + + if (ED_IS_SHORTCUT("animation_marker_edit/toggle_marker_names", p_event)) { + if (!read_only) { + _menu_selected(MENU_KEY_TOGGLE_MARKER_NAMES); + } + } + } + + Ref<InputEventMouseButton> mb = p_event; + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + Point2 pos = mb->get_position(); + if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) { + accept_event(); + } else if (!_is_ui_pos_in_current_section(pos)) { + _clear_selection_for_anim(animation); + } + } + + if (mb.is_valid() && moving_selection_attempt) { + if (!mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + moving_selection_attempt = false; + if (moving_selection && moving_selection_effective) { + if (Math::abs(moving_selection_offset) > CMP_EPSILON) { + _move_selection_commit(); + accept_event(); // So play position doesn't snap to the end of move selection. + } + } else if (select_single_attempt) { + call_deferred("_select_key", select_single_attempt, true); + + // First select click should not affect play position. + if (!selection.has(select_single_attempt)) { + accept_event(); + } else { + // Second click and onwards should snap to marker time. + double ofs = animation->get_marker_time(select_single_attempt); + timeline->set_play_position(ofs); + timeline->emit_signal(SNAME("timeline_changed"), ofs, mb->is_alt_pressed()); + accept_event(); + } + } else { + // First select click should not affect play position. + if (!selection.has(select_single_attempt)) { + accept_event(); + } + } + + moving_selection = false; + select_single_attempt = StringName(); + } + + if (moving_selection && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + moving_selection_attempt = false; + moving_selection = false; + _move_selection_cancel(); + } + } + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) { + Point2 pos = mb->get_position(); + if (pos.x >= timeline->get_name_limit() && pos.x <= get_size().width - timeline->get_buttons_width()) { + // Can do something with menu too! show insert key. + float offset = (pos.x - timeline->get_name_limit()) / timeline->get_zoom_scale(); + if (!read_only) { + bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false); + + menu->clear(); + menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Marker..."), MENU_KEY_INSERT); + + if (selected || selection.size() > 0) { + menu->add_icon_item(get_editor_theme_icon(SNAME("Edit")), TTR("Rename Marker"), MENU_KEY_RENAME); + menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Marker(s)"), MENU_KEY_DELETE); + } + + menu->add_icon_item(get_editor_theme_icon(should_show_all_marker_names ? SNAME("GuiChecked") : SNAME("GuiUnchecked")), TTR("Show All Marker Names"), MENU_KEY_TOGGLE_MARKER_NAMES); + menu->reset_size(); + + moving_selection_attempt = false; + moving_selection = false; + + menu->set_position(get_screen_position() + get_local_mouse_position()); + menu->popup(); + + insert_at_pos = offset + timeline->get_value(); + accept_event(); + } + } + } + + Ref<InputEventMouseMotion> mm = p_event; + + if (mm.is_valid()) { + const StringName previous_hovering_marker = hovering_marker; + + // Hovering compressed keyframes for editing is not possible. + const float scale = timeline->get_zoom_scale(); + const int limit = timeline->get_name_limit(); + const int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + const int limit_start_hitbox = limit - type_icon->get_width(); + const Point2 pos = mm->get_position(); + + if (pos.x >= limit_start_hitbox && pos.x <= limit_end) { + // Use the same logic as key selection to ensure that hovering accurately represents + // which key will be selected when clicking. + int key_idx = -1; + float key_distance = 1e20; + + hovering_marker = StringName(); + + PackedStringArray names = animation->get_marker_names(); + + // Hovering should happen in the opposite order of drawing for more accurate overlap hovering. + for (int i = names.size() - 1; i >= 0; i--) { + StringName name = names[i]; + Rect2 rect = get_key_rect(scale); + float offset = animation->get_marker_time(name) - timeline->get_value(); + offset = offset * scale + limit; + rect.position.x += offset; + + if (rect.has_point(pos)) { + if (is_key_selectable_by_distance()) { + const float distance = Math::abs(offset - pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + hovering_marker = name; + } + } else { + // First one does it. + hovering_marker = name; + break; + } + } + } + + if (hovering_marker != previous_hovering_marker) { + // Required to draw keyframe hover feedback on the correct keyframe. + queue_redraw(); + } + } + } + + if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT) && moving_selection_attempt) { + if (!moving_selection) { + moving_selection = true; + _move_selection_begin(); + } + + float moving_begin_time = ((moving_selection_mouse_begin_x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float new_time = ((mm->get_position().x - timeline->get_name_limit()) / timeline->get_zoom_scale()) + timeline->get_value(); + float delta = new_time - moving_begin_time; + float snapped_time = editor->snap_time(moving_selection_pivot + delta); + + float offset = 0.0; + if (Math::abs(editor->get_moving_selection_offset()) > CMP_EPSILON || (snapped_time > moving_selection_pivot && delta > CMP_EPSILON) || (snapped_time < moving_selection_pivot && delta < -CMP_EPSILON)) { + offset = snapped_time - moving_selection_pivot; + moving_selection_effective = true; + } + + _move_selection(offset); + } +} + +String AnimationMarkerEdit::get_tooltip(const Point2 &p_pos) const { + if (animation.is_null()) { + return Control::get_tooltip(p_pos); + } + + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + + PackedStringArray names = animation->get_marker_names(); + + // Select should happen in the opposite order of drawing for more accurate overlap select. + for (int i = names.size() - 1; i >= 0; i--) { + StringName name = names[i]; + Rect2 rect = const_cast<AnimationMarkerEdit *>(this)->get_key_rect(timeline->get_zoom_scale()); + float offset = animation->get_marker_time(name) - timeline->get_value(); + offset = offset * timeline->get_zoom_scale() + limit; + rect.position.x += offset; + + if (rect.has_point(p_pos)) { + if (const_cast<AnimationMarkerEdit *>(this)->is_key_selectable_by_distance()) { + float distance = ABS(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + break; + } + } + } + + if (key_idx != -1) { + String name = names[key_idx]; + String text = TTR("Time (s):") + " " + TS->format_number(rtos(Math::snapped(animation->get_marker_time(name), 0.0001))) + "\n"; + text += TTR("Marker:") + " " + name + "\n"; + return text; + } + } + + return Control::get_tooltip(p_pos); +} + +int AnimationMarkerEdit::get_key_height() const { + if (animation.is_null()) { + return 0; + } + + return type_icon->get_height(); +} + +Rect2 AnimationMarkerEdit::get_key_rect(float p_pixels_sec) const { + if (animation.is_null()) { + return Rect2(); + } + + Rect2 rect = Rect2(-type_icon->get_width() / 2, get_size().height - type_icon->get_size().height, type_icon->get_width(), type_icon->get_size().height); + + // Make it a big easier to click. + rect.position.x -= rect.size.x * 0.5; + rect.size.x *= 2; + return rect; +} + +PackedStringArray AnimationMarkerEdit::get_selected_section() const { + if (selection.size() >= 2) { + PackedStringArray arr; + arr.push_back(""); // Marker with smallest time. + arr.push_back(""); // Marker with largest time. + double min_time = INFINITY; + double max_time = -INFINITY; + for (const StringName &marker_name : selection) { + double time = animation->get_marker_time(marker_name); + if (time < min_time) { + arr.set(0, marker_name); + min_time = time; + } + if (time > max_time) { + arr.set(1, marker_name); + max_time = time; + } + } + return arr; + } + + return PackedStringArray(); +} + +bool AnimationMarkerEdit::is_marker_selected(const StringName &p_marker) const { + return selection.has(p_marker); +} + +bool AnimationMarkerEdit::is_key_selectable_by_distance() const { + return true; +} + +void AnimationMarkerEdit::draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right) { + if (animation.is_null()) { + return; + } + + if (p_x < p_clip_left || p_x > p_clip_right) { + return; + } + + Ref<Texture2D> icon_to_draw = p_selected ? selected_icon : type_icon; + + Vector2 ofs(p_x - icon_to_draw->get_width() / 2, int(get_size().height - icon_to_draw->get_height())); + + // Don't apply custom marker color when the key is selected. + Color marker_color = p_selected ? Color(1, 1, 1) : animation->get_marker_color(p_name); + + // Use a different color for the currently hovered key. + // The color multiplier is chosen to work with both dark and light editor themes, + // and on both unselected and selected key icons. + draw_texture( + icon_to_draw, + ofs, + p_name == hovering_marker ? get_theme_color(SNAME("folder_icon_color"), SNAME("FileDialog")) : marker_color); +} + +void AnimationMarkerEdit::draw_bg(int p_clip_left, int p_clip_right) { +} + +void AnimationMarkerEdit::draw_fg(int p_clip_left, int p_clip_right) { +} + +Ref<Animation> AnimationMarkerEdit::get_animation() const { + return animation; +} + +void AnimationMarkerEdit::set_animation(const Ref<Animation> &p_animation, bool p_read_only) { + if (animation.is_valid()) { + _clear_selection_for_anim(animation); + } + animation = p_animation; + read_only = p_read_only; + type_icon = get_editor_theme_icon(SNAME("Marker")); + selected_icon = get_editor_theme_icon(SNAME("MarkerSelected")); + + queue_redraw(); +} + +Size2 AnimationMarkerEdit::get_minimum_size() const { + Ref<Texture2D> texture = get_editor_theme_icon(SNAME("Object")); + Ref<Font> font = get_theme_font(SceneStringName(font), SNAME("Label")); + int font_size = get_theme_font_size(SceneStringName(font_size), SNAME("Label")); + int separation = get_theme_constant(SNAME("v_separation"), SNAME("ItemList")); + + int max_h = MAX(texture->get_height(), font->get_height(font_size)); + max_h = MAX(max_h, get_key_height()); + + return Vector2(1, max_h + separation); +} + +void AnimationMarkerEdit::set_timeline(AnimationTimelineEdit *p_timeline) { + timeline = p_timeline; + timeline->connect("zoom_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed)); + timeline->connect("name_limit_changed", callable_mp(this, &AnimationMarkerEdit::_zoom_changed)); +} + +void AnimationMarkerEdit::set_editor(AnimationTrackEditor *p_editor) { + editor = p_editor; +} + +void AnimationMarkerEdit::set_play_position(float p_pos) { + play_position_pos = p_pos; + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::update_play_position() { + play_position->queue_redraw(); +} + +void AnimationMarkerEdit::set_use_fps(bool p_use_fps) { + if (key_edit) { + key_edit->use_fps = p_use_fps; + key_edit->notify_property_list_changed(); + } +} + +void AnimationMarkerEdit::_move_selection_begin() { + moving_selection = true; + moving_selection_offset = 0; +} + +void AnimationMarkerEdit::_move_selection(float p_offset) { + moving_selection_offset = p_offset; + queue_redraw(); +} + +void AnimationMarkerEdit::_move_selection_commit() { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Move Markers")); + + for (HashSet<StringName>::Iterator E = selection.last(); E; --E) { + StringName name = *E; + double time = animation->get_marker_time(name); + float newpos = time + moving_selection_offset; + undo_redo->add_do_method(animation.ptr(), "remove_marker", name); + undo_redo->add_do_method(animation.ptr(), "add_marker", name, newpos); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + + // add_marker will overwrite the overlapped key on the redo pass, so we add it back on the undo pass. + if (StringName overlap = animation->get_marker_at_time(newpos)) { + if (select_single_attempt == overlap) { + select_single_attempt = ""; + } + undo_redo->add_undo_method(animation.ptr(), "add_marker", overlap, newpos); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", overlap, animation->get_marker_color(overlap)); + } + } + + moving_selection = false; + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + PackedStringArray selected_section = get_selected_section(); + if (selected_section.size() >= 2) { + undo_redo->add_do_method(player, "set_section_with_markers", selected_section[0], selected_section[1]); + undo_redo->add_undo_method(player, "set_section_with_markers", selected_section[0], selected_section[1]); + } + } + undo_redo->add_do_method(timeline, "queue_redraw"); + undo_redo->add_undo_method(timeline, "queue_redraw"); + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); + _update_key_edit(); +} + +void AnimationMarkerEdit::_delete_selected_markers() { + if (selection.size()) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Delete Keys")); + for (const StringName &name : selection) { + double time = animation->get_marker_time(name); + undo_redo->add_do_method(animation.ptr(), "remove_marker", name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", name, time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", name, animation->get_marker_color(name)); + } + _clear_selection_for_anim(animation); + + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); + _update_key_edit(); + } +} + +void AnimationMarkerEdit::_move_selection_cancel() { + moving_selection = false; + queue_redraw(); +} + +void AnimationMarkerEdit::_clear_selection(bool p_update) { + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + player->reset_section(); + } + + selection.clear(); + + if (p_update) { + queue_redraw(); + } + + _clear_key_edit(); +} + +void AnimationMarkerEdit::_clear_selection_for_anim(const Ref<Animation> &p_anim) { + if (animation != p_anim) { + return; + } + + _clear_selection(true); +} + +void AnimationMarkerEdit::_select_key(const StringName &p_name, bool is_single) { + if (is_single) { + _clear_selection(false); + } + + selection.insert(p_name); + + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + if (selection.size() >= 2) { + PackedStringArray selected_section = get_selected_section(); + double start_time = animation->get_marker_time(selected_section[0]); + double end_time = animation->get_marker_time(selected_section[1]); + player->set_section(start_time, end_time); + } else { + player->reset_section(); + } + } + + queue_redraw(); + _update_key_edit(); + + editor->_clear_selection(editor->is_selection_active()); +} + +void AnimationMarkerEdit::_deselect_key(const StringName &p_name) { + selection.erase(p_name); + + AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); + if (player) { + if (selection.size() >= 2) { + PackedStringArray selected_section = get_selected_section(); + double start_time = animation->get_marker_time(selected_section[0]); + double end_time = animation->get_marker_time(selected_section[1]); + player->set_section(start_time, end_time); + } else { + player->reset_section(); + } + } + + queue_redraw(); + _update_key_edit(); +} + +void AnimationMarkerEdit::_insert_marker(float p_ofs) { + if (editor->is_snap_timeline_enabled()) { + p_ofs = editor->snap_time(p_ofs); + } + + marker_insert_confirm->popup_centered(Size2(200, 100) * EDSCALE); + marker_insert_color->set_pick_color(Color(1, 1, 1)); + + String base = "new_marker"; + int count = 1; + while (true) { + String attempt = base; + if (count > 1) { + attempt += vformat("_%d", count); + } + if (animation->has_marker(attempt)) { + count++; + continue; + } + base = attempt; + break; + } + + marker_insert_new_name->set_text(base); + _marker_insert_new_name_changed(base); + marker_insert_ofs = p_ofs; +} + +void AnimationMarkerEdit::_rename_marker(const StringName &p_name) { + marker_rename_confirm->popup_centered(Size2i(200, 0) * EDSCALE); + marker_rename_prev_name = p_name; + marker_rename_new_name->set_text(p_name); +} + +void AnimationMarkerEdit::_marker_insert_confirmed() { + StringName name = marker_insert_new_name->get_text(); + + if (animation->has_marker(name)) { + marker_insert_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), name)); + marker_insert_error_dialog->popup_centered(); + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + undo_redo->create_action(TTR("Add Marker Key")); + undo_redo->add_do_method(animation.ptr(), "add_marker", name, marker_insert_ofs); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", name); + StringName existing_marker = animation->get_marker_at_time(marker_insert_ofs); + if (existing_marker) { + undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, marker_insert_ofs); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker)); + } + undo_redo->add_do_method(animation.ptr(), "set_marker_color", name, marker_insert_color->get_pick_color()); + + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + + undo_redo->commit_action(); + + marker_insert_confirm->hide(); +} + +void AnimationMarkerEdit::_marker_insert_new_name_changed(const String &p_text) { + marker_insert_confirm->get_ok_button()->set_disabled(p_text.is_empty()); +} + +void AnimationMarkerEdit::_marker_rename_confirmed() { + StringName new_name = marker_rename_new_name->get_text(); + StringName prev_name = marker_rename_prev_name; + + if (new_name == StringName()) { + marker_rename_error_dialog->set_text(TTR("Empty marker names are not allowed.")); + marker_rename_error_dialog->popup_centered(); + return; + } + + if (new_name != prev_name && animation->has_marker(new_name)) { + marker_rename_error_dialog->set_text(vformat(TTR("Marker '%s' already exists!"), new_name)); + marker_rename_error_dialog->popup_centered(); + return; + } + + if (prev_name != new_name) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Rename Marker")); + undo_redo->add_do_method(animation.ptr(), "remove_marker", prev_name); + undo_redo->add_do_method(animation.ptr(), "add_marker", new_name, animation->get_marker_time(prev_name)); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", new_name, animation->get_marker_color(prev_name)); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", new_name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", prev_name, animation->get_marker_time(prev_name)); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", prev_name, animation->get_marker_color(prev_name)); + undo_redo->add_do_method(this, "_select_key", new_name, true); + undo_redo->add_undo_method(this, "_select_key", prev_name, true); + undo_redo->commit_action(); + select_single_attempt = StringName(); + } + marker_rename_confirm->hide(); +} + +void AnimationMarkerEdit::_marker_rename_new_name_changed(const String &p_text) { + marker_rename_confirm->get_ok_button()->set_disabled(p_text.is_empty()); +} + +AnimationMarkerEdit::AnimationMarkerEdit() { + play_position = memnew(Control); + play_position->set_mouse_filter(MOUSE_FILTER_PASS); + add_child(play_position); + play_position->connect(SceneStringName(draw), callable_mp(this, &AnimationMarkerEdit::_play_position_draw)); + set_focus_mode(FOCUS_CLICK); + set_mouse_filter(MOUSE_FILTER_PASS); // Scroll has to work too for selection. + + menu = memnew(PopupMenu); + add_child(menu); + menu->connect(SceneStringName(id_pressed), callable_mp(this, &AnimationMarkerEdit::_menu_selected)); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/rename_marker", TTR("Rename Marker"), Key::R), MENU_KEY_RENAME); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/delete_selection", TTR("Delete Markers (s)"), Key::KEY_DELETE), MENU_KEY_DELETE); + menu->add_shortcut(ED_SHORTCUT("animation_marker_edit/toggle_marker_names", TTR("Show All Marker Names"), Key::M), MENU_KEY_TOGGLE_MARKER_NAMES); + + marker_insert_confirm = memnew(ConfirmationDialog); + marker_insert_confirm->set_title(TTR("Insert Marker")); + marker_insert_confirm->set_hide_on_ok(false); + marker_insert_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_confirmed)); + add_child(marker_insert_confirm); + VBoxContainer *marker_insert_vbox = memnew(VBoxContainer); + marker_insert_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_insert_confirm->add_child(marker_insert_vbox); + marker_insert_new_name = memnew(LineEdit); + marker_insert_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_insert_new_name_changed)); + marker_insert_confirm->register_text_enter(marker_insert_new_name); + marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Name"), marker_insert_new_name)); + marker_insert_color = memnew(ColorPickerButton); + marker_insert_color->set_edit_alpha(false); + marker_insert_color->get_popup()->connect("about_to_popup", callable_mp(EditorNode::get_singleton(), &EditorNode::setup_color_picker).bind(marker_insert_color->get_picker())); + marker_insert_vbox->add_child(_create_hbox_labeled_control(TTR("Marker Color"), marker_insert_color)); + marker_insert_error_dialog = memnew(AcceptDialog); + marker_insert_error_dialog->set_ok_button_text(TTR("Close")); + marker_insert_error_dialog->set_title(TTR("Error!")); + marker_insert_confirm->add_child(marker_insert_error_dialog); + + marker_rename_confirm = memnew(ConfirmationDialog); + marker_rename_confirm->set_title(TTR("Rename Marker")); + marker_rename_confirm->set_hide_on_ok(false); + marker_rename_confirm->connect(SceneStringName(confirmed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_confirmed)); + add_child(marker_rename_confirm); + VBoxContainer *marker_rename_vbox = memnew(VBoxContainer); + marker_rename_vbox->set_anchors_and_offsets_preset(Control::LayoutPreset::PRESET_FULL_RECT); + marker_rename_confirm->add_child(marker_rename_vbox); + Label *marker_rename_new_name_label = memnew(Label); + marker_rename_new_name_label->set_text(TTR("Change Marker Name:")); + marker_rename_vbox->add_child(marker_rename_new_name_label); + marker_rename_new_name = memnew(LineEdit); + marker_rename_new_name->connect(SceneStringName(text_changed), callable_mp(this, &AnimationMarkerEdit::_marker_rename_new_name_changed)); + marker_rename_confirm->register_text_enter(marker_rename_new_name); + marker_rename_vbox->add_child(marker_rename_new_name); + + marker_rename_error_dialog = memnew(AcceptDialog); + marker_rename_error_dialog->set_ok_button_text(TTR("Close")); + marker_rename_error_dialog->set_title(TTR("Error!")); + marker_rename_confirm->add_child(marker_rename_error_dialog); +} + +AnimationMarkerEdit::~AnimationMarkerEdit() { +} + +float AnimationMarkerKeyEdit::get_time() const { + return animation->get_marker_time(marker_name); +} + +void AnimationMarkerKeyEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMarkerKeyEdit::_hide_script_from_inspector); + ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMarkerKeyEdit::_hide_metadata_from_inspector); + ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMarkerKeyEdit::_dont_undo_redo); + ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMarkerKeyEdit::_is_read_only); + ClassDB::bind_method(D_METHOD("_set_marker_name"), &AnimationMarkerKeyEdit::_set_marker_name); +} + +void AnimationMarkerKeyEdit::_set_marker_name(const StringName &p_name) { + marker_name = p_name; +} + +bool AnimationMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + + if (p_name == "color") { + Color color = p_value; + Color prev_color = animation->get_marker_color(marker_name); + if (color != prev_color) { + undo_redo->create_action(TTR("Edit Marker Color"), UndoRedo::MERGE_ENDS); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, prev_color); + undo_redo->add_do_method(marker_edit, "queue_redraw"); + undo_redo->add_undo_method(marker_edit, "queue_redraw"); + undo_redo->commit_action(); + } + return true; + } + + return false; +} + +bool AnimationMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "name") { + r_ret = marker_name; + return true; + } + + if (p_name == "color") { + r_ret = animation->get_marker_color(marker_name); + return true; + } + + return false; +} + +void AnimationMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const { + if (animation.is_null()) { + return; + } + + p_list->push_back(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_READ_ONLY | PROPERTY_USAGE_EDITOR)); + p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA)); +} + +void AnimationMultiMarkerKeyEdit::_bind_methods() { + ClassDB::bind_method(D_METHOD("_hide_script_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_script_from_inspector); + ClassDB::bind_method(D_METHOD("_hide_metadata_from_inspector"), &AnimationMultiMarkerKeyEdit::_hide_metadata_from_inspector); + ClassDB::bind_method(D_METHOD("_dont_undo_redo"), &AnimationMultiMarkerKeyEdit::_dont_undo_redo); + ClassDB::bind_method(D_METHOD("_is_read_only"), &AnimationMultiMarkerKeyEdit::_is_read_only); +} + +bool AnimationMultiMarkerKeyEdit::_set(const StringName &p_name, const Variant &p_value) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + if (p_name == "color") { + Color color = p_value; + + undo_redo->create_action(TTR("Multi Edit Marker Color"), UndoRedo::MERGE_ENDS); + + for (const StringName &marker_name : marker_names) { + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, animation->get_marker_color(marker_name)); + } + + undo_redo->add_do_method(marker_edit, "queue_redraw"); + undo_redo->add_undo_method(marker_edit, "queue_redraw"); + undo_redo->commit_action(); + + return true; + } + + return false; +} + +bool AnimationMultiMarkerKeyEdit::_get(const StringName &p_name, Variant &r_ret) const { + if (p_name == "color") { + r_ret = animation->get_marker_color(marker_names[0]); + return true; + } + + return false; +} + +void AnimationMultiMarkerKeyEdit::_get_property_list(List<PropertyInfo> *p_list) const { + if (animation.is_null()) { + return; + } + + p_list->push_back(PropertyInfo(Variant::COLOR, "color", PROPERTY_HINT_COLOR_NO_ALPHA)); +} + +// AnimationMarkerKeyEditEditorPlugin + +void AnimationMarkerKeyEditEditor::_time_edit_entered() { +} + +void AnimationMarkerKeyEditEditor::_time_edit_exited() { + real_t new_time = spinner->get_value(); + + if (use_fps) { + real_t fps = animation->get_step(); + if (fps > 0) { + fps = 1.0 / fps; + } + new_time /= fps; + } + + real_t prev_time = animation->get_marker_time(marker_name); + + if (Math::is_equal_approx(new_time, prev_time)) { + return; // No change. + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Change Marker Time"), UndoRedo::MERGE_ENDS); + + Color color = animation->get_marker_color(marker_name); + undo_redo->add_do_method(animation.ptr(), "add_marker", marker_name, new_time); + undo_redo->add_do_method(animation.ptr(), "set_marker_color", marker_name, color); + undo_redo->add_undo_method(animation.ptr(), "remove_marker", marker_name); + undo_redo->add_undo_method(animation.ptr(), "add_marker", marker_name, prev_time); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", marker_name, color); + StringName existing_marker = animation->get_marker_at_time(new_time); + if (existing_marker) { + undo_redo->add_undo_method(animation.ptr(), "add_marker", existing_marker, animation->get_marker_time(existing_marker)); + undo_redo->add_undo_method(animation.ptr(), "set_marker_color", existing_marker, animation->get_marker_color(existing_marker)); + } + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + AnimationTrackEditor *ate = ape->get_track_editor(); + if (ate) { + AnimationMarkerEdit *ame = ate->marker_edit; + undo_redo->add_do_method(ame, "queue_redraw"); + undo_redo->add_undo_method(ame, "queue_redraw"); + } + } + undo_redo->commit_action(); +} + +AnimationMarkerKeyEditEditor::AnimationMarkerKeyEditEditor(Ref<Animation> p_animation, const StringName &p_name, bool p_use_fps) { + if (p_animation.is_null()) { + return; + } + + animation = p_animation; + use_fps = p_use_fps; + marker_name = p_name; + + set_label("Time"); + + spinner = memnew(EditorSpinSlider); + spinner->set_focus_mode(Control::FOCUS_CLICK); + spinner->set_min(0); + spinner->set_allow_greater(true); + spinner->set_allow_lesser(true); + + float time = animation->get_marker_time(marker_name); + + if (use_fps) { + spinner->set_step(FPS_DECIMAL); + real_t fps = animation->get_step(); + if (fps > 0) { + fps = 1.0 / fps; + } + spinner->set_value(time * fps); + } else { + spinner->set_step(SECOND_DECIMAL); + spinner->set_value(time); + spinner->set_max(animation->get_length()); + } + + add_child(spinner); + + spinner->connect("grabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED); + spinner->connect("ungrabbed", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED); + spinner->connect("value_focus_entered", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_entered), CONNECT_DEFERRED); + spinner->connect("value_focus_exited", callable_mp(this, &AnimationMarkerKeyEditEditor::_time_edit_exited), CONNECT_DEFERRED); +} + +AnimationMarkerKeyEditEditor::~AnimationMarkerKeyEditEditor() { +} diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index 6b9140ddaa..0da474afd4 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -41,9 +41,11 @@ #include "scene/gui/tree.h" #include "scene/resources/animation.h" +class AnimationMarkerEdit; class AnimationTrackEditor; class AnimationTrackEdit; class CheckBox; +class ColorPickerButton; class EditorSpinSlider; class HSlider; class OptionButton; @@ -52,6 +54,7 @@ class SceneTreeDialog; class SpinBox; class TextureRect; class ViewPanner; +class EditorValidationPanel; class AnimationTrackKeyEdit : public Object { GDCLASS(AnimationTrackKeyEdit, Object); @@ -128,6 +131,58 @@ protected: void _get_property_list(List<PropertyInfo> *p_list) const; }; +class AnimationMarkerKeyEdit : public Object { + GDCLASS(AnimationMarkerKeyEdit, Object); + +public: + bool animation_read_only = false; + + Ref<Animation> animation; + StringName marker_name; + bool use_fps = false; + + AnimationMarkerEdit *marker_edit = nullptr; + + bool _hide_script_from_inspector() { return true; } + bool _hide_metadata_from_inspector() { return true; } + bool _dont_undo_redo() { return true; } + + bool _is_read_only() { return animation_read_only; } + + float get_time() const; + +protected: + static void _bind_methods(); + void _set_marker_name(const StringName &p_name); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; +}; + +class AnimationMultiMarkerKeyEdit : public Object { + GDCLASS(AnimationMultiMarkerKeyEdit, Object); + +public: + bool animation_read_only = false; + + Ref<Animation> animation; + Vector<StringName> marker_names; + + AnimationMarkerEdit *marker_edit = nullptr; + + bool _hide_script_from_inspector() { return true; } + bool _hide_metadata_from_inspector() { return true; } + bool _dont_undo_redo() { return true; } + + bool _is_read_only() { return animation_read_only; } + +protected: + static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; +}; + class AnimationTimelineEdit : public Range { GDCLASS(AnimationTimelineEdit, Range); @@ -218,6 +273,140 @@ public: AnimationTimelineEdit(); }; +class AnimationMarkerEdit : public Control { + GDCLASS(AnimationMarkerEdit, Control); + friend class AnimationTimelineEdit; + + enum { + MENU_KEY_INSERT, + MENU_KEY_RENAME, + MENU_KEY_DELETE, + MENU_KEY_TOGGLE_MARKER_NAMES, + }; + + AnimationTimelineEdit *timeline = nullptr; + Control *play_position = nullptr; // Separate control used to draw so updates for only position changed are much faster. + float play_position_pos = 0.0f; + + HashSet<StringName> selection; + + Ref<Animation> animation; + bool read_only = false; + + Ref<Texture2D> type_icon; + Ref<Texture2D> selected_icon; + + PopupMenu *menu = nullptr; + + bool hovered = false; + StringName hovering_marker; + + void _zoom_changed(); + + Ref<Texture2D> icon_cache; + + void _menu_selected(int p_index); + + void _play_position_draw(); + bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable); + bool _is_ui_pos_in_current_section(const Point2 &p_pos); + + float insert_at_pos = 0.0f; + bool moving_selection_attempt = false; + bool moving_selection_effective = false; + float moving_selection_offset = 0.0f; + float moving_selection_pivot = 0.0f; + float moving_selection_mouse_begin_x = 0.0f; + float moving_selection_mouse_begin_y = 0.0f; + StringName select_single_attempt; + bool moving_selection = false; + void _move_selection_begin(); + void _move_selection(float p_offset); + void _move_selection_commit(); + void _move_selection_cancel(); + + void _clear_selection_for_anim(const Ref<Animation> &p_anim); + void _select_key(const StringName &p_name, bool is_single = false); + void _deselect_key(const StringName &p_name); + + void _insert_marker(float p_ofs); + void _rename_marker(const StringName &p_name); + void _delete_selected_markers(); + + ConfirmationDialog *marker_insert_confirm = nullptr; + LineEdit *marker_insert_new_name = nullptr; + ColorPickerButton *marker_insert_color = nullptr; + AcceptDialog *marker_insert_error_dialog = nullptr; + float marker_insert_ofs = 0; + + ConfirmationDialog *marker_rename_confirm = nullptr; + LineEdit *marker_rename_new_name = nullptr; + StringName marker_rename_prev_name; + + AcceptDialog *marker_rename_error_dialog = nullptr; + + bool should_show_all_marker_names = false; + + ////////////// edit menu stuff + + void _marker_insert_confirmed(); + void _marker_insert_new_name_changed(const String &p_text); + void _marker_rename_confirmed(); + void _marker_rename_new_name_changed(const String &p_text); + + AnimationTrackEditor *editor = nullptr; + + HBoxContainer *_create_hbox_labeled_control(const String &p_text, Control *p_control) const; + + void _update_key_edit(); + void _clear_key_edit(); + + AnimationMarkerKeyEdit *key_edit = nullptr; + AnimationMultiMarkerKeyEdit *multi_key_edit = nullptr; + +protected: + static void _bind_methods(); + void _notification(int p_what); + + virtual void gui_input(const Ref<InputEvent> &p_event) override; + +public: + virtual String get_tooltip(const Point2 &p_pos) const override; + + virtual int get_key_height() const; + virtual Rect2 get_key_rect(float p_pixels_sec) const; + virtual bool is_key_selectable_by_distance() const; + virtual void draw_key(const StringName &p_name, float p_pixels_sec, int p_x, bool p_selected, int p_clip_left, int p_clip_right); + virtual void draw_bg(int p_clip_left, int p_clip_right); + virtual void draw_fg(int p_clip_left, int p_clip_right); + + Ref<Animation> get_animation() const; + AnimationTimelineEdit *get_timeline() const { return timeline; } + AnimationTrackEditor *get_editor() const { return editor; } + bool is_selection_active() const { return !selection.is_empty(); } + bool is_moving_selection() const { return moving_selection; } + float get_moving_selection_offset() const { return moving_selection_offset; } + void set_animation(const Ref<Animation> &p_animation, bool p_read_only); + virtual Size2 get_minimum_size() const override; + + void set_timeline(AnimationTimelineEdit *p_timeline); + void set_editor(AnimationTrackEditor *p_editor); + + void set_play_position(float p_pos); + void update_play_position(); + + void set_use_fps(bool p_use_fps); + + PackedStringArray get_selected_section() const; + bool is_marker_selected(const StringName &p_marker) const; + + // For use by AnimationTrackEditor. + void _clear_selection(bool p_update); + + AnimationMarkerEdit(); + ~AnimationMarkerEdit(); +}; + class AnimationTrackEdit : public Control { GDCLASS(AnimationTrackEdit, Control); friend class AnimationTimelineEdit; @@ -367,6 +556,7 @@ class AnimationTrackEditGroup : public Control { NodePath node; Node *root = nullptr; AnimationTimelineEdit *timeline = nullptr; + AnimationTrackEditor *editor = nullptr; void _zoom_changed(); @@ -380,6 +570,7 @@ public: virtual Size2 get_minimum_size() const override; void set_timeline(AnimationTimelineEdit *p_timeline); void set_root(Node *p_root); + void set_editor(AnimationTrackEditor *p_editor); AnimationTrackEditGroup(); }; @@ -388,6 +579,7 @@ class AnimationTrackEditor : public VBoxContainer { GDCLASS(AnimationTrackEditor, VBoxContainer); friend class AnimationTimelineEdit; friend class AnimationBezierTrackEdit; + friend class AnimationMarkerKeyEditEditor; Ref<Animation> animation; bool read_only = false; @@ -405,6 +597,7 @@ class AnimationTrackEditor : public VBoxContainer { Label *info_message = nullptr; AnimationTimelineEdit *timeline = nullptr; + AnimationMarkerEdit *marker_edit = nullptr; HSlider *zoom = nullptr; EditorSpinSlider *step = nullptr; TextureRect *zoom_icon = nullptr; @@ -743,6 +936,10 @@ public: float get_moving_selection_offset() const; float snap_time(float p_value, bool p_relative = false); bool is_grouping_tracks(); + PackedStringArray get_selected_section() const; + bool is_marker_selected(const StringName &p_marker) const; + bool is_marker_moving_selection() const; + float get_marker_moving_selection_offset() const; /** If `p_from_mouse_event` is `true`, handle Shift key presses for precise snapping. */ void goto_prev_step(bool p_from_mouse_event); @@ -781,4 +978,23 @@ public: ~AnimationTrackKeyEditEditor(); }; +// AnimationMarkerKeyEditEditorPlugin + +class AnimationMarkerKeyEditEditor : public EditorProperty { + GDCLASS(AnimationMarkerKeyEditEditor, EditorProperty); + + Ref<Animation> animation; + StringName marker_name; + bool use_fps = false; + + EditorSpinSlider *spinner = nullptr; + + void _time_edit_entered(); + void _time_edit_exited(); + +public: + AnimationMarkerKeyEditEditor(Ref<Animation> p_animation, const StringName &p_name, bool p_use_fps); + ~AnimationMarkerKeyEditEditor(); +}; + #endif // ANIMATION_TRACK_EDITOR_H diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index 4febb8bf04..f847d3be7b 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -966,7 +966,7 @@ void DebugAdapterProtocol::on_debug_stack_frame_var(const Array &p_data) { List<int> scope_ids = stackframe_list.find(frame)->value; ERR_FAIL_COND(scope_ids.size() != 3); - ERR_FAIL_INDEX(stack_var.type, 3); + ERR_FAIL_INDEX(stack_var.type, 4); int var_id = scope_ids.get(stack_var.type); DAP::Variable variable; diff --git a/editor/debugger/editor_debugger_inspector.cpp b/editor/debugger/editor_debugger_inspector.cpp index cb5e4375a6..e085e2e448 100644 --- a/editor/debugger/editor_debugger_inspector.cpp +++ b/editor/debugger/editor_debugger_inspector.cpp @@ -223,7 +223,7 @@ Object *EditorDebuggerInspector::get_object(ObjectID p_id) { return nullptr; } -void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { +void EditorDebuggerInspector::add_stack_variable(const Array &p_array, int p_offset) { DebuggerMarshalls::ScriptStackVariable var; var.deserialize(p_array); String n = var.name; @@ -248,6 +248,9 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { case 2: type = "Globals/"; break; + case 3: + type = "Evaluated/"; + break; default: type = "Unknown/"; } @@ -258,7 +261,15 @@ void EditorDebuggerInspector::add_stack_variable(const Array &p_array) { pinfo.hint = h; pinfo.hint_string = hs; - variables->prop_list.push_back(pinfo); + if ((p_offset == -1) || variables->prop_list.is_empty()) { + variables->prop_list.push_back(pinfo); + } else { + List<PropertyInfo>::Element *current = variables->prop_list.front(); + for (int i = 0; i < p_offset; i++) { + current = current->next(); + } + variables->prop_list.insert_before(current, pinfo); + } variables->prop_values[type + n] = v; variables->update(); edit(variables); diff --git a/editor/debugger/editor_debugger_inspector.h b/editor/debugger/editor_debugger_inspector.h index 73dd773750..fac9525943 100644 --- a/editor/debugger/editor_debugger_inspector.h +++ b/editor/debugger/editor_debugger_inspector.h @@ -90,7 +90,7 @@ public: // Stack Dump variables String get_stack_variable(const String &p_var); - void add_stack_variable(const Array &p_arr); + void add_stack_variable(const Array &p_arr, int p_offset = -1); void clear_stack_variables(); }; diff --git a/editor/debugger/editor_debugger_server.cpp b/editor/debugger/editor_debugger_server.cpp index c0efc6a1fc..9ec6058132 100644 --- a/editor/debugger/editor_debugger_server.cpp +++ b/editor/debugger/editor_debugger_server.cpp @@ -77,8 +77,8 @@ Error EditorDebuggerServerTCP::start(const String &p_uri) { // Optionally override if (!p_uri.is_empty() && p_uri != "tcp://") { - String scheme, path; - Error err = p_uri.parse_url(scheme, bind_host, bind_port, path); + String scheme, path, fragment; + Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment); ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } diff --git a/editor/debugger/editor_expression_evaluator.cpp b/editor/debugger/editor_expression_evaluator.cpp new file mode 100644 index 0000000000..e8b1e33d20 --- /dev/null +++ b/editor/debugger/editor_expression_evaluator.cpp @@ -0,0 +1,148 @@ +/**************************************************************************/ +/* editor_expression_evaluator.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_expression_evaluator.h" + +#include "editor/debugger/editor_debugger_inspector.h" +#include "editor/debugger/script_editor_debugger.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" + +void EditorExpressionEvaluator::on_start() { + expression_input->set_editable(false); + evaluate_btn->set_disabled(true); + + if (clear_on_run_checkbox->is_pressed()) { + inspector->clear_stack_variables(); + } +} + +void EditorExpressionEvaluator::set_editor_debugger(ScriptEditorDebugger *p_editor_debugger) { + editor_debugger = p_editor_debugger; +} + +void EditorExpressionEvaluator::add_value(const Array &p_array) { + inspector->add_stack_variable(p_array, 0); + inspector->set_v_scroll(0); + inspector->set_h_scroll(0); +} + +void EditorExpressionEvaluator::_evaluate() { + const String &expression = expression_input->get_text(); + if (expression.is_empty()) { + return; + } + + if (!editor_debugger->is_session_active()) { + return; + } + + Array expr_data; + expr_data.push_back(expression); + expr_data.push_back(editor_debugger->get_stack_script_frame()); + editor_debugger->send_message("evaluate", expr_data); + + expression_input->clear(); +} + +void EditorExpressionEvaluator::_clear() { + inspector->clear_stack_variables(); +} + +void EditorExpressionEvaluator::_remote_object_selected(ObjectID p_id) { + editor_debugger->emit_signal(SNAME("remote_object_requested"), p_id); +} + +void EditorExpressionEvaluator::_on_expression_input_changed(const String &p_expression) { + evaluate_btn->set_disabled(p_expression.is_empty()); +} + +void EditorExpressionEvaluator::_on_debugger_breaked(bool p_breaked, bool p_can_debug) { + expression_input->set_editable(p_breaked); + evaluate_btn->set_disabled(!p_breaked); +} + +void EditorExpressionEvaluator::_on_debugger_clear_execution(Ref<Script> p_stack_script) { + expression_input->set_editable(false); + evaluate_btn->set_disabled(true); +} + +void EditorExpressionEvaluator::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: { + EditorDebuggerNode::get_singleton()->connect("breaked", callable_mp(this, &EditorExpressionEvaluator::_on_debugger_breaked)); + EditorDebuggerNode::get_singleton()->connect("clear_execution", callable_mp(this, &EditorExpressionEvaluator::_on_debugger_clear_execution)); + } break; + } +} + +EditorExpressionEvaluator::EditorExpressionEvaluator() { + set_h_size_flags(SIZE_EXPAND_FILL); + + HBoxContainer *hb = memnew(HBoxContainer); + add_child(hb); + + expression_input = memnew(LineEdit); + expression_input->set_h_size_flags(Control::SIZE_EXPAND_FILL); + expression_input->set_placeholder(TTR("Expression to evaluate")); + expression_input->set_clear_button_enabled(true); + expression_input->connect("text_submitted", callable_mp(this, &EditorExpressionEvaluator::_evaluate).unbind(1)); + expression_input->connect(SceneStringName(text_changed), callable_mp(this, &EditorExpressionEvaluator::_on_expression_input_changed)); + hb->add_child(expression_input); + + clear_on_run_checkbox = memnew(CheckBox); + clear_on_run_checkbox->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + clear_on_run_checkbox->set_text(TTR("Clear on Run")); + clear_on_run_checkbox->set_pressed(true); + hb->add_child(clear_on_run_checkbox); + + evaluate_btn = memnew(Button); + evaluate_btn->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + evaluate_btn->set_text(TTR("Evaluate")); + evaluate_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorExpressionEvaluator::_evaluate)); + hb->add_child(evaluate_btn); + + clear_btn = memnew(Button); + clear_btn->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + clear_btn->set_text(TTR("Clear")); + clear_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorExpressionEvaluator::_clear)); + hb->add_child(clear_btn); + + inspector = memnew(EditorDebuggerInspector); + inspector->set_v_size_flags(SIZE_EXPAND_FILL); + inspector->set_property_name_style(EditorPropertyNameProcessor::STYLE_RAW); + inspector->set_read_only(true); + inspector->connect("object_selected", callable_mp(this, &EditorExpressionEvaluator::_remote_object_selected)); + inspector->set_use_filter(true); + add_child(inspector); + + expression_input->set_editable(false); + evaluate_btn->set_disabled(true); +} diff --git a/editor/editor_quick_open.h b/editor/debugger/editor_expression_evaluator.h index 815cc0c8fe..0548784695 100644 --- a/editor/editor_quick_open.h +++ b/editor/debugger/editor_expression_evaluator.h @@ -1,5 +1,5 @@ /**************************************************************************/ -/* editor_quick_open.h */ +/* editor_expression_evaluator.h */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,62 +28,50 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef EDITOR_QUICK_OPEN_H -#define EDITOR_QUICK_OPEN_H +#ifndef EDITOR_EXPRESSION_EVALUATOR_H +#define EDITOR_EXPRESSION_EVALUATOR_H -#include "core/templates/oa_hash_map.h" -#include "editor/editor_file_system.h" -#include "scene/gui/dialogs.h" -#include "scene/gui/tree.h" +#include "scene/gui/box_container.h" -class EditorQuickOpen : public ConfirmationDialog { - GDCLASS(EditorQuickOpen, ConfirmationDialog); +class Button; +class CheckBox; +class EditorDebuggerInspector; +class LineEdit; +class RemoteDebuggerPeer; +class ScriptEditorDebugger; - static Rect2i prev_rect; - static bool was_showed; +class EditorExpressionEvaluator : public VBoxContainer { + GDCLASS(EditorExpressionEvaluator, VBoxContainer) - LineEdit *search_box = nullptr; - Tree *search_options = nullptr; - String base_type; - bool allow_multi_select = false; +private: + Ref<RemoteDebuggerPeer> peer; - Vector<String> files; - OAHashMap<String, Ref<Texture2D>> icons; + LineEdit *expression_input = nullptr; + CheckBox *clear_on_run_checkbox = nullptr; + Button *evaluate_btn = nullptr; + Button *clear_btn = nullptr; - struct Entry { - String path; - float score = 0; - }; + EditorDebuggerInspector *inspector = nullptr; - struct EntryComparator { - _FORCE_INLINE_ bool operator()(const Entry &A, const Entry &B) const { - return A.score > B.score; - } - }; + void _evaluate(); + void _clear(); - void _update_search(); - void _build_search_cache(EditorFileSystemDirectory *p_efsd); - float _score_search_result(const PackedStringArray &p_search_tokens, const String &p_path); - - void _confirmed(); - virtual void cancel_pressed() override; - void _cleanup(); - - void _sbox_input(const Ref<InputEvent> &p_event); - void _text_changed(const String &p_newtext); + void _remote_object_selected(ObjectID p_id); + void _on_expression_input_changed(const String &p_expression); + void _on_debugger_breaked(bool p_breaked, bool p_can_debug); + void _on_debugger_clear_execution(Ref<Script> p_stack_script); protected: + ScriptEditorDebugger *editor_debugger = nullptr; + void _notification(int p_what); - static void _bind_methods(); public: - String get_base_type() const; - - String get_selected() const; - Vector<String> get_selected_files() const; + void on_start(); + void set_editor_debugger(ScriptEditorDebugger *p_editor_debugger); + void add_value(const Array &p_array); - void popup_dialog(const String &p_base, bool p_enable_multi = false, bool p_dontclear = false); - EditorQuickOpen(); + EditorExpressionEvaluator(); }; -#endif // EDITOR_QUICK_OPEN_H +#endif // EDITOR_EXPRESSION_EVALUATOR_H diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index b798bdf9c1..642244ebeb 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -37,6 +37,7 @@ #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" #include "editor/debugger/editor_visual_profiler.h" @@ -811,6 +812,8 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, uint64_t p_thread if (EditorFileSystem::get_singleton()) { EditorFileSystem::get_singleton()->update_file(p_data[0]); } + } else if (p_msg == "evaluation_return") { + expression_evaluator->add_value(p_data); } else { int colon_index = p_msg.find_char(':'); ERR_FAIL_COND_MSG(colon_index < 1, "Invalid message received"); @@ -854,8 +857,9 @@ void ScriptEditorDebugger::_notification(int p_what) { error_tree->connect(SceneStringName(item_selected), callable_mp(this, &ScriptEditorDebugger::_error_selected)); error_tree->connect("item_activated", callable_mp(this, &ScriptEditorDebugger::_error_activated)); breakpoints_tree->connect("item_activated", callable_mp(this, &ScriptEditorDebugger::_breakpoint_tree_clicked)); - [[fallthrough]]; - } + connect("started", callable_mp(expression_evaluator, &EditorExpressionEvaluator::on_start)); + } break; + case NOTIFICATION_THEME_CHANGED: { tabs->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("DebuggerPanel"), EditorStringName(EditorStyles))); @@ -2010,6 +2014,13 @@ ScriptEditorDebugger::ScriptEditorDebugger() { add_child(file_dialog); } + { // Expression evaluator + expression_evaluator = memnew(EditorExpressionEvaluator); + expression_evaluator->set_name(TTR("Evaluator")); + expression_evaluator->set_editor_debugger(this); + tabs->add_child(expression_evaluator); + } + { //profiler profiler = memnew(EditorProfiler); profiler->set_name(TTR("Profiler")); diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h index bd0b0c7d85..26106849f9 100644 --- a/editor/debugger/script_editor_debugger.h +++ b/editor/debugger/script_editor_debugger.h @@ -56,6 +56,7 @@ class SceneDebuggerTree; class EditorDebuggerPlugin; class DebugAdapterProtocol; class DebugAdapterParser; +class EditorExpressionEvaluator; class ScriptEditorDebugger : public MarginContainer { GDCLASS(ScriptEditorDebugger, MarginContainer); @@ -152,6 +153,7 @@ private: EditorProfiler *profiler = nullptr; EditorVisualProfiler *visual_profiler = nullptr; EditorPerformanceProfiler *performance_profiler = nullptr; + EditorExpressionEvaluator *expression_evaluator = nullptr; OS::ProcessID remote_pid = 0; bool move_to_foreground = true; diff --git a/editor/editor_about.cpp b/editor/editor_about.cpp index dc943fc783..34f432aa7e 100644 --- a/editor/editor_about.cpp +++ b/editor/editor_about.cpp @@ -33,16 +33,12 @@ #include "core/authors.gen.h" #include "core/donors.gen.h" #include "core/license.gen.h" -#include "core/os/time.h" -#include "core/version.h" #include "editor/editor_string_names.h" +#include "editor/gui/editor_version_button.h" #include "editor/themes/editor_scale.h" #include "scene/gui/item_list.h" #include "scene/resources/style_box.h" -// The metadata key used to store and retrieve the version text to copy to the clipboard. -const String EditorAbout::META_TEXT_TO_COPY = "text_to_copy"; - void EditorAbout::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { @@ -81,10 +77,6 @@ void EditorAbout::_license_tree_selected() { _tpl_text->set_text(selected->get_metadata(0)); } -void EditorAbout::_version_button_pressed() { - DisplayServer::get_singleton()->clipboard_set(version_btn->get_meta(META_TEXT_TO_COPY)); -} - void EditorAbout::_item_with_website_selected(int p_id, ItemList *p_il) { const String website = p_il->get_item_metadata(p_id); if (!website.is_empty()) { @@ -198,25 +190,7 @@ EditorAbout::EditorAbout() { Control *v_spacer = memnew(Control); version_info_vbc->add_child(v_spacer); - version_btn = memnew(LinkButton); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = " " + vformat("[%s]", hash.left(9)); - } - version_btn->set_text(VERSION_FULL_NAME + hash); - // Set the text to copy in metadata as it slightly differs from the button's text. - version_btn->set_meta(META_TEXT_TO_COPY, "v" VERSION_FULL_BUILD + hash); - version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); - String build_date; - if (VERSION_TIMESTAMP > 0) { - build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; - } else { - build_date = TTR("(unknown)"); - } - version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version number."), build_date)); - - version_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorAbout::_version_button_pressed)); - version_info_vbc->add_child(version_btn); + version_info_vbc->add_child(memnew(EditorVersionButton(EditorVersionButton::FORMAT_WITH_NAME_AND_BUILD))); Label *about_text = memnew(Label); about_text->set_v_size_flags(Control::SIZE_SHRINK_CENTER); diff --git a/editor/editor_about.h b/editor/editor_about.h index fc3d6cedce..6f33d502d7 100644 --- a/editor/editor_about.h +++ b/editor/editor_about.h @@ -33,7 +33,6 @@ #include "scene/gui/dialogs.h" #include "scene/gui/item_list.h" -#include "scene/gui/link_button.h" #include "scene/gui/rich_text_label.h" #include "scene/gui/scroll_container.h" #include "scene/gui/separator.h" @@ -49,16 +48,12 @@ class EditorAbout : public AcceptDialog { GDCLASS(EditorAbout, AcceptDialog); - static const String META_TEXT_TO_COPY; - private: void _license_tree_selected(); - void _version_button_pressed(); void _item_with_website_selected(int p_id, ItemList *p_il); void _item_list_resized(ItemList *p_il); ScrollContainer *_populate_list(const String &p_name, const List<String> &p_sections, const char *const *const p_src[], int p_single_column_flags = 0, bool p_allow_website = false); - LinkButton *version_btn = nullptr; Tree *_tpl_tree = nullptr; RichTextLabel *license_text_label = nullptr; RichTextLabel *_tpl_text = nullptr; diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index a215662f16..21f67772ea 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -2763,8 +2763,9 @@ void EditorInspector::update_tree() { // TODO: Can be useful to store more context for the focusable, such as the caret position in LineEdit. StringName current_selected = property_selected; int current_focusable = -1; - // Temporarily disable focus following to avoid jumping while the inspector is updating. - set_follow_focus(false); + + // Temporarily disable focus following on the root inspector to avoid jumping while the inspector is updating. + get_root_inspector()->set_follow_focus(false); if (property_focusable != -1) { // Check that focusable is actually focusable. @@ -2792,6 +2793,7 @@ void EditorInspector::update_tree() { _clear(!object); if (!object) { + get_root_inspector()->set_follow_focus(true); return; } @@ -3529,7 +3531,8 @@ void EditorInspector::update_tree() { // Updating inspector might invalidate some editing owners. EditorNode::get_singleton()->hide_unused_editors(); } - set_follow_focus(true); + + get_root_inspector()->set_follow_focus(true); } void EditorInspector::update_property(const String &p_prop) { @@ -3774,11 +3777,10 @@ void EditorInspector::set_use_wide_editors(bool p_enable) { wide_editors = p_enable; } -void EditorInspector::set_sub_inspector(bool p_enable) { - sub_inspector = p_enable; - if (!is_inside_tree()) { - return; - } +void EditorInspector::set_root_inspector(EditorInspector *p_root_inspector) { + root_inspector = p_root_inspector; + // Only the root inspector should follow focus. + set_follow_focus(false); } void EditorInspector::set_use_deletable_properties(bool p_enabled) { @@ -4096,13 +4098,13 @@ void EditorInspector::_notification(int p_what) { EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &EditorInspector::_feature_profile_changed)); set_process(is_visible_in_tree()); add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree"))); - if (!sub_inspector) { + if (!is_sub_inspector()) { get_tree()->connect("node_removed", callable_mp(this, &EditorInspector::_node_removed)); } } break; case NOTIFICATION_PREDELETE: { - if (!sub_inspector && is_inside_tree()) { + if (!is_sub_inspector() && is_inside_tree()) { get_tree()->disconnect("node_removed", callable_mp(this, &EditorInspector::_node_removed)); } edit(nullptr); @@ -4161,7 +4163,7 @@ void EditorInspector::_notification(int p_what) { case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { bool needs_update = false; - if (EditorThemeManager::is_generated_theme_outdated() && !sub_inspector) { + if (!is_sub_inspector() && EditorThemeManager::is_generated_theme_outdated()) { add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree"))); } diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index 14b6ff0907..0309213b76 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -486,6 +486,7 @@ class EditorInspector : public ScrollContainer { static Ref<EditorInspectorPlugin> inspector_plugins[MAX_PLUGINS]; static int inspector_plugin_count; + EditorInspector *root_inspector = nullptr; VBoxContainer *main_vbox = nullptr; // Map used to cache the instantiated editors. @@ -514,7 +515,6 @@ class EditorInspector : public ScrollContainer { bool update_all_pending = false; bool read_only = false; bool keying = false; - bool sub_inspector = false; bool wide_editors = false; bool deletable_properties = false; @@ -644,8 +644,9 @@ public: String get_object_class() const; void set_use_wide_editors(bool p_enable); - void set_sub_inspector(bool p_enable); - bool is_sub_inspector() const { return sub_inspector; } + void set_root_inspector(EditorInspector *p_root_inspector); + EditorInspector *get_root_inspector() { return is_sub_inspector() ? root_inspector : this; } + bool is_sub_inspector() const { return root_inspector != nullptr; } void set_use_deletable_properties(bool p_enabled); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 665255b9b2..2853ebc499 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -94,7 +94,7 @@ #include "editor/editor_paths.h" #include "editor/editor_properties.h" #include "editor/editor_property_name_processor.h" -#include "editor/editor_quick_open.h" +#include "editor/editor_resource_picker.h" #include "editor/editor_resource_preview.h" #include "editor/editor_run.h" #include "editor/editor_run_native.h" @@ -109,6 +109,7 @@ #include "editor/filesystem_dock.h" #include "editor/gui/editor_bottom_panel.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/gui/editor_quick_open_dialog.h" #include "editor/gui/editor_run_bar.h" #include "editor/gui/editor_scene_tabs.h" #include "editor/gui/editor_title_bar.h" @@ -2669,19 +2670,13 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) { } break; case FILE_QUICK_OPEN: { - quick_open->popup_dialog("Resource", true); - quick_open->set_title(TTR("Quick Open...")); - + quick_open_dialog->popup_dialog({ "Resource" }, callable_mp(this, &EditorNode::_quick_opened)); } break; case FILE_QUICK_OPEN_SCENE: { - quick_open->popup_dialog("PackedScene", true); - quick_open->set_title(TTR("Quick Open Scene...")); - + quick_open_dialog->popup_dialog({ "PackedScene" }, callable_mp(this, &EditorNode::_quick_opened)); } break; case FILE_QUICK_OPEN_SCRIPT: { - quick_open->popup_dialog("Script", true); - quick_open->set_title(TTR("Quick Open Script...")); - + quick_open_dialog->popup_dialog({ "Script" }, callable_mp(this, &EditorNode::_quick_opened)); } break; case FILE_OPEN_PREV: { if (previous_scenes.is_empty()) { @@ -4599,17 +4594,11 @@ void EditorNode::_update_recent_scenes() { recent_scenes->reset_size(); } -void EditorNode::_quick_opened() { - Vector<String> files = quick_open->get_selected_files(); - - bool open_scene_dialog = quick_open->get_base_type() == "PackedScene"; - for (int i = 0; i < files.size(); i++) { - const String &res_path = files[i]; - if (open_scene_dialog || ClassDB::is_parent_class(ResourceLoader::get_resource_type(res_path), "PackedScene")) { - open_request(res_path); - } else { - load_resource(res_path); - } +void EditorNode::_quick_opened(const String &p_file_path) { + if (ClassDB::is_parent_class(ResourceLoader::get_resource_type(p_file_path), "PackedScene")) { + open_request(p_file_path); + } else { + load_resource(p_file_path); } } @@ -7722,6 +7711,7 @@ EditorNode::EditorNode() { add_editor_plugin(memnew(AnimationPlayerEditorPlugin)); add_editor_plugin(memnew(AnimationTrackKeyEditEditorPlugin)); + add_editor_plugin(memnew(AnimationMarkerKeyEditEditorPlugin)); add_editor_plugin(memnew(CanvasItemEditorPlugin)); add_editor_plugin(memnew(Node3DEditorPlugin)); add_editor_plugin(memnew(ScriptEditorPlugin)); @@ -7847,9 +7837,8 @@ EditorNode::EditorNode() { open_imported->connect("custom_action", callable_mp(this, &EditorNode::_inherit_imported)); gui_base->add_child(open_imported); - quick_open = memnew(EditorQuickOpen); - gui_base->add_child(quick_open); - quick_open->connect("quick_open", callable_mp(this, &EditorNode::_quick_opened)); + quick_open_dialog = memnew(EditorQuickOpenDialog); + gui_base->add_child(quick_open_dialog); _update_recent_scenes(); diff --git a/editor/editor_node.h b/editor/editor_node.h index 36332e3d78..55caed4bb4 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -81,7 +81,6 @@ class EditorLog; class EditorMainScreen; class EditorNativeShaderSourceVisualizer; class EditorPluginList; -class EditorQuickOpen; class EditorResourcePreview; class EditorResourceConversionPlugin; class EditorRunBar; @@ -90,6 +89,7 @@ class EditorSelectionHistory; class EditorSettingsDialog; class EditorTitleBar; class ExportTemplateManager; +class EditorQuickOpenDialog; class FBXImporterManager; class FileSystemDock; class HistoryDock; @@ -257,13 +257,13 @@ private: EditorSelectionHistory editor_history; EditorCommandPalette *command_palette = nullptr; + EditorQuickOpenDialog *quick_open_dialog = nullptr; EditorExport *editor_export = nullptr; EditorLog *log = nullptr; EditorNativeShaderSourceVisualizer *native_shader_source_visualizer = nullptr; EditorPluginList *editor_plugins_force_input_forwarding = nullptr; EditorPluginList *editor_plugins_force_over = nullptr; EditorPluginList *editor_plugins_over = nullptr; - EditorQuickOpen *quick_open = nullptr; EditorResourcePreview *resource_preview = nullptr; EditorSelection *editor_selection = nullptr; EditorSettingsDialog *editor_settings_dialog = nullptr; @@ -572,7 +572,7 @@ private: void _inherit_request(String p_file); void _instantiate_request(const Vector<String> &p_files); - void _quick_opened(); + void _quick_opened(const String &p_file_path); void _open_command_palette(); void _project_run_started(); @@ -913,6 +913,8 @@ public: Dictionary drag_resource(const Ref<Resource> &p_res, Control *p_from); Dictionary drag_files_and_dirs(const Vector<String> &p_paths, Control *p_from); + EditorQuickOpenDialog *get_quick_open_dialog() { return quick_open_dialog; } + void add_tool_menu_item(const String &p_name, const Callable &p_callback); void add_tool_submenu_item(const String &p_name, PopupMenu *p_submenu); void remove_tool_menu_item(const String &p_name); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 0fb57ce40e..897e7835fd 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -3246,7 +3246,10 @@ void EditorPropertyResource::update_property() { sub_inspector->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED); sub_inspector->set_use_doc_hints(true); - sub_inspector->set_sub_inspector(true); + EditorInspector *parent_inspector = get_parent_inspector(); + ERR_FAIL_NULL(parent_inspector); + sub_inspector->set_root_inspector(parent_inspector->get_root_inspector()); + sub_inspector->set_property_name_style(InspectorDock::get_singleton()->get_property_name_style()); sub_inspector->connect("property_keyed", callable_mp(this, &EditorPropertyResource::_sub_inspector_property_keyed)); diff --git a/editor/editor_quick_open.cpp b/editor/editor_quick_open.cpp deleted file mode 100644 index bd30fc28d1..0000000000 --- a/editor/editor_quick_open.cpp +++ /dev/null @@ -1,308 +0,0 @@ -/**************************************************************************/ -/* editor_quick_open.cpp */ -/**************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/**************************************************************************/ -/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ -/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ -/* */ -/* Permission is hereby granted, free of charge, to any person obtaining */ -/* a copy of this software and associated documentation files (the */ -/* "Software"), to deal in the Software without restriction, including */ -/* without limitation the rights to use, copy, modify, merge, publish, */ -/* distribute, sublicense, and/or sell copies of the Software, and to */ -/* permit persons to whom the Software is furnished to do so, subject to */ -/* the following conditions: */ -/* */ -/* The above copyright notice and this permission notice shall be */ -/* included in all copies or substantial portions of the Software. */ -/* */ -/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ -/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ -/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ -/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ -/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ -/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ -/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/**************************************************************************/ - -#include "editor_quick_open.h" - -#include "core/os/keyboard.h" -#include "editor/editor_node.h" -#include "editor/editor_string_names.h" -#include "editor/themes/editor_scale.h" - -Rect2i EditorQuickOpen::prev_rect = Rect2i(); -bool EditorQuickOpen::was_showed = false; - -void EditorQuickOpen::popup_dialog(const String &p_base, bool p_enable_multi, bool p_dont_clear) { - base_type = p_base; - allow_multi_select = p_enable_multi; - search_options->set_select_mode(allow_multi_select ? Tree::SELECT_MULTI : Tree::SELECT_SINGLE); - - if (was_showed) { - popup(prev_rect); - } else { - popup_centered_clamped(Size2(600, 440) * EDSCALE, 0.8f); - } - - EditorFileSystemDirectory *efsd = EditorFileSystem::get_singleton()->get_filesystem(); - _build_search_cache(efsd); - - if (p_dont_clear) { - search_box->select_all(); - _update_search(); - } else { - search_box->clear(); // This will emit text_changed. - } - search_box->grab_focus(); -} - -void EditorQuickOpen::_build_search_cache(EditorFileSystemDirectory *p_efsd) { - for (int i = 0; i < p_efsd->get_subdir_count(); i++) { - _build_search_cache(p_efsd->get_subdir(i)); - } - - Vector<String> base_types = base_type.split(","); - for (int i = 0; i < p_efsd->get_file_count(); i++) { - String file = p_efsd->get_file_path(i); - String engine_type = p_efsd->get_file_type(i); - String script_type = p_efsd->get_file_resource_script_class(i); - String actual_type = script_type.is_empty() ? engine_type : script_type; - - // Iterate all possible base types. - for (String &parent_type : base_types) { - if (ClassDB::is_parent_class(engine_type, parent_type) || EditorNode::get_editor_data().script_class_is_parent(script_type, parent_type)) { - files.push_back(file.substr(6, file.length())); - - // Store refs to used icons. - String ext = file.get_extension(); - if (!icons.has(ext)) { - icons.insert(ext, EditorNode::get_singleton()->get_class_icon(actual_type, "Object")); - } - - // Stop testing base types as soon as we got a match. - break; - } - } - } -} - -void EditorQuickOpen::_update_search() { - const PackedStringArray search_tokens = search_box->get_text().to_lower().replace("/", " ").split(" ", false); - const bool empty_search = search_tokens.is_empty(); - - // Filter possible candidates. - Vector<Entry> entries; - for (int i = 0; i < files.size(); i++) { - Entry r; - r.path = files[i]; - if (empty_search) { - entries.push_back(r); - } else { - r.score = _score_search_result(search_tokens, r.path.to_lower()); - if (r.score > 0) { - entries.push_back(r); - } - } - } - - // Display results - TreeItem *root = search_options->get_root(); - root->clear_children(); - - if (entries.size() > 0) { - if (!empty_search) { - SortArray<Entry, EntryComparator> sorter; - sorter.sort(entries.ptrw(), entries.size()); - } - - const int class_icon_size = search_options->get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor)); - const int entry_limit = MIN(entries.size(), 300); - for (int i = 0; i < entry_limit; i++) { - TreeItem *ti = search_options->create_item(root); - ti->set_text(0, entries[i].path); - ti->set_icon_max_width(0, class_icon_size); - ti->set_icon(0, *icons.lookup_ptr(entries[i].path.get_extension())); - } - - TreeItem *to_select = root->get_first_child(); - to_select->select(0); - to_select->set_as_cursor(0); - search_options->scroll_to_item(to_select); - - get_ok_button()->set_disabled(false); - } else { - search_options->deselect_all(); - - get_ok_button()->set_disabled(true); - } -} - -float EditorQuickOpen::_score_search_result(const PackedStringArray &p_search_tokens, const String &p_path) { - float score = 0.0f; - int prev_min_match_idx = -1; - - for (const String &s : p_search_tokens) { - int min_match_idx = p_path.find(s); - - if (min_match_idx == -1) { - return 0.0f; - } - - float token_score = s.length(); - - int max_match_idx = p_path.rfind(s); - - // Prioritize the actual file name over folder. - if (max_match_idx > p_path.rfind("/")) { - token_score *= 2.0f; - } - - // Prioritize matches at the front of the path token. - if (min_match_idx == 0 || p_path.contains("/" + s)) { - token_score += 1.0f; - } - - score += token_score; - - // Prioritize tokens which appear in order. - if (prev_min_match_idx != -1 && max_match_idx > prev_min_match_idx) { - score += 1.0f; - } - - prev_min_match_idx = min_match_idx; - } - - return score; -} - -void EditorQuickOpen::_confirmed() { - if (!search_options->get_selected()) { - return; - } - _cleanup(); - hide(); - emit_signal(SNAME("quick_open")); -} - -void EditorQuickOpen::cancel_pressed() { - _cleanup(); -} - -void EditorQuickOpen::_cleanup() { - files.clear(); - icons.clear(); -} - -void EditorQuickOpen::_text_changed(const String &p_newtext) { - _update_search(); -} - -void EditorQuickOpen::_sbox_input(const Ref<InputEvent> &p_event) { - // Redirect navigational key events to the tree. - Ref<InputEventKey> key = p_event; - if (key.is_valid()) { - if (key->is_action("ui_up", true) || key->is_action("ui_down", true) || key->is_action("ui_page_up") || key->is_action("ui_page_down")) { - search_options->gui_input(key); - search_box->accept_event(); - - if (allow_multi_select) { - TreeItem *root = search_options->get_root(); - if (!root->get_first_child()) { - return; - } - - TreeItem *current = search_options->get_selected(); - TreeItem *item = search_options->get_next_selected(root); - while (item) { - item->deselect(0); - item = search_options->get_next_selected(item); - } - - current->select(0); - current->set_as_cursor(0); - } - } - } -} - -String EditorQuickOpen::get_selected() const { - TreeItem *ti = search_options->get_selected(); - if (!ti) { - return String(); - } - - return "res://" + ti->get_text(0); -} - -Vector<String> EditorQuickOpen::get_selected_files() const { - Vector<String> selected_files; - - TreeItem *item = search_options->get_next_selected(search_options->get_root()); - while (item) { - selected_files.push_back("res://" + item->get_text(0)); - item = search_options->get_next_selected(item); - } - - return selected_files; -} - -String EditorQuickOpen::get_base_type() const { - return base_type; -} - -void EditorQuickOpen::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - connect(SceneStringName(confirmed), callable_mp(this, &EditorQuickOpen::_confirmed)); - - search_box->set_clear_button_enabled(true); - } break; - - case NOTIFICATION_VISIBILITY_CHANGED: { - if (!is_visible()) { - prev_rect = Rect2i(get_position(), get_size()); - was_showed = true; - } - } break; - - case NOTIFICATION_THEME_CHANGED: { - search_box->set_right_icon(get_editor_theme_icon(SNAME("Search"))); - } break; - - case NOTIFICATION_EXIT_TREE: { - disconnect(SceneStringName(confirmed), callable_mp(this, &EditorQuickOpen::_confirmed)); - } break; - } -} - -void EditorQuickOpen::_bind_methods() { - ADD_SIGNAL(MethodInfo("quick_open")); -} - -EditorQuickOpen::EditorQuickOpen() { - VBoxContainer *vbc = memnew(VBoxContainer); - add_child(vbc); - - search_box = memnew(LineEdit); - search_box->connect(SceneStringName(text_changed), callable_mp(this, &EditorQuickOpen::_text_changed)); - search_box->connect(SceneStringName(gui_input), callable_mp(this, &EditorQuickOpen::_sbox_input)); - vbc->add_margin_child(TTR("Search:"), search_box); - register_text_enter(search_box); - - search_options = memnew(Tree); - search_options->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); - search_options->connect("item_activated", callable_mp(this, &EditorQuickOpen::_confirmed)); - search_options->create_item(); - search_options->set_hide_root(true); - search_options->set_hide_folding(true); - search_options->add_theme_constant_override("draw_guides", 1); - vbc->add_margin_child(TTR("Matches:"), search_options, true); - - set_ok_button_text(TTR("Open")); - set_hide_on_ok(false); -} diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index a81db5fdaa..e4ae2a6202 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -33,12 +33,12 @@ #include "editor/audio_stream_preview.h" #include "editor/editor_help.h" #include "editor/editor_node.h" -#include "editor/editor_quick_open.h" #include "editor/editor_resource_preview.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/filesystem_dock.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/gui/editor_quick_open_dialog.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/scene_tree_dock.h" @@ -171,10 +171,6 @@ void EditorResourcePicker::_file_selected(const String &p_path) { _update_resource(); } -void EditorResourcePicker::_file_quick_selected() { - _file_selected(quick_open->get_selected()); -} - void EditorResourcePicker::_resource_saved(Object *p_resource) { if (edited_resource.is_valid() && p_resource == edited_resource.ptr()) { emit_signal(SNAME("resource_changed"), edited_resource); @@ -339,14 +335,14 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { } break; case OBJ_MENU_QUICKLOAD: { - if (!quick_open) { - quick_open = memnew(EditorQuickOpen); - add_child(quick_open); - quick_open->connect("quick_open", callable_mp(this, &EditorResourcePicker::_file_quick_selected)); + const Vector<String> &base_types_string = base_type.split(","); + + Vector<StringName> base_types; + for (const String &type : base_types_string) { + base_types.push_back(type); } - quick_open->popup_dialog(base_type); - quick_open->set_title(TTR("Resource")); + EditorNode::get_singleton()->get_quick_open_dialog()->popup_dialog(base_types, callable_mp(this, &EditorResourcePicker::_file_selected)); } break; case OBJ_MENU_INSPECT: { diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index 05e392da2c..c39d9af764 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -36,7 +36,6 @@ class Button; class ConfirmationDialog; class EditorFileDialog; -class EditorQuickOpen; class PopupMenu; class TextureRect; class Tree; @@ -59,7 +58,6 @@ class EditorResourcePicker : public HBoxContainer { TextureRect *preview_rect = nullptr; Button *edit_button = nullptr; EditorFileDialog *file_dialog = nullptr; - EditorQuickOpen *quick_open = nullptr; ConfirmationDialog *duplicate_resources_dialog = nullptr; Tree *duplicate_resources_tree = nullptr; @@ -88,7 +86,6 @@ class EditorResourcePicker : public HBoxContainer { void _update_resource_preview(const String &p_path, const Ref<Texture2D> &p_preview, const Ref<Texture2D> &p_small_preview, ObjectID p_obj); void _resource_selected(); - void _file_quick_selected(); void _file_selected(const String &p_path); void _resource_saved(Object *p_resource); diff --git a/editor/editor_resource_preview.cpp b/editor/editor_resource_preview.cpp index a50b2f3dcc..581a01a5ed 100644 --- a/editor/editor_resource_preview.cpp +++ b/editor/editor_resource_preview.cpp @@ -414,6 +414,24 @@ void EditorResourcePreview::_update_thumbnail_sizes() { } } +EditorResourcePreview::PreviewItem EditorResourcePreview::get_resource_preview_if_available(const String &p_path) { + PreviewItem item; + { + MutexLock lock(preview_mutex); + + HashMap<String, EditorResourcePreview::Item>::Iterator I = cache.find(p_path); + if (!I) { + return item; + } + + EditorResourcePreview::Item &cached_item = I->value; + item.preview = cached_item.preview; + item.small_preview = cached_item.small_preview; + } + preview_sem.post(); + return item; +} + void EditorResourcePreview::queue_edited_resource_preview(const Ref<Resource> &p_res, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) { ERR_FAIL_NULL(p_receiver); ERR_FAIL_COND(!p_res.is_valid()); diff --git a/editor/editor_resource_preview.h b/editor/editor_resource_preview.h index 57b6e4cedb..88cd753a58 100644 --- a/editor/editor_resource_preview.h +++ b/editor/editor_resource_preview.h @@ -128,12 +128,19 @@ protected: public: static EditorResourcePreview *get_singleton(); + struct PreviewItem { + Ref<Texture2D> preview; + Ref<Texture2D> small_preview; + }; + // p_receiver_func callback has signature (String p_path, Ref<Texture2D> p_preview, Ref<Texture2D> p_preview_small, Variant p_userdata) // p_preview will be null if there was an error void queue_resource_preview(const String &p_path, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata); void queue_edited_resource_preview(const Ref<Resource> &p_res, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata); const Dictionary get_preview_metadata(const String &p_path) const; + PreviewItem get_resource_preview_if_available(const String &p_path); + void add_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator); void remove_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator); void check_for_invalidation(const String &p_path); diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index c972a6ab27..ceaffb64c4 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -426,12 +426,17 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/display_scale", 0, display_scale_hint_string, PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING) EDITOR_SETTING_USAGE(Variant::FLOAT, PROPERTY_HINT_RANGE, "interface/editor/custom_display_scale", 1.0, "0.5,3,0.01", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING) - String ed_screen_hints = "Screen With Mouse Pointer:-4,Screen With Keyboard Focus:-3,Primary Screen:-2"; // Note: Main Window Screen:-1 is not used for the main window. + String ed_screen_hints = "Auto (Remembers last position):-5,Screen With Mouse Pointer:-4,Screen With Keyboard Focus:-3,Primary Screen:-2"; for (int i = 0; i < DisplayServer::get_singleton()->get_screen_count(); i++) { ed_screen_hints += ",Screen " + itos(i + 1) + ":" + itos(i); } - EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/editor_screen", -2, ed_screen_hints) - EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/project_manager_screen", -2, ed_screen_hints) + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/editor_screen", EditorSettings::InitialScreen::INITIAL_SCREEN_AUTO, ed_screen_hints) + + String project_manager_screen_hints = "Screen With Mouse Pointer:-4,Screen With Keyboard Focus:-3,Primary Screen:-2"; + for (int i = 0; i < DisplayServer::get_singleton()->get_screen_count(); i++) { + project_manager_screen_hints += ",Screen " + itos(i + 1) + ":" + itos(i); + } + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "interface/editor/project_manager_screen", EditorSettings::InitialScreen::INITIAL_SCREEN_PRIMARY, project_manager_screen_hints) { EngineUpdateLabel::UpdateMode default_update_mode = EngineUpdateLabel::UpdateMode::NEWEST_UNSTABLE; @@ -466,7 +471,6 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { _initial_set("interface/editor/separate_distraction_mode", false, true); _initial_set("interface/editor/automatically_open_screenshots", true, true); EDITOR_SETTING_USAGE(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/single_window_mode", false, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING) - _initial_set("interface/editor/remember_window_size_and_position", true, true); _initial_set("interface/editor/mouse_extra_buttons_navigate_history", true); _initial_set("interface/editor/save_each_scene_on_quit", true, true); // Regression EDITOR_SETTING_BASIC(Variant::BOOL, PROPERTY_HINT_NONE, "interface/editor/save_on_focus_loss", false, "") @@ -598,6 +602,10 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "filesystem/file_dialog/display_mode", 0, "Thumbnails,List") EDITOR_SETTING(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/file_dialog/thumbnail_size", 64, "32,128,16") + // Quick Open dialog + _initial_set("filesystem/quick_open_dialog/include_addons", false); + EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "filesystem/quick_open_dialog/default_display_mode", 0, "Adaptive,Last Used") + // Import (for glft module) EDITOR_SETTING_USAGE(Variant::STRING, PROPERTY_HINT_GLOBAL_FILE, "filesystem/import/blender/blender_path", "", "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED | PROPERTY_USAGE_EDITOR_BASIC_SETTING) EDITOR_SETTING_USAGE(Variant::INT, PROPERTY_HINT_RANGE, "filesystem/import/blender/rpc_port", 6011, "0,65535,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED) diff --git a/editor/editor_settings.h b/editor/editor_settings.h index e850406839..d1ccedfe6c 100644 --- a/editor/editor_settings.h +++ b/editor/editor_settings.h @@ -62,6 +62,13 @@ public: NETWORK_ONLINE, }; + enum InitialScreen { + INITIAL_SCREEN_AUTO = -5, // Remembers last screen position. + INITIAL_SCREEN_WITH_MOUSE_FOCUS = -4, + INITIAL_SCREEN_WITH_KEYBOARD_FOCUS = -3, + INITIAL_SCREEN_PRIMARY = -2, + }; + private: struct VariantContainer { int order = 0; diff --git a/editor/export/editor_export_plugin.cpp b/editor/export/editor_export_plugin.cpp index 2f57010c2c..bd9c3503f7 100644 --- a/editor/export/editor_export_plugin.cpp +++ b/editor/export/editor_export_plugin.cpp @@ -295,6 +295,12 @@ bool EditorExportPlugin::_should_update_export_options(const Ref<EditorExportPla return ret; } +bool EditorExportPlugin::_get_export_option_visibility(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const { + bool ret = true; + GDVIRTUAL_CALL(_get_export_option_visibility, p_export_platform, p_option_name, ret); + return ret; +} + String EditorExportPlugin::_get_export_option_warning(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const { String ret; GDVIRTUAL_CALL(_get_export_option_warning, p_export_platform, p_option_name, ret); @@ -354,6 +360,7 @@ void EditorExportPlugin::_bind_methods() { GDVIRTUAL_BIND(_get_export_options, "platform"); GDVIRTUAL_BIND(_get_export_options_overrides, "platform"); GDVIRTUAL_BIND(_should_update_export_options, "platform"); + GDVIRTUAL_BIND(_get_export_option_visibility, "platform", "option"); GDVIRTUAL_BIND(_get_export_option_warning, "platform", "option"); GDVIRTUAL_BIND(_get_export_features, "platform", "debug"); diff --git a/editor/export/editor_export_plugin.h b/editor/export/editor_export_plugin.h index 8c744b3108..79d3d0954d 100644 --- a/editor/export/editor_export_plugin.h +++ b/editor/export/editor_export_plugin.h @@ -132,6 +132,7 @@ protected: GDVIRTUAL1RC(TypedArray<Dictionary>, _get_export_options, const Ref<EditorExportPlatform> &); GDVIRTUAL1RC(Dictionary, _get_export_options_overrides, const Ref<EditorExportPlatform> &); GDVIRTUAL1RC(bool, _should_update_export_options, const Ref<EditorExportPlatform> &); + GDVIRTUAL2RC(bool, _get_export_option_visibility, const Ref<EditorExportPlatform> &, String); GDVIRTUAL2RC(String, _get_export_option_warning, const Ref<EditorExportPlatform> &, String); GDVIRTUAL0RC_REQUIRED(String, _get_name) @@ -160,6 +161,7 @@ protected: virtual void _get_export_options(const Ref<EditorExportPlatform> &p_export_platform, List<EditorExportPlatform::ExportOption> *r_options) const; virtual Dictionary _get_export_options_overrides(const Ref<EditorExportPlatform> &p_export_platform) const; virtual bool _should_update_export_options(const Ref<EditorExportPlatform> &p_export_platform) const; + virtual bool _get_export_option_visibility(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const; virtual String _get_export_option_warning(const Ref<EditorExportPlatform> &p_export_platform, const String &p_option_name) const; public: diff --git a/editor/export/editor_export_preset.cpp b/editor/export/editor_export_preset.cpp index 1ca72348e2..da7059b777 100644 --- a/editor/export/editor_export_preset.cpp +++ b/editor/export/editor_export_preset.cpp @@ -136,8 +136,29 @@ String EditorExportPreset::_get_property_warning(const StringName &p_name) const void EditorExportPreset::_get_property_list(List<PropertyInfo> *p_list) const { for (const KeyValue<StringName, PropertyInfo> &E : properties) { - if (!value_overrides.has(E.key) && platform->get_export_option_visibility(this, E.key)) { - p_list->push_back(E.value); + if (!value_overrides.has(E.key)) { + bool property_visible = platform->get_export_option_visibility(this, E.key); + if (!property_visible) { + continue; + } + + // Get option visibility from editor export plugins. + Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); + for (int i = 0; i < export_plugins.size(); i++) { + if (!export_plugins[i]->supports_platform(platform)) { + continue; + } + + export_plugins.write[i]->set_export_preset(Ref<EditorExportPreset>(this)); + property_visible = export_plugins[i]->_get_export_option_visibility(platform, E.key); + if (!property_visible) { + break; + } + } + + if (property_visible) { + p_list->push_back(E.value); + } } } } diff --git a/editor/gui/editor_bottom_panel.cpp b/editor/gui/editor_bottom_panel.cpp index 4b2fd9cb2f..f6ba74fe95 100644 --- a/editor/gui/editor_bottom_panel.cpp +++ b/editor/gui/editor_bottom_panel.cpp @@ -30,8 +30,6 @@ #include "editor_bottom_panel.h" -#include "core/os/time.h" -#include "core/version.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_about.h" #include "editor/editor_command_palette.h" @@ -39,13 +37,10 @@ #include "editor/editor_string_names.h" #include "editor/engine_update_label.h" #include "editor/gui/editor_toaster.h" +#include "editor/gui/editor_version_button.h" #include "editor/themes/editor_scale.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" -#include "scene/gui/link_button.h" - -// The metadata key used to store and retrieve the version text to copy to the clipboard. -static const String META_TEXT_TO_COPY = "text_to_copy"; void EditorBottomPanel::_notification(int p_what) { switch (p_what) { @@ -110,10 +105,6 @@ void EditorBottomPanel::_expand_button_toggled(bool p_pressed) { EditorNode::get_top_split()->set_visible(!p_pressed); } -void EditorBottomPanel::_version_button_pressed() { - DisplayServer::get_singleton()->clipboard_set(version_btn->get_meta(META_TEXT_TO_COPY)); -} - bool EditorBottomPanel::_button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control) { if (!p_button->is_pressed()) { _switch_by_control(true, p_control); @@ -262,25 +253,9 @@ EditorBottomPanel::EditorBottomPanel() { editor_toaster = memnew(EditorToaster); bottom_hbox->add_child(editor_toaster); - version_btn = memnew(LinkButton); - version_btn->set_text(VERSION_FULL_CONFIG); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = " " + vformat("[%s]", hash.left(9)); - } - // Set the text to copy in metadata as it slightly differs from the button's text. - version_btn->set_meta(META_TEXT_TO_COPY, "v" VERSION_FULL_BUILD + hash); + EditorVersionButton *version_btn = memnew(EditorVersionButton(EditorVersionButton::FORMAT_BASIC)); // Fade out the version label to be less prominent, but still readable. version_btn->set_self_modulate(Color(1, 1, 1, 0.65)); - version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); - String build_date; - if (VERSION_TIMESTAMP > 0) { - build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; - } else { - build_date = TTR("(unknown)"); - } - version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date)); - version_btn->connect(SceneStringName(pressed), callable_mp(this, &EditorBottomPanel::_version_button_pressed)); version_btn->set_v_size_flags(Control::SIZE_SHRINK_CENTER); bottom_hbox->add_child(version_btn); diff --git a/editor/gui/editor_bottom_panel.h b/editor/gui/editor_bottom_panel.h index 95c767dae5..3d44b3750a 100644 --- a/editor/gui/editor_bottom_panel.h +++ b/editor/gui/editor_bottom_panel.h @@ -37,7 +37,6 @@ class Button; class ConfigFile; class EditorToaster; class HBoxContainer; -class LinkButton; class VBoxContainer; class EditorBottomPanel : public PanelContainer { @@ -55,14 +54,12 @@ class EditorBottomPanel : public PanelContainer { HBoxContainer *bottom_hbox = nullptr; HBoxContainer *button_hbox = nullptr; EditorToaster *editor_toaster = nullptr; - LinkButton *version_btn = nullptr; Button *expand_button = nullptr; Control *last_opened_control = nullptr; void _switch_by_control(bool p_visible, Control *p_control); void _switch_to_item(bool p_visible, int p_idx); void _expand_button_toggled(bool p_pressed); - void _version_button_pressed(); bool _button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control); diff --git a/editor/gui/editor_quick_open_dialog.cpp b/editor/gui/editor_quick_open_dialog.cpp new file mode 100644 index 0000000000..d24d37b302 --- /dev/null +++ b/editor/gui/editor_quick_open_dialog.cpp @@ -0,0 +1,961 @@ +/**************************************************************************/ +/* editor_quick_open_dialog.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_quick_open_dialog.h" + +#include "editor/editor_file_system.h" +#include "editor/editor_node.h" +#include "editor/editor_resource_preview.h" +#include "editor/editor_settings.h" +#include "editor/editor_string_names.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/center_container.h" +#include "scene/gui/check_button.h" +#include "scene/gui/flow_container.h" +#include "scene/gui/margin_container.h" +#include "scene/gui/panel_container.h" +#include "scene/gui/separator.h" +#include "scene/gui/texture_rect.h" +#include "scene/gui/tree.h" + +EditorQuickOpenDialog::EditorQuickOpenDialog() { + VBoxContainer *vbc = memnew(VBoxContainer); + vbc->add_theme_constant_override("separation", 0); + add_child(vbc); + + { + // Search bar + MarginContainer *mc = memnew(MarginContainer); + mc->add_theme_constant_override("margin_top", 6); + mc->add_theme_constant_override("margin_bottom", 6); + mc->add_theme_constant_override("margin_left", 1); + mc->add_theme_constant_override("margin_right", 1); + vbc->add_child(mc); + + search_box = memnew(LineEdit); + search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL); + search_box->set_placeholder(TTR("Search files...")); + search_box->set_clear_button_enabled(true); + mc->add_child(search_box); + } + + { + container = memnew(QuickOpenResultContainer); + container->connect("result_clicked", callable_mp(this, &EditorQuickOpenDialog::ok_pressed)); + vbc->add_child(container); + } + + search_box->connect(SceneStringName(text_changed), callable_mp(this, &EditorQuickOpenDialog::_search_box_text_changed)); + search_box->connect(SceneStringName(gui_input), callable_mp(container, &QuickOpenResultContainer::handle_search_box_input)); + register_text_enter(search_box); + get_ok_button()->hide(); +} + +String EditorQuickOpenDialog::get_dialog_title(const Vector<StringName> &p_base_types) { + if (p_base_types.size() > 1) { + return TTR("Select Resource"); + } + + if (p_base_types[0] == SNAME("PackedScene")) { + return TTR("Select Scene"); + } + + return TTR("Select") + " " + p_base_types[0]; +} + +void EditorQuickOpenDialog::popup_dialog(const Vector<StringName> &p_base_types, const Callable &p_item_selected_callback) { + ERR_FAIL_COND(p_base_types.is_empty()); + ERR_FAIL_COND(!p_item_selected_callback.is_valid()); + + item_selected_callback = p_item_selected_callback; + + container->init(p_base_types); + get_ok_button()->set_disabled(container->has_nothing_selected()); + + set_title(get_dialog_title(p_base_types)); + popup_centered_clamped(Size2(710, 650) * EDSCALE, 0.8f); + search_box->grab_focus(); +} + +void EditorQuickOpenDialog::ok_pressed() { + item_selected_callback.call(container->get_selected()); + + container->save_selected_item(); + container->cleanup(); + search_box->clear(); + hide(); +} + +void EditorQuickOpenDialog::cancel_pressed() { + container->cleanup(); + search_box->clear(); +} + +void EditorQuickOpenDialog::_search_box_text_changed(const String &p_query) { + container->update_results(p_query.to_lower()); + + get_ok_button()->set_disabled(container->has_nothing_selected()); +} + +//------------------------- Result Container + +QuickOpenResultContainer::QuickOpenResultContainer() { + set_h_size_flags(Control::SIZE_EXPAND_FILL); + set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_theme_constant_override("separation", 0); + + { + // Results section + panel_container = memnew(PanelContainer); + panel_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_child(panel_container); + + { + // No search results + no_results_container = memnew(CenterContainer); + no_results_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + no_results_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + panel_container->add_child(no_results_container); + + no_results_label = memnew(Label); + no_results_label->add_theme_font_size_override(SceneStringName(font_size), 24 * EDSCALE); + no_results_container->add_child(no_results_label); + no_results_container->hide(); + } + + { + // Search results + scroll_container = memnew(ScrollContainer); + scroll_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + scroll_container->set_v_size_flags(Control::SIZE_EXPAND_FILL); + scroll_container->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED); + scroll_container->hide(); + panel_container->add_child(scroll_container); + + list = memnew(VBoxContainer); + list->set_h_size_flags(Control::SIZE_EXPAND_FILL); + list->hide(); + scroll_container->add_child(list); + + grid = memnew(HFlowContainer); + grid->set_h_size_flags(Control::SIZE_EXPAND_FILL); + grid->set_v_size_flags(Control::SIZE_EXPAND_FILL); + grid->add_theme_constant_override("v_separation", 18); + grid->add_theme_constant_override("h_separation", 4); + grid->hide(); + scroll_container->add_child(grid); + } + } + + { + // Bottom bar + HBoxContainer *bottom_bar = memnew(HBoxContainer); + add_child(bottom_bar); + + file_details_path = memnew(Label); + file_details_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); + file_details_path->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER); + file_details_path->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); + bottom_bar->add_child(file_details_path); + + { + HBoxContainer *hbc = memnew(HBoxContainer); + hbc->add_theme_constant_override("separation", 3); + bottom_bar->add_child(hbc); + + include_addons_toggle = memnew(CheckButton); + include_addons_toggle->set_flat(true); + include_addons_toggle->set_focus_mode(Control::FOCUS_NONE); + include_addons_toggle->set_default_cursor_shape(CURSOR_POINTING_HAND); + include_addons_toggle->set_tooltip_text(TTR("Include files from addons")); + include_addons_toggle->connect(SceneStringName(toggled), callable_mp(this, &QuickOpenResultContainer::_toggle_include_addons)); + hbc->add_child(include_addons_toggle); + + VSeparator *vsep = memnew(VSeparator); + vsep->set_v_size_flags(Control::SIZE_SHRINK_CENTER); + vsep->set_custom_minimum_size(Size2i(0, 14 * EDSCALE)); + hbc->add_child(vsep); + + display_mode_toggle = memnew(Button); + display_mode_toggle->set_flat(true); + display_mode_toggle->set_focus_mode(Control::FOCUS_NONE); + display_mode_toggle->set_default_cursor_shape(CURSOR_POINTING_HAND); + display_mode_toggle->connect(SceneStringName(pressed), callable_mp(this, &QuickOpenResultContainer::_toggle_display_mode)); + hbc->add_child(display_mode_toggle); + } + } + + // Creating and deleting nodes while searching is slow, so we allocate + // a bunch of result nodes and fill in the content based on result ranking. + result_items.resize(TOTAL_ALLOCATED_RESULT_ITEMS); + for (int i = 0; i < TOTAL_ALLOCATED_RESULT_ITEMS; i++) { + QuickOpenResultItem *item = memnew(QuickOpenResultItem); + item->connect(SceneStringName(gui_input), callable_mp(this, &QuickOpenResultContainer::_item_input).bind(i)); + result_items.write[i] = item; + } +} + +QuickOpenResultContainer::~QuickOpenResultContainer() { + for (QuickOpenResultItem *E : result_items) { + memdelete(E); + } +} + +void QuickOpenResultContainer::init(const Vector<StringName> &p_base_types) { + base_types = p_base_types; + + const int display_mode_behavior = EDITOR_GET("filesystem/quick_open_dialog/default_display_mode"); + const bool adaptive_display_mode = (display_mode_behavior == 0); + + if (adaptive_display_mode) { + _set_display_mode(get_adaptive_display_mode(p_base_types)); + } + + const bool include_addons = EDITOR_GET("filesystem/quick_open_dialog/include_addons"); + include_addons_toggle->set_pressed_no_signal(include_addons); + + _create_initial_results(include_addons); +} + +void QuickOpenResultContainer::_create_initial_results(bool p_include_addons) { + file_type_icons.insert("__default_icon", get_editor_theme_icon(SNAME("Object"))); + _find_candidates_in_folder(EditorFileSystem::get_singleton()->get_filesystem(), p_include_addons); + max_total_results = MIN(candidates.size(), TOTAL_ALLOCATED_RESULT_ITEMS); + file_type_icons.clear(); + + update_results(query); +} + +void QuickOpenResultContainer::_find_candidates_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons) { + for (int i = 0; i < p_directory->get_subdir_count(); i++) { + if (p_include_addons || p_directory->get_name() != "addons") { + _find_candidates_in_folder(p_directory->get_subdir(i), p_include_addons); + } + } + + for (int i = 0; i < p_directory->get_file_count(); i++) { + String file_path = p_directory->get_file_path(i); + + const StringName engine_type = p_directory->get_file_type(i); + const StringName script_type = p_directory->get_file_resource_script_class(i); + + const bool is_engine_type = script_type == StringName(); + const StringName &actual_type = is_engine_type ? engine_type : script_type; + + for (const StringName &parent_type : base_types) { + bool is_valid = ClassDB::is_parent_class(engine_type, parent_type) || (!is_engine_type && EditorNode::get_editor_data().script_class_is_parent(script_type, parent_type)); + + if (is_valid) { + Candidate c; + c.file_name = file_path.get_file(); + c.file_directory = file_path.get_base_dir(); + + EditorResourcePreview::PreviewItem item = EditorResourcePreview::get_singleton()->get_resource_preview_if_available(file_path); + if (item.preview.is_valid()) { + c.thumbnail = item.preview; + } else if (file_type_icons.has(actual_type)) { + c.thumbnail = *file_type_icons.lookup_ptr(actual_type); + } else if (has_theme_icon(actual_type, EditorStringName(EditorIcons))) { + c.thumbnail = get_editor_theme_icon(actual_type); + file_type_icons.insert(actual_type, c.thumbnail); + } else { + c.thumbnail = *file_type_icons.lookup_ptr("__default_icon"); + } + + candidates.push_back(c); + + break; // Stop testing base types as soon as we get a match. + } + } + } +} + +void QuickOpenResultContainer::update_results(const String &p_query) { + query = p_query; + + int relevant_candidates = _sort_candidates(p_query); + _update_result_items(MIN(relevant_candidates, max_total_results), 0); +} + +int QuickOpenResultContainer::_sort_candidates(const String &p_query) { + if (p_query.is_empty()) { + return 0; + } + + const PackedStringArray search_tokens = p_query.to_lower().replace("/", " ").split(" ", false); + + if (search_tokens.is_empty()) { + return 0; + } + + // First, we assign a score to each candidate. + int num_relevant_candidates = 0; + for (Candidate &c : candidates) { + c.score = 0; + int prev_token_match_pos = -1; + + for (const String &token : search_tokens) { + const int file_pos = c.file_name.findn(token); + const int dir_pos = c.file_directory.findn(token); + + const bool file_match = file_pos > -1; + const bool dir_match = dir_pos > -1; + if (!file_match && !dir_match) { + c.score = -1.0f; + break; + } + + float token_score = file_match ? 0.6f : 0.1999f; + + // Add bias for shorter filenames/paths: they resemble the query more. + const String &matched_string = file_match ? c.file_name : c.file_directory; + int matched_string_token_pos = file_match ? file_pos : dir_pos; + token_score += 0.1f * (1.0f - ((float)matched_string_token_pos / (float)matched_string.length())); + + // Add bias if the match happened in the file name, not the extension. + if (file_match) { + int ext_pos = matched_string.rfind("."); + if (ext_pos == -1 || ext_pos > matched_string_token_pos) { + token_score += 0.1f; + } + } + + // Add bias if token is in order. + { + int candidate_string_token_pos = file_match ? (c.file_directory.length() + file_pos) : dir_pos; + + if (prev_token_match_pos != -1 && candidate_string_token_pos > prev_token_match_pos) { + token_score += 0.2f; + } + + prev_token_match_pos = candidate_string_token_pos; + } + + c.score += token_score; + } + + if (c.score > 0.0f) { + num_relevant_candidates++; + } + } + + // Now we will sort the candidates based on score, resolving ties by favoring: + // 1. Shorter file length. + // 2. Shorter directory length. + // 3. Lower alphabetic order. + struct CandidateComparator { + _FORCE_INLINE_ bool operator()(const Candidate &p_a, const Candidate &p_b) const { + if (!Math::is_equal_approx(p_a.score, p_b.score)) { + return p_a.score > p_b.score; + } + + if (p_a.file_name.length() != p_b.file_name.length()) { + return p_a.file_name.length() < p_b.file_name.length(); + } + + if (p_a.file_directory.length() != p_b.file_directory.length()) { + return p_a.file_directory.length() < p_b.file_directory.length(); + } + + return p_a.file_name < p_b.file_name; + } + }; + candidates.sort_custom<CandidateComparator>(); + + return num_relevant_candidates; +} + +void QuickOpenResultContainer::_update_result_items(int p_new_visible_results_count, int p_new_selection_index) { + List<Candidate> *type_history = nullptr; + + showing_history = false; + + if (query.is_empty()) { + if (candidates.size() <= SHOW_ALL_FILES_THRESHOLD) { + p_new_visible_results_count = candidates.size(); + } else { + p_new_visible_results_count = 0; + + if (base_types.size() == 1) { + type_history = selected_history.lookup_ptr(base_types[0]); + if (type_history) { + p_new_visible_results_count = type_history->size(); + showing_history = true; + } + } + } + } + + // Only need to update items that were not hidden in previous update. + int num_items_needing_updates = MAX(num_visible_results, p_new_visible_results_count); + num_visible_results = p_new_visible_results_count; + + for (int i = 0; i < num_items_needing_updates; i++) { + QuickOpenResultItem *item = result_items[i]; + + if (i < num_visible_results) { + if (type_history) { + const Candidate &c = type_history->get(i); + item->set_content(c.thumbnail, c.file_name, c.file_directory); + } else { + const Candidate &c = candidates[i]; + item->set_content(c.thumbnail, c.file_name, c.file_directory); + } + } else { + item->reset(); + } + }; + + const bool any_results = num_visible_results > 0; + _select_item(any_results ? p_new_selection_index : -1); + + scroll_container->set_visible(any_results); + no_results_container->set_visible(!any_results); + + if (!any_results) { + if (candidates.is_empty()) { + no_results_label->set_text(TTR("No files found for this type")); + } else if (query.is_empty()) { + no_results_label->set_text(TTR("Start searching to find files...")); + } else { + no_results_label->set_text(TTR("No results found")); + } + } +} + +void QuickOpenResultContainer::handle_search_box_input(const Ref<InputEvent> &p_ie) { + if (num_visible_results < 0) { + return; + } + + Ref<InputEventKey> key_event = p_ie; + if (key_event.is_valid() && key_event->is_pressed()) { + bool move_selection = false; + + switch (key_event->get_keycode()) { + case Key::UP: + case Key::DOWN: + case Key::PAGEUP: + case Key::PAGEDOWN: { + move_selection = true; + } break; + case Key::LEFT: + case Key::RIGHT: { + // Both grid and the search box use left/right keys. By default, grid will take it. + // It would be nice if we could check for ALT to give the event to the searchbox cursor. + // However, if you press ALT, the searchbox also denies the input. + move_selection = (content_display_mode == QuickOpenDisplayMode::GRID); + } break; + default: + break; // Let the event through so it will reach the search box. + } + + if (move_selection) { + _move_selection_index(key_event->get_keycode()); + queue_redraw(); + accept_event(); + } + } +} + +void QuickOpenResultContainer::_move_selection_index(Key p_key) { + const int max_index = num_visible_results - 1; + + int idx = selection_index; + if (content_display_mode == QuickOpenDisplayMode::LIST) { + if (p_key == Key::UP) { + idx = (idx == 0) ? max_index : (idx - 1); + } else if (p_key == Key::DOWN) { + idx = (idx == max_index) ? 0 : (idx + 1); + } else if (p_key == Key::PAGEUP) { + idx = (idx == 0) ? idx : MAX(idx - 10, 0); + } else if (p_key == Key::PAGEDOWN) { + idx = (idx == max_index) ? idx : MIN(idx + 10, max_index); + } + } else { + int column_count = grid->get_line_max_child_count(); + + if (p_key == Key::LEFT) { + idx = (idx == 0) ? max_index : (idx - 1); + } else if (p_key == Key::RIGHT) { + idx = (idx == max_index) ? 0 : (idx + 1); + } else if (p_key == Key::UP) { + idx = (idx == 0) ? max_index : MAX(idx - column_count, 0); + } else if (p_key == Key::DOWN) { + idx = (idx == max_index) ? 0 : MIN(idx + column_count, max_index); + } else if (p_key == Key::PAGEUP) { + idx = (idx == 0) ? idx : MAX(idx - (3 * column_count), 0); + } else if (p_key == Key::PAGEDOWN) { + idx = (idx == max_index) ? idx : MIN(idx + (3 * column_count), max_index); + } + } + + _select_item(idx); +} + +void QuickOpenResultContainer::_select_item(int p_index) { + if (!has_nothing_selected()) { + result_items[selection_index]->highlight_item(false); + } + + selection_index = p_index; + + if (has_nothing_selected()) { + file_details_path->set_text(""); + return; + } + + result_items[selection_index]->highlight_item(true); + file_details_path->set_text(get_selected() + (showing_history ? TTR(" (recently opened)") : "")); + + const QuickOpenResultItem *item = result_items[selection_index]; + + // Copied from Tree. + const int selected_position = item->get_position().y; + const int selected_size = item->get_size().y; + const int scroll_window_size = scroll_container->get_size().y; + const int scroll_position = scroll_container->get_v_scroll(); + + if (selected_position <= scroll_position) { + scroll_container->set_v_scroll(selected_position); + } else if (selected_position + selected_size > scroll_position + scroll_window_size) { + scroll_container->set_v_scroll(selected_position + selected_size - scroll_window_size); + } +} + +void QuickOpenResultContainer::_item_input(const Ref<InputEvent> &p_ev, int p_index) { + Ref<InputEventMouseButton> mb = p_ev; + + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { + _select_item(p_index); + emit_signal(SNAME("result_clicked")); + } +} + +void QuickOpenResultContainer::_toggle_include_addons(bool p_pressed) { + EditorSettings::get_singleton()->set("filesystem/quick_open_dialog/include_addons", p_pressed); + + cleanup(); + _create_initial_results(p_pressed); +} + +void QuickOpenResultContainer::_toggle_display_mode() { + QuickOpenDisplayMode new_display_mode = (content_display_mode == QuickOpenDisplayMode::LIST) ? QuickOpenDisplayMode::GRID : QuickOpenDisplayMode::LIST; + _set_display_mode(new_display_mode); +} + +void QuickOpenResultContainer::_set_display_mode(QuickOpenDisplayMode p_display_mode) { + content_display_mode = p_display_mode; + + const bool first_time = !list->is_visible() && !grid->is_visible(); + + if (!first_time) { + const bool show_list = (content_display_mode == QuickOpenDisplayMode::LIST); + if ((show_list && list->is_visible()) || (!show_list && grid->is_visible())) { + return; + } + } + + hide(); + + // Move result item nodes from one container to the other. + CanvasItem *prev_root; + CanvasItem *next_root; + if (content_display_mode == QuickOpenDisplayMode::LIST) { + prev_root = Object::cast_to<CanvasItem>(grid); + next_root = Object::cast_to<CanvasItem>(list); + } else { + prev_root = Object::cast_to<CanvasItem>(list); + next_root = Object::cast_to<CanvasItem>(grid); + } + + prev_root->hide(); + for (QuickOpenResultItem *item : result_items) { + item->set_display_mode(content_display_mode); + + if (!first_time) { + prev_root->remove_child(item); + } + + next_root->add_child(item); + } + next_root->show(); + show(); + + _update_result_items(num_visible_results, selection_index); + + if (content_display_mode == QuickOpenDisplayMode::LIST) { + display_mode_toggle->set_icon(get_editor_theme_icon(SNAME("FileThumbnail"))); + display_mode_toggle->set_tooltip_text(TTR("Grid view")); + } else { + display_mode_toggle->set_icon(get_editor_theme_icon(SNAME("FileList"))); + display_mode_toggle->set_tooltip_text(TTR("List view")); + } +} + +bool QuickOpenResultContainer::has_nothing_selected() const { + return selection_index < 0; +} + +String QuickOpenResultContainer::get_selected() const { + ERR_FAIL_COND_V_MSG(has_nothing_selected(), String(), "Tried to get selected file, but nothing was selected."); + + if (showing_history) { + const List<Candidate> *type_history = selected_history.lookup_ptr(base_types[0]); + + const Candidate &c = type_history->get(selection_index); + return c.file_directory.path_join(c.file_name); + } else { + const Candidate &c = candidates[selection_index]; + return c.file_directory.path_join(c.file_name); + } +} + +QuickOpenDisplayMode QuickOpenResultContainer::get_adaptive_display_mode(const Vector<StringName> &p_base_types) { + static const Vector<StringName> grid_preferred_types = { + "Font", + "Texture2D", + "Material", + "Mesh" + }; + + for (const StringName &type : grid_preferred_types) { + for (const StringName &base_type : p_base_types) { + if (base_type == type || ClassDB::is_parent_class(base_type, type)) + return QuickOpenDisplayMode::GRID; + } + } + + return QuickOpenDisplayMode::LIST; +} + +void QuickOpenResultContainer::save_selected_item() { + if (base_types.size() > 1) { + // Getting the type of the file and checking which base type it belongs to should be possible. + // However, for now these are not supported, and we don't record this. + return; + } + + if (showing_history) { + // Selecting from history, so already added. + return; + } + + const StringName &base_type = base_types[0]; + + List<Candidate> *type_history = selected_history.lookup_ptr(base_type); + if (!type_history) { + selected_history.insert(base_type, List<Candidate>()); + type_history = selected_history.lookup_ptr(base_type); + } else { + const Candidate &selected = candidates[selection_index]; + + for (const Candidate &candidate : *type_history) { + if (candidate.file_directory == selected.file_directory && candidate.file_name == selected.file_name) { + return; + } + } + + if (type_history->size() > 8) { + type_history->pop_back(); + } + } + + type_history->push_front(candidates[selection_index]); +} + +void QuickOpenResultContainer::cleanup() { + num_visible_results = 0; + candidates.clear(); + _select_item(-1); + + for (QuickOpenResultItem *item : result_items) { + item->reset(); + } +} + +void QuickOpenResultContainer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + Color text_color = get_theme_color("font_readonly_color", EditorStringName(Editor)); + file_details_path->add_theme_color_override(SceneStringName(font_color), text_color); + no_results_label->add_theme_color_override(SceneStringName(font_color), text_color); + + panel_container->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SNAME("QuickOpenBackgroundPanel"), EditorStringName(EditorStyles))); + + if (content_display_mode == QuickOpenDisplayMode::LIST) { + display_mode_toggle->set_icon(get_editor_theme_icon(SNAME("FileThumbnail"))); + } else { + display_mode_toggle->set_icon(get_editor_theme_icon(SNAME("FileList"))); + } + } break; + } +} + +void QuickOpenResultContainer::_bind_methods() { + ADD_SIGNAL(MethodInfo("result_clicked")); +} + +//------------------------- Result Item + +QuickOpenResultItem::QuickOpenResultItem() { + set_focus_mode(FocusMode::FOCUS_ALL); + _set_enabled(false); + set_default_cursor_shape(CURSOR_POINTING_HAND); + + list_item = memnew(QuickOpenResultListItem); + list_item->hide(); + add_child(list_item); + + grid_item = memnew(QuickOpenResultGridItem); + grid_item->hide(); + add_child(grid_item); +} + +void QuickOpenResultItem::set_display_mode(QuickOpenDisplayMode p_display_mode) { + if (p_display_mode == QuickOpenDisplayMode::LIST) { + grid_item->hide(); + list_item->show(); + } else { + list_item->hide(); + grid_item->show(); + } + + queue_redraw(); +} + +void QuickOpenResultItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file, const String &p_file_directory) { + _set_enabled(true); + + if (list_item->is_visible()) { + list_item->set_content(p_thumbnail, p_file, p_file_directory); + } else { + grid_item->set_content(p_thumbnail, p_file); + } +} + +void QuickOpenResultItem::reset() { + _set_enabled(false); + + is_hovering = false; + is_selected = false; + + if (list_item->is_visible()) { + list_item->reset(); + } else { + grid_item->reset(); + } +} + +void QuickOpenResultItem::highlight_item(bool p_enabled) { + is_selected = p_enabled; + + if (list_item->is_visible()) { + if (p_enabled) { + list_item->highlight_item(highlighted_font_color); + } else { + list_item->remove_highlight(); + } + } else { + if (p_enabled) { + grid_item->highlight_item(highlighted_font_color); + } else { + grid_item->remove_highlight(); + } + } + + queue_redraw(); +} + +void QuickOpenResultItem::_set_enabled(bool p_enabled) { + set_visible(p_enabled); + set_process(p_enabled); + set_process_input(p_enabled); +} + +void QuickOpenResultItem::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_MOUSE_ENTER: + case NOTIFICATION_MOUSE_EXIT: { + is_hovering = is_visible() && p_what == NOTIFICATION_MOUSE_ENTER; + queue_redraw(); + } break; + case NOTIFICATION_THEME_CHANGED: { + selected_stylebox = get_theme_stylebox("selected", "Tree"); + hovering_stylebox = get_theme_stylebox("hover", "Tree"); + highlighted_font_color = get_theme_color("font_focus_color", EditorStringName(Editor)); + } break; + case NOTIFICATION_DRAW: { + if (is_selected) { + draw_style_box(selected_stylebox, Rect2(Point2(), get_size())); + } else if (is_hovering) { + draw_style_box(hovering_stylebox, Rect2(Point2(), get_size())); + } + } break; + } +} + +//----------------- List item + +QuickOpenResultListItem::QuickOpenResultListItem() { + set_h_size_flags(Control::SIZE_EXPAND_FILL); + add_theme_constant_override("separation", 4 * EDSCALE); + + { + image_container = memnew(MarginContainer); + image_container->add_theme_constant_override("margin_top", 2 * EDSCALE); + image_container->add_theme_constant_override("margin_bottom", 2 * EDSCALE); + image_container->add_theme_constant_override("margin_left", CONTAINER_MARGIN * EDSCALE); + image_container->add_theme_constant_override("margin_right", 0); + add_child(image_container); + + thumbnail = memnew(TextureRect); + thumbnail->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + thumbnail->set_v_size_flags(Control::SIZE_SHRINK_CENTER); + thumbnail->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE); + thumbnail->set_stretch_mode(TextureRect::StretchMode::STRETCH_SCALE); + image_container->add_child(thumbnail); + } + + { + text_container = memnew(VBoxContainer); + text_container->add_theme_constant_override("separation", -6 * EDSCALE); + text_container->set_h_size_flags(Control::SIZE_EXPAND_FILL); + text_container->set_v_size_flags(Control::SIZE_FILL); + add_child(text_container); + + name = memnew(Label); + name->set_h_size_flags(Control::SIZE_EXPAND_FILL); + name->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); + name->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_LEFT); + text_container->add_child(name); + + path = memnew(Label); + path->set_h_size_flags(Control::SIZE_EXPAND_FILL); + path->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); + path->add_theme_font_size_override(SceneStringName(font_size), 12 * EDSCALE); + text_container->add_child(path); + } +} + +void QuickOpenResultListItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file, const String &p_file_directory) { + thumbnail->set_texture(p_thumbnail); + name->set_text(p_file); + path->set_text(p_file_directory); + + const int max_size = 32 * EDSCALE; + bool uses_icon = p_thumbnail->get_width() < max_size; + + if (uses_icon) { + thumbnail->set_custom_minimum_size(p_thumbnail->get_size()); + + int margin_needed = (max_size - p_thumbnail->get_width()) / 2; + image_container->add_theme_constant_override("margin_left", CONTAINER_MARGIN + margin_needed); + image_container->add_theme_constant_override("margin_right", margin_needed); + } else { + thumbnail->set_custom_minimum_size(Size2i(max_size, max_size)); + image_container->add_theme_constant_override("margin_left", CONTAINER_MARGIN); + image_container->add_theme_constant_override("margin_right", 0); + } +} + +void QuickOpenResultListItem::reset() { + name->set_text(""); + thumbnail->set_texture(nullptr); + path->set_text(""); +} + +void QuickOpenResultListItem::highlight_item(const Color &p_color) { + name->add_theme_color_override(SceneStringName(font_color), p_color); +} + +void QuickOpenResultListItem::remove_highlight() { + name->remove_theme_color_override(SceneStringName(font_color)); +} + +void QuickOpenResultListItem::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + path->add_theme_color_override(SceneStringName(font_color), get_theme_color("font_disabled_color", EditorStringName(Editor))); + } break; + } +} + +//--------------- Grid Item + +QuickOpenResultGridItem::QuickOpenResultGridItem() { + set_h_size_flags(Control::SIZE_FILL); + set_v_size_flags(Control::SIZE_EXPAND_FILL); + add_theme_constant_override("separation", -2 * EDSCALE); + + thumbnail = memnew(TextureRect); + thumbnail->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + thumbnail->set_v_size_flags(Control::SIZE_SHRINK_CENTER); + thumbnail->set_custom_minimum_size(Size2i(80 * EDSCALE, 64 * EDSCALE)); + add_child(thumbnail); + + name = memnew(Label); + name->set_h_size_flags(Control::SIZE_EXPAND_FILL); + name->set_text_overrun_behavior(TextServer::OVERRUN_TRIM_ELLIPSIS); + name->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER); + name->add_theme_font_size_override(SceneStringName(font_size), 13 * EDSCALE); + add_child(name); +} + +void QuickOpenResultGridItem::set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file) { + thumbnail->set_texture(p_thumbnail); + + const String &file_name = p_file.get_basename(); + name->set_text(file_name); + name->set_tooltip_text(file_name); + + bool uses_icon = p_thumbnail->get_width() < (32 * EDSCALE); + + if (uses_icon || p_thumbnail->get_height() <= thumbnail->get_custom_minimum_size().y) { + thumbnail->set_expand_mode(TextureRect::EXPAND_KEEP_SIZE); + thumbnail->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED); + } else { + thumbnail->set_expand_mode(TextureRect::EXPAND_FIT_WIDTH_PROPORTIONAL); + thumbnail->set_stretch_mode(TextureRect::StretchMode::STRETCH_SCALE); + } +} + +void QuickOpenResultGridItem::reset() { + name->set_text(""); + thumbnail->set_texture(nullptr); +} + +void QuickOpenResultGridItem::highlight_item(const Color &p_color) { + name->add_theme_color_override(SceneStringName(font_color), p_color); +} + +void QuickOpenResultGridItem::remove_highlight() { + name->remove_theme_color_override(SceneStringName(font_color)); +} diff --git a/editor/gui/editor_quick_open_dialog.h b/editor/gui/editor_quick_open_dialog.h new file mode 100644 index 0000000000..d2177375dc --- /dev/null +++ b/editor/gui/editor_quick_open_dialog.h @@ -0,0 +1,230 @@ +/**************************************************************************/ +/* editor_quick_open_dialog.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EDITOR_QUICK_OPEN_DIALOG_H +#define EDITOR_QUICK_OPEN_DIALOG_H + +#include "core/templates/oa_hash_map.h" +#include "scene/gui/dialogs.h" + +class Button; +class CenterContainer; +class CheckButton; +class EditorFileSystemDirectory; +class LineEdit; +class HFlowContainer; +class MarginContainer; +class PanelContainer; +class ScrollContainer; +class StringName; +class Texture2D; +class TextureRect; +class VBoxContainer; + +class QuickOpenResultItem; + +enum class QuickOpenDisplayMode { + GRID, + LIST, +}; + +class QuickOpenResultContainer : public VBoxContainer { + GDCLASS(QuickOpenResultContainer, VBoxContainer) + +public: + void init(const Vector<StringName> &p_base_types); + void handle_search_box_input(const Ref<InputEvent> &p_ie); + void update_results(const String &p_query); + + bool has_nothing_selected() const; + String get_selected() const; + + void save_selected_item(); + void cleanup(); + + QuickOpenResultContainer(); + ~QuickOpenResultContainer(); + +protected: + void _notification(int p_what); + +private: + static const int TOTAL_ALLOCATED_RESULT_ITEMS = 100; + static const int SHOW_ALL_FILES_THRESHOLD = 30; + + struct Candidate { + String file_name; + String file_directory; + + Ref<Texture2D> thumbnail; + float score = 0; + }; + + Vector<StringName> base_types; + Vector<Candidate> candidates; + + OAHashMap<StringName, List<Candidate>> selected_history; + + String query; + int selection_index = -1; + int num_visible_results = 0; + int max_total_results = 0; + bool showing_history = false; + + QuickOpenDisplayMode content_display_mode = QuickOpenDisplayMode::LIST; + Vector<QuickOpenResultItem *> result_items; + + ScrollContainer *scroll_container = nullptr; + VBoxContainer *list = nullptr; + HFlowContainer *grid = nullptr; + + PanelContainer *panel_container = nullptr; + CenterContainer *no_results_container = nullptr; + Label *no_results_label = nullptr; + + Label *file_details_path = nullptr; + Button *display_mode_toggle = nullptr; + CheckButton *include_addons_toggle = nullptr; + + OAHashMap<StringName, Ref<Texture2D>> file_type_icons; + + static QuickOpenDisplayMode get_adaptive_display_mode(const Vector<StringName> &p_base_types); + + void _create_initial_results(bool p_include_addons); + void _find_candidates_in_folder(EditorFileSystemDirectory *p_directory, bool p_include_addons); + + int _sort_candidates(const String &p_query); + void _update_result_items(int p_new_visible_results_count, int p_new_selection_index); + + void _move_selection_index(Key p_key); + void _select_item(int p_index); + + void _item_input(const Ref<InputEvent> &p_ev, int p_index); + + void _set_display_mode(QuickOpenDisplayMode p_display_mode); + void _toggle_display_mode(); + void _toggle_include_addons(bool p_pressed); + + static void _bind_methods(); +}; + +class QuickOpenResultGridItem : public VBoxContainer { + GDCLASS(QuickOpenResultGridItem, VBoxContainer) + +public: + QuickOpenResultGridItem(); + + void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name); + void reset(); + void highlight_item(const Color &p_color); + void remove_highlight(); + +private: + TextureRect *thumbnail = nullptr; + Label *name = nullptr; +}; + +class QuickOpenResultListItem : public HBoxContainer { + GDCLASS(QuickOpenResultListItem, HBoxContainer) + +public: + QuickOpenResultListItem(); + + void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name, const String &p_file_directory); + void reset(); + void highlight_item(const Color &p_color); + void remove_highlight(); + +protected: + void _notification(int p_what); + +private: + static const int CONTAINER_MARGIN = 8; + + MarginContainer *image_container = nullptr; + VBoxContainer *text_container = nullptr; + + TextureRect *thumbnail = nullptr; + Label *name = nullptr; + Label *path = nullptr; +}; + +class QuickOpenResultItem : public HBoxContainer { + GDCLASS(QuickOpenResultItem, HBoxContainer) + +public: + QuickOpenResultItem(); + + void set_content(const Ref<Texture2D> &p_thumbnail, const String &p_file_name, const String &p_file_directory); + void set_display_mode(QuickOpenDisplayMode p_display_mode); + void reset(); + + void highlight_item(bool p_enabled); + +protected: + void _notification(int p_what); + +private: + QuickOpenResultListItem *list_item = nullptr; + QuickOpenResultGridItem *grid_item = nullptr; + + Ref<StyleBox> selected_stylebox; + Ref<StyleBox> hovering_stylebox; + Color highlighted_font_color; + + bool is_hovering = false; + bool is_selected = false; + + void _set_enabled(bool p_enabled); +}; + +class EditorQuickOpenDialog : public AcceptDialog { + GDCLASS(EditorQuickOpenDialog, AcceptDialog); + +public: + void popup_dialog(const Vector<StringName> &p_base_types, const Callable &p_item_selected_callback); + EditorQuickOpenDialog(); + +protected: + virtual void cancel_pressed() override; + virtual void ok_pressed() override; + +private: + static String get_dialog_title(const Vector<StringName> &p_base_types); + + LineEdit *search_box = nullptr; + QuickOpenResultContainer *container = nullptr; + + Callable item_selected_callback; + + void _search_box_text_changed(const String &p_query); +}; + +#endif // EDITOR_QUICK_OPEN_DIALOG_H diff --git a/editor/gui/editor_run_bar.cpp b/editor/gui/editor_run_bar.cpp index 9050ee0cd4..908b1e6719 100644 --- a/editor/gui/editor_run_bar.cpp +++ b/editor/gui/editor_run_bar.cpp @@ -34,10 +34,10 @@ #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_command_palette.h" #include "editor/editor_node.h" -#include "editor/editor_quick_open.h" #include "editor/editor_run_native.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" +#include "editor/gui/editor_quick_open_dialog.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/panel_container.h" @@ -121,16 +121,15 @@ void EditorRunBar::_write_movie_toggled(bool p_enabled) { } } -void EditorRunBar::_quick_run_selected() { - play_custom_scene(quick_run->get_selected()); +void EditorRunBar::_quick_run_selected(const String &p_file_path) { + play_custom_scene(p_file_path); } void EditorRunBar::_play_custom_pressed() { if (editor_run.get_status() == EditorRun::STATUS_STOP || current_mode != RunMode::RUN_CUSTOM) { stop_playing(); - quick_run->popup_dialog("PackedScene", true); - quick_run->set_title(TTR("Quick Run Scene...")); + EditorNode::get_singleton()->get_quick_open_dialog()->popup_dialog({ "PackedScene" }, callable_mp(this, &EditorRunBar::_quick_run_selected)); play_custom_scene_button->set_pressed(false); } else { // Reload if already running a custom scene. @@ -446,8 +445,4 @@ EditorRunBar::EditorRunBar() { write_movie_button->set_focus_mode(Control::FOCUS_NONE); write_movie_button->set_tooltip_text(TTR("Enable Movie Maker mode.\nThe project will run at stable FPS and the visual and audio output will be recorded to a video file.")); write_movie_button->connect(SceneStringName(toggled), callable_mp(this, &EditorRunBar::_write_movie_toggled)); - - quick_run = memnew(EditorQuickOpen); - add_child(quick_run); - quick_run->connect("quick_open", callable_mp(this, &EditorRunBar::_quick_run_selected)); } diff --git a/editor/gui/editor_run_bar.h b/editor/gui/editor_run_bar.h index 1cb999612a..d8238aa2c5 100644 --- a/editor/gui/editor_run_bar.h +++ b/editor/gui/editor_run_bar.h @@ -37,7 +37,6 @@ class Button; class EditorRunNative; -class EditorQuickOpen; class PanelContainer; class HBoxContainer; @@ -68,8 +67,6 @@ class EditorRunBar : public MarginContainer { PanelContainer *write_movie_panel = nullptr; Button *write_movie_button = nullptr; - EditorQuickOpen *quick_run = nullptr; - RunMode current_mode = RunMode::STOPPED; String run_custom_filename; String run_current_filename; @@ -78,7 +75,7 @@ class EditorRunBar : public MarginContainer { void _update_play_buttons(); void _write_movie_toggled(bool p_enabled); - void _quick_run_selected(); + void _quick_run_selected(const String &p_file_path); void _play_current_pressed(); void _play_custom_pressed(); diff --git a/editor/gui/editor_version_button.cpp b/editor/gui/editor_version_button.cpp new file mode 100644 index 0000000000..635d66f42a --- /dev/null +++ b/editor/gui/editor_version_button.cpp @@ -0,0 +1,85 @@ +/**************************************************************************/ +/* editor_version_button.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_version_button.h" + +#include "core/os/time.h" +#include "core/version.h" + +String _get_version_string(EditorVersionButton::VersionFormat p_format) { + String main; + switch (p_format) { + case EditorVersionButton::FORMAT_BASIC: { + return VERSION_FULL_CONFIG; + } break; + case EditorVersionButton::FORMAT_WITH_BUILD: { + main = "v" VERSION_FULL_BUILD; + } break; + case EditorVersionButton::FORMAT_WITH_NAME_AND_BUILD: { + main = VERSION_FULL_NAME; + } break; + default: { + ERR_FAIL_V_MSG(VERSION_FULL_NAME, "Unexpected format: " + itos(p_format)); + } break; + } + + String hash = VERSION_HASH; + if (!hash.is_empty()) { + hash = vformat(" [%s]", hash.left(9)); + } + return main + hash; +} + +void EditorVersionButton::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: { + // This can't be done in the constructor because theme cache is not ready yet. + set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + set_text(_get_version_string(format)); + } break; + } +} + +void EditorVersionButton::pressed() { + DisplayServer::get_singleton()->clipboard_set(_get_version_string(FORMAT_WITH_BUILD)); +} + +EditorVersionButton::EditorVersionButton(VersionFormat p_format) { + format = p_format; + set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); + + String build_date; + if (VERSION_TIMESTAMP > 0) { + build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; + } else { + build_date = TTR("(unknown)"); + } + set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date)); +} diff --git a/editor/gui/editor_version_button.h b/editor/gui/editor_version_button.h new file mode 100644 index 0000000000..591c3d483e --- /dev/null +++ b/editor/gui/editor_version_button.h @@ -0,0 +1,61 @@ +/**************************************************************************/ +/* editor_version_button.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EDITOR_VERSION_BUTTON_H +#define EDITOR_VERSION_BUTTON_H + +#include "scene/gui/link_button.h" + +class EditorVersionButton : public LinkButton { + GDCLASS(EditorVersionButton, LinkButton); + +public: + enum VersionFormat { + // 4.3.2.stable + FORMAT_BASIC, + // v4.3.2.stable.mono [HASH] + FORMAT_WITH_BUILD, + // Godot Engine v4.3.2.stable.mono.official [HASH] + FORMAT_WITH_NAME_AND_BUILD, + }; + +private: + VersionFormat format = FORMAT_WITH_NAME_AND_BUILD; + +protected: + void _notification(int p_what); + + virtual void pressed() override; + +public: + EditorVersionButton(VersionFormat p_format); +}; + +#endif // EDITOR_VERSION_BUTTON_H diff --git a/editor/icons/Marker.svg b/editor/icons/Marker.svg new file mode 100644 index 0000000000..ff91a4a947 --- /dev/null +++ b/editor/icons/Marker.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="-959.5 540.5 10 10"><path fill="#e0e0e0" d="m-954.5 550-3-3v-6h6v6z"/></svg>
\ No newline at end of file diff --git a/editor/icons/MarkerSelected.svg b/editor/icons/MarkerSelected.svg new file mode 100644 index 0000000000..c581a3a651 --- /dev/null +++ b/editor/icons/MarkerSelected.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="-959.5 540.5 10 10"><path fill="#5fb2ff" d="m-952 541.5v5.086l-2.5 2.5-2.5-2.5v-5.086zm1-1h-7v6.5l3.5 3.5 3.5-3.5z"/><path fill="#003e7a" d="m-957 546.586 2.5 2.5 2.5-2.5v-5.086h-5z"/></svg>
\ No newline at end of file diff --git a/editor/inspector_dock.cpp b/editor/inspector_dock.cpp index 3a3598c0b9..d13a022d52 100644 --- a/editor/inspector_dock.cpp +++ b/editor/inspector_dock.cpp @@ -800,7 +800,7 @@ InspectorDock::InspectorDock(EditorData &p_editor_data) { inspector->set_use_folding(!bool(EDITOR_GET("interface/inspector/disable_folding"))); inspector->register_text_enter(search); - inspector->set_use_filter(true); // TODO: check me + inspector->set_use_filter(true); inspector->connect("resource_selected", callable_mp(this, &InspectorDock::_resource_selected)); diff --git a/editor/plugins/animation_blend_tree_editor_plugin.cpp b/editor/plugins/animation_blend_tree_editor_plugin.cpp index a28fe01666..9e282cb3fa 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.cpp +++ b/editor/plugins/animation_blend_tree_editor_plugin.cpp @@ -36,6 +36,7 @@ #include "core/os/keyboard.h" #include "editor/editor_inspector.h" #include "editor/editor_node.h" +#include "editor/editor_properties.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" @@ -45,6 +46,7 @@ #include "scene/animation/animation_player.h" #include "scene/gui/check_box.h" #include "scene/gui/menu_button.h" +#include "scene/gui/option_button.h" #include "scene/gui/panel.h" #include "scene/gui/progress_bar.h" #include "scene/gui/separator.h" @@ -1262,4 +1264,168 @@ AnimationNodeBlendTreeEditor::AnimationNodeBlendTreeEditor() { open_file->set_title(TTR("Open Animation Node")); open_file->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); open_file->connect("file_selected", callable_mp(this, &AnimationNodeBlendTreeEditor::_file_opened)); + + animation_node_inspector_plugin = Ref<EditorInspectorPluginAnimationNodeAnimation>(memnew(EditorInspectorPluginAnimationNodeAnimation)); + EditorInspector::add_inspector_plugin(animation_node_inspector_plugin); +} + +AnimationNodeBlendTreeEditor::~AnimationNodeBlendTreeEditor() { +} + +// EditorPluginAnimationNodeAnimation + +void AnimationNodeAnimationEditor::_open_set_custom_timeline_from_marker_dialog() { + AnimationTree *tree = AnimationTreeEditor::get_singleton()->get_animation_tree(); + StringName anim_name = animation_node_animation->get_animation(); + PackedStringArray markers = tree->has_animation(anim_name) ? tree->get_animation(anim_name)->get_marker_names() : PackedStringArray(); + + dialog->select_start->clear(); + dialog->select_start->add_icon_item(get_editor_theme_icon(SNAME("PlayStart")), TTR("Start of Animation")); + dialog->select_start->add_separator(); + dialog->select_end->clear(); + dialog->select_end->add_icon_item(get_editor_theme_icon(SNAME("PlayStartBackwards")), TTR("End of Animation")); + dialog->select_end->add_separator(); + + for (const String &marker : markers) { + dialog->select_start->add_item(marker); + dialog->select_end->add_item(marker); + } + + // Because the default selections are always valid, and marker times won't change during the dialog, we can ensure that the user can only select valid markers. + // This invariant is maintained by _validate_markers. + dialog->select_start->select(0); + dialog->select_end->select(0); + + dialog->popup_centered(Size2(200, 0) * EDSCALE); +} + +void AnimationNodeAnimationEditor::_validate_markers(int p_id) { + // Note: p_id is ignored. It is included because OptionButton's item_changed signal always passes it. + int start_id = dialog->select_start->get_selected_id(); + int end_id = dialog->select_end->get_selected_id(); + + StringName anim_name = animation_node_animation->get_animation(); + Ref<Animation> animation = AnimationTreeEditor::get_singleton()->get_animation_tree()->get_animation(anim_name); + ERR_FAIL_COND(animation.is_null()); + + double start_time = start_id < 2 ? 0 : animation->get_marker_time(dialog->select_start->get_item_text(start_id)); + double end_time = end_id < 2 ? animation->get_length() : animation->get_marker_time(dialog->select_end->get_item_text(end_id)); + + // p_start and p_end have the same item count. + for (int i = 2; i < dialog->select_start->get_item_count(); i++) { + String start_marker = dialog->select_start->get_item_text(i); + String end_marker = dialog->select_end->get_item_text(i); + dialog->select_start->set_item_disabled(i, end_id >= 2 && (i == end_id || animation->get_marker_time(start_marker) > end_time)); + dialog->select_end->set_item_disabled(i, start_id >= 2 && (i == start_id || start_time > animation->get_marker_time(end_marker))); + } +} + +void AnimationNodeAnimationEditor::_confirm_set_custom_timeline_from_marker_dialog() { + int start_id = dialog->select_start->get_selected_id(); + int end_id = dialog->select_end->get_selected_id(); + + Ref<Animation> animation = AnimationTreeEditor::get_singleton()->get_animation_tree()->get_animation(animation_node_animation->get_animation()); + ERR_FAIL_COND(animation.is_null()); + double start_time = start_id < 2 ? 0 : animation->get_marker_time(dialog->select_start->get_item_text(start_id)); + double end_time = end_id < 2 ? animation->get_length() : animation->get_marker_time(dialog->select_end->get_item_text(end_id)); + double length = end_time - start_time; + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Set Custom Timeline from Marker")); + undo_redo->add_do_method(*animation_node_animation, "set_start_offset", start_time); + undo_redo->add_undo_method(*animation_node_animation, "set_start_offset", animation_node_animation->get_start_offset()); + undo_redo->add_do_method(*animation_node_animation, "set_stretch_time_scale", false); + undo_redo->add_undo_method(*animation_node_animation, "set_stretch_time_scale", animation_node_animation->is_stretching_time_scale()); + undo_redo->add_do_method(*animation_node_animation, "set_timeline_length", length); + undo_redo->add_undo_method(*animation_node_animation, "set_timeline_length", animation_node_animation->get_timeline_length()); + undo_redo->add_do_method(*animation_node_animation, "notify_property_list_changed"); + undo_redo->add_undo_method(*animation_node_animation, "notify_property_list_changed"); + undo_redo->commit_action(); +} + +AnimationNodeAnimationEditor::AnimationNodeAnimationEditor(Ref<AnimationNodeAnimation> p_animation_node_animation) { + animation_node_animation = p_animation_node_animation; + + dialog = memnew(AnimationNodeAnimationEditorDialog); + add_child(dialog); + dialog->set_hide_on_ok(false); + dialog->select_start->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeAnimationEditor::_validate_markers)); + dialog->select_end->connect(SceneStringName(item_selected), callable_mp(this, &AnimationNodeAnimationEditor::_validate_markers)); + dialog->connect(SceneStringName(confirmed), callable_mp(this, &AnimationNodeAnimationEditor::_confirm_set_custom_timeline_from_marker_dialog)); + + Control *top_spacer = memnew(Control); + add_child(top_spacer); + top_spacer->set_custom_minimum_size(Size2(0, 2) * EDSCALE); + + button = memnew(Button); + add_child(button); + button->set_text(TTR("Set Custom Timeline from Marker")); + button->set_h_size_flags(Control::SIZE_SHRINK_CENTER); + button->connect(SceneStringName(pressed), callable_mp(this, &AnimationNodeAnimationEditor::_open_set_custom_timeline_from_marker_dialog)); + + Control *bottom_spacer = memnew(Control); + add_child(bottom_spacer); + bottom_spacer->set_custom_minimum_size(Size2(0, 2) * EDSCALE); +} + +AnimationNodeAnimationEditor::~AnimationNodeAnimationEditor() { +} + +void AnimationNodeAnimationEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + button->set_theme_type_variation(SNAME("InspectorActionButton")); + button->set_icon(get_editor_theme_icon(SNAME("Edit"))); + } break; + } +} + +bool EditorInspectorPluginAnimationNodeAnimation::can_handle(Object *p_object) { + Ref<AnimationNodeAnimation> ana(Object::cast_to<AnimationNodeAnimation>(p_object)); + return ana.is_valid() && ana->is_using_custom_timeline(); +} + +bool EditorInspectorPluginAnimationNodeAnimation::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) { + Ref<AnimationNodeAnimation> ana(Object::cast_to<AnimationNodeAnimation>(p_object)); + ERR_FAIL_COND_V(ana.is_null(), false); + + if (p_path == "timeline_length") { + add_custom_control(memnew(AnimationNodeAnimationEditor(ana))); + } + + return false; +} + +AnimationNodeAnimationEditorDialog::AnimationNodeAnimationEditorDialog() { + set_title(TTR("Select Markers...")); + VBoxContainer *vbox = memnew(VBoxContainer); + add_child(vbox); + vbox->set_offsets_preset(Control::PRESET_FULL_RECT); + + HBoxContainer *container_start = memnew(HBoxContainer); + vbox->add_child(container_start); + Label *label_start = memnew(Label); + container_start->add_child(label_start); + label_start->set_h_size_flags(Control::SIZE_EXPAND_FILL); + label_start->set_stretch_ratio(1); + label_start->set_text(TTR("Start Marker")); + select_start = memnew(OptionButton); + container_start->add_child(select_start); + select_start->set_h_size_flags(Control::SIZE_EXPAND_FILL); + select_start->set_stretch_ratio(2); + + HBoxContainer *container_end = memnew(HBoxContainer); + vbox->add_child(container_end); + Label *label_end = memnew(Label); + container_end->add_child(label_end); + label_end->set_h_size_flags(Control::SIZE_EXPAND_FILL); + label_end->set_stretch_ratio(1); + label_end->set_text(TTR("End Marker")); + select_end = memnew(OptionButton); + container_end->add_child(select_end); + select_end->set_h_size_flags(Control::SIZE_EXPAND_FILL); + select_end->set_stretch_ratio(2); +} + +AnimationNodeAnimationEditorDialog::~AnimationNodeAnimationEditorDialog() { } diff --git a/editor/plugins/animation_blend_tree_editor_plugin.h b/editor/plugins/animation_blend_tree_editor_plugin.h index ee6f087e07..9e7793977b 100644 --- a/editor/plugins/animation_blend_tree_editor_plugin.h +++ b/editor/plugins/animation_blend_tree_editor_plugin.h @@ -32,9 +32,11 @@ #define ANIMATION_BLEND_TREE_EDITOR_PLUGIN_H #include "core/object/script_language.h" +#include "editor/editor_inspector.h" #include "editor/plugins/animation_tree_editor_plugin.h" #include "scene/animation/animation_blend_tree.h" #include "scene/gui/button.h" +#include "scene/gui/dialogs.h" #include "scene/gui/graph_edit.h" #include "scene/gui/panel_container.h" #include "scene/gui/popup.h" @@ -47,6 +49,7 @@ class EditorFileDialog; class EditorProperty; class MenuButton; class PanelContainer; +class EditorInspectorPluginAnimationNodeAnimation; class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { GDCLASS(AnimationNodeBlendTreeEditor, AnimationTreeNodeEditorPlugin); @@ -147,6 +150,8 @@ class AnimationNodeBlendTreeEditor : public AnimationTreeNodeEditorPlugin { MENU_LOAD_FILE_CONFIRM = 1002 }; + Ref<EditorInspectorPluginAnimationNodeAnimation> animation_node_inspector_plugin; + protected: void _notification(int p_what); static void _bind_methods(); @@ -165,6 +170,48 @@ public: void update_graph(); AnimationNodeBlendTreeEditor(); + ~AnimationNodeBlendTreeEditor(); +}; + +// EditorPluginAnimationNodeAnimation + +class EditorInspectorPluginAnimationNodeAnimation : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginAnimationNodeAnimation, EditorInspectorPlugin); + +public: + virtual bool can_handle(Object *p_object) override; + virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) override; +}; + +class AnimationNodeAnimationEditorDialog : public ConfirmationDialog { + GDCLASS(AnimationNodeAnimationEditorDialog, ConfirmationDialog); + + friend class AnimationNodeAnimationEditor; + + OptionButton *select_start = nullptr; + OptionButton *select_end = nullptr; + +public: + AnimationNodeAnimationEditorDialog(); + ~AnimationNodeAnimationEditorDialog(); +}; + +class AnimationNodeAnimationEditor : public VBoxContainer { + GDCLASS(AnimationNodeAnimationEditor, VBoxContainer); + + Ref<AnimationNodeAnimation> animation_node_animation; + Button *button = nullptr; + AnimationNodeAnimationEditorDialog *dialog = nullptr; + void _open_set_custom_timeline_from_marker_dialog(); + void _validate_markers(int p_id); + void _confirm_set_custom_timeline_from_marker_dialog(); + +public: + AnimationNodeAnimationEditor(Ref<AnimationNodeAnimation> p_animation_node_animation); + ~AnimationNodeAnimationEditor(); + +protected: + void _notification(int p_what); }; #endif // ANIMATION_BLEND_TREE_EDITOR_PLUGIN_H diff --git a/editor/plugins/animation_player_editor_plugin.cpp b/editor/plugins/animation_player_editor_plugin.cpp index 5cb558abbe..e6afc85e9e 100644 --- a/editor/plugins/animation_player_editor_plugin.cpp +++ b/editor/plugins/animation_player_editor_plugin.cpp @@ -41,6 +41,7 @@ #include "editor/editor_undo_redo_manager.h" #include "editor/gui/editor_bottom_panel.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/gui/editor_validation_panel.h" #include "editor/inspector_dock.h" #include "editor/plugins/canvas_item_editor_plugin.h" // For onion skinning. #include "editor/plugins/node_3d_editor_plugin.h" // For onion skinning. @@ -295,7 +296,14 @@ void AnimationPlayerEditor::_play_pressed() { player->stop(); //so it won't blend with itself } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); - player->play(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers(current, start_marker, end_marker); + } else { + player->play(current); + } } //unstop @@ -312,7 +320,14 @@ void AnimationPlayerEditor::_play_from_pressed() { } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); player->seek_internal(time, true, true, true); - player->play(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers(current, start_marker, end_marker); + } else { + player->play(current); + } } //unstop @@ -333,7 +348,14 @@ void AnimationPlayerEditor::_play_bw_pressed() { player->stop(); //so it won't blend with itself } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); - player->play_backwards(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers_backwards(current, start_marker, end_marker); + } else { + player->play_backwards(current); + } } //unstop @@ -350,7 +372,14 @@ void AnimationPlayerEditor::_play_bw_from_pressed() { } ERR_FAIL_COND_EDMSG(!_validate_tracks(player->get_animation(current)), "Animation tracks may have any invalid key, abort playing."); player->seek_internal(time, true, true, true); - player->play_backwards(current); + PackedStringArray markers = track_editor->get_selected_section(); + if (markers.size() == 2) { + StringName start_marker = markers[0]; + StringName end_marker = markers[1]; + player->play_section_with_markers_backwards(current, start_marker, end_marker); + } else { + player->play_backwards(current); + } } //unstop @@ -2397,3 +2426,24 @@ AnimationTrackKeyEditEditorPlugin::AnimationTrackKeyEditEditorPlugin() { bool AnimationTrackKeyEditEditorPlugin::handles(Object *p_object) const { return p_object->is_class("AnimationTrackKeyEdit"); } + +bool EditorInspectorPluginAnimationMarkerKeyEdit::can_handle(Object *p_object) { + return Object::cast_to<AnimationMarkerKeyEdit>(p_object) != nullptr; +} + +void EditorInspectorPluginAnimationMarkerKeyEdit::parse_begin(Object *p_object) { + AnimationMarkerKeyEdit *amk = Object::cast_to<AnimationMarkerKeyEdit>(p_object); + ERR_FAIL_NULL(amk); + + amk_editor = memnew(AnimationMarkerKeyEditEditor(amk->animation, amk->marker_name, amk->use_fps)); + add_custom_control(amk_editor); +} + +AnimationMarkerKeyEditEditorPlugin::AnimationMarkerKeyEditEditorPlugin() { + amk_plugin = memnew(EditorInspectorPluginAnimationMarkerKeyEdit); + EditorInspector::add_inspector_plugin(amk_plugin); +} + +bool AnimationMarkerKeyEditEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("AnimationMarkerKeyEdit"); +} diff --git a/editor/plugins/animation_player_editor_plugin.h b/editor/plugins/animation_player_editor_plugin.h index e4ca6c17c3..349ed7b5cd 100644 --- a/editor/plugins/animation_player_editor_plugin.h +++ b/editor/plugins/animation_player_editor_plugin.h @@ -338,4 +338,30 @@ public: AnimationTrackKeyEditEditorPlugin(); }; +// AnimationMarkerKeyEditEditorPlugin + +class EditorInspectorPluginAnimationMarkerKeyEdit : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginAnimationMarkerKeyEdit, EditorInspectorPlugin); + + AnimationMarkerKeyEditEditor *amk_editor = nullptr; + +public: + virtual bool can_handle(Object *p_object) override; + virtual void parse_begin(Object *p_object) override; +}; + +class AnimationMarkerKeyEditEditorPlugin : public EditorPlugin { + GDCLASS(AnimationMarkerKeyEditEditorPlugin, EditorPlugin); + + EditorInspectorPluginAnimationMarkerKeyEdit *amk_plugin = nullptr; + +public: + bool has_main_screen() const override { return false; } + virtual bool handles(Object *p_object) const override; + + virtual String get_name() const override { return "AnimationMarkerKeyEdit"; } + + AnimationMarkerKeyEditEditorPlugin(); +}; + #endif // ANIMATION_PLAYER_EDITOR_PLUGIN_H diff --git a/editor/plugins/asset_library_editor_plugin.cpp b/editor/plugins/asset_library_editor_plugin.cpp index af4b3a1643..fec8d4b897 100644 --- a/editor/plugins/asset_library_editor_plugin.cpp +++ b/editor/plugins/asset_library_editor_plugin.cpp @@ -993,7 +993,8 @@ void EditorAssetLibrary::_request_image(ObjectID p_for, int p_asset_id, String p String url_host; int url_port; String url_path; - Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path); + String url_fragment; + Error err = trimmed_url.parse_url(url_scheme, url_host, url_port, url_path, url_fragment); if (err != OK) { if (is_print_verbose_enabled()) { ERR_PRINT(vformat("Asset Library: Invalid image URL '%s' for asset # %d.", trimmed_url, p_asset_id)); diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 8411c0edea..30878a2488 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -38,7 +38,6 @@ #include "core/io/stream_peer_tls.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/os/time.h" #include "core/version.h" #include "editor/editor_about.h" #include "editor/editor_settings.h" @@ -46,6 +45,7 @@ #include "editor/engine_update_label.h" #include "editor/gui/editor_file_dialog.h" #include "editor/gui/editor_title_bar.h" +#include "editor/gui/editor_version_button.h" #include "editor/plugins/asset_library_editor_plugin.h" #include "editor/project_manager/project_dialog.h" #include "editor/project_manager/project_list.h" @@ -398,12 +398,6 @@ void ProjectManager::_restart_confirmed() { get_tree()->quit(); } -// Footer. - -void ProjectManager::_version_button_pressed() { - DisplayServer::get_singleton()->clipboard_set(version_btn->get_text()); -} - // Project list. void ProjectManager::_update_list_placeholder() { @@ -1459,23 +1453,9 @@ ProjectManager::ProjectManager() { update_label->connect("offline_clicked", callable_mp(this, &ProjectManager::_show_quick_settings)); #endif - version_btn = memnew(LinkButton); - String hash = String(VERSION_HASH); - if (hash.length() != 0) { - hash = " " + vformat("[%s]", hash.left(9)); - } - version_btn->set_text("v" VERSION_FULL_BUILD + hash); + EditorVersionButton *version_btn = memnew(EditorVersionButton(EditorVersionButton::FORMAT_WITH_BUILD)); // Fade the version label to be less prominent, but still readable. version_btn->set_self_modulate(Color(1, 1, 1, 0.6)); - version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER); - String build_date; - if (VERSION_TIMESTAMP > 0) { - build_date = Time::get_singleton()->get_datetime_string_from_unix_time(VERSION_TIMESTAMP, true) + " UTC"; - } else { - build_date = TTR("(unknown)"); - } - version_btn->set_tooltip_text(vformat(TTR("Git commit date: %s\nClick to copy the version information."), build_date)); - version_btn->connect(SceneStringName(pressed), callable_mp(this, &ProjectManager::_version_button_pressed)); footer_bar->add_child(version_btn); } diff --git a/editor/project_manager.h b/editor/project_manager.h index aad51d0e98..07da0059c0 100644 --- a/editor/project_manager.h +++ b/editor/project_manager.h @@ -41,7 +41,6 @@ class EditorFileDialog; class EditorTitleBar; class HFlowContainer; class LineEdit; -class LinkButton; class MarginContainer; class OptionButton; class PanelContainer; @@ -124,12 +123,6 @@ class ProjectManager : public Control { void _show_quick_settings(); void _restart_confirmed(); - // Footer. - - LinkButton *version_btn = nullptr; - - void _version_button_pressed(); - // Project list. VBoxContainer *empty_list_placeholder = nullptr; diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 5767293718..418f4e4932 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -389,7 +389,7 @@ void ProjectSettingsEditor::_action_added(const String &p_name) { Dictionary action; action["events"] = Array(); - action["deadzone"] = 0.5f; + action["deadzone"] = 0.2f; EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Add Input Action")); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 7187da851e..bcab0c2883 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -37,15 +37,16 @@ #include "core/os/keyboard.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/editor_feature_profile.h" +#include "editor/editor_file_system.h" #include "editor/editor_main_screen.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" -#include "editor/editor_quick_open.h" #include "editor/editor_settings.h" #include "editor/editor_string_names.h" #include "editor/editor_undo_redo_manager.h" #include "editor/filesystem_dock.h" #include "editor/gui/editor_file_dialog.h" +#include "editor/gui/editor_quick_open_dialog.h" #include "editor/inspector_dock.h" #include "editor/multi_node_edit.h" #include "editor/node_dock.h" @@ -73,8 +74,8 @@ void SceneTreeDock::_nodes_drag_begin() { pending_click_select = nullptr; } -void SceneTreeDock::_quick_open() { - instantiate_scenes(quick_open->get_selected_files(), scene_tree->get_selected()); +void SceneTreeDock::_quick_open(const String &p_file_path) { + instantiate_scenes({ p_file_path }, scene_tree->get_selected()); } void SceneTreeDock::_inspect_hovered_node() { @@ -88,6 +89,8 @@ void SceneTreeDock::_inspect_hovered_node() { tree_item_inspected = item; tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor))); } + EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history(); + editor_history->add_object(node_hovered_now->get_instance_id()); InspectorDock::get_inspector_singleton()->edit(node_hovered_now); InspectorDock::get_inspector_singleton()->propagate_notification(NOTIFICATION_DRAG_BEGIN); // Enable inspector drag preview after it updated. InspectorDock::get_singleton()->update(node_hovered_now); @@ -133,14 +136,6 @@ void SceneTreeDock::input(const Ref<InputEvent> &p_event) { _push_item(pending_click_select); pending_click_select = nullptr; } - - if (mb->is_released()) { - if (tree_item_inspected) { - tree_item_inspected->clear_custom_color(0); - tree_item_inspected = nullptr; - } - _reset_hovering_timer(); - } } if (tree_clicked && get_viewport()->gui_is_dragging()) { @@ -436,6 +431,22 @@ void SceneTreeDock::_replace_with_branch_scene(const String &p_file, Node *base) instantiated_scene->set_unique_name_in_owner(base->is_unique_name_in_owner()); + Node2D *copy_2d = Object::cast_to<Node2D>(instantiated_scene); + Node2D *base_2d = Object::cast_to<Node2D>(base); + if (copy_2d && base_2d) { + copy_2d->set_position(base_2d->get_position()); + copy_2d->set_rotation(base_2d->get_rotation()); + copy_2d->set_scale(base_2d->get_scale()); + } + + Node3D *copy_3d = Object::cast_to<Node3D>(instantiated_scene); + Node3D *base_3d = Object::cast_to<Node3D>(base); + if (copy_3d && base_3d) { + copy_3d->set_position(base_3d->get_position()); + copy_3d->set_rotation(base_3d->get_rotation()); + copy_3d->set_scale(base_3d->get_scale()); + } + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Replace with Branch Scene")); @@ -599,8 +610,7 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { break; } - quick_open->popup_dialog("PackedScene", true); - quick_open->set_title(TTR("Instantiate Child Scene")); + EditorNode::get_singleton()->get_quick_open_dialog()->popup_dialog({ "PackedScene" }, callable_mp(this, &SceneTreeDock::_quick_open)); if (!p_confirm_override) { emit_signal(SNAME("add_node_used")); } @@ -1702,13 +1712,30 @@ void SceneTreeDock::_notification(int p_what) { case NOTIFICATION_DRAG_END: { _reset_hovering_timer(); - if (select_node_hovered_at_end_of_drag && !hovered_but_reparenting) { - Node *node_inspected = Object::cast_to<Node>(InspectorDock::get_inspector_singleton()->get_edited_object()); - if (node_inspected) { + if (tree_item_inspected) { + tree_item_inspected->clear_custom_color(0); + tree_item_inspected = nullptr; + } else { + return; + } + if (!hovered_but_reparenting) { + InspectorDock *inspector_dock = InspectorDock::get_singleton(); + if (!inspector_dock->get_rect().has_point(inspector_dock->get_local_mouse_position())) { + List<Node *> full_selection = editor_selection->get_full_selected_node_list(); editor_selection->clear(); - editor_selection->add_node(node_inspected); - scene_tree->set_selected(node_inspected); - select_node_hovered_at_end_of_drag = false; + for (Node *E : full_selection) { + editor_selection->add_node(E); + } + return; + } + if (select_node_hovered_at_end_of_drag) { + Node *node_inspected = Object::cast_to<Node>(InspectorDock::get_inspector_singleton()->get_edited_object()); + if (node_inspected) { + editor_selection->clear(); + editor_selection->add_node(node_inspected); + scene_tree->set_selected(node_inspected); + select_node_hovered_at_end_of_drag = false; + } } } hovered_but_reparenting = false; @@ -3259,6 +3286,36 @@ void SceneTreeDock::_new_scene_from(const String &p_file) { // Root node cannot ever be unique name in its own Scene! copy->set_unique_name_in_owner(false); + const Dictionary dict = new_scene_from_dialog->get_selected_options(); + bool reset_position = dict.get(TTR("Reset Position"), true); + bool reset_scale = dict.get(TTR("Reset Scale"), false); + bool reset_rotation = dict.get(TTR("Reset Rotation"), false); + + Node2D *copy_2d = Object::cast_to<Node2D>(copy); + if (copy_2d != nullptr) { + if (reset_position) { + copy_2d->set_position(Vector2(0, 0)); + } + if (reset_rotation) { + copy_2d->set_rotation(0); + } + if (reset_scale) { + copy_2d->set_scale(Size2(1, 1)); + } + } + Node3D *copy_3d = Object::cast_to<Node3D>(copy); + if (copy_3d != nullptr) { + if (reset_position) { + copy_3d->set_position(Vector3(0, 0, 0)); + } + if (reset_rotation) { + copy_3d->set_rotation(Vector3(0, 0, 0)); + } + if (reset_scale) { + copy_3d->set_scale(Vector3(0, 0, 0)); + } + } + Ref<PackedScene> sdata = memnew(PackedScene); Error err = sdata->pack(copy); memdelete(copy); @@ -4598,7 +4655,6 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec scene_tree->connect("files_dropped", callable_mp(this, &SceneTreeDock::_files_dropped)); scene_tree->connect("script_dropped", callable_mp(this, &SceneTreeDock::_script_dropped)); scene_tree->connect("nodes_dragged", callable_mp(this, &SceneTreeDock::_nodes_drag_begin)); - scene_tree->connect(SceneStringName(mouse_exited), callable_mp(this, &SceneTreeDock::_reset_hovering_timer)); scene_tree->get_scene_tree()->connect(SceneStringName(gui_input), callable_mp(this, &SceneTreeDock::_scene_tree_gui_input)); scene_tree->get_scene_tree()->connect("item_icon_double_clicked", callable_mp(this, &SceneTreeDock::_focus_node)); @@ -4638,10 +4694,6 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec accept = memnew(AcceptDialog); add_child(accept); - quick_open = memnew(EditorQuickOpen); - add_child(quick_open); - quick_open->connect("quick_open", callable_mp(this, &SceneTreeDock::_quick_open)); - set_process_shortcut_input(true); delete_dialog = memnew(ConfirmationDialog); @@ -4668,6 +4720,9 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec new_scene_from_dialog = memnew(EditorFileDialog); new_scene_from_dialog->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); + new_scene_from_dialog->add_option(TTR("Reset Position"), Vector<String>(), true); + new_scene_from_dialog->add_option(TTR("Reset Rotation"), Vector<String>(), false); + new_scene_from_dialog->add_option(TTR("Reset Scale"), Vector<String>(), false); add_child(new_scene_from_dialog); new_scene_from_dialog->connect("file_selected", callable_mp(this, &SceneTreeDock::_new_scene_from)); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index d0cdaf8dd1..05ad0f36e4 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -39,7 +39,6 @@ class CheckBox; class EditorData; class EditorSelection; -class EditorQuickOpen; class MenuButton; class ReparentDialog; class ShaderCreateDialog; @@ -159,7 +158,6 @@ class SceneTreeDock : public VBoxContainer { ConfirmationDialog *placeholder_editable_instance_remove_dialog = nullptr; ReparentDialog *reparent_dialog = nullptr; - EditorQuickOpen *quick_open = nullptr; EditorFileDialog *new_scene_from_dialog = nullptr; enum FilterMenuItems { @@ -267,7 +265,7 @@ class SceneTreeDock : public VBoxContainer { void _nodes_dragged(const Array &p_nodes, NodePath p_to, int p_type); void _files_dropped(const Vector<String> &p_files, NodePath p_to, int p_type); void _script_dropped(const String &p_file, NodePath p_to); - void _quick_open(); + void _quick_open(const String &p_file_path); void _tree_rmb(const Vector2 &p_menu_pos); void _update_tree_menu(); diff --git a/main/main.cpp b/main/main.cpp index 9014bfad29..36912c4fa3 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -2755,7 +2755,6 @@ Error Main::setup2(bool p_show_boot_logo) { bool prefer_wayland_found = false; bool prefer_wayland = false; - bool remember_window_size_and_position_found = false; if (editor) { screen_property = "interface/editor/editor_screen"; @@ -2771,7 +2770,7 @@ Error Main::setup2(bool p_show_boot_logo) { prefer_wayland_found = true; } - while (!screen_found || !prefer_wayland_found || !remember_window_size_and_position_found) { + while (!screen_found || !prefer_wayland_found) { assign = Variant(); next_tag.fields.clear(); next_tag.name = String(); @@ -2785,17 +2784,16 @@ Error Main::setup2(bool p_show_boot_logo) { if (!screen_found && assign == screen_property) { init_screen = value; screen_found = true; + + if (editor) { + restore_editor_window_layout = value.operator int() == EditorSettings::InitialScreen::INITIAL_SCREEN_AUTO; + } } if (!prefer_wayland_found && assign == "run/platforms/linuxbsd/prefer_wayland") { prefer_wayland = value; prefer_wayland_found = true; } - - if (!remember_window_size_and_position_found && assign == "interface/editor/remember_window_size_and_position") { - restore_editor_window_layout = value; - remember_window_size_and_position_found = true; - } } } @@ -4095,8 +4093,7 @@ int Main::start() { if (editor_embed_subwindows) { sml->get_root()->set_embedding_subwindows(true); } - restore_editor_window_layout = EditorSettings::get_singleton()->get_setting( - "interface/editor/remember_window_size_and_position"); + restore_editor_window_layout = EditorSettings::get_singleton()->get_setting("interface/editor/editor_screen").operator int() == EditorSettings::InitialScreen::INITIAL_SCREEN_AUTO; } #endif diff --git a/methods.py b/methods.py index 04541c72ad..9e881773c9 100644 --- a/methods.py +++ b/methods.py @@ -828,7 +828,7 @@ def get_compiler_version(env): sem_ver = split[1].split(".") ret["major"] = int(sem_ver[0]) ret["minor"] = int(sem_ver[1]) - ret["patch"] = int(sem_ver[2]) + ret["patch"] = int(sem_ver[2].split()[0]) # Could potentially add section for determining preview version, but # that can wait until metadata is actually used for something. if split[0] == "catalog_buildVersion": diff --git a/misc/dist/linux/org.godotengine.Godot.desktop b/misc/dist/linux/org.godotengine.Godot.desktop index 28669548d6..6483f51d92 100644 --- a/misc/dist/linux/org.godotengine.Godot.desktop +++ b/misc/dist/linux/org.godotengine.Godot.desktop @@ -5,12 +5,14 @@ GenericName[el]=Ελεύθερη μηχανή παιχνιδιού GenericName[fr]=Moteur de jeu libre GenericName[nl]=Libre game-engine GenericName[ru]=Свободный игровой движок +GenericName[uk]=Вільний ігровий рушій GenericName[zh_CN]=自由的游戏引擎 Comment=Multi-platform 2D and 3D game engine with a feature-rich editor Comment[el]=2D και 3D μηχανή παιχνιδιού πολλαπλών πλατφορμών με επεξεργαστή πλούσιο σε χαρακτηριστικά Comment[fr]=Moteur de jeu 2D et 3D multiplateforme avec un éditeur riche en fonctionnalités Comment[nl]=Multi-platform 2D- en 3D-game-engine met een veelzijdige editor Comment[ru]=Кроссплатформенный движок с многофункциональным редактором для 2D- и 3D-игр +Comment[uk]=Багатофункціональний кросплатформний рушій для створення 2D та 3D ігор Comment[zh_CN]=多平台 2D 和 3D 游戏引擎,带有功能丰富的编辑器 Exec=godot %f Icon=godot diff --git a/misc/extension_api_validation/4.3-stable.expected b/misc/extension_api_validation/4.3-stable.expected index 06933b5841..960edd0575 100644 --- a/misc/extension_api_validation/4.3-stable.expected +++ b/misc/extension_api_validation/4.3-stable.expected @@ -87,3 +87,11 @@ GH-94684 Validate extension JSON: Error: Field 'classes/SoftBody3D/methods/set_point_pinned/arguments': size changed value in new API, from 3 to 4. Optional argument added to allow for adding pin point at specific index. Compatibility method registered. + + +GH-97281 +-------- +Validate extension JSON: Error: Field 'classes/InputMap/methods/add_action/arguments/1': default_value changed value in new API, from "0.5" to "0.2". + +Default deadzone value was changed. No adjustments should be necessary. +Compatibility method registered. diff --git a/modules/enet/doc_classes/ENetPacketPeer.xml b/modules/enet/doc_classes/ENetPacketPeer.xml index 3171da1f6d..659cea974c 100644 --- a/modules/enet/doc_classes/ENetPacketPeer.xml +++ b/modules/enet/doc_classes/ENetPacketPeer.xml @@ -18,6 +18,12 @@ Returns the number of channels allocated for communication with peer. </description> </method> + <method name="get_packet_flags" qualifiers="const"> + <return type="int" /> + <description> + Returns the ENet flags of the next packet in the received queue. See [code]FLAG_*[/code] constants for available packet flags. Note that not all flags are replicated from the sending peer to the receiving peer. + </description> + </method> <method name="get_remote_address" qualifiers="const"> <return type="String" /> <description> diff --git a/modules/enet/enet_packet_peer.cpp b/modules/enet/enet_packet_peer.cpp index edb33fc96b..9ec68465a5 100644 --- a/modules/enet/enet_packet_peer.cpp +++ b/modules/enet/enet_packet_peer.cpp @@ -175,6 +175,11 @@ int ENetPacketPeer::get_channels() const { return peer->channelCount; } +int ENetPacketPeer::get_packet_flags() const { + ERR_FAIL_COND_V(packet_queue.is_empty(), 0); + return packet_queue.front()->get()->flags; +} + void ENetPacketPeer::_on_disconnect() { if (peer) { peer->data = nullptr; @@ -206,6 +211,7 @@ void ENetPacketPeer::_bind_methods() { ClassDB::bind_method(D_METHOD("send", "channel", "packet", "flags"), &ENetPacketPeer::_send); ClassDB::bind_method(D_METHOD("throttle_configure", "interval", "acceleration", "deceleration"), &ENetPacketPeer::throttle_configure); ClassDB::bind_method(D_METHOD("set_timeout", "timeout", "timeout_min", "timeout_max"), &ENetPacketPeer::set_timeout); + ClassDB::bind_method(D_METHOD("get_packet_flags"), &ENetPacketPeer::get_packet_flags); ClassDB::bind_method(D_METHOD("get_remote_address"), &ENetPacketPeer::get_remote_address); ClassDB::bind_method(D_METHOD("get_remote_port"), &ENetPacketPeer::get_remote_port); ClassDB::bind_method(D_METHOD("get_statistic", "statistic"), &ENetPacketPeer::get_statistic); diff --git a/modules/enet/enet_packet_peer.h b/modules/enet/enet_packet_peer.h index fe40d06188..b41d67e86b 100644 --- a/modules/enet/enet_packet_peer.h +++ b/modules/enet/enet_packet_peer.h @@ -113,6 +113,7 @@ public: double get_statistic(PeerStatistic p_stat); PeerState get_state() const; int get_channels() const; + int get_packet_flags() const; // Extras IPAddress get_remote_address() const; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 73f2b1d618..0fd891aa80 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -864,7 +864,8 @@ static void _get_directory_contents(EditorFileSystemDirectory *p_dir, HashMap<St } } -static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_annotation, int p_argument, const String p_quote_style, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result) { +static void _find_annotation_arguments(const GDScriptParser::AnnotationNode *p_annotation, int p_argument, const String p_quote_style, HashMap<String, ScriptLanguage::CodeCompletionOption> &r_result, String &r_arghint) { + r_arghint = _make_arguments_hint(p_annotation->info->info, p_argument, true); if (p_annotation->name == SNAME("@export_range")) { if (p_argument == 3 || p_argument == 4 || p_argument == 5) { // Slider hint. @@ -2975,11 +2976,6 @@ static bool _get_subscript_type(GDScriptParser::CompletionContext &p_context, co } break; case GDScriptParser::Node::IDENTIFIER: { - if (p_subscript->base->datatype.type_source == GDScriptParser::DataType::ANNOTATED_EXPLICIT) { - // Annotated type takes precedence. - return false; - } - const GDScriptParser::IdentifierNode *identifier_node = static_cast<GDScriptParser::IdentifierNode *>(p_subscript->base); switch (identifier_node->source) { @@ -3017,6 +3013,14 @@ static bool _get_subscript_type(GDScriptParser::CompletionContext &p_context, co if (get_node != nullptr) { const Object *node = p_context.base->call("get_node_or_null", NodePath(get_node->full_path)); if (node != nullptr) { + GDScriptParser::DataType assigned_type = _type_from_variant(node, p_context).type; + GDScriptParser::DataType base_type = p_subscript->base->datatype; + + if (p_subscript->base->type == GDScriptParser::Node::IDENTIFIER && base_type.type_source == GDScriptParser::DataType::ANNOTATED_EXPLICIT && (assigned_type.kind != base_type.kind || assigned_type.script_path != base_type.script_path || assigned_type.native_type != base_type.native_type)) { + // Annotated type takes precedence. + return false; + } + if (r_base != nullptr) { *r_base = node; } @@ -3183,7 +3187,7 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c break; } const GDScriptParser::AnnotationNode *annotation = static_cast<const GDScriptParser::AnnotationNode *>(completion_context.node); - _find_annotation_arguments(annotation, completion_context.current_argument, quote_style, options); + _find_annotation_arguments(annotation, completion_context.current_argument, quote_style, options, r_call_hint); r_forced = true; } break; case GDScriptParser::COMPLETION_BUILT_IN_TYPE_CONSTANT_OR_STATIC_METHOD: { diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index e169566705..111a39d730 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -1640,23 +1640,29 @@ GDScriptParser::AnnotationNode *GDScriptParser::parse_annotation(uint32_t p_vali advance(); // Arguments. push_completion_call(annotation); - make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, 0); int argument_index = 0; do { + make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index); + set_last_completion_call_arg(argument_index); if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { // Allow for trailing comma. break; } - make_completion_context(COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index); - set_last_completion_call_arg(argument_index++); ExpressionNode *argument = parse_expression(false); + if (argument == nullptr) { push_error("Expected expression as the annotation argument."); valid = false; - continue; + } else { + annotation->arguments.push_back(argument); + + if (argument->type == Node::LITERAL) { + override_completion_context(argument, COMPLETION_ANNOTATION_ARGUMENTS, annotation, argument_index); + } } - annotation->arguments.push_back(argument); + + argument_index++; } while (match(GDScriptTokenizer::Token::COMMA) && !is_at_end()); pop_multiline(); diff --git a/modules/gdscript/tests/scripts/project.godot b/modules/gdscript/tests/scripts/project.godot index c9035ecab9..0757bec5c4 100644 --- a/modules/gdscript/tests/scripts/project.godot +++ b/modules/gdscript/tests/scripts/project.godot @@ -12,6 +12,6 @@ config/name="GDScript Integration Test Suite" [input] test_input_action={ -"deadzone": 0.5, +"deadzone": 0.2, "events": [] } diff --git a/modules/multiplayer/SCsub b/modules/multiplayer/SCsub index 97f91c5674..f9f4e579e8 100644 --- a/modules/multiplayer/SCsub +++ b/modules/multiplayer/SCsub @@ -13,3 +13,10 @@ if env.editor_build: env_mp.add_source_files(module_obj, "editor/*.cpp") env.modules_sources += module_obj + +if env["tests"]: + env_mp.Append(CPPDEFINES=["TESTS_ENABLED"]) + env_mp.add_source_files(env.modules_sources, "./tests/*.cpp") + + if env["disable_exceptions"]: + env_mp.Append(CPPDEFINES=["DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"]) diff --git a/modules/multiplayer/doc_classes/SceneMultiplayer.xml b/modules/multiplayer/doc_classes/SceneMultiplayer.xml index 42f32d4848..3277f1ff3e 100644 --- a/modules/multiplayer/doc_classes/SceneMultiplayer.xml +++ b/modules/multiplayer/doc_classes/SceneMultiplayer.xml @@ -65,7 +65,7 @@ [b]Warning:[/b] Deserialized objects can contain code which gets executed. Do not use this option if the serialized object comes from untrusted sources to avoid potential security threat such as remote code execution. </member> <member name="auth_callback" type="Callable" setter="set_auth_callback" getter="get_auth_callback" default="Callable()"> - The callback to execute when when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. + The callback to execute when receiving authentication data sent via [method send_auth]. If the [Callable] is empty (default), peers will be automatically accepted as soon as they connect. </member> <member name="auth_timeout" type="float" setter="set_auth_timeout" getter="get_auth_timeout" default="3.0"> If set to a value greater than [code]0.0[/code], the maximum duration in seconds peers can stay in the authenticating state, after which the authentication will automatically fail. See the [signal peer_authenticating] and [signal peer_authentication_failed] signals. diff --git a/modules/multiplayer/tests/test_scene_multiplayer.h b/modules/multiplayer/tests/test_scene_multiplayer.h new file mode 100644 index 0000000000..5e526c9be6 --- /dev/null +++ b/modules/multiplayer/tests/test_scene_multiplayer.h @@ -0,0 +1,284 @@ +/**************************************************************************/ +/* test_scene_multiplayer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_SCENE_MULTIPLAYER_H +#define TEST_SCENE_MULTIPLAYER_H + +#include "tests/test_macros.h" +#include "tests/test_utils.h" + +#include "../scene_multiplayer.h" + +namespace TestSceneMultiplayer { + +static inline Array build_array() { + return Array(); +} +template <typename... Targs> +static inline Array build_array(Variant item, Targs... Fargs) { + Array a = build_array(Fargs...); + a.push_front(item); + return a; +} + +TEST_CASE("[Multiplayer][SceneMultiplayer] Defaults") { + Ref<SceneMultiplayer> scene_multiplayer; + scene_multiplayer.instantiate(); + + REQUIRE(scene_multiplayer->has_multiplayer_peer()); + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + REQUIRE_MESSAGE(Object::cast_to<OfflineMultiplayerPeer>(multiplayer_peer.ptr()) != nullptr, "By default it must be an OfflineMultiplayerPeer instance."); + CHECK_EQ(scene_multiplayer->poll(), Error::OK); + CHECK_EQ(scene_multiplayer->get_unique_id(), MultiplayerPeer::TARGET_PEER_SERVER); + CHECK_EQ(scene_multiplayer->get_peer_ids(), Vector<int>()); + CHECK_EQ(scene_multiplayer->get_remote_sender_id(), 0); + CHECK_EQ(scene_multiplayer->get_root_path(), NodePath()); + CHECK(scene_multiplayer->get_connected_peers().is_empty()); + CHECK_FALSE(scene_multiplayer->is_refusing_new_connections()); + CHECK_FALSE(scene_multiplayer->is_object_decoding_allowed()); + CHECK(scene_multiplayer->is_server_relay_enabled()); + CHECK_EQ(scene_multiplayer->get_max_sync_packet_size(), 1350); + CHECK_EQ(scene_multiplayer->get_max_delta_packet_size(), 65535); + CHECK(scene_multiplayer->is_server()); +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] SceneTree has a OfflineMultiplayerPeer by default") { + Ref<SceneMultiplayer> scene_multiplayer = SceneTree::get_singleton()->get_multiplayer(); + REQUIRE(scene_multiplayer->has_multiplayer_peer()); + + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + REQUIRE_MESSAGE(Object::cast_to<OfflineMultiplayerPeer>(multiplayer_peer.ptr()) != nullptr, "By default it must be an OfflineMultiplayerPeer instance."); +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Object configuration add/remove") { + Ref<SceneMultiplayer> scene_multiplayer; + scene_multiplayer.instantiate(); + + SUBCASE("Returns invalid parameter") { + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, "ImInvalid"), Error::ERR_INVALID_PARAMETER); + CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, "ImInvalid"), Error::ERR_INVALID_PARAMETER); + + NodePath foo_path("/Foo"); + NodePath bar_path("/Bar"); + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK); + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, bar_path), Error::ERR_INVALID_PARAMETER); + ERR_PRINT_ON; + } + + SUBCASE("Sets root path") { + NodePath foo_path("/Foo"); + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK); + + CHECK_EQ(scene_multiplayer->get_root_path(), foo_path); + } + + SUBCASE("Unsets root path") { + NodePath foo_path("/Foo"); + CHECK_EQ(scene_multiplayer->object_configuration_add(nullptr, foo_path), Error::OK); + + CHECK_EQ(scene_multiplayer->object_configuration_remove(nullptr, foo_path), Error::OK); + CHECK_EQ(scene_multiplayer->get_root_path(), NodePath()); + } + + SUBCASE("Add/Remove a MultiplayerSpawner") { + Node2D *node = memnew(Node2D); + MultiplayerSpawner *spawner = memnew(MultiplayerSpawner); + + CHECK_EQ(scene_multiplayer->object_configuration_add(node, spawner), Error::OK); + CHECK_EQ(scene_multiplayer->object_configuration_remove(node, spawner), Error::OK); + + memdelete(spawner); + memdelete(node); + } + + SUBCASE("Add/Remove a MultiplayerSynchronizer") { + Node2D *node = memnew(Node2D); + MultiplayerSynchronizer *synchronizer = memnew(MultiplayerSynchronizer); + + CHECK_EQ(scene_multiplayer->object_configuration_add(node, synchronizer), Error::OK); + CHECK_EQ(scene_multiplayer->object_configuration_remove(node, synchronizer), Error::OK); + + memdelete(synchronizer); + memdelete(node); + } +} + +TEST_CASE("[Multiplayer][SceneMultiplayer] Root Path") { + Ref<SceneMultiplayer> scene_multiplayer; + scene_multiplayer.instantiate(); + + SUBCASE("Is set") { + NodePath foo_path("/Foo"); + scene_multiplayer->set_root_path(foo_path); + + CHECK_EQ(scene_multiplayer->get_root_path(), foo_path); + } + + SUBCASE("Fails when path is empty") { + ERR_PRINT_OFF; + scene_multiplayer->set_root_path(NodePath()); + ERR_PRINT_ON; + } + + SUBCASE("Fails when path is relative") { + NodePath foo_path("Foo"); + ERR_PRINT_OFF; + scene_multiplayer->set_root_path(foo_path); + ERR_PRINT_ON; + + CHECK_EQ(scene_multiplayer->get_root_path(), NodePath()); + } +} + +// This one could be a dummy callback because the current set of test is not actually testing the full auth flow. +static Variant auth_callback(Variant sv, Variant pvav) { + return Variant(); +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Send Authentication") { + Ref<SceneMultiplayer> scene_multiplayer; + scene_multiplayer.instantiate(); + SceneTree::get_singleton()->set_multiplayer(scene_multiplayer); + scene_multiplayer->set_auth_callback(callable_mp_static(auth_callback)); + + SUBCASE("Is properly sent") { + SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authenticating"); + + // Adding a peer to MultiplayerPeer. + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + SIGNAL_CHECK("peer_authenticating", build_array(build_array(peer_id))); + + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK); + + Vector<int> expected_peer_ids = { peer_id }; + CHECK_EQ(scene_multiplayer->get_authenticating_peer_ids(), expected_peer_ids); + + SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authenticating"); + } + + SUBCASE("peer_authentication_failed is emitted when a peer is deleted before authentication is completed") { + SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + + // Adding a peer to MultiplayerPeer. + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + multiplayer_peer->emit_signal(SNAME("peer_disconnected"), peer_id); + SIGNAL_CHECK("peer_authentication_failed", build_array(build_array(peer_id))); + + SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + } + + SUBCASE("peer_authentication_failed is emitted when authentication timeout") { + SIGNAL_WATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + scene_multiplayer->set_auth_timeout(0.01); + CHECK_EQ(scene_multiplayer->get_auth_timeout(), 0.01); + + // Adding two peesr to MultiplayerPeer. + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int first_peer_id = 42; + int second_peer_id = 84; + multiplayer_peer->emit_signal(SNAME("peer_connected"), first_peer_id); + multiplayer_peer->emit_signal(SNAME("peer_connected"), second_peer_id); + + // Let timeout happens. + OS::get_singleton()->delay_usec(500000); + + CHECK_EQ(scene_multiplayer->poll(), Error::OK); + + SIGNAL_CHECK("peer_authentication_failed", build_array(build_array(first_peer_id), build_array(second_peer_id))); + + SIGNAL_UNWATCH(scene_multiplayer.ptr(), "peer_authentication_failed"); + } + + SUBCASE("Fails when there is no MultiplayerPeer configured") { + scene_multiplayer->set_multiplayer_peer(nullptr); + + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->send_auth(42, Vector<uint8_t>()), Error::ERR_UNCONFIGURED); + ERR_PRINT_ON; + } + + SUBCASE("Fails when the peer to send the auth is not pending") { + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->send_auth(42, String("It's me").to_ascii_buffer()), Error::ERR_INVALID_PARAMETER); + ERR_PRINT_ON; + } +} + +TEST_CASE("[Multiplayer][SceneMultiplayer][SceneTree] Complete Authentication") { + Ref<SceneMultiplayer> scene_multiplayer; + scene_multiplayer.instantiate(); + SceneTree::get_singleton()->set_multiplayer(scene_multiplayer); + scene_multiplayer->set_auth_callback(callable_mp_static(auth_callback)); + + SUBCASE("Is properly completed") { + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK); + + CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::OK); + } + + SUBCASE("Fails when there is no MultiplayerPeer configured") { + scene_multiplayer->set_multiplayer_peer(nullptr); + + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->complete_auth(42), Error::ERR_UNCONFIGURED); + ERR_PRINT_ON; + } + + SUBCASE("Fails when the peer to complete the auth is not pending") { + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->complete_auth(42), Error::ERR_INVALID_PARAMETER); + ERR_PRINT_ON; + } + + SUBCASE("Fails to send auth or completed for a second time") { + Ref<MultiplayerPeer> multiplayer_peer = scene_multiplayer->get_multiplayer_peer(); + int peer_id = 42; + multiplayer_peer->emit_signal(SNAME("peer_connected"), peer_id); + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::OK); + CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::OK); + + ERR_PRINT_OFF; + CHECK_EQ(scene_multiplayer->send_auth(peer_id, String("It's me").to_ascii_buffer()), Error::ERR_FILE_CANT_WRITE); + CHECK_EQ(scene_multiplayer->complete_auth(peer_id), Error::ERR_FILE_CANT_WRITE); + ERR_PRINT_ON; + } +} + +} // namespace TestSceneMultiplayer + +#endif // TEST_SCENE_MULTIPLAYER_H diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 3322300dda..1c6e62650a 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -442,6 +442,8 @@ bool TextServerAdvanced::_load_support_data(const String &p_filename) { } #else if (!icu_data_loaded) { + UErrorCode err = U_ZERO_ERROR; +#ifdef ICU_DATA_NAME String filename = (p_filename.is_empty()) ? String("res://") + _MKSTR(ICU_DATA_NAME) : p_filename; Ref<FileAccess> f = FileAccess::open(filename, FileAccess::READ); @@ -451,13 +453,13 @@ bool TextServerAdvanced::_load_support_data(const String &p_filename) { uint64_t len = f->get_length(); icu_data = f->get_buffer(len); - UErrorCode err = U_ZERO_ERROR; udata_setCommonData(icu_data.ptr(), &err); if (U_FAILURE(err)) { ERR_FAIL_V_MSG(false, u_errorName(err)); } err = U_ZERO_ERROR; +#endif u_init(&err); if (U_FAILURE(err)) { ERR_FAIL_V_MSG(false, u_errorName(err)); @@ -1357,7 +1359,7 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_glyph(FontAdvanced *p_font_data, return false; } -_FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size, FontForSizeAdvanced *&r_cache_for_size) const { +_FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size, FontForSizeAdvanced *&r_cache_for_size, bool p_silent) const { ERR_FAIL_COND_V(p_size.x <= 0, false); HashMap<Vector2i, FontForSizeAdvanced *>::Iterator E = p_font_data->cache.find(p_size); @@ -1378,7 +1380,11 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f error = FT_Init_FreeType(&ft_library); if (error != 0) { memdelete(fd); - ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + if (p_silent) { + return false; + } else { + ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + } } #ifdef MODULE_SVG_ENABLED FT_Property_Set(ft_library, "ot-svg", "svg-hooks", get_tvg_svg_in_ot_hooks()); @@ -1412,7 +1418,11 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f FT_Done_Face(fd->face); fd->face = nullptr; memdelete(fd); - ERR_FAIL_V_MSG(false, "FreeType: Error loading font: '" + String(FT_Error_String(error)) + "'."); + if (p_silent) { + return false; + } else { + ERR_FAIL_V_MSG(false, "FreeType: Error loading font: '" + String(FT_Error_String(error)) + "'."); + } } } @@ -1847,7 +1857,11 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f } #else memdelete(fd); - ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); + if (p_silent) { + return false; + } else { + ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); + } #endif } else { // Init bitmap font. @@ -1858,6 +1872,16 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f return true; } +_FORCE_INLINE_ bool TextServerAdvanced::_font_validate(const RID &p_font_rid) const { + FontAdvanced *fd = _get_font_data(p_font_rid); + ERR_FAIL_NULL_V(fd, false); + + MutexLock lock(fd->mutex); + Vector2i size = _get_size(fd, 16); + FontForSizeAdvanced *ffsd = nullptr; + return _ensure_cache_for_size(fd, size, ffsd, true); +} + _FORCE_INLINE_ void TextServerAdvanced::_font_clear_cache(FontAdvanced *p_font_data) { MutexLock ftlock(ft_mutex); @@ -5106,6 +5130,10 @@ RID TextServerAdvanced::_find_sys_font_for_text(const RID &p_fdef, const String SystemFontCacheRec sysf; sysf.rid = _create_font(); _font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size()); + if (!_font_validate(sysf.rid)) { + _free_rid(sysf.rid); + continue; + } Dictionary var = dvar; // Select matching style from collection. diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index c63389b1c6..9c8d75b358 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -365,7 +365,8 @@ class TextServerAdvanced : public TextServerExtension { _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeAdvanced *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const; #endif _FORCE_INLINE_ bool _ensure_glyph(FontAdvanced *p_font_data, const Vector2i &p_size, int32_t p_glyph, FontGlyph &r_glyph) const; - _FORCE_INLINE_ bool _ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size, FontForSizeAdvanced *&r_cache_for_size) const; + _FORCE_INLINE_ bool _ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size, FontForSizeAdvanced *&r_cache_for_size, bool p_silent = false) const; + _FORCE_INLINE_ bool _font_validate(const RID &p_font_rid) const; _FORCE_INLINE_ void _font_clear_cache(FontAdvanced *p_font_data); static void _generateMTSDF_threaded(void *p_td, uint32_t p_y); diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 540ba19cac..ce95622f09 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -791,7 +791,7 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_glyph(FontFallback *p_font_data, return false; } -_FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size, FontForSizeFallback *&r_cache_for_size) const { +_FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size, FontForSizeFallback *&r_cache_for_size, bool p_silent) const { ERR_FAIL_COND_V(p_size.x <= 0, false); HashMap<Vector2i, FontForSizeFallback *>::Iterator E = p_font_data->cache.find(p_size); @@ -813,7 +813,11 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f error = FT_Init_FreeType(&ft_library); if (error != 0) { memdelete(fd); - ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + if (p_silent) { + return false; + } else { + ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); + } } #ifdef MODULE_SVG_ENABLED FT_Property_Set(ft_library, "ot-svg", "svg-hooks", get_tvg_svg_in_ot_hooks()); @@ -847,7 +851,11 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f FT_Done_Face(fd->face); fd->face = nullptr; memdelete(fd); - ERR_FAIL_V_MSG(false, "FreeType: Error loading font: '" + String(FT_Error_String(error)) + "'."); + if (p_silent) { + return false; + } else { + ERR_FAIL_V_MSG(false, "FreeType: Error loading font: '" + String(FT_Error_String(error)) + "'."); + } } } @@ -980,7 +988,11 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f } #else memdelete(fd); - ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); + if (p_silent) { + return false; + } else { + ERR_FAIL_V_MSG(false, "FreeType: Can't load dynamic font, engine is compiled without FreeType support!"); + } #endif } @@ -989,6 +1001,16 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f return true; } +_FORCE_INLINE_ bool TextServerFallback::_font_validate(const RID &p_font_rid) const { + FontFallback *fd = _get_font_data(p_font_rid); + ERR_FAIL_NULL_V(fd, false); + + MutexLock lock(fd->mutex); + Vector2i size = _get_size(fd, 16); + FontForSizeFallback *ffsd = nullptr; + return _ensure_cache_for_size(fd, size, ffsd, true); +} + _FORCE_INLINE_ void TextServerFallback::_font_clear_cache(FontFallback *p_font_data) { MutexLock ftlock(ft_mutex); @@ -3920,6 +3942,10 @@ RID TextServerFallback::_find_sys_font_for_text(const RID &p_fdef, const String SystemFontCacheRec sysf; sysf.rid = _create_font(); _font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size()); + if (!_font_validate(sysf.rid)) { + _free_rid(sysf.rid); + continue; + } Dictionary var = dvar; // Select matching style from collection. diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 7f12ad593b..56626c1f6c 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -314,7 +314,8 @@ class TextServerFallback : public TextServerExtension { _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeFallback *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const; #endif _FORCE_INLINE_ bool _ensure_glyph(FontFallback *p_font_data, const Vector2i &p_size, int32_t p_glyph, FontGlyph &r_glyph) const; - _FORCE_INLINE_ bool _ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size, FontForSizeFallback *&r_cache_for_size) const; + _FORCE_INLINE_ bool _ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size, FontForSizeFallback *&r_cache_for_size, bool p_silent = false) const; + _FORCE_INLINE_ bool _font_validate(const RID &p_font_rid) const; _FORCE_INLINE_ void _font_clear_cache(FontFallback *p_font_data); static void _generateMTSDF_threaded(void *p_td, uint32_t p_y); diff --git a/modules/websocket/editor/editor_debugger_server_websocket.cpp b/modules/websocket/editor/editor_debugger_server_websocket.cpp index a28fc53440..344a0356c5 100644 --- a/modules/websocket/editor/editor_debugger_server_websocket.cpp +++ b/modules/websocket/editor/editor_debugger_server_websocket.cpp @@ -77,8 +77,8 @@ Error EditorDebuggerServerWebSocket::start(const String &p_uri) { // Optionally override if (!p_uri.is_empty() && p_uri != "ws://") { - String scheme, path; - Error err = p_uri.parse_url(scheme, bind_host, bind_port, path); + String scheme, path, fragment; + Error err = p_uri.parse_url(scheme, bind_host, bind_port, path, fragment); ERR_FAIL_COND_V(err != OK, ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(!bind_host.is_valid_ip_address() && bind_host != "*", ERR_INVALID_PARAMETER); } diff --git a/modules/websocket/emws_peer.cpp b/modules/websocket/emws_peer.cpp index 03a530909b..c5768c9f0b 100644 --- a/modules/websocket/emws_peer.cpp +++ b/modules/websocket/emws_peer.cpp @@ -68,8 +68,9 @@ Error EMWSPeer::connect_to_url(const String &p_url, Ref<TLSOptions> p_tls_option String host; String path; String scheme; + String fragment; int port = 0; - Error err = p_url.parse_url(scheme, host, port, path); + Error err = p_url.parse_url(scheme, host, port, path, fragment); ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); if (scheme.is_empty()) { diff --git a/modules/websocket/wsl_peer.cpp b/modules/websocket/wsl_peer.cpp index 0a9a4053e3..0c0a046805 100644 --- a/modules/websocket/wsl_peer.cpp +++ b/modules/websocket/wsl_peer.cpp @@ -482,8 +482,9 @@ Error WSLPeer::connect_to_url(const String &p_url, Ref<TLSOptions> p_options) { String host; String path; String scheme; + String fragment; int port = 0; - Error err = p_url.parse_url(scheme, host, port, path); + Error err = p_url.parse_url(scheme, host, port, path, fragment); ERR_FAIL_COND_V_MSG(err != OK, err, "Invalid URL: " + p_url); if (scheme.is_empty()) { scheme = "ws://"; diff --git a/modules/webxr/doc_classes/WebXRInterface.xml b/modules/webxr/doc_classes/WebXRInterface.xml index bd7192520a..829279e7bb 100644 --- a/modules/webxr/doc_classes/WebXRInterface.xml +++ b/modules/webxr/doc_classes/WebXRInterface.xml @@ -283,7 +283,7 @@ </signals> <constants> <constant name="TARGET_RAY_MODE_UNKNOWN" value="0" enum="TargetRayMode"> - We don't know the the target ray mode. + We don't know the target ray mode. </constant> <constant name="TARGET_RAY_MODE_GAZE" value="1" enum="TargetRayMode"> Target ray originates at the viewer's eyes and points in the direction they are looking. diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index 6920f801e5..078b9ab748 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -41,13 +41,11 @@ static JavaClassWrapper *java_class_wrapper = nullptr; void register_android_api() { #if !defined(ANDROID_ENABLED) - // On Android platforms, the `java_class_wrapper` instantiation and the - // `JNISingleton` registration occurs in + // On Android platforms, the `java_class_wrapper` instantiation occurs in // `platform/android/java_godot_lib_jni.cpp#Java_org_godotengine_godot_GodotLib_setup` - java_class_wrapper = memnew(JavaClassWrapper); // Dummy - GDREGISTER_CLASS(JNISingleton); + java_class_wrapper = memnew(JavaClassWrapper); #endif - + GDREGISTER_CLASS(JNISingleton); GDREGISTER_CLASS(JavaClass); GDREGISTER_CLASS(JavaObject); GDREGISTER_CLASS(JavaClassWrapper); @@ -108,7 +106,7 @@ Ref<JavaClass> JavaObject::get_java_class() const { JavaClassWrapper *JavaClassWrapper::singleton = nullptr; -Ref<JavaClass> JavaClassWrapper::wrap(const String &) { +Ref<JavaClass> JavaClassWrapper::_wrap(const String &, bool) { return Ref<JavaClass>(); } diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index 71f9c32318..c74cef8dd0 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -262,6 +262,8 @@ class JavaClassWrapper : public Object { bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig); #endif + Ref<JavaClass> _wrap(const String &p_class, bool p_allow_private_methods_access); + static JavaClassWrapper *singleton; protected: @@ -270,15 +272,14 @@ protected: public: static JavaClassWrapper *get_singleton() { return singleton; } - Ref<JavaClass> wrap(const String &p_class); + Ref<JavaClass> wrap(const String &p_class) { + return _wrap(p_class, false); + } #ifdef ANDROID_ENABLED - Ref<JavaClass> wrap_jclass(jclass p_class); - - JavaClassWrapper(jobject p_activity = nullptr); -#else - JavaClassWrapper(); + Ref<JavaClass> wrap_jclass(jclass p_class, bool p_allow_private_methods_access = false); #endif + JavaClassWrapper(); }; #endif // JAVA_CLASS_WRAPPER_H diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index 06afc4eb78..5e940819bc 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -31,193 +31,53 @@ #ifndef JNI_SINGLETON_H #define JNI_SINGLETON_H +#include "java_class_wrapper.h" + #include "core/config/engine.h" #include "core/variant/variant.h" -#ifdef ANDROID_ENABLED -#include "jni_utils.h" -#endif - class JNISingleton : public Object { GDCLASS(JNISingleton, Object); -#ifdef ANDROID_ENABLED struct MethodData { - jmethodID method; Variant::Type ret_type; Vector<Variant::Type> argtypes; }; - jobject instance; RBMap<StringName, MethodData> method_map; -#endif + Ref<JavaObject> wrapped_object; public: virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override { -#ifdef ANDROID_ENABLED - RBMap<StringName, MethodData>::Element *E = method_map.find(p_method); - - // Check the method we're looking for is in the JNISingleton map and that - // the arguments match. - bool call_error = !E || E->get().argtypes.size() != p_argcount; - if (!call_error) { - for (int i = 0; i < p_argcount; i++) { - if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) { - call_error = true; - break; + if (wrapped_object.is_valid()) { + RBMap<StringName, MethodData>::Element *E = method_map.find(p_method); + + // Check the method we're looking for is in the JNISingleton map and that + // the arguments match. + bool call_error = !E || E->get().argtypes.size() != p_argcount; + if (!call_error) { + for (int i = 0; i < p_argcount; i++) { + if (!Variant::can_convert(p_args[i]->get_type(), E->get().argtypes[i])) { + call_error = true; + break; + } } } - } - - if (call_error) { - // The method is not in this map, defaulting to the regular instance calls. - return Object::callp(p_method, p_args, p_argcount, r_error); - } - - ERR_FAIL_NULL_V(instance, Variant()); - - r_error.error = Callable::CallError::CALL_OK; - - jvalue *v = nullptr; - if (p_argcount) { - v = (jvalue *)alloca(sizeof(jvalue) * p_argcount); - } - - JNIEnv *env = get_jni_env(); - - int res = env->PushLocalFrame(16); - - ERR_FAIL_COND_V(res != 0, Variant()); - - List<jobject> to_erase; - for (int i = 0; i < p_argcount; i++) { - jvalret vr = _variant_to_jvalue(env, E->get().argtypes[i], p_args[i]); - v[i] = vr.val; - if (vr.obj) { - to_erase.push_back(vr.obj); + if (!call_error) { + return wrapped_object->callp(p_method, p_args, p_argcount, r_error); } } - Variant ret; - - switch (E->get().ret_type) { - case Variant::NIL: { - env->CallVoidMethodA(instance, E->get().method, v); - } break; - case Variant::BOOL: { - ret = env->CallBooleanMethodA(instance, E->get().method, v) == JNI_TRUE; - } break; - case Variant::INT: { - ret = env->CallIntMethodA(instance, E->get().method, v); - } break; - case Variant::FLOAT: { - ret = env->CallFloatMethodA(instance, E->get().method, v); - } break; - case Variant::STRING: { - jobject o = env->CallObjectMethodA(instance, E->get().method, v); - ret = jstring_to_string((jstring)o, env); - env->DeleteLocalRef(o); - } break; - case Variant::PACKED_STRING_ARRAY: { - jobjectArray arr = (jobjectArray)env->CallObjectMethodA(instance, E->get().method, v); - - ret = _jobject_to_variant(env, arr); - - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_INT32_ARRAY: { - jintArray arr = (jintArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector<int> sarr; - sarr.resize(fCount); - - int *w = sarr.ptrw(); - env->GetIntArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_INT64_ARRAY: { - jlongArray arr = (jlongArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector<int64_t> sarr; - sarr.resize(fCount); - - int64_t *w = sarr.ptrw(); - env->GetLongArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_FLOAT32_ARRAY: { - jfloatArray arr = (jfloatArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector<float> sarr; - sarr.resize(fCount); - - float *w = sarr.ptrw(); - env->GetFloatArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::PACKED_FLOAT64_ARRAY: { - jdoubleArray arr = (jdoubleArray)env->CallObjectMethodA(instance, E->get().method, v); - - int fCount = env->GetArrayLength(arr); - Vector<double> sarr; - sarr.resize(fCount); - - double *w = sarr.ptrw(); - env->GetDoubleArrayRegion(arr, 0, fCount, w); - ret = sarr; - env->DeleteLocalRef(arr); - } break; - case Variant::DICTIONARY: { - jobject obj = env->CallObjectMethodA(instance, E->get().method, v); - ret = _jobject_to_variant(env, obj); - env->DeleteLocalRef(obj); - - } break; - case Variant::OBJECT: { - jobject obj = env->CallObjectMethodA(instance, E->get().method, v); - ret = _jobject_to_variant(env, obj); - env->DeleteLocalRef(obj); - } break; - default: { - env->PopLocalFrame(nullptr); - ERR_FAIL_V(Variant()); - } break; - } - - while (to_erase.size()) { - env->DeleteLocalRef(to_erase.front()->get()); - to_erase.pop_front(); - } - - env->PopLocalFrame(nullptr); - - return ret; -#else // ANDROID_ENABLED - - // Defaulting to the regular instance calls. return Object::callp(p_method, p_args, p_argcount, r_error); -#endif } -#ifdef ANDROID_ENABLED - jobject get_instance() const { - return instance; + Ref<JavaObject> get_wrapped_object() const { + return wrapped_object; } - void set_instance(jobject p_instance) { - instance = p_instance; - } - - void add_method(const StringName &p_name, jmethodID p_method, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) { + void add_method(const StringName &p_name, const Vector<Variant::Type> &p_args, Variant::Type p_ret_type) { MethodData md; - md.method = p_method; md.argtypes = p_args; md.ret_type = p_ret_type; method_map[p_name] = md; @@ -232,24 +92,15 @@ public: ADD_SIGNAL(mi); } -#endif + JNISingleton() {} - JNISingleton() { -#ifdef ANDROID_ENABLED - instance = nullptr; -#endif + JNISingleton(const Ref<JavaObject> &p_wrapped_object) { + wrapped_object = p_wrapped_object; } ~JNISingleton() { -#ifdef ANDROID_ENABLED method_map.clear(); - if (instance) { - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL(env); - - env->DeleteGlobalRef(instance); - } -#endif + wrapped_object.unref(); } }; diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index cfd258cddc..41f460ca8f 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -3263,8 +3263,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP cmdline.push_back(apk_build_command); } + String addons_directory = ProjectSettings::get_singleton()->globalize_path("res://addons"); + cmdline.push_back("-p"); // argument to specify the start directory. cmdline.push_back(build_path); // start directory. + cmdline.push_back("-Paddons_directory=" + addons_directory); // path to the addon directory as it may contain jar or aar dependencies cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code. cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name. diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index fdc5753798..308f126d5d 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -63,6 +63,12 @@ dependencies { implementation files(pluginsBinaries) } + // Automatically pick up local dependencies in res://addons + String addonsDirectory = getAddonsDirectory() + if (addonsDirectory != null && !addonsDirectory.isBlank()) { + implementation fileTree(dir: "$addonsDirectory", include: ['*.jar', '*.aar']) + } + // .NET dependencies String jar = '../../../../modules/mono/thirdparty/libSystem.Security.Cryptography.Native.Android.jar' if (file(jar).exists()) { diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index 597a4d5c14..e8921e1bb1 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -408,3 +408,8 @@ ext.shouldUseLegacyPackaging = { -> // Default behavior for minSdk >= 23 return false } + +ext.getAddonsDirectory = { -> + String addonsDirectory = project.hasProperty("addons_directory") ? project.property("addons_directory") : "" + return addonsDirectory +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index 5b1d09e749..567b134234 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -58,6 +58,8 @@ import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler +import org.godotengine.godot.plugin.AndroidRuntimePlugin +import org.godotengine.godot.plugin.GodotPlugin import org.godotengine.godot.plugin.GodotPluginRegistry import org.godotengine.godot.tts.GodotTTS import org.godotengine.godot.utils.CommandLineFileParser @@ -228,7 +230,9 @@ class Godot(private val context: Context) { window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) Log.v(TAG, "Initializing Godot plugin registry") - GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this)) + val runtimePlugins = mutableSetOf<GodotPlugin>(AndroidRuntimePlugin(this)) + runtimePlugins.addAll(primaryHost.getHostPlugins(this)) + GodotPluginRegistry.initializePluginRegistry(this, runtimePlugins) if (io == null) { io = GodotIO(activity) } diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt b/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt new file mode 100644 index 0000000000..edb4e7c357 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt @@ -0,0 +1,63 @@ +/**************************************************************************/ +/* AndroidRuntimePlugin.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.plugin + +import org.godotengine.godot.Godot + +/** + * Provides access to the Android runtime capabilities. + * + * For example, from gdscript, developers can use [getApplicationContext] to access system services + * and check if the device supports vibration. + * + * var android_runtime = Engine.get_singleton("AndroidRuntime") + * if android_runtime: + * print("Checking if the device supports vibration") + * var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator") + * if vibrator_service: + * if vibrator_service.hasVibrator(): + * print("Vibration is supported on device!") + * else: + * printerr("Vibration is not supported on device") + * else: + * printerr("Unable to retrieve the vibrator service") + * else: + * printerr("Couldn't find AndroidRuntime singleton") + */ +class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { + override fun getPluginName() = "AndroidRuntime" + + @UsedByGodot + fun getApplicationContext() = activity?.applicationContext + + @UsedByGodot + override fun getActivity() = super.getActivity() +} diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index c92717e922..6bedbfd157 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -1120,7 +1120,7 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va return false; } -Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { +Ref<JavaClass> JavaClassWrapper::_wrap(const String &p_class, bool p_allow_private_methods_access) { String class_name_dots = p_class.replace("/", "."); if (class_cache.has(class_name_dots)) { return class_cache[class_name_dots]; @@ -1175,7 +1175,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { jint mods = env->CallIntMethod(obj, is_constructor ? Constructor_getModifiers : Method_getModifiers); - if (!(mods & 0x0001)) { + if (!(mods & 0x0001) && (is_constructor || !p_allow_private_methods_access)) { env->DeleteLocalRef(obj); continue; //not public bye } @@ -1336,7 +1336,7 @@ Ref<JavaClass> JavaClassWrapper::wrap(const String &p_class) { return java_class; } -Ref<JavaClass> JavaClassWrapper::wrap_jclass(jclass p_class) { +Ref<JavaClass> JavaClassWrapper::wrap_jclass(jclass p_class, bool p_allow_private_methods_access) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, Ref<JavaClass>()); @@ -1344,12 +1344,12 @@ Ref<JavaClass> JavaClassWrapper::wrap_jclass(jclass p_class) { String class_name_string = jstring_to_string(class_name, env); env->DeleteLocalRef(class_name); - return wrap(class_name_string); + return _wrap(class_name_string, p_allow_private_methods_access); } JavaClassWrapper *JavaClassWrapper::singleton = nullptr; -JavaClassWrapper::JavaClassWrapper(jobject p_activity) { +JavaClassWrapper::JavaClassWrapper() { singleton = this; JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 6086f67a1e..1a256959cd 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -32,7 +32,6 @@ #include "android_input_handler.h" #include "api/java_class_wrapper.h" -#include "api/jni_singleton.h" #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" @@ -209,8 +208,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env TTS_Android::setup(p_godot_tts); - java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity())); - GDREGISTER_CLASS(JNISingleton); + java_class_wrapper = memnew(JavaClassWrapper); return true; } diff --git a/platform/android/plugin/godot_plugin_jni.cpp b/platform/android/plugin/godot_plugin_jni.cpp index 75c8dd9528..acb18cc5c5 100644 --- a/platform/android/plugin/godot_plugin_jni.cpp +++ b/platform/android/plugin/godot_plugin_jni.cpp @@ -30,6 +30,7 @@ #include "godot_plugin_jni.h" +#include "api/java_class_wrapper.h" #include "api/jni_singleton.h" #include "jni_utils.h" #include "string_android.h" @@ -57,11 +58,15 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeR ERR_FAIL_COND_V(jni_singletons.has(singname), false); - JNISingleton *s = (JNISingleton *)ClassDB::instantiate("JNISingleton"); - s->set_instance(env->NewGlobalRef(obj)); - jni_singletons[singname] = s; + jclass java_class = env->GetObjectClass(obj); + Ref<JavaClass> java_class_wrapped = JavaClassWrapper::get_singleton()->wrap_jclass(java_class, true); + env->DeleteLocalRef(java_class); - Engine::get_singleton()->add_singleton(Engine::Singleton(singname, s)); + Ref<JavaObject> plugin_object = memnew(JavaObject(java_class_wrapped, obj)); + JNISingleton *plugin_singleton = memnew(JNISingleton(plugin_object)); + jni_singletons[singname] = plugin_singleton; + + Engine::get_singleton()->add_singleton(Engine::Singleton(singname, plugin_singleton)); return true; } @@ -75,7 +80,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis String mname = jstring_to_string(name, env); String retval = jstring_to_string(ret, env); Vector<Variant::Type> types; - String cs = "("; int stringCount = env->GetArrayLength(args); @@ -83,18 +87,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis jstring string = (jstring)env->GetObjectArrayElement(args, i); const String rawString = jstring_to_string(string, env); types.push_back(get_jni_type(rawString)); - cs += get_jni_sig(rawString); - } - - cs += ")"; - cs += get_jni_sig(retval); - jclass cls = env->GetObjectClass(s->get_instance()); - jmethodID mid = env->GetMethodID(cls, mname.ascii().get_data(), cs.ascii().get_data()); - if (!mid) { - print_line("Failed getting method ID " + mname); } - s->add_method(mname, mid, types, get_jni_type(retval)); + s->add_method(mname, types, get_jni_type(retval)); } JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types) { diff --git a/platform/android/rendering_context_driver_vulkan_android.cpp b/platform/android/rendering_context_driver_vulkan_android.cpp index a306a121f8..51fb1ca18f 100644 --- a/platform/android/rendering_context_driver_vulkan_android.cpp +++ b/platform/android/rendering_context_driver_vulkan_android.cpp @@ -32,11 +32,7 @@ #ifdef VULKAN_ENABLED -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanAndroid::_get_platform_surface_extension() const { return VK_KHR_ANDROID_SURFACE_EXTENSION_NAME; diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index bbb758074d..0631b50f0a 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -41,11 +41,7 @@ #if defined(VULKAN_ENABLED) #import "rendering_context_driver_vulkan_ios.h" -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" #endif // VULKAN_ENABLED #if defined(METAL_ENABLED) diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 35b87ea647..590238be77 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -56,11 +56,7 @@ #import <QuartzCore/CAMetalLayer.h> #if defined(VULKAN_ENABLED) -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" #endif // VULKAN_ENABLED #endif diff --git a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp index 0417ba95eb..8abcc464ba 100644 --- a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp +++ b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp @@ -32,11 +32,7 @@ #include "rendering_context_driver_vulkan_wayland.h" -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanWayland::_get_platform_surface_extension() const { return VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME; diff --git a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp index 3f505d000c..cbcf07852b 100644 --- a/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp +++ b/platform/linuxbsd/x11/rendering_context_driver_vulkan_x11.cpp @@ -32,11 +32,7 @@ #include "rendering_context_driver_vulkan_x11.h" -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanX11::_get_platform_surface_extension() const { return VK_KHR_XLIB_SURFACE_EXTENSION_NAME; diff --git a/platform/windows/rendering_context_driver_vulkan_windows.cpp b/platform/windows/rendering_context_driver_vulkan_windows.cpp index 445388af89..8ca677fe64 100644 --- a/platform/windows/rendering_context_driver_vulkan_windows.cpp +++ b/platform/windows/rendering_context_driver_vulkan_windows.cpp @@ -34,11 +34,7 @@ #include "rendering_context_driver_vulkan_windows.h" -#ifdef USE_VOLK -#include <volk.h> -#else -#include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" const char *RenderingContextDriverVulkanWindows::_get_platform_surface_extension() const { return VK_KHR_WIN32_SURFACE_EXTENSION_NAME; diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp index 9c9ba93b41..754afb0527 100644 --- a/scene/2d/cpu_particles_2d.cpp +++ b/scene/2d/cpu_particles_2d.cpp @@ -1288,7 +1288,7 @@ void CPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "emitting"), "set_emitting", "is_emitting"); ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount"); ADD_GROUP("Time", ""); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,suffix:s"), "set_lifetime", "get_lifetime"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,exp,suffix:s"), "set_lifetime", "get_lifetime"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"), "set_one_shot", "get_one_shot"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,suffix:s"), "set_pre_process_time", "get_pre_process_time"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_speed_scale", "get_speed_scale"); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index bfbdb49f22..cfdcbee86a 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -821,19 +821,17 @@ void GPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "amount", PROPERTY_HINT_RANGE, "1,1000000,1,exp"), "set_amount", "get_amount"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "amount_ratio", PROPERTY_HINT_RANGE, "0,1,0.0001"), "set_amount_ratio", "get_amount_ratio"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "sub_emitter", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "GPUParticles2D"), "set_sub_emitter", "get_sub_emitter"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "process_material", PROPERTY_HINT_RESOURCE_TYPE, "ParticleProcessMaterial,ShaderMaterial"), "set_process_material", "get_process_material"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture"); ADD_GROUP("Time", ""); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,suffix:s"), "set_lifetime", "get_lifetime"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lifetime", PROPERTY_HINT_RANGE, "0.01,600.0,0.01,or_greater,exp,suffix:s"), "set_lifetime", "get_lifetime"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "interp_to_end", PROPERTY_HINT_RANGE, "0.00,1.0,0.001"), "set_interp_to_end", "get_interp_to_end"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "one_shot"), "set_one_shot", "get_one_shot"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,suffix:s"), "set_pre_process_time", "get_pre_process_time"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "preprocess", PROPERTY_HINT_RANGE, "0.00,600.0,0.01,exp,suffix:s"), "set_pre_process_time", "get_pre_process_time"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed_scale", PROPERTY_HINT_RANGE, "0,64,0.01"), "set_speed_scale", "get_speed_scale"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "explosiveness", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_explosiveness_ratio", "get_explosiveness_ratio"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "randomness", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_randomness_ratio", "get_randomness_ratio"); ADD_PROPERTY(PropertyInfo(Variant::INT, "fixed_fps", PROPERTY_HINT_RANGE, "0,1000,1,suffix:FPS"), "set_fixed_fps", "get_fixed_fps"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "interpolate"), "set_interpolate", "get_interpolate"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fract_delta"), "set_fractional_delta", "get_fractional_delta"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "interp_to_end", PROPERTY_HINT_RANGE, "0.00,1.0,0.001"), "set_interp_to_end", "get_interp_to_end"); ADD_GROUP("Collision", "collision_"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "collision_base_size", PROPERTY_HINT_RANGE, "0,128,0.01,or_greater"), "set_collision_base_size", "get_collision_base_size"); ADD_GROUP("Drawing", ""); @@ -845,7 +843,9 @@ void GPUParticles2D::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "trail_lifetime", PROPERTY_HINT_RANGE, "0.01,10,0.01,or_greater,suffix:s"), "set_trail_lifetime", "get_trail_lifetime"); ADD_PROPERTY(PropertyInfo(Variant::INT, "trail_sections", PROPERTY_HINT_RANGE, "2,128,1"), "set_trail_sections", "get_trail_sections"); ADD_PROPERTY(PropertyInfo(Variant::INT, "trail_section_subdivisions", PROPERTY_HINT_RANGE, "1,1024,1"), "set_trail_section_subdivisions", "get_trail_section_subdivisions"); - + ADD_GROUP("Process Material", ""); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "process_material", PROPERTY_HINT_RESOURCE_TYPE, "ParticleProcessMaterial,ShaderMaterial"), "set_process_material", "get_process_material"); BIND_ENUM_CONSTANT(DRAW_ORDER_INDEX); BIND_ENUM_CONSTANT(DRAW_ORDER_LIFETIME); BIND_ENUM_CONSTANT(DRAW_ORDER_REVERSE_LIFETIME); diff --git a/scene/animation/animation_blend_tree.cpp b/scene/animation/animation_blend_tree.cpp index a96417738f..a2aef60417 100644 --- a/scene/animation/animation_blend_tree.cpp +++ b/scene/animation/animation_blend_tree.cpp @@ -245,6 +245,8 @@ AnimationNode::NodeTimeInfo AnimationNodeAnimation::_process(const AnimationMixe if (!p_test_only) { AnimationMixer::PlaybackInfo pi = p_playback_info; + pi.start = 0.0; + pi.end = cur_len; if (play_mode == PLAY_MODE_FORWARD) { pi.time = cur_playback_time; pi.delta = cur_delta; diff --git a/scene/animation/animation_mixer.cpp b/scene/animation/animation_mixer.cpp index 664302d45e..eb8bc8c382 100644 --- a/scene/animation/animation_mixer.cpp +++ b/scene/animation/animation_mixer.cpp @@ -1117,6 +1117,8 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { Ref<Animation> a = ai.animation_data.animation; double time = ai.playback_info.time; double delta = ai.playback_info.delta; + double start = ai.playback_info.start; + double end = ai.playback_info.end; bool seeked = ai.playback_info.seeked; Animation::LoopedFlag looped_flag = ai.playback_info.looped_flag; bool is_external_seeking = ai.playback_info.is_external_seeking; @@ -1168,32 +1170,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (track->root_motion && calc_root) { double prev_time = time - delta; if (!backward) { - if (Animation::is_less_approx(prev_time, 0)) { + if (Animation::is_less_approx(prev_time, start)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = 0; + prev_time = start; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a_length)) { + if (Animation::is_greater_approx(prev_time, end)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a_length; + prev_time = end; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; @@ -1208,10 +1210,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx); - a->try_position_track_interpolate(i, (double)a_length, &loc[1]); + a->try_position_track_interpolate(i, end, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = 0; + prev_time = start; } } else { if (Animation::is_less_approx(prev_time, time)) { @@ -1220,10 +1222,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } loc[0] = post_process_key_value(a, i, loc[0], t->object_id, t->bone_idx); - a->try_position_track_interpolate(i, 0, &loc[1]); + a->try_position_track_interpolate(i, start, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = (double)a_length; + prev_time = end; } } Error err = a->try_position_track_interpolate(i, prev_time, &loc[0]); @@ -1234,7 +1236,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_position_track_interpolate(i, time, &loc[1]); loc[1] = post_process_key_value(a, i, loc[1], t->object_id, t->bone_idx); root_motion_cache.loc += (loc[1] - loc[0]) * blend; - prev_time = !backward ? 0 : (double)a_length; + prev_time = !backward ? start : end; } { Vector3 loc; @@ -1256,32 +1258,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (track->root_motion && calc_root) { double prev_time = time - delta; if (!backward) { - if (Animation::is_less_approx(prev_time, 0)) { + if (Animation::is_less_approx(prev_time, start)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = 0; + prev_time = start; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a_length)) { + if (Animation::is_greater_approx(prev_time, end)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a_length; + prev_time = end; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; @@ -1296,10 +1298,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx); - a->try_rotation_track_interpolate(i, (double)a_length, &rot[1]); + a->try_rotation_track_interpolate(i, end, &rot[1]); rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = 0; + prev_time = start; } } else { if (Animation::is_less_approx(prev_time, time)) { @@ -1308,9 +1310,9 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } rot[0] = post_process_key_value(a, i, rot[0], t->object_id, t->bone_idx); - a->try_rotation_track_interpolate(i, 0, &rot[1]); + a->try_rotation_track_interpolate(i, start, &rot[1]); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = (double)a_length; + prev_time = end; } } Error err = a->try_rotation_track_interpolate(i, prev_time, &rot[0]); @@ -1321,7 +1323,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_rotation_track_interpolate(i, time, &rot[1]); rot[1] = post_process_key_value(a, i, rot[1], t->object_id, t->bone_idx); root_motion_cache.rot = (root_motion_cache.rot * Quaternion().slerp(rot[0].inverse() * rot[1], blend)).normalized(); - prev_time = !backward ? 0 : (double)a_length; + prev_time = !backward ? start : end; } { Quaternion rot; @@ -1343,32 +1345,32 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (track->root_motion && calc_root) { double prev_time = time - delta; if (!backward) { - if (Animation::is_less_approx(prev_time, 0)) { + if (Animation::is_less_approx(prev_time, start)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = 0; + prev_time = start; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; } } } else { - if (Animation::is_greater_approx(prev_time, (double)a_length)) { + if (Animation::is_greater_approx(prev_time, end)) { switch (a->get_loop_mode()) { case Animation::LOOP_NONE: { - prev_time = (double)a_length; + prev_time = end; } break; case Animation::LOOP_LINEAR: { - prev_time = Math::fposmod(prev_time, (double)a_length); + prev_time = Math::fposmod(prev_time - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - prev_time = Math::pingpong(prev_time, (double)a_length); + prev_time = Math::pingpong(prev_time - start, end - start) + start; } break; default: break; @@ -1383,10 +1385,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx); - a->try_scale_track_interpolate(i, (double)a_length, &scale[1]); + a->try_scale_track_interpolate(i, end, &scale[1]); root_motion_cache.scale += (scale[1] - scale[0]) * blend; scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); - prev_time = 0; + prev_time = start; } } else { if (Animation::is_less_approx(prev_time, time)) { @@ -1395,10 +1397,10 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } scale[0] = post_process_key_value(a, i, scale[0], t->object_id, t->bone_idx); - a->try_scale_track_interpolate(i, 0, &scale[1]); + a->try_scale_track_interpolate(i, start, &scale[1]); scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); root_motion_cache.scale += (scale[1] - scale[0]) * blend; - prev_time = (double)a_length; + prev_time = end; } } Error err = a->try_scale_track_interpolate(i, prev_time, &scale[0]); @@ -1409,7 +1411,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { a->try_scale_track_interpolate(i, time, &scale[1]); scale[1] = post_process_key_value(a, i, scale[1], t->object_id, t->bone_idx); root_motion_cache.scale += (scale[1] - scale[0]) * blend; - prev_time = !backward ? 0 : (double)a_length; + prev_time = !backward ? start : end; } { Vector3 scale; @@ -1671,6 +1673,7 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { if (!player2) { continue; } + // TODO: Make it possible to embed section info in animation track keys. if (seeked) { // Seek. int idx = a->track_find_key(i, time, Animation::FIND_MODE_NEAREST, true); @@ -1683,19 +1686,19 @@ void AnimationMixer::_blend_process(double p_delta, bool p_update_only) { continue; } Ref<Animation> anim = player2->get_animation(anim_name); - double at_anim_pos = 0.0; + double at_anim_pos = start; switch (anim->get_loop_mode()) { case Animation::LOOP_NONE: { - if (!is_external_seeking && ((!backward && Animation::is_greater_or_equal_approx(time, pos + (double)anim->get_length())) || (backward && Animation::is_less_or_equal_approx(time, pos)))) { + if (!is_external_seeking && ((!backward && Animation::is_greater_or_equal_approx(time, pos + end)) || (backward && Animation::is_less_or_equal_approx(time, pos + start)))) { continue; // Do nothing if current time is outside of length when started. } - at_anim_pos = MIN((double)anim->get_length(), time - pos); // Seek to end. + at_anim_pos = MIN(end, time - pos); // Seek to end. } break; case Animation::LOOP_LINEAR: { - at_anim_pos = Math::fposmod(time - pos, (double)anim->get_length()); // Seek to loop. + at_anim_pos = Math::fposmod(time - pos - start, end - start) + start; // Seek to loop. } break; case Animation::LOOP_PINGPONG: { - at_anim_pos = Math::pingpong(time - pos, (double)a_length); + at_anim_pos = Math::pingpong(time - pos - start, end - start) + start; } break; default: break; @@ -2092,6 +2095,8 @@ Ref<AnimatedValuesBackup> AnimationMixer::make_backup() { PlaybackInfo pi; pi.time = 0; pi.delta = 0; + pi.start = 0; + pi.end = reset_anim->get_length(); pi.seeked = true; pi.weight = 1.0; make_animation_instance(SceneStringName(RESET), pi); diff --git a/scene/animation/animation_mixer.h b/scene/animation/animation_mixer.h index 5482197fbd..27c9a00a9c 100644 --- a/scene/animation/animation_mixer.h +++ b/scene/animation/animation_mixer.h @@ -85,6 +85,8 @@ public: struct PlaybackInfo { double time = 0.0; double delta = 0.0; + double start = 0.0; + double end = 0.0; bool seeked = false; bool is_external_seeking = false; Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE; diff --git a/scene/animation/animation_player.cpp b/scene/animation/animation_player.cpp index a4aa383a9d..bc951e4e14 100644 --- a/scene/animation/animation_player.cpp +++ b/scene/animation/animation_player.cpp @@ -164,39 +164,41 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f double delta = p_started ? 0 : p_delta * speed; double next_pos = cd.pos + delta; - double len = cd.from->animation->get_length(); + double start = get_section_start_time(); + double end = get_section_end_time(); + Animation::LoopedFlag looped_flag = Animation::LOOPED_FLAG_NONE; switch (cd.from->animation->get_loop_mode()) { case Animation::LOOP_NONE: { - if (Animation::is_less_approx(next_pos, 0)) { - next_pos = 0; - } else if (Animation::is_greater_approx(next_pos, len)) { - next_pos = len; + if (Animation::is_less_approx(next_pos, start)) { + next_pos = start; + } else if (Animation::is_greater_approx(next_pos, end)) { + next_pos = end; } delta = next_pos - cd.pos; // Fix delta (after determination of backwards because negative zero is lost here). } break; case Animation::LOOP_LINEAR: { - if (Animation::is_less_approx(next_pos, 0) && Animation::is_greater_or_equal_approx(cd.pos, 0)) { + if (Animation::is_less_approx(next_pos, start) && Animation::is_greater_or_equal_approx(cd.pos, start)) { looped_flag = Animation::LOOPED_FLAG_START; } - if (Animation::is_greater_approx(next_pos, len) && Animation::is_less_or_equal_approx(cd.pos, len)) { + if (Animation::is_greater_approx(next_pos, end) && Animation::is_less_or_equal_approx(cd.pos, end)) { looped_flag = Animation::LOOPED_FLAG_END; } - next_pos = Math::fposmod(next_pos, (double)len); + next_pos = Math::fposmod(next_pos - start, end - start) + start; } break; case Animation::LOOP_PINGPONG: { - if (Animation::is_less_approx(next_pos, 0) && Animation::is_greater_or_equal_approx(cd.pos, 0)) { + if (Animation::is_less_approx(next_pos, start) && Animation::is_greater_or_equal_approx(cd.pos, start)) { cd.speed_scale *= -1.0; looped_flag = Animation::LOOPED_FLAG_START; } - if (Animation::is_greater_approx(next_pos, len) && Animation::is_less_or_equal_approx(cd.pos, len)) { + if (Animation::is_greater_approx(next_pos, end) && Animation::is_less_or_equal_approx(cd.pos, end)) { cd.speed_scale *= -1.0; looped_flag = Animation::LOOPED_FLAG_END; } - next_pos = Math::pingpong(next_pos, (double)len); + next_pos = Math::pingpong(next_pos - start, end - start) + start; } break; default: @@ -208,18 +210,18 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f // End detection. if (p_is_current) { if (cd.from->animation->get_loop_mode() == Animation::LOOP_NONE) { - if (!backwards && Animation::is_less_or_equal_approx(prev_pos, len) && Math::is_equal_approx(next_pos, len)) { + if (!backwards && Animation::is_less_or_equal_approx(prev_pos, end) && Math::is_equal_approx(next_pos, end)) { // Playback finished. - next_pos = len; // Snap to the edge. + next_pos = end; // Snap to the edge. end_reached = true; - end_notify = Animation::is_less_approx(prev_pos, len); // Notify only if not already at the end. + end_notify = Animation::is_less_approx(prev_pos, end); // Notify only if not already at the end. p_blend = 1.0; } - if (backwards && Animation::is_greater_or_equal_approx(prev_pos, 0) && Math::is_equal_approx(next_pos, 0)) { + if (backwards && Animation::is_greater_or_equal_approx(prev_pos, start) && Math::is_equal_approx(next_pos, start)) { // Playback finished. - next_pos = 0; // Snap to the edge. + next_pos = start; // Snap to the edge. end_reached = true; - end_notify = Animation::is_greater_approx(prev_pos, 0); // Notify only if not already at the beginning. + end_notify = Animation::is_greater_approx(prev_pos, start); // Notify only if not already at the beginning. p_blend = 1.0; } } @@ -231,10 +233,14 @@ void AnimationPlayer::_process_playback_data(PlaybackData &cd, double p_delta, f if (p_started) { pi.time = prev_pos; pi.delta = 0; + pi.start = start; + pi.end = end; pi.seeked = true; } else { pi.time = next_pos; pi.delta = delta; + pi.start = start; + pi.end = end; pi.seeked = p_seeked; } if (Math::is_zero_approx(pi.delta) && backwards) { @@ -378,6 +384,14 @@ void AnimationPlayer::play_backwards(const StringName &p_name, double p_custom_b play(p_name, p_custom_blend, -1, true); } +void AnimationPlayer::play_section_with_markers_backwards(const StringName &p_name, const StringName &p_start_marker, const StringName &p_end_marker, double p_custom_blend) { + play_section_with_markers(p_name, p_start_marker, p_end_marker, p_custom_blend, -1, true); +} + +void AnimationPlayer::play_section_backwards(const StringName &p_name, double p_start_time, double p_end_time, double p_custom_blend) { + play_section(p_name, p_start_time, p_end_time, -1, true); +} + void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { if (auto_capture) { play_with_capture(p_name, auto_capture_duration, p_custom_blend, p_custom_scale, p_from_end, auto_capture_transition_type, auto_capture_ease_type); @@ -387,6 +401,10 @@ void AnimationPlayer::play(const StringName &p_name, double p_custom_blend, floa } void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, float p_custom_scale, bool p_from_end) { + play_section_with_markers(p_name, StringName(), StringName(), p_custom_blend, p_custom_scale, p_from_end); +} + +void AnimationPlayer::play_section_with_markers(const StringName &p_name, const StringName &p_start_marker, const StringName &p_end_marker, double p_custom_blend, float p_custom_scale, bool p_from_end) { StringName name = p_name; if (name == StringName()) { @@ -395,6 +413,38 @@ void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, flo ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name)); + Ref<Animation> animation = animation_set[name].animation; + + ERR_FAIL_COND_MSG(p_start_marker == p_end_marker && p_start_marker, vformat("Start marker and end marker cannot be the same marker: %s.", p_start_marker)); + ERR_FAIL_COND_MSG(p_start_marker && !animation->has_marker(p_start_marker), vformat("Marker %s not found in animation: %s.", p_start_marker, name)); + ERR_FAIL_COND_MSG(p_end_marker && !animation->has_marker(p_end_marker), vformat("Marker %s not found in animation: %s.", p_end_marker, name)); + + double start_time = p_start_marker ? animation->get_marker_time(p_start_marker) : -1; + double end_time = p_end_marker ? animation->get_marker_time(p_end_marker) : -1; + + ERR_FAIL_COND_MSG(p_start_marker && p_end_marker && Animation::is_greater_approx(start_time, end_time), vformat("End marker %s is placed earlier than start marker %s in animation: %s.", p_end_marker, p_start_marker, name)); + + if (p_start_marker && Animation::is_less_approx(start_time, 0)) { + WARN_PRINT_ED(vformat("Negative time start marker: %s is invalid in the section, so the start of the animation: %s is used instead.", p_start_marker, playback.current.from->animation->get_name())); + } + if (p_end_marker && Animation::is_less_approx(end_time, 0)) { + WARN_PRINT_ED(vformat("Negative time end marker: %s is invalid in the section, so the end of the animation: %s is used instead.", p_end_marker, playback.current.from->animation->get_name())); + } + + play_section(name, start_time, end_time, p_custom_blend, p_custom_scale, p_from_end); +} + +void AnimationPlayer::play_section(const StringName &p_name, double p_start_time, double p_end_time, double p_custom_blend, float p_custom_scale, bool p_from_end) { + StringName name = p_name; + + if (name == StringName()) { + name = playback.assigned; + } + + ERR_FAIL_COND_MSG(!animation_set.has(name), vformat("Animation not found: %s.", name)); + ERR_FAIL_COND_MSG(p_start_time >= 0 && p_end_time >= 0 && Math::is_equal_approx(p_start_time, p_end_time), "Start time and end time must not equal to each other."); + ERR_FAIL_COND_MSG(p_start_time >= 0 && p_end_time >= 0 && Animation::is_greater_approx(p_start_time, p_end_time), vformat("Start time %f is greater than end time %f.", p_start_time, p_end_time)); + Playback &c = playback; if (c.current.from) { @@ -442,22 +492,27 @@ void AnimationPlayer::_play(const StringName &p_name, double p_custom_blend, flo c.current.from = &animation_set[name]; c.current.speed_scale = p_custom_scale; + c.current.start_time = p_start_time; + c.current.end_time = p_end_time; + + double start = get_section_start_time(); + double end = get_section_end_time(); if (!end_reached) { playback_queue.clear(); } if (c.assigned != name) { // Reset. - c.current.pos = p_from_end ? c.current.from->animation->get_length() : 0; + c.current.pos = p_from_end ? end : start; c.assigned = name; emit_signal(SNAME("current_animation_changed"), c.assigned); } else { - if (p_from_end && Math::is_zero_approx(c.current.pos)) { + if (p_from_end && Math::is_equal_approx(c.current.pos, start)) { // Animation reset but played backwards, set position to the end. - seek_internal(c.current.from->animation->get_length(), true, true, true); - } else if (!p_from_end && Math::is_equal_approx(c.current.pos, (double)c.current.from->animation->get_length())) { + seek_internal(end, true, true, true); + } else if (!p_from_end && Math::is_equal_approx(c.current.pos, end)) { // Animation resumed but already ended, set position to the beginning. - seek_internal(0, true, true, true); + seek_internal(start, true, true, true); } else if (playing) { return; } @@ -551,6 +606,8 @@ void AnimationPlayer::set_assigned_animation(const String &p_animation) { ERR_FAIL_COND_MSG(!animation_set.has(p_animation), vformat("Animation not found: %s.", p_animation)); playback.current.pos = 0; playback.current.from = &animation_set[p_animation]; + playback.current.start_time = -1; + playback.current.end_time = -1; playback.assigned = p_animation; emit_signal(SNAME("current_animation_changed"), playback.assigned); } @@ -603,6 +660,12 @@ void AnimationPlayer::seek_internal(double p_time, bool p_update, bool p_update_ } } + double start = get_section_start_time(); + double end = get_section_end_time(); + + // Clamp the seek position. + p_time = CLAMP(p_time, start, end); + playback.seeked = true; playback.internal_seeked = p_is_internal_seek; @@ -641,6 +704,55 @@ double AnimationPlayer::get_current_animation_length() const { return playback.current.from->animation->get_length(); } +void AnimationPlayer::set_section_with_markers(const StringName &p_start_marker, const StringName &p_end_marker) { + ERR_FAIL_NULL_MSG(playback.current.from, "AnimationPlayer has no current animation."); + ERR_FAIL_COND_MSG(p_start_marker == p_end_marker && p_start_marker, vformat("Start marker and end marker cannot be the same marker: %s.", p_start_marker)); + ERR_FAIL_COND_MSG(p_start_marker && !playback.current.from->animation->has_marker(p_start_marker), vformat("Marker %s not found in animation: %s.", p_start_marker, playback.current.from->animation->get_name())); + ERR_FAIL_COND_MSG(p_end_marker && !playback.current.from->animation->has_marker(p_end_marker), vformat("Marker %s not found in animation: %s.", p_end_marker, playback.current.from->animation->get_name())); + double start_time = p_start_marker ? playback.current.from->animation->get_marker_time(p_start_marker) : -1; + double end_time = p_end_marker ? playback.current.from->animation->get_marker_time(p_end_marker) : -1; + if (p_start_marker && Animation::is_less_approx(start_time, 0)) { + WARN_PRINT_ONCE_ED(vformat("Marker %s time must be positive in animation: %s.", p_start_marker, playback.current.from->animation->get_name())); + } + if (p_end_marker && Animation::is_less_approx(end_time, 0)) { + WARN_PRINT_ONCE_ED(vformat("Marker %s time must be positive in animation: %s.", p_end_marker, playback.current.from->animation->get_name())); + } + set_section(start_time, end_time); +} + +void AnimationPlayer::set_section(double p_start_time, double p_end_time) { + ERR_FAIL_NULL_MSG(playback.current.from, "AnimationPlayer has no current animation."); + ERR_FAIL_COND_MSG(Animation::is_greater_or_equal_approx(p_start_time, 0) && Animation::is_greater_or_equal_approx(p_end_time, 0) && Animation::is_greater_or_equal_approx(p_start_time, p_end_time), vformat("Start time %f is greater than end time %f.", p_start_time, p_end_time)); + playback.current.start_time = p_start_time; + playback.current.end_time = p_end_time; + playback.current.pos = CLAMP(playback.current.pos, get_section_start_time(), get_section_end_time()); +} + +void AnimationPlayer::reset_section() { + playback.current.start_time = -1; + playback.current.end_time = -1; +} + +double AnimationPlayer::get_section_start_time() const { + ERR_FAIL_NULL_V_MSG(playback.current.from, playback.current.start_time, "AnimationPlayer has no current animation."); + if (Animation::is_less_approx(playback.current.start_time, 0) || playback.current.start_time > playback.current.from->animation->get_length()) { + return 0; + } + return playback.current.start_time; +} + +double AnimationPlayer::get_section_end_time() const { + ERR_FAIL_NULL_V_MSG(playback.current.from, playback.current.end_time, "AnimationPlayer has no current animation."); + if (Animation::is_less_approx(playback.current.end_time, 0) || playback.current.end_time > playback.current.from->animation->get_length()) { + return playback.current.from->animation->get_length(); + } + return playback.current.end_time; +} + +bool AnimationPlayer::has_section() const { + return Animation::is_greater_or_equal_approx(playback.current.start_time, 0) || Animation::is_greater_or_equal_approx(playback.current.end_time, 0); +} + void AnimationPlayer::set_autoplay(const String &p_name) { if (is_inside_tree() && !Engine::get_singleton()->is_editor_hint()) { WARN_PRINT("Setting autoplay after the node has been added to the scene has no effect."); @@ -665,13 +777,14 @@ void AnimationPlayer::_stop_internal(bool p_reset, bool p_keep_state) { _clear_caches(); Playback &c = playback; // c.blend.clear(); + double start = get_section_start_time(); if (p_reset) { c.blend.clear(); if (p_keep_state) { - c.current.pos = 0; + c.current.pos = start; } else { is_stopping = true; - seek_internal(0, true, true, true); + seek_internal(start, true, true, true); is_stopping = false; } c.current.from = nullptr; @@ -763,20 +876,6 @@ Tween::EaseType AnimationPlayer::get_auto_capture_ease_type() const { return auto_capture_ease_type; } -#ifdef TOOLS_ENABLED -void AnimationPlayer::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const { - const String pf = p_function; - if (p_idx == 0 && (pf == "play" || pf == "play_backwards" || pf == "has_animation" || pf == "queue")) { - List<StringName> al; - get_animation_list(&al); - for (const StringName &name : al) { - r_options->push_back(String(name).quote()); - } - } - AnimationMixer::get_argument_options(p_function, p_idx, r_options); -} -#endif - void AnimationPlayer::_animation_removed(const StringName &p_name, const StringName &p_library) { AnimationMixer::_animation_removed(p_name, p_library); @@ -863,7 +962,11 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_auto_capture_ease_type"), &AnimationPlayer::get_auto_capture_ease_type); ClassDB::bind_method(D_METHOD("play", "name", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("play_section_with_markers", "name", "start_marker", "end_marker", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play_section_with_markers, DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); + ClassDB::bind_method(D_METHOD("play_section", "name", "start_time", "end_time", "custom_blend", "custom_speed", "from_end"), &AnimationPlayer::play_section, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(-1), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false)); ClassDB::bind_method(D_METHOD("play_backwards", "name", "custom_blend"), &AnimationPlayer::play_backwards, DEFVAL(StringName()), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("play_section_with_markers_backwards", "name", "start_marker", "end_marker", "custom_blend"), &AnimationPlayer::play_section_with_markers_backwards, DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(StringName()), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("play_section_backwards", "name", "start_time", "end_time", "custom_blend"), &AnimationPlayer::play_section_backwards, DEFVAL(StringName()), DEFVAL(-1), DEFVAL(-1), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("play_with_capture", "name", "duration", "custom_blend", "custom_speed", "from_end", "trans_type", "ease_type"), &AnimationPlayer::play_with_capture, DEFVAL(StringName()), DEFVAL(-1.0), DEFVAL(-1), DEFVAL(1.0), DEFVAL(false), DEFVAL(Tween::TRANS_LINEAR), DEFVAL(Tween::EASE_IN)); ClassDB::bind_method(D_METHOD("pause"), &AnimationPlayer::pause); ClassDB::bind_method(D_METHOD("stop", "keep_state"), &AnimationPlayer::stop, DEFVAL(false)); @@ -893,6 +996,14 @@ void AnimationPlayer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_current_animation_position"), &AnimationPlayer::get_current_animation_position); ClassDB::bind_method(D_METHOD("get_current_animation_length"), &AnimationPlayer::get_current_animation_length); + ClassDB::bind_method(D_METHOD("set_section_with_markers", "start_marker", "end_marker"), &AnimationPlayer::set_section_with_markers, DEFVAL(StringName()), DEFVAL(StringName())); + ClassDB::bind_method(D_METHOD("set_section", "start_time", "end_time"), &AnimationPlayer::set_section, DEFVAL(-1), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("reset_section"), &AnimationPlayer::reset_section); + + ClassDB::bind_method(D_METHOD("get_section_start_time"), &AnimationPlayer::get_section_start_time); + ClassDB::bind_method(D_METHOD("get_section_end_time"), &AnimationPlayer::get_section_end_time); + ClassDB::bind_method(D_METHOD("has_section"), &AnimationPlayer::has_section); + ClassDB::bind_method(D_METHOD("seek", "seconds", "update", "update_only"), &AnimationPlayer::seek, DEFVAL(false), DEFVAL(false)); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "current_animation", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_EDITOR), "set_current_animation", "get_current_animation"); diff --git a/scene/animation/animation_player.h b/scene/animation/animation_player.h index e05a2c9935..3223e2522d 100644 --- a/scene/animation/animation_player.h +++ b/scene/animation/animation_player.h @@ -68,6 +68,8 @@ private: AnimationData *from = nullptr; double pos = 0.0; float speed_scale = 1.0; + double start_time = 0.0; + double end_time = 0.0; }; struct Blend { @@ -177,7 +179,11 @@ public: Tween::EaseType get_auto_capture_ease_type() const; void play(const StringName &p_name = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); + void play_section_with_markers(const StringName &p_name = StringName(), const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName(), double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); + void play_section(const StringName &p_name = StringName(), double p_start_time = -1, double p_end_time = -1, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false); void play_backwards(const StringName &p_name = StringName(), double p_custom_blend = -1); + void play_section_with_markers_backwards(const StringName &p_name = StringName(), const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName(), double p_custom_blend = -1); + void play_section_backwards(const StringName &p_name = StringName(), double p_start_time = -1, double p_end_time = -1, double p_custom_blend = -1); void play_with_capture(const StringName &p_name = StringName(), double p_duration = -1.0, double p_custom_blend = -1, float p_custom_scale = 1.0, bool p_from_end = false, Tween::TransitionType p_trans_type = Tween::TRANS_LINEAR, Tween::EaseType p_ease_type = Tween::EASE_IN); void queue(const StringName &p_name); Vector<String> get_queue(); @@ -207,9 +213,13 @@ public: double get_current_animation_position() const; double get_current_animation_length() const; -#ifdef TOOLS_ENABLED - void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override; -#endif + void set_section_with_markers(const StringName &p_start_marker = StringName(), const StringName &p_end_marker = StringName()); + void set_section(double p_start_time = -1, double p_end_time = -1); + void reset_section(); + + double get_section_start_time() const; + double get_section_end_time() const; + bool has_section() const; virtual void advance(double p_time) override; diff --git a/scene/gui/flow_container.cpp b/scene/gui/flow_container.cpp index d32c75fbcd..90d5b6b36d 100644 --- a/scene/gui/flow_container.cpp +++ b/scene/gui/flow_container.cpp @@ -261,6 +261,7 @@ void FlowContainer::_resort() { } cached_size = (vertical ? ofs.x : ofs.y) + line_height; cached_line_count = lines_data.size(); + cached_line_max_child_count = lines_data.size() > 0 ? lines_data[0].child_count : 0; } Size2 FlowContainer::get_minimum_size() const { @@ -339,6 +340,10 @@ int FlowContainer::get_line_count() const { return cached_line_count; } +int FlowContainer::get_line_max_child_count() const { + return cached_line_max_child_count; +} + void FlowContainer::set_alignment(AlignmentMode p_alignment) { if (alignment == p_alignment) { return; diff --git a/scene/gui/flow_container.h b/scene/gui/flow_container.h index 65ebc89c78..6a00e5b0e5 100644 --- a/scene/gui/flow_container.h +++ b/scene/gui/flow_container.h @@ -52,6 +52,8 @@ public: private: int cached_size = 0; int cached_line_count = 0; + int cached_line_max_child_count = 0; + int cached_items_on_last_row = 0; bool vertical = false; bool reverse_fill = false; @@ -74,6 +76,7 @@ protected: public: int get_line_count() const; + int get_line_max_child_count() const; void set_alignment(AlignmentMode p_alignment); AlignmentMode get_alignment() const; diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index cf8815679f..9040693a6d 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -260,6 +260,8 @@ PackedStringArray GraphEdit::get_configuration_warnings() const { } Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { + ERR_FAIL_NULL_V_MSG(connections_layer, FAILED, "connections_layer is missing."); + if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) { return OK; } @@ -313,6 +315,8 @@ bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, con } void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + for (const List<Ref<Connection>>::Element *E = connections.front(); E; E = E->next()) { if (E->get()->from_node == p_from && E->get()->from_port == p_from_port && E->get()->to_node == p_to && E->get()->to_port == p_to_port) { connection_map[p_from].erase(E->get()); @@ -356,6 +360,8 @@ void GraphEdit::_scroll_moved(double) { } void GraphEdit::_update_scroll_offset() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + set_block_minimum_size_adjust(true); for (int i = 0; i < get_child_count(); i++) { @@ -524,6 +530,8 @@ void GraphEdit::_graph_element_resize_request(const Vector2 &p_new_minsize, Node } void GraphEdit::_graph_frame_autoshrink_changed(const Vector2 &p_new_minsize, GraphFrame *p_frame) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + _update_graph_frame(p_frame); minimap->queue_redraw(); @@ -535,6 +543,7 @@ void GraphEdit::_graph_frame_autoshrink_changed(const Vector2 &p_new_minsize, Gr void GraphEdit::_graph_element_moved(Node *p_node) { GraphElement *graph_element = Object::cast_to<GraphElement>(p_node); ERR_FAIL_NULL(graph_element); + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); minimap->queue_redraw(); queue_redraw(); @@ -543,6 +552,7 @@ void GraphEdit::_graph_element_moved(Node *p_node) { } void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); GraphNode *graph_node = Object::cast_to<GraphNode>(p_node); ERR_FAIL_NULL(graph_node); @@ -558,6 +568,8 @@ void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) { } void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + // Only invalidate the cache when zooming or the node is moved/resized in graph space. if (panner->is_panning()) { return; @@ -566,7 +578,6 @@ void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) { for (Ref<Connection> &c : connection_map[p_node->get_name()]) { c->_cache.dirty = true; } - connections_layer->queue_redraw(); callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred(); @@ -623,7 +634,9 @@ void GraphEdit::add_child_notify(Node *p_child) { } graph_element->connect("raise_request", callable_mp(this, &GraphEdit::_ensure_node_order_from).bind(graph_element)); graph_element->connect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resize_request).bind(graph_element)); - graph_element->connect(SceneStringName(item_rect_changed), callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw)); + if (connections_layer != nullptr && connections_layer->is_inside_tree()) { + graph_element->connect(SceneStringName(item_rect_changed), callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw)); + } graph_element->connect(SceneStringName(item_rect_changed), callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw)); graph_element->set_scale(Vector2(zoom, zoom)); @@ -640,6 +653,7 @@ void GraphEdit::remove_child_notify(Node *p_child) { minimap = nullptr; } else if (p_child == connections_layer) { connections_layer = nullptr; + WARN_PRINT("GraphEdit's connection_layer removed. This should not be done. If you like to remove all GraphElements from a GraphEdit node, do not simply remove all non-internal children but check their type since the connection layer has to be kept non-internal due to technical reasons."); } if (top_layer != nullptr && is_inside_tree()) { @@ -662,7 +676,9 @@ void GraphEdit::remove_child_notify(Node *p_child) { for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) { conn->_cache.dirty = true; } - connections_layer->queue_redraw(); + if (connections_layer != nullptr && connections_layer->is_inside_tree()) { + connections_layer->queue_redraw(); + } } GraphFrame *frame = Object::cast_to<GraphFrame>(graph_element); @@ -1690,6 +1706,8 @@ void GraphEdit::set_selected(Node *p_child) { } void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + ERR_FAIL_COND(p_ev.is_null()); if (panner->gui_input(p_ev, warped_panning ? get_global_rect() : Rect2())) { return; @@ -2025,6 +2043,8 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { } void GraphEdit::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + h_scrollbar->set_value(h_scrollbar->get_value() - p_scroll_vec.x); v_scrollbar->set_value(v_scrollbar->get_value() - p_scroll_vec.y); @@ -2040,6 +2060,8 @@ void GraphEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputE } void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + for (Ref<Connection> &c : connection_map[p_from]) { if (c->from_node == p_from && c->from_port == p_from_port && c->to_node == p_to && c->to_port == p_to_port) { if (!Math::is_equal_approx(c->activity, p_activity)) { @@ -2056,6 +2078,8 @@ void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_por } void GraphEdit::reset_all_connection_activity() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + bool changed = false; for (Ref<Connection> &conn : connections) { if (conn->activity > 0) { @@ -2070,6 +2094,8 @@ void GraphEdit::reset_all_connection_activity() { } void GraphEdit::clear_connections() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + for (Ref<Connection> &c : connections) { c->_cache.line->queue_free(); } @@ -2083,7 +2109,9 @@ void GraphEdit::clear_connections() { } void GraphEdit::force_connection_drag_end() { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); ERR_FAIL_COND_MSG(!connecting, "Drag end requested without active drag!"); + connecting = false; connecting_valid = false; minimap->queue_redraw(); @@ -2113,6 +2141,8 @@ void GraphEdit::set_zoom(float p_zoom) { } void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + p_zoom = CLAMP(p_zoom, zoom_min, zoom_max); if (zoom == p_zoom) { return; @@ -2521,6 +2551,8 @@ bool GraphEdit::is_showing_arrange_button() const { } void GraphEdit::override_connections_shader(const Ref<Shader> &p_shader) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + connections_shader = p_shader; _invalidate_connection_line_cache(); @@ -2539,6 +2571,8 @@ void GraphEdit::_minimap_toggled() { } void GraphEdit::set_connection_lines_curvature(float p_curvature) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + lines_curvature = p_curvature; _invalidate_connection_line_cache(); connections_layer->queue_redraw(); @@ -2550,7 +2584,9 @@ float GraphEdit::get_connection_lines_curvature() const { } void GraphEdit::set_connection_lines_thickness(float p_thickness) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); ERR_FAIL_COND_MSG(p_thickness < 0, "Connection lines thickness must be greater than or equal to 0."); + if (lines_thickness == p_thickness) { return; } @@ -2565,6 +2601,8 @@ float GraphEdit::get_connection_lines_thickness() const { } void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) { + ERR_FAIL_NULL_MSG(connections_layer, "connections_layer is missing."); + if (lines_antialiased == p_antialiased) { return; } diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index bf16c0699e..790a088368 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -1039,7 +1039,7 @@ void ItemList::_notification(int p_what) { scroll_bar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -theme_cache.panel_style->get_margin(SIDE_BOTTOM)); Size2 size = get_size(); - int width = size.width - theme_cache.panel_style->get_minimum_size().width; + int width = size.width - theme_cache.panel_style->get_margin(SIDE_RIGHT); if (scroll_bar->is_visible()) { width -= scroll_bar_minwidth; } @@ -1192,9 +1192,9 @@ void ItemList::_notification(int p_what) { Point2 pos = items[i].rect_cache.position + icon_ofs + base_ofs; if (icon_mode == ICON_MODE_TOP) { - pos.y += theme_cache.v_separation / 2; + pos.y += MAX(theme_cache.v_separation, 0) / 2; } else { - pos.x += theme_cache.h_separation / 2; + pos.x += MAX(theme_cache.h_separation, 0) / 2; } if (icon_mode == ICON_MODE_TOP) { @@ -1243,8 +1243,8 @@ void ItemList::_notification(int p_what) { } Point2 draw_pos = items[i].rect_cache.position; - draw_pos.x += theme_cache.h_separation / 2; - draw_pos.y += theme_cache.v_separation / 2; + draw_pos.x += MAX(theme_cache.h_separation, 0) / 2; + draw_pos.y += MAX(theme_cache.v_separation, 0) / 2; if (rtl) { draw_pos.x = size.width - draw_pos.x - tag_icon_size.x; } @@ -1283,8 +1283,7 @@ void ItemList::_notification(int p_what) { text_ofs += base_ofs; text_ofs += items[i].rect_cache.position; - text_ofs.x += theme_cache.h_separation / 2; - text_ofs.y += theme_cache.v_separation / 2; + text_ofs.y += MAX(theme_cache.v_separation, 0) / 2; if (rtl) { text_ofs.x = size.width - text_ofs.x - max_len; @@ -1292,7 +1291,7 @@ void ItemList::_notification(int p_what) { items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_CENTER); - float text_w = items[i].rect_cache.size.width - theme_cache.h_separation; + float text_w = items[i].rect_cache.size.width; items.write[i].text_buf->set_width(text_w); if (theme_cache.font_outline_size > 0 && theme_cache.font_outline_color.a > 0) { @@ -1307,17 +1306,17 @@ void ItemList::_notification(int p_what) { if (icon_mode == ICON_MODE_TOP) { text_ofs.x += (items[i].rect_cache.size.width - size2.x) / 2; - text_ofs.x += theme_cache.h_separation / 2; - text_ofs.y += theme_cache.v_separation / 2; + text_ofs.x += MAX(theme_cache.h_separation, 0) / 2; + text_ofs.y += MAX(theme_cache.v_separation, 0) / 2; } else { text_ofs.y += (items[i].rect_cache.size.height - size2.y) / 2; - text_ofs.x += theme_cache.h_separation / 2; + text_ofs.x += MAX(theme_cache.h_separation, 0) / 2; } text_ofs += base_ofs; text_ofs += items[i].rect_cache.position; - float text_w = width - text_ofs.x - theme_cache.h_separation; + float text_w = width - text_ofs.x; items.write[i].text_buf->set_width(text_w); if (rtl) { @@ -1410,14 +1409,14 @@ void ItemList::force_update_list_size() { max_column_width = MAX(max_column_width, minsize.x); // Elements need to adapt to the selected size. - minsize.y += theme_cache.v_separation; - minsize.x += theme_cache.h_separation; + minsize.y += MAX(theme_cache.v_separation, 0); + minsize.x += MAX(theme_cache.h_separation, 0); items.write[i].rect_cache.size = minsize; items.write[i].min_rect_cache.size = minsize; } - int fit_size = size.x - theme_cache.panel_style->get_minimum_size().width - scroll_bar_minwidth; + int fit_size = size.x - theme_cache.panel_style->get_minimum_size().width; //2-attempt best fit current_columns = 0x7FFFFFFF; @@ -1443,7 +1442,7 @@ void ItemList::force_update_list_size() { } if (same_column_width) { - items.write[i].rect_cache.size.x = max_column_width + theme_cache.h_separation; + items.write[i].rect_cache.size.x = max_column_width + MAX(theme_cache.h_separation, 0); } items.write[i].rect_cache.position = ofs; @@ -1468,13 +1467,17 @@ void ItemList::force_update_list_size() { } } + float page = MAX(0, size.height - theme_cache.panel_style->get_minimum_size().height); + float max = MAX(page, ofs.y + max_h); + if (page >= max) { + fit_size -= scroll_bar_minwidth; + } + if (all_fit) { for (int j = items.size() - 1; j >= 0 && col > 0; j--, col--) { items.write[j].rect_cache.size.y = max_h; } - float page = MAX(0, size.height - theme_cache.panel_style->get_minimum_size().height); - float max = MAX(page, ofs.y + max_h); if (auto_height) { auto_height_value = ofs.y + max_h + theme_cache.panel_style->get_minimum_size().height; } diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index e4f52ee8ee..8238d54381 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -185,6 +185,26 @@ TreeItem::TreeCellMode TreeItem::get_cell_mode(int p_column) const { return cells[p_column].mode; } +/* auto translate mode */ +void TreeItem::set_auto_translate_mode(int p_column, Node::AutoTranslateMode p_mode) { + ERR_FAIL_INDEX(p_column, cells.size()); + + if (cells[p_column].auto_translate_mode == p_mode) { + return; + } + + cells.write[p_column].auto_translate_mode = p_mode; + cells.write[p_column].dirty = true; + cells.write[p_column].cached_minimum_size_dirty = true; + + _changed_notify(p_column); +} + +Node::AutoTranslateMode TreeItem::get_auto_translate_mode(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Node::AUTO_TRANSLATE_MODE_INHERIT); + return cells[p_column].auto_translate_mode; +} + /* multiline editable */ void TreeItem::set_edit_multiline(int p_column, bool p_multiline) { ERR_FAIL_INDEX(p_column, cells.size()); @@ -247,6 +267,24 @@ void TreeItem::propagate_check(int p_column, bool p_emit_signal) { _propagate_check_through_parents(p_column, p_emit_signal); } +String TreeItem::atr(int p_column, const String &p_text) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), tree->atr(p_text)); + + switch (cells[p_column].auto_translate_mode) { + case Node::AUTO_TRANSLATE_MODE_INHERIT: { + return tree->atr(p_text); + } break; + case Node::AUTO_TRANSLATE_MODE_ALWAYS: { + return tree->tr(p_text); + } break; + case Node::AUTO_TRANSLATE_MODE_DISABLED: { + return p_text; + } break; + } + + ERR_FAIL_V_MSG(tree->atr(p_text), "Unexpected auto translate mode: " + itos(cells[p_column].auto_translate_mode)); +} + void TreeItem::_propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal) { TreeItem *current = get_first_child(); while (current) { @@ -323,7 +361,7 @@ void TreeItem::set_text(int p_column, String p_text) { } else { // Don't auto translate if it's in string mode and editable, as the text can be changed to anything by the user. if (tree && (!cells[p_column].editable || cells[p_column].mode != TreeItem::CELL_MODE_STRING)) { - cells.write[p_column].xl_text = tree->atr(p_text); + cells.write[p_column].xl_text = atr(p_column, p_text); } else { cells.write[p_column].xl_text = p_text; } @@ -1621,6 +1659,9 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_cell_mode", "column", "mode"), &TreeItem::set_cell_mode); ClassDB::bind_method(D_METHOD("get_cell_mode", "column"), &TreeItem::get_cell_mode); + ClassDB::bind_method(D_METHOD("set_auto_translate_mode", "column", "mode"), &TreeItem::set_auto_translate_mode); + ClassDB::bind_method(D_METHOD("get_auto_translate_mode", "column"), &TreeItem::get_auto_translate_mode); + ClassDB::bind_method(D_METHOD("set_edit_multiline", "column", "multiline"), &TreeItem::set_edit_multiline); ClassDB::bind_method(D_METHOD("is_edit_multiline", "column"), &TreeItem::is_edit_multiline); @@ -2009,7 +2050,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { int option = (int)p_item->cells[p_col].val; - valtext = atr(ETR("(Other)")); + valtext = p_item->atr(p_col, ETR("(Other)")); Vector<String> strings = p_item->cells[p_col].text.split(","); for (int j = 0; j < strings.size(); j++) { int value = j; @@ -2017,7 +2058,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { value = strings[j].get_slicec(':', 1).to_int(); } if (option == value) { - valtext = atr(strings[j].get_slicec(':', 0)); + valtext = p_item->atr(p_col, strings[j].get_slicec(':', 0)); break; } } @@ -2028,7 +2069,7 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { } else { // Don't auto translate if it's in string mode and editable, as the text can be changed to anything by the user. if (!p_item->cells[p_col].editable || p_item->cells[p_col].mode != TreeItem::CELL_MODE_STRING) { - p_item->cells.write[p_col].xl_text = atr(p_item->cells[p_col].text); + p_item->cells.write[p_col].xl_text = p_item->atr(p_col, p_item->cells[p_col].text); } else { p_item->cells.write[p_col].xl_text = p_item->cells[p_col].text; } diff --git a/scene/gui/tree.h b/scene/gui/tree.h index 17ea31a733..86efdfec52 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -64,6 +64,7 @@ private: Rect2i icon_region; String text; String xl_text; + Node::AutoTranslateMode auto_translate_mode = Node::AUTO_TRANSLATE_MODE_INHERIT; bool edit_multiline = false; String suffix; Ref<TextParagraph> text_buf; @@ -210,6 +211,10 @@ public: void set_cell_mode(int p_column, TreeCellMode p_mode); TreeCellMode get_cell_mode(int p_column) const; + /* auto translate mode */ + void set_auto_translate_mode(int p_column, Node::AutoTranslateMode p_mode); + Node::AutoTranslateMode get_auto_translate_mode(int p_column) const; + /* multiline editable */ void set_edit_multiline(int p_column, bool p_multiline); bool is_edit_multiline(int p_column) const; @@ -222,6 +227,8 @@ public: void propagate_check(int p_column, bool p_emit_signal = true); + String atr(int p_column, const String &p_text) const; + private: // Check helpers. void _propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal); diff --git a/scene/main/http_request.cpp b/scene/main/http_request.cpp index 3469b806a6..8526611093 100644 --- a/scene/main/http_request.cpp +++ b/scene/main/http_request.cpp @@ -49,7 +49,8 @@ Error HTTPRequest::_parse_url(const String &p_url) { redirections = 0; String scheme; - Error err = p_url.parse_url(scheme, url, port, request_string); + String fragment; + Error err = p_url.parse_url(scheme, url, port, request_string, fragment); ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Error parsing URL: '%s'.", p_url)); if (scheme == "https://") { diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index 1dac4b97ad..57a4e35f7a 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -63,6 +63,23 @@ bool Animation::_set(const StringName &p_name, const Variant &p_value) { } compression.enabled = true; return true; + } else if (prop_name == SNAME("markers")) { + Array markers = p_value; + for (const Dictionary marker : markers) { + ERR_FAIL_COND_V(!marker.has("name"), false); + ERR_FAIL_COND_V(!marker.has("time"), false); + StringName marker_name = marker["name"]; + double time = marker["time"]; + _marker_insert(time, marker_names, MarkerKey(time, marker_name)); + marker_times.insert(marker_name, time); + Color color = Color(1, 1, 1); + if (marker.has("color")) { + color = marker["color"]; + } + marker_colors.insert(marker_name, color); + } + + return true; } else if (prop_name.begins_with("tracks/")) { int track = prop_name.get_slicec('/', 1).to_int(); String what = prop_name.get_slicec('/', 2); @@ -470,6 +487,18 @@ bool Animation::_get(const StringName &p_name, Variant &r_ret) const { r_ret = comp; return true; + } else if (prop_name == SNAME("markers")) { + Array markers; + + for (HashMap<StringName, double>::ConstIterator E = marker_times.begin(); E; ++E) { + Dictionary d; + d["name"] = E->key; + d["time"] = E->value; + d["color"] = marker_colors[E->key]; + markers.push_back(d); + } + + r_ret = markers; } else if (prop_name == "length") { r_ret = length; } else if (prop_name == "loop_mode") { @@ -839,6 +868,7 @@ void Animation::_get_property_list(List<PropertyInfo> *p_list) const { if (compression.enabled) { p_list->push_back(PropertyInfo(Variant::DICTIONARY, "_compression", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); } + p_list->push_back(PropertyInfo(Variant::ARRAY, "markers", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); for (int i = 0; i < tracks.size(); i++) { p_list->push_back(PropertyInfo(Variant::STRING, "tracks/" + itos(i) + "/type", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); p_list->push_back(PropertyInfo(Variant::BOOL, "tracks/" + itos(i) + "/imported", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL)); @@ -1087,6 +1117,27 @@ int Animation::_insert(double p_time, T &p_keys, const V &p_value) { return -1; } +int Animation::_marker_insert(double p_time, Vector<MarkerKey> &p_keys, const MarkerKey &p_value) { + int idx = p_keys.size(); + + while (true) { + // Condition for replacement. + if (idx > 0 && Math::is_equal_approx((double)p_keys[idx - 1].time, p_time)) { + p_keys.write[idx - 1] = p_value; + return idx - 1; + + // Condition for insert. + } else if (idx == 0 || p_keys[idx - 1].time < p_time) { + p_keys.insert(idx, p_value); + return idx; + } + + idx--; + } + + return -1; +} + template <typename T> void Animation::_clear(T &p_keys) { p_keys.clear(); @@ -3163,6 +3214,90 @@ void Animation::track_get_key_indices_in_range(int p_track, double p_time, doubl } } +void Animation::add_marker(const StringName &p_name, double p_time) { + int idx = _find(marker_names, p_time); + + if (idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(p_time, marker_names[idx].time)) { + marker_times.erase(marker_names[idx].name); + marker_colors.erase(marker_names[idx].name); + marker_names.write[idx].name = p_name; + marker_times.insert(p_name, p_time); + marker_colors.insert(p_name, Color(1, 1, 1)); + } else { + _marker_insert(p_time, marker_names, MarkerKey(p_time, p_name)); + marker_times.insert(p_name, p_time); + marker_colors.insert(p_name, Color(1, 1, 1)); + } +} + +void Animation::remove_marker(const StringName &p_name) { + HashMap<StringName, double>::Iterator E = marker_times.find(p_name); + ERR_FAIL_COND(!E); + int idx = _find(marker_names, E->value); + bool success = idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(marker_names[idx].time, E->value); + ERR_FAIL_COND(!success); + marker_names.remove_at(idx); + marker_times.remove(E); + marker_colors.erase(p_name); +} + +bool Animation::has_marker(const StringName &p_name) const { + return marker_times.has(p_name); +} + +StringName Animation::get_marker_at_time(double p_time) const { + int idx = _find(marker_names, p_time); + + if (idx >= 0 && idx < marker_names.size() && Math::is_equal_approx(marker_names[idx].time, p_time)) { + return marker_names[idx].name; + } + + return StringName(); +} + +StringName Animation::get_next_marker(double p_time) const { + int idx = _find(marker_names, p_time); + + if (idx >= -1 && idx < marker_names.size() - 1) { + // _find ensures that the time at idx is always the closest time to p_time that is also smaller to it. + // So we add 1 to get the next marker. + return marker_names[idx + 1].name; + } + return StringName(); +} + +StringName Animation::get_prev_marker(double p_time) const { + int idx = _find(marker_names, p_time); + + if (idx >= 0 && idx < marker_names.size()) { + return marker_names[idx].name; + } + return StringName(); +} + +double Animation::get_marker_time(const StringName &p_name) const { + ERR_FAIL_COND_V(!marker_times.has(p_name), -1); + return marker_times.get(p_name); +} + +PackedStringArray Animation::get_marker_names() const { + PackedStringArray names; + // We iterate on marker_names so the result is sorted by time. + for (const MarkerKey &marker_name : marker_names) { + names.push_back(marker_name.name); + } + return names; +} + +Color Animation::get_marker_color(const StringName &p_name) const { + ERR_FAIL_COND_V(!marker_colors.has(p_name), Color()); + return marker_colors[p_name]; +} + +void Animation::set_marker_color(const StringName &p_name, const Color &p_color) { + marker_colors[p_name] = p_color; +} + Vector<Variant> Animation::method_track_get_params(int p_track, int p_key_idx) const { ERR_FAIL_INDEX_V(p_track, tracks.size(), Vector<Variant>()); Track *t = tracks[p_track]; @@ -3894,6 +4029,17 @@ void Animation::_bind_methods() { ClassDB::bind_method(D_METHOD("animation_track_set_key_animation", "track_idx", "key_idx", "animation"), &Animation::animation_track_set_key_animation); ClassDB::bind_method(D_METHOD("animation_track_get_key_animation", "track_idx", "key_idx"), &Animation::animation_track_get_key_animation); + ClassDB::bind_method(D_METHOD("add_marker", "name", "time"), &Animation::add_marker); + ClassDB::bind_method(D_METHOD("remove_marker", "name"), &Animation::remove_marker); + ClassDB::bind_method(D_METHOD("has_marker", "name"), &Animation::has_marker); + ClassDB::bind_method(D_METHOD("get_marker_at_time", "time"), &Animation::get_marker_at_time); + ClassDB::bind_method(D_METHOD("get_next_marker", "time"), &Animation::get_next_marker); + ClassDB::bind_method(D_METHOD("get_prev_marker", "time"), &Animation::get_prev_marker); + ClassDB::bind_method(D_METHOD("get_marker_time", "name"), &Animation::get_marker_time); + ClassDB::bind_method(D_METHOD("get_marker_names"), &Animation::get_marker_names); + ClassDB::bind_method(D_METHOD("get_marker_color", "name"), &Animation::get_marker_color); + ClassDB::bind_method(D_METHOD("set_marker_color", "name", "color"), &Animation::set_marker_color); + ClassDB::bind_method(D_METHOD("set_length", "time_sec"), &Animation::set_length); ClassDB::bind_method(D_METHOD("get_length"), &Animation::get_length); diff --git a/scene/resources/animation.h b/scene/resources/animation.h index 0c29790ea4..618dc9ca17 100644 --- a/scene/resources/animation.h +++ b/scene/resources/animation.h @@ -237,6 +237,20 @@ private: } }; + /* Marker */ + + struct MarkerKey { + double time; + StringName name; + MarkerKey(double p_time, const StringName &p_name) : + time(p_time), name(p_name) {} + MarkerKey() = default; + }; + + Vector<MarkerKey> marker_names; // time -> name + HashMap<StringName, double> marker_times; // name -> time + HashMap<StringName, Color> marker_colors; // name -> color + Vector<Track *> tracks; template <typename T> @@ -245,6 +259,8 @@ private: template <typename T, typename V> int _insert(double p_time, T &p_keys, const V &p_value); + int _marker_insert(double p_time, Vector<MarkerKey> &p_keys, const MarkerKey &p_value); + template <typename K> inline int _find(const Vector<K> &p_keys, double p_time, bool p_backward = false, bool p_limit = false) const; @@ -501,6 +517,17 @@ public: void track_get_key_indices_in_range(int p_track, double p_time, double p_delta, List<int> *p_indices, Animation::LoopedFlag p_looped_flag = Animation::LOOPED_FLAG_NONE) const; + void add_marker(const StringName &p_name, double p_time); + void remove_marker(const StringName &p_name); + bool has_marker(const StringName &p_name) const; + StringName get_marker_at_time(double p_time) const; + StringName get_next_marker(double p_time) const; + StringName get_prev_marker(double p_time) const; + double get_marker_time(const StringName &p_time) const; + PackedStringArray get_marker_names() const; + Color get_marker_color(const StringName &p_name) const; + void set_marker_color(const StringName &p_name, const Color &p_color); + void set_length(real_t p_length); real_t get_length() const; diff --git a/scene/resources/material.cpp b/scene/resources/material.cpp index ab25aabb81..9bd3f2edce 100644 --- a/scene/resources/material.cpp +++ b/scene/resources/material.cpp @@ -946,7 +946,7 @@ uniform vec4 refraction_texture_channel; code += "uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_linear_mipmap;\n"; } - if (proximity_fade_enabled) { + if (features[FEATURE_REFRACTION] || proximity_fade_enabled) { code += "uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;\n"; } @@ -1627,7 +1627,14 @@ void fragment() {)"; } code += R"( float ref_amount = 1.0 - albedo.a * albedo_tex.a; - EMISSION += textureLod(screen_texture, ref_ofs, ROUGHNESS * 8.0).rgb * ref_amount * EXPOSURE; + + float refraction_depth_tex = textureLod(depth_texture, ref_ofs, 0.0).r; + vec4 refraction_view_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, refraction_depth_tex, 1.0); + refraction_view_pos.xyz /= refraction_view_pos.w; + + // If the depth buffer is lower then the model's Z position, use the refracted UV, otherwise use the normal screen UV. + // At low depth differences, decrease refraction intensity to avoid sudden discontinuities. + EMISSION += textureLod(screen_texture, mix(SCREEN_UV, ref_ofs, smoothstep(0.0, 1.0, VERTEX.z - refraction_view_pos.z)), ROUGHNESS * 8.0).rgb * ref_amount * EXPOSURE; ALBEDO *= 1.0 - ref_amount; // Force transparency on the material (required for refraction). ALPHA = 1.0; @@ -1649,10 +1656,10 @@ void fragment() {)"; if (proximity_fade_enabled) { code += R"( // Proximity Fade: Enabled - float depth_tex = textureLod(depth_texture, SCREEN_UV, 0.0).r; - vec4 world_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, depth_tex, 1.0); - world_pos.xyz /= world_pos.w; - ALPHA *= clamp(1.0 - smoothstep(world_pos.z + proximity_fade_distance, world_pos.z, VERTEX.z), 0.0, 1.0); + float proximity_depth_tex = textureLod(depth_texture, SCREEN_UV, 0.0).r; + vec4 proximity_view_pos = INV_PROJECTION_MATRIX * vec4(SCREEN_UV * 2.0 - 1.0, proximity_depth_tex, 1.0); + proximity_view_pos.xyz /= proximity_view_pos.w; + ALPHA *= clamp(1.0 - smoothstep(proximity_view_pos.z + proximity_fade_distance, proximity_view_pos.z, VERTEX.z), 0.0, 1.0); )"; } diff --git a/scene/resources/style_box_flat.cpp b/scene/resources/style_box_flat.cpp index 60b91ef0cb..15816925c1 100644 --- a/scene/resources/style_box_flat.cpp +++ b/scene/resources/style_box_flat.cpp @@ -226,33 +226,16 @@ inline void set_inner_corner_radius(const Rect2 style_rect, const Rect2 inner_re real_t border_right = style_rect.size.width - inner_rect.size.width - border_left; real_t border_bottom = style_rect.size.height - inner_rect.size.height - border_top; - real_t rad; - - // Top left. - rad = MIN(border_top, border_left); - inner_corner_radius[0] = MAX(corner_radius[0] - rad, 0); - - // Top right; - rad = MIN(border_top, border_right); - inner_corner_radius[1] = MAX(corner_radius[1] - rad, 0); - - // Bottom right. - rad = MIN(border_bottom, border_right); - inner_corner_radius[2] = MAX(corner_radius[2] - rad, 0); - - // Bottom left. - rad = MIN(border_bottom, border_left); - inner_corner_radius[3] = MAX(corner_radius[3] - rad, 0); + inner_corner_radius[0] = MAX(corner_radius[0] - MIN(border_top, border_left), 0); // Top left. + inner_corner_radius[1] = MAX(corner_radius[1] - MIN(border_top, border_right), 0); // Top right. + inner_corner_radius[2] = MAX(corner_radius[2] - MIN(border_bottom, border_right), 0); // Bottom right. + inner_corner_radius[3] = MAX(corner_radius[3] - MIN(border_bottom, border_left), 0); // Bottom left. } inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices, Vector<Color> &colors, const Rect2 &style_rect, const real_t corner_radius[4], const Rect2 &ring_rect, const Rect2 &inner_rect, const Color &inner_color, const Color &outer_color, const int corner_detail, const Vector2 &skew, bool is_filled = false) { int vert_offset = verts.size(); - if (!vert_offset) { - vert_offset = 0; - } - - int adapted_corner_detail = (corner_radius[0] == 0 && corner_radius[1] == 0 && corner_radius[2] == 0 && corner_radius[3] == 0) ? 1 : corner_detail; + int adapted_corner_detail = (corner_radius[0] > 0) || (corner_radius[1] > 0) || (corner_radius[2] > 0) || (corner_radius[3] > 0) ? corner_detail : 1; bool draw_border = !is_filled; @@ -280,30 +263,44 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices, // If the center is filled, we do not draw the border and directly use the inner ring as reference. Because all calls to this // method either draw a ring or a filled rounded rectangle, but not both. - int max_inner_outer = draw_border ? 2 : 1; - - for (int corner_index = 0; corner_index < 4; corner_index++) { + real_t quarter_arc_rad = Math_PI / 2.0; + Point2 style_rect_center = style_rect.get_center(); + + int colors_size = colors.size(); + int verts_size = verts.size(); + int new_verts_amount = (adapted_corner_detail + 1) * (draw_border ? 8 : 4); + colors.resize(colors_size + new_verts_amount); + verts.resize(verts_size + new_verts_amount); + Color *colors_ptr = colors.ptrw(); + Vector2 *verts_ptr = verts.ptrw(); + + for (int corner_idx = 0; corner_idx < 4; corner_idx++) { for (int detail = 0; detail <= adapted_corner_detail; detail++) { - for (int inner_outer = 0; inner_outer < max_inner_outer; inner_outer++) { - real_t radius; - Color color; - Point2 corner_point; - if (inner_outer == 0) { - radius = inner_corner_radius[corner_index]; - color = inner_color; - corner_point = inner_points[corner_index]; - } else { - radius = ring_corner_radius[corner_index]; - color = outer_color; - corner_point = outer_points[corner_index]; - } + int idx_ofs = (adapted_corner_detail + 1) * corner_idx + detail; + if (draw_border) { + idx_ofs *= 2; + } - const real_t x = radius * (real_t)cos((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.x; - const real_t y = radius * (real_t)sin((corner_index + detail / (double)adapted_corner_detail) * (Math_TAU / 4.0) + Math_PI) + corner_point.y; - const float x_skew = -skew.x * (y - style_rect.get_center().y); - const float y_skew = -skew.y * (x - style_rect.get_center().x); - verts.push_back(Vector2(x + x_skew, y + y_skew)); - colors.push_back(color); + const real_t pt_angle = (corner_idx + detail / (double)adapted_corner_detail) * quarter_arc_rad + Math_PI; + const real_t angle_cosine = cos(pt_angle); + const real_t angle_sine = sin(pt_angle); + + { + const real_t x = inner_corner_radius[corner_idx] * angle_cosine + inner_points[corner_idx].x; + const real_t y = inner_corner_radius[corner_idx] * angle_sine + inner_points[corner_idx].y; + const float x_skew = -skew.x * (y - style_rect_center.y); + const float y_skew = -skew.y * (x - style_rect_center.x); + verts_ptr[verts_size + idx_ofs] = Vector2(x + x_skew, y + y_skew); + colors_ptr[colors_size + idx_ofs] = inner_color; + } + + if (draw_border) { + const real_t x = ring_corner_radius[corner_idx] * angle_cosine + outer_points[corner_idx].x; + const real_t y = ring_corner_radius[corner_idx] * angle_sine + outer_points[corner_idx].y; + const float x_skew = -skew.x * (y - style_rect_center.y); + const float y_skew = -skew.y * (x - style_rect_center.x); + verts_ptr[verts_size + idx_ofs + 1] = Vector2(x + x_skew, y + y_skew); + colors_ptr[colors_size + idx_ofs + 1] = outer_color; } } } @@ -313,10 +310,15 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices, // Fill the indices and the colors for the border. if (draw_border) { + int indices_size = indices.size(); + indices.resize(indices_size + ring_vert_count * 3); + int *indices_ptr = indices.ptrw(); + for (int i = 0; i < ring_vert_count; i++) { - indices.push_back(vert_offset + ((i + 0) % ring_vert_count)); - indices.push_back(vert_offset + ((i + 2) % ring_vert_count)); - indices.push_back(vert_offset + ((i + 1) % ring_vert_count)); + int idx_ofs = indices_size + i * 3; + indices_ptr[idx_ofs] = vert_offset + i % ring_vert_count; + indices_ptr[idx_ofs + 1] = vert_offset + (i + 2) % ring_vert_count; + indices_ptr[idx_ofs + 2] = vert_offset + (i + 1) % ring_vert_count; } } @@ -327,40 +329,30 @@ inline void draw_rounded_rectangle(Vector<Vector2> &verts, Vector<int> &indices, int stripes_count = ring_vert_count / 2 - 1; int last_vert_id = ring_vert_count - 1; + int indices_size = indices.size(); + indices.resize(indices_size + stripes_count * 6); + int *indices_ptr = indices.ptrw(); + for (int i = 0; i < stripes_count; i++) { + int idx_ofs = indices_size + i * 6; // Polygon 1. - indices.push_back(vert_offset + i); - indices.push_back(vert_offset + last_vert_id - i - 1); - indices.push_back(vert_offset + i + 1); + indices_ptr[idx_ofs] = vert_offset + i; + indices_ptr[idx_ofs + 1] = vert_offset + last_vert_id - i - 1; + indices_ptr[idx_ofs + 2] = vert_offset + i + 1; // Polygon 2. - indices.push_back(vert_offset + i); - indices.push_back(vert_offset + last_vert_id - 0 - i); - indices.push_back(vert_offset + last_vert_id - 1 - i); + indices_ptr[idx_ofs + 3] = vert_offset + i; + indices_ptr[idx_ofs + 4] = vert_offset + last_vert_id - i; + indices_ptr[idx_ofs + 5] = vert_offset + last_vert_id - i - 1; } } } inline void adapt_values(int p_index_a, int p_index_b, real_t *adapted_values, const real_t *p_values, const real_t p_width, const real_t p_max_a, const real_t p_max_b) { - if (p_values[p_index_a] + p_values[p_index_b] > p_width) { - real_t factor; - real_t new_value; - - factor = (real_t)p_width / (real_t)(p_values[p_index_a] + p_values[p_index_b]); - - new_value = (p_values[p_index_a] * factor); - if (new_value < adapted_values[p_index_a]) { - adapted_values[p_index_a] = new_value; - } - new_value = (p_values[p_index_b] * factor); - if (new_value < adapted_values[p_index_b]) { - adapted_values[p_index_b] = new_value; - } - } else { - adapted_values[p_index_a] = MIN(p_values[p_index_a], adapted_values[p_index_a]); - adapted_values[p_index_b] = MIN(p_values[p_index_b], adapted_values[p_index_b]); - } - adapted_values[p_index_a] = MIN(p_max_a, adapted_values[p_index_a]); - adapted_values[p_index_b] = MIN(p_max_b, adapted_values[p_index_b]); + real_t value_a = p_values[p_index_a]; + real_t value_b = p_values[p_index_b]; + real_t factor = MIN(1.0, p_width / (value_a + value_b)); + adapted_values[p_index_a] = MIN(MIN(value_a * factor, p_max_a), adapted_values[p_index_a]); + adapted_values[p_index_b] = MIN(MIN(value_b * factor, p_max_b), adapted_values[p_index_b]); } Rect2 StyleBoxFlat::get_draw_rect(const Rect2 &p_rect) const { @@ -388,7 +380,7 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const { } const bool rounded_corners = (corner_radius[0] > 0) || (corner_radius[1] > 0) || (corner_radius[2] > 0) || (corner_radius[3] > 0); - // Only enable antialiasing if it is actually needed. This improve performances + // Only enable antialiasing if it is actually needed. This improves performance // and maximizes sharpness for non-skewed StyleBoxes with sharp corners. const bool aa_on = (rounded_corners || !skew.is_zero_approx()) && anti_aliased; @@ -428,7 +420,7 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const { Vector<Color> colors; Vector<Point2> uvs; - // Create shadow + // Create shadow. if (draw_shadow) { Rect2 shadow_inner_rect = style_rect; shadow_inner_rect.position += shadow_offset; @@ -538,9 +530,10 @@ void StyleBoxFlat::draw(RID p_canvas_item, const Rect2 &p_rect) const { // Compute UV coordinates. Rect2 uv_rect = style_rect.grow(aa_on ? aa_size : 0); uvs.resize(verts.size()); + Point2 *uvs_ptr = uvs.ptrw(); for (int i = 0; i < verts.size(); i++) { - uvs.write[i].x = (verts[i].x - uv_rect.position.x) / uv_rect.size.width; - uvs.write[i].y = (verts[i].y - uv_rect.position.y) / uv_rect.size.height; + uvs_ptr[i].x = (verts[i].x - uv_rect.position.x) / uv_rect.size.width; + uvs_ptr[i].y = (verts[i].y - uv_rect.position.y) / uv_rect.size.height; } // Draw stylebox. diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index 5835ecfed0..70ef88e36d 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -1248,6 +1248,8 @@ void AudioServer::stop_playback_stream(Ref<AudioStreamPlayback> p_playback) { return; } + p_playback->stop(); + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); if (!playback_node) { return; diff --git a/servers/rendering/renderer_rd/shaders/canvas.glsl b/servers/rendering/renderer_rd/shaders/canvas.glsl index 704aafdfa5..fd0cd5bfad 100644 --- a/servers/rendering/renderer_rd/shaders/canvas.glsl +++ b/servers/rendering/renderer_rd/shaders/canvas.glsl @@ -493,7 +493,8 @@ void main() { #endif if (bool(draw_data.flags & FLAGS_CLIP_RECT_UV)) { - uv = clamp(uv, draw_data.src_rect.xy, draw_data.src_rect.xy + abs(draw_data.src_rect.zw)); + vec2 half_texpixel = draw_data.color_texture_pixel_size * 0.5; + uv = clamp(uv, draw_data.src_rect.xy + half_texpixel, draw_data.src_rect.xy + abs(draw_data.src_rect.zw) - half_texpixel); } #endif diff --git a/tests/core/io/test_packet_peer.h b/tests/core/io/test_packet_peer.h new file mode 100644 index 0000000000..59c8dadad8 --- /dev/null +++ b/tests/core/io/test_packet_peer.h @@ -0,0 +1,204 @@ +/**************************************************************************/ +/* test_packet_peer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_PACKET_PEER_H +#define TEST_PACKET_PEER_H + +#include "core/io/packet_peer.h" +#include "tests/test_macros.h" + +namespace TestPacketPeer { + +TEST_CASE("[PacketPeer][PacketPeerStream] Encode buffer max size") { + Ref<PacketPeerStream> pps; + pps.instantiate(); + + SUBCASE("Default value") { + CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024); + } + + SUBCASE("Max encode buffer must be at least 1024 bytes") { + ERR_PRINT_OFF; + pps->set_encode_buffer_max_size(42); + ERR_PRINT_ON; + + CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024); + } + + SUBCASE("Max encode buffer cannot exceed 256 MiB") { + ERR_PRINT_OFF; + pps->set_encode_buffer_max_size((256 * 1024 * 1024) + 42); + ERR_PRINT_ON; + + CHECK_EQ(pps->get_encode_buffer_max_size(), 8 * 1024 * 1024); + } + + SUBCASE("Should be next power of two") { + pps->set_encode_buffer_max_size(2000); + + CHECK_EQ(pps->get_encode_buffer_max_size(), 2048); + } +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Read a variant from peer") { + String godot_rules = "Godot Rules!!!"; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + spb->put_var(godot_rules); + spb->seek(0); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + + Variant value; + CHECK_EQ(pps->get_var(value), Error::OK); + CHECK_EQ(String(value), godot_rules); +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Read a variant from peer fails") { + Ref<PacketPeerStream> pps; + pps.instantiate(); + + Variant value; + ERR_PRINT_OFF; + CHECK_EQ(pps->get_var(value), Error::ERR_UNCONFIGURED); + ERR_PRINT_ON; +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Put a variant to peer") { + String godot_rules = "Godot Rules!!!"; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + + CHECK_EQ(pps->put_var(godot_rules), Error::OK); + + spb->seek(0); + CHECK_EQ(String(spb->get_var()), godot_rules); +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Put a variant to peer out of memory failure") { + String more_than_1mb = String("*").repeat(1024 + 1); + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + pps->set_encode_buffer_max_size(1024); + + ERR_PRINT_OFF; + CHECK_EQ(pps->put_var(more_than_1mb), Error::ERR_OUT_OF_MEMORY); + ERR_PRINT_ON; +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Get packet buffer") { + String godot_rules = "Godot Rules!!!"; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + // First 4 bytes are the length of the string. + CharString cs = godot_rules.ascii(); + Vector<uint8_t> buffer = { (uint8_t)(cs.length() + 1), 0, 0, 0 }; + buffer.resize_zeroed(4 + cs.length() + 1); + memcpy(buffer.ptrw() + 4, cs.get_data(), cs.length()); + spb->set_data_array(buffer); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + + buffer.clear(); + CHECK_EQ(pps->get_packet_buffer(buffer), Error::OK); + + CHECK_EQ(String(reinterpret_cast<const char *>(buffer.ptr())), godot_rules); +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Get packet buffer from an empty peer") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + + Vector<uint8_t> buffer; + ERR_PRINT_OFF; + CHECK_EQ(pps->get_packet_buffer(buffer), Error::ERR_UNAVAILABLE); + ERR_PRINT_ON; + CHECK_EQ(buffer.size(), 0); +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Put packet buffer") { + String godot_rules = "Godot Rules!!!"; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + + CHECK_EQ(pps->put_packet_buffer(godot_rules.to_ascii_buffer()), Error::OK); + + spb->seek(0); + CHECK_EQ(spb->get_string(), godot_rules); + // First 4 bytes are the length of the string. + CharString cs = godot_rules.ascii(); + Vector<uint8_t> buffer = { (uint8_t)cs.length(), 0, 0, 0 }; + buffer.resize(4 + cs.length()); + memcpy(buffer.ptrw() + 4, cs.get_data(), cs.length()); + CHECK_EQ(spb->get_data_array(), buffer); +} + +TEST_CASE("[PacketPeer][PacketPeerStream] Put packet buffer when is empty") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + Ref<PacketPeerStream> pps; + pps.instantiate(); + pps->set_stream_peer(spb); + + Vector<uint8_t> buffer; + CHECK_EQ(pps->put_packet_buffer(buffer), Error::OK); + + CHECK_EQ(spb->get_size(), 0); +} + +} // namespace TestPacketPeer + +#endif // TEST_PACKET_PEER_H diff --git a/tests/core/io/test_stream_peer.h b/tests/core/io/test_stream_peer.h new file mode 100644 index 0000000000..31bd69edd0 --- /dev/null +++ b/tests/core/io/test_stream_peer.h @@ -0,0 +1,289 @@ +/**************************************************************************/ +/* test_stream_peer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_STREAM_PEER_H +#define TEST_STREAM_PEER_H + +#include "core/io/stream_peer.h" +#include "tests/test_macros.h" + +namespace TestStreamPeer { + +TEST_CASE("[StreamPeer] Initialization through StreamPeerBuffer") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + CHECK_EQ(spb->is_big_endian_enabled(), false); +} + +TEST_CASE("[StreamPeer] Get and sets through StreamPeerBuffer") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + SUBCASE("A int8_t value") { + int8_t value = 42; + + spb->clear(); + spb->put_8(value); + spb->seek(0); + + CHECK_EQ(spb->get_8(), value); + } + + SUBCASE("A uint8_t value") { + uint8_t value = 42; + + spb->clear(); + spb->put_u8(value); + spb->seek(0); + + CHECK_EQ(spb->get_u8(), value); + } + + SUBCASE("A int16_t value") { + int16_t value = 42; + + spb->clear(); + spb->put_16(value); + spb->seek(0); + + CHECK_EQ(spb->get_16(), value); + } + + SUBCASE("A uint16_t value") { + uint16_t value = 42; + + spb->clear(); + spb->put_u16(value); + spb->seek(0); + + CHECK_EQ(spb->get_u16(), value); + } + + SUBCASE("A int32_t value") { + int32_t value = 42; + + spb->clear(); + spb->put_32(value); + spb->seek(0); + + CHECK_EQ(spb->get_32(), value); + } + + SUBCASE("A uint32_t value") { + uint32_t value = 42; + + spb->clear(); + spb->put_u32(value); + spb->seek(0); + + CHECK_EQ(spb->get_u32(), value); + } + + SUBCASE("A int64_t value") { + int64_t value = 42; + + spb->clear(); + spb->put_64(value); + spb->seek(0); + + CHECK_EQ(spb->get_64(), value); + } + + SUBCASE("A int64_t value") { + uint64_t value = 42; + + spb->clear(); + spb->put_u64(value); + spb->seek(0); + + CHECK_EQ(spb->get_u64(), value); + } + + SUBCASE("A float value") { + float value = 42.0f; + + spb->clear(); + spb->put_float(value); + spb->seek(0); + + CHECK_EQ(spb->get_float(), value); + } + + SUBCASE("A double value") { + double value = 42.0; + + spb->clear(); + spb->put_double(value); + spb->seek(0); + + CHECK_EQ(spb->get_double(), value); + } + + SUBCASE("A string value") { + String value = "Hello, World!"; + + spb->clear(); + spb->put_string(value); + spb->seek(0); + + CHECK_EQ(spb->get_string(), value); + } + + SUBCASE("A utf8 string value") { + String value = String::utf8("Hello✩, World✩!"); + + spb->clear(); + spb->put_utf8_string(value); + spb->seek(0); + + CHECK_EQ(spb->get_utf8_string(), value); + } + + SUBCASE("A variant value") { + Array value; + value.push_front(42); + value.push_front("Hello, World!"); + + spb->clear(); + spb->put_var(value); + spb->seek(0); + + CHECK_EQ(spb->get_var(), value); + } +} + +TEST_CASE("[StreamPeer] Get and sets big endian through StreamPeerBuffer") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + spb->set_big_endian(true); + + SUBCASE("A int16_t value") { + int16_t value = 42; + + spb->clear(); + spb->put_16(value); + spb->seek(0); + + CHECK_EQ(spb->get_16(), value); + } + + SUBCASE("A uint16_t value") { + uint16_t value = 42; + + spb->clear(); + spb->put_u16(value); + spb->seek(0); + + CHECK_EQ(spb->get_u16(), value); + } + + SUBCASE("A int32_t value") { + int32_t value = 42; + + spb->clear(); + spb->put_32(value); + spb->seek(0); + + CHECK_EQ(spb->get_32(), value); + } + + SUBCASE("A uint32_t value") { + uint32_t value = 42; + + spb->clear(); + spb->put_u32(value); + spb->seek(0); + + CHECK_EQ(spb->get_u32(), value); + } + + SUBCASE("A int64_t value") { + int64_t value = 42; + + spb->clear(); + spb->put_64(value); + spb->seek(0); + + CHECK_EQ(spb->get_64(), value); + } + + SUBCASE("A int64_t value") { + uint64_t value = 42; + + spb->clear(); + spb->put_u64(value); + spb->seek(0); + + CHECK_EQ(spb->get_u64(), value); + } + + SUBCASE("A float value") { + float value = 42.0f; + + spb->clear(); + spb->put_float(value); + spb->seek(0); + + CHECK_EQ(spb->get_float(), value); + } + + SUBCASE("A double value") { + double value = 42.0; + + spb->clear(); + spb->put_double(value); + spb->seek(0); + + CHECK_EQ(spb->get_double(), value); + } +} + +TEST_CASE("[StreamPeer] Get string when there is no string") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + ERR_PRINT_OFF; + CHECK_EQ(spb->get_string(), ""); + ERR_PRINT_ON; +} + +TEST_CASE("[StreamPeer] Get UTF8 string when there is no string") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + + ERR_PRINT_OFF; + CHECK_EQ(spb->get_utf8_string(), ""); + ERR_PRINT_ON; +} + +} // namespace TestStreamPeer + +#endif // TEST_STREAM_PEER_H diff --git a/tests/core/io/test_stream_peer_buffer.h b/tests/core/io/test_stream_peer_buffer.h new file mode 100644 index 0000000000..8ba9c0a72c --- /dev/null +++ b/tests/core/io/test_stream_peer_buffer.h @@ -0,0 +1,185 @@ +/**************************************************************************/ +/* test_stream_peer_buffer.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_STREAM_PEER_BUFFER_H +#define TEST_STREAM_PEER_BUFFER_H + +#include "core/io/stream_peer.h" +#include "tests/test_macros.h" + +namespace TestStreamPeerBuffer { + +TEST_CASE("[StreamPeerBuffer] Initialization") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + CHECK_EQ(spb->get_size(), 0); + CHECK_EQ(spb->get_position(), 0); + CHECK_EQ(spb->get_available_bytes(), 0); +} + +TEST_CASE("[StreamPeerBuffer] Seek") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + uint8_t first = 5; + uint8_t second = 7; + uint8_t third = 11; + + spb->put_u8(first); + spb->put_u8(second); + spb->put_u8(third); + + spb->seek(0); + CHECK_EQ(spb->get_u8(), first); + CHECK_EQ(spb->get_u8(), second); + CHECK_EQ(spb->get_u8(), third); + + spb->seek(1); + CHECK_EQ(spb->get_position(), 1); + CHECK_EQ(spb->get_u8(), second); + + spb->seek(1); + ERR_PRINT_OFF; + spb->seek(-1); + ERR_PRINT_ON; + CHECK_EQ(spb->get_position(), 1); + ERR_PRINT_OFF; + spb->seek(5); + ERR_PRINT_ON; + CHECK_EQ(spb->get_position(), 1); +} + +TEST_CASE("[StreamPeerBuffer] Resize") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + CHECK_EQ(spb->get_size(), 0); + CHECK_EQ(spb->get_position(), 0); + CHECK_EQ(spb->get_available_bytes(), 0); + + spb->resize(42); + CHECK_EQ(spb->get_size(), 42); + CHECK_EQ(spb->get_position(), 0); + CHECK_EQ(spb->get_available_bytes(), 42); + + spb->seek(21); + CHECK_EQ(spb->get_size(), 42); + CHECK_EQ(spb->get_position(), 21); + CHECK_EQ(spb->get_available_bytes(), 21); +} + +TEST_CASE("[StreamPeerBuffer] Get underlying data array") { + uint8_t first = 5; + uint8_t second = 7; + uint8_t third = 11; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + spb->put_u8(first); + spb->put_u8(second); + spb->put_u8(third); + + Vector<uint8_t> data_array = spb->get_data_array(); + + CHECK_EQ(data_array[0], first); + CHECK_EQ(data_array[1], second); + CHECK_EQ(data_array[2], third); +} + +TEST_CASE("[StreamPeerBuffer] Set underlying data array") { + uint8_t first = 5; + uint8_t second = 7; + uint8_t third = 11; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + spb->put_u8(1); + spb->put_u8(2); + spb->put_u8(3); + + Vector<uint8_t> new_data_array; + new_data_array.push_back(first); + new_data_array.push_back(second); + new_data_array.push_back(third); + + spb->set_data_array(new_data_array); + + CHECK_EQ(spb->get_u8(), first); + CHECK_EQ(spb->get_u8(), second); + CHECK_EQ(spb->get_u8(), third); +} + +TEST_CASE("[StreamPeerBuffer] Duplicate") { + uint8_t first = 5; + uint8_t second = 7; + uint8_t third = 11; + + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + spb->put_u8(first); + spb->put_u8(second); + spb->put_u8(third); + + Ref<StreamPeerBuffer> spb2 = spb->duplicate(); + + CHECK_EQ(spb2->get_u8(), first); + CHECK_EQ(spb2->get_u8(), second); + CHECK_EQ(spb2->get_u8(), third); +} + +TEST_CASE("[StreamPeerBuffer] Put data with size equal to zero does nothing") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + uint8_t data = 42; + + Error error = spb->put_data((const uint8_t *)&data, 0); + + CHECK_EQ(error, OK); + CHECK_EQ(spb->get_size(), 0); + CHECK_EQ(spb->get_position(), 0); + CHECK_EQ(spb->get_available_bytes(), 0); +} + +TEST_CASE("[StreamPeerBuffer] Get data with invalid size returns an error") { + Ref<StreamPeerBuffer> spb; + spb.instantiate(); + uint8_t data = 42; + spb->put_u8(data); + spb->seek(0); + + uint8_t data_out = 0; + Error error = spb->get_data(&data_out, 3); + + CHECK_EQ(error, ERR_INVALID_PARAMETER); + CHECK_EQ(spb->get_size(), 1); + CHECK_EQ(spb->get_position(), 1); +} + +} // namespace TestStreamPeerBuffer + +#endif // TEST_STREAM_PEER_BUFFER_H diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index 301771a3de..8559737e74 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -1988,6 +1988,61 @@ TEST_CASE("[String] Variant ptr indexed set") { CHECK_EQ(s, String("azcd")); } +TEST_CASE("[String][URL] Parse URL") { +#define CHECK_URL(m_url_to_parse, m_expected_schema, m_expected_host, m_expected_port, m_expected_path, m_expected_fragment, m_expected_error) \ + if (true) { \ + int port; \ + String url(m_url_to_parse), schema, host, path, fragment; \ + \ + CHECK_EQ(url.parse_url(schema, host, port, path, fragment), m_expected_error); \ + CHECK_EQ(schema, m_expected_schema); \ + CHECK_EQ(host, m_expected_host); \ + CHECK_EQ(path, m_expected_path); \ + CHECK_EQ(fragment, m_expected_fragment); \ + CHECK_EQ(port, m_expected_port); \ + } else \ + ((void)0) + + // All elements. + CHECK_URL("https://www.example.com:8080/path/to/file.html#fragment", "https://", "www.example.com", 8080, "/path/to/file.html", "fragment", Error::OK); + + // Valid URLs. + CHECK_URL("https://godotengine.org", "https://", "godotengine.org", 0, "", "", Error::OK); + CHECK_URL("https://godotengine.org/", "https://", "godotengine.org", 0, "/", "", Error::OK); + CHECK_URL("godotengine.org/", "", "godotengine.org", 0, "/", "", Error::OK); + CHECK_URL("HTTPS://godotengine.org/", "https://", "godotengine.org", 0, "/", "", Error::OK); + CHECK_URL("https://GODOTENGINE.ORG/", "https://", "godotengine.org", 0, "/", "", Error::OK); + CHECK_URL("http://godotengine.org", "http://", "godotengine.org", 0, "", "", Error::OK); + CHECK_URL("https://godotengine.org:8080", "https://", "godotengine.org", 8080, "", "", Error::OK); + CHECK_URL("https://godotengine.org/blog", "https://", "godotengine.org", 0, "/blog", "", Error::OK); + CHECK_URL("https://godotengine.org/blog/", "https://", "godotengine.org", 0, "/blog/", "", Error::OK); + CHECK_URL("https://docs.godotengine.org/en/stable", "https://", "docs.godotengine.org", 0, "/en/stable", "", Error::OK); + CHECK_URL("https://docs.godotengine.org/en/stable/", "https://", "docs.godotengine.org", 0, "/en/stable/", "", Error::OK); + CHECK_URL("https://me:secret@godotengine.org", "https://", "godotengine.org", 0, "", "", Error::OK); + CHECK_URL("https://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]/ipv6", "https://", "fedc:ba98:7654:3210:fedc:ba98:7654:3210", 0, "/ipv6", "", Error::OK); + + // Scheme vs Fragment. + CHECK_URL("google.com/#goto=http://redirect_url/", "", "google.com", 0, "/", "goto=http://redirect_url/", Error::OK); + + // Invalid URLs. + + // Invalid Scheme. + CHECK_URL("https_://godotengine.org", "", "https_", 0, "//godotengine.org", "", Error::ERR_INVALID_PARAMETER); + + // Multiple ports. + CHECK_URL("https://godotengine.org:8080:433", "https://", "", 0, "", "", Error::ERR_INVALID_PARAMETER); + // Missing ] on literal IPv6. + CHECK_URL("https://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210/ipv6", "https://", "", 0, "/ipv6", "", Error::ERR_INVALID_PARAMETER); + // Missing host. + CHECK_URL("https:///blog", "https://", "", 0, "/blog", "", Error::ERR_INVALID_PARAMETER); + // Invalid ports. + CHECK_URL("https://godotengine.org:notaport", "https://", "godotengine.org", 0, "", "", Error::ERR_INVALID_PARAMETER); + CHECK_URL("https://godotengine.org:-8080", "https://", "godotengine.org", -8080, "", "", Error::ERR_INVALID_PARAMETER); + CHECK_URL("https://godotengine.org:88888", "https://", "godotengine.org", 88888, "", "", Error::ERR_INVALID_PARAMETER); + +#undef CHECK_URL +} + TEST_CASE("[Stress][String] Empty via ' == String()'") { for (int i = 0; i < 100000; ++i) { String str = "Hello World!"; diff --git a/tests/scene/test_sky.h b/tests/scene/test_sky.h new file mode 100644 index 0000000000..812ea9b5ad --- /dev/null +++ b/tests/scene/test_sky.h @@ -0,0 +1,141 @@ +/**************************************************************************/ +/* test_sky.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_SKY_H +#define TEST_SKY_H + +#include "scene/resources/sky.h" + +#include "tests/test_macros.h" + +namespace TestSky { + +TEST_CASE("[SceneTree][Sky] Constructor") { + Sky *test_sky = memnew(Sky); + + CHECK(test_sky->get_process_mode() == Sky::PROCESS_MODE_AUTOMATIC); + CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_256); + CHECK(test_sky->get_material().is_null()); + memdelete(test_sky); +} + +TEST_CASE("[SceneTree][Sky] Radiance size setter and getter") { + Sky *test_sky = memnew(Sky); + + // Check default. + CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_256); + + test_sky->set_radiance_size(Sky::RADIANCE_SIZE_1024); + CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_1024); + + ERR_PRINT_OFF; + // Check setting invalid radiance size. + test_sky->set_radiance_size(Sky::RADIANCE_SIZE_MAX); + ERR_PRINT_ON; + + CHECK(test_sky->get_radiance_size() == Sky::RADIANCE_SIZE_1024); + + memdelete(test_sky); +} + +TEST_CASE("[SceneTree][Sky] Process mode setter and getter") { + Sky *test_sky = memnew(Sky); + + // Check default. + CHECK(test_sky->get_process_mode() == Sky::PROCESS_MODE_AUTOMATIC); + + test_sky->set_process_mode(Sky::PROCESS_MODE_INCREMENTAL); + CHECK(test_sky->get_process_mode() == Sky::PROCESS_MODE_INCREMENTAL); + + memdelete(test_sky); +} + +TEST_CASE("[SceneTree][Sky] Material setter and getter") { + Sky *test_sky = memnew(Sky); + Ref<Material> material = memnew(Material); + + SUBCASE("Material passed to the class should remain the same") { + test_sky->set_material(material); + CHECK(test_sky->get_material() == material); + } + SUBCASE("Material passed many times to the class should remain the same") { + test_sky->set_material(material); + test_sky->set_material(material); + test_sky->set_material(material); + CHECK(test_sky->get_material() == material); + } + SUBCASE("Material rewrite testing") { + Ref<Material> material1 = memnew(Material); + Ref<Material> material2 = memnew(Material); + + test_sky->set_material(material1); + test_sky->set_material(material2); + CHECK_MESSAGE(test_sky->get_material() != material1, + "After rewrite, second material should be in class."); + CHECK_MESSAGE(test_sky->get_material() == material2, + "After rewrite, second material should be in class."); + } + + SUBCASE("Assign same material to two skys") { + Sky *sky2 = memnew(Sky); + + test_sky->set_material(material); + sky2->set_material(material); + CHECK_MESSAGE(test_sky->get_material() == sky2->get_material(), + "Both skys should have the same material."); + memdelete(sky2); + } + + SUBCASE("Swapping materials between two skys") { + Sky *sky2 = memnew(Sky); + Ref<Material> material1 = memnew(Material); + Ref<Material> material2 = memnew(Material); + + test_sky->set_material(material1); + sky2->set_material(material2); + CHECK(test_sky->get_material() == material1); + CHECK(sky2->get_material() == material2); + + // Do the swap. + Ref<Material> temp = test_sky->get_material(); + test_sky->set_material(sky2->get_material()); + sky2->set_material(temp); + + CHECK(test_sky->get_material() == material2); + CHECK(sky2->get_material() == material1); + memdelete(sky2); + } + + memdelete(test_sky); +} + +} // namespace TestSky + +#endif // TEST_SKY_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 12ff3ad4bc..14a7855832 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -50,8 +50,11 @@ #include "tests/core/io/test_json.h" #include "tests/core/io/test_json_native.h" #include "tests/core/io/test_marshalls.h" +#include "tests/core/io/test_packet_peer.h" #include "tests/core/io/test_pck_packer.h" #include "tests/core/io/test_resource.h" +#include "tests/core/io/test_stream_peer.h" +#include "tests/core/io/test_stream_peer_buffer.h" #include "tests/core/io/test_xml_parser.h" #include "tests/core/math/test_aabb.h" #include "tests/core/math/test_astar.h" @@ -161,6 +164,7 @@ #include "tests/scene/test_path_follow_3d.h" #include "tests/scene/test_primitives.h" #include "tests/scene/test_skeleton_3d.h" +#include "tests/scene/test_sky.h" #endif // _3D_DISABLED #include "modules/modules_tests.gen.h" diff --git a/thirdparty/README.md b/thirdparty/README.md index a219839afc..0bd7c47292 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -652,7 +652,7 @@ Collection of single-file libraries used in Godot components. - `bcdec.h` * Upstream: https://github.com/iOrange/bcdec - * Version: git (026acf98ea271045cb10713daa96ba98528badb7, 2022) + * Version: git (3b29f8f44466c7d59852670f82f53905cf627d48, 2024) * License: MIT - `clipper.{cpp,hpp}` * Upstream: https://sourceforge.net/projects/polyclipping diff --git a/thirdparty/misc/bcdec.h b/thirdparty/misc/bcdec.h index 78074a0c90..275ee05d94 100644 --- a/thirdparty/misc/bcdec.h +++ b/thirdparty/misc/bcdec.h @@ -1,4 +1,4 @@ -/* bcdec.h - v0.96 +/* bcdec.h - v0.97 provides functions to decompress blocks of BC compressed images written by Sergii "iOrange" Kudlai in 2022 @@ -30,6 +30,11 @@ - Split BC6H decompression function into 'half' and 'float' variants + Michael Schmidt (@RunDevelopment) - Found better "magic" coefficients for integer interpolation + of reference colors in BC1 color block, that match with + the floating point interpolation. This also made it faster + than integer division by 3! + bugfixes: @linkmauve @@ -39,6 +44,9 @@ #ifndef BCDEC_HEADER_INCLUDED #define BCDEC_HEADER_INCLUDED +#define BCDEC_VERSION_MAJOR 0 +#define BCDEC_VERSION_MINOR 97 + /* if BCDEC_STATIC causes problems, try defining BCDECDEF to 'inline' or 'static inline' */ #ifndef BCDECDEF #ifdef BCDEC_STATIC @@ -96,6 +104,7 @@ BCDECDEF void bcdec_bc6h_float(const void* compressedBlock, void* decompressedBl BCDECDEF void bcdec_bc6h_half(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#endif /* BCDEC_HEADER_INCLUDED */ #ifdef BCDEC_IMPLEMENTATION @@ -110,35 +119,44 @@ static void bcdec__color_block(const void* compressedBlock, void* decompressedBl c0 = ((unsigned short*)compressedBlock)[0]; c1 = ((unsigned short*)compressedBlock)[1]; + /* Unpack 565 ref colors */ + r0 = (c0 >> 11) & 0x1F; + g0 = (c0 >> 5) & 0x3F; + b0 = c0 & 0x1F; + + r1 = (c1 >> 11) & 0x1F; + g1 = (c1 >> 5) & 0x3F; + b1 = c1 & 0x1F; + /* Expand 565 ref colors to 888 */ - r0 = (((c0 >> 11) & 0x1F) * 527 + 23) >> 6; - g0 = (((c0 >> 5) & 0x3F) * 259 + 33) >> 6; - b0 = ((c0 & 0x1F) * 527 + 23) >> 6; - refColors[0] = 0xFF000000 | (b0 << 16) | (g0 << 8) | r0; + r = (r0 * 527 + 23) >> 6; + g = (g0 * 259 + 33) >> 6; + b = (b0 * 527 + 23) >> 6; + refColors[0] = 0xFF000000 | (b << 16) | (g << 8) | r; - r1 = (((c1 >> 11) & 0x1F) * 527 + 23) >> 6; - g1 = (((c1 >> 5) & 0x3F) * 259 + 33) >> 6; - b1 = ((c1 & 0x1F) * 527 + 23) >> 6; - refColors[1] = 0xFF000000 | (b1 << 16) | (g1 << 8) | r1; + r = (r1 * 527 + 23) >> 6; + g = (g1 * 259 + 33) >> 6; + b = (b1 * 527 + 23) >> 6; + refColors[1] = 0xFF000000 | (b << 16) | (g << 8) | r; if (c0 > c1 || onlyOpaqueMode) { /* Standard BC1 mode (also BC3 color block uses ONLY this mode) */ /* color_2 = 2/3*color_0 + 1/3*color_1 color_3 = 1/3*color_0 + 2/3*color_1 */ - r = (2 * r0 + r1 + 1) / 3; - g = (2 * g0 + g1 + 1) / 3; - b = (2 * b0 + b1 + 1) / 3; + r = ((2 * r0 + r1) * 351 + 61) >> 7; + g = ((2 * g0 + g1) * 2763 + 1039) >> 11; + b = ((2 * b0 + b1) * 351 + 61) >> 7; refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; - r = (r0 + 2 * r1 + 1) / 3; - g = (g0 + 2 * g1 + 1) / 3; - b = (b0 + 2 * b1 + 1) / 3; + r = ((r0 + r1 * 2) * 351 + 61) >> 7; + g = ((g0 + g1 * 2) * 2763 + 1039) >> 11; + b = ((b0 + b1 * 2) * 351 + 61) >> 7; refColors[3] = 0xFF000000 | (b << 16) | (g << 8) | r; } else { /* Quite rare BC1A mode */ /* color_2 = 1/2*color_0 + 1/2*color_1; color_3 = 0; */ - r = (r0 + r1 + 1) >> 1; - g = (g0 + g1 + 1) >> 1; - b = (b0 + b1 + 1) >> 1; + r = ((r0 + r1) * 1053 + 125) >> 8; + g = ((g0 + g1) * 4145 + 1019) >> 11; + b = ((b0 + b1) * 1053 + 125) >> 8; refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; refColors[3] = 0x00000000; @@ -1269,8 +1287,6 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in #endif /* BCDEC_IMPLEMENTATION */ -#endif /* BCDEC_HEADER_INCLUDED */ - /* LICENSE: This software is available under 2 licenses -- choose whichever you prefer. @@ -1326,4 +1342,4 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to <https://unlicense.org> -*/
\ No newline at end of file +*/ diff --git a/thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch b/thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch index 8517b277d0..6b56d60181 100644 --- a/thirdparty/vulkan/patches/VKEnumStringHelper-use-volk.patch +++ b/thirdparty/vulkan/patches/VKEnumStringHelper-use-godot-vulkan.patch @@ -1,17 +1,13 @@ diff --git a/thirdparty/vulkan/vk_enum_string_helper.h b/thirdparty/vulkan/vk_enum_string_helper.h -index 9d2af46344..d61dbb1290 100644 +index 8026787ad4..7a54b12a38 100644 --- a/thirdparty/vulkan/vk_enum_string_helper.h +++ b/thirdparty/vulkan/vk_enum_string_helper.h -@@ -13,7 +13,11 @@ +@@ -13,7 +13,7 @@ #ifdef __cplusplus #include <string> #endif -#include <vulkan/vulkan.h> -+#ifdef USE_VOLK -+ #include <volk.h> -+#else -+ #include <vulkan/vulkan.h> -+#endif ++#include "drivers/vulkan/godot_vulkan.h" static inline const char* string_VkResult(VkResult input_value) { switch (input_value) { case VK_SUCCESS: diff --git a/thirdparty/vulkan/patches/VMA-use-volk.patch b/thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch index e2e5ea5ad4..a6c546e3d8 100644 --- a/thirdparty/vulkan/patches/VMA-use-volk.patch +++ b/thirdparty/vulkan/patches/VMA-use-godot-vulkan.patch @@ -1,17 +1,18 @@ diff --git a/thirdparty/vulkan/vk_mem_alloc.h b/thirdparty/vulkan/vk_mem_alloc.h -index 711f486571..e5eaa80e74 100644 +index 2307325d4e..ecb84094b9 100644 --- a/thirdparty/vulkan/vk_mem_alloc.h +++ b/thirdparty/vulkan/vk_mem_alloc.h -@@ -127,7 +127,11 @@ See documentation chapter: \ref statistics. +@@ -122,12 +122,12 @@ for user-defined purpose without allocating any real GPU memory. + See documentation chapter: \ref statistics. + */ + ++#include "drivers/vulkan/godot_vulkan.h" + + #ifdef __cplusplus extern "C" { #endif -#include <vulkan/vulkan.h> -+#ifdef USE_VOLK -+ #include <volk.h> -+#else -+ #include <vulkan/vulkan.h> -+#endif #if !defined(VMA_VULKAN_VERSION) #if defined(VK_VERSION_1_3) diff --git a/thirdparty/vulkan/vk_enum_string_helper.h b/thirdparty/vulkan/vk_enum_string_helper.h index 598453e745..7a54b12a38 100644 --- a/thirdparty/vulkan/vk_enum_string_helper.h +++ b/thirdparty/vulkan/vk_enum_string_helper.h @@ -13,11 +13,7 @@ #ifdef __cplusplus #include <string> #endif -#ifdef USE_VOLK - #include <volk.h> -#else - #include <vulkan/vulkan.h> -#endif +#include "drivers/vulkan/godot_vulkan.h" static inline const char* string_VkResult(VkResult input_value) { switch (input_value) { case VK_SUCCESS: diff --git a/thirdparty/vulkan/vk_mem_alloc.h b/thirdparty/vulkan/vk_mem_alloc.h index b39b73b17d..ecb84094b9 100644 --- a/thirdparty/vulkan/vk_mem_alloc.h +++ b/thirdparty/vulkan/vk_mem_alloc.h @@ -122,16 +122,12 @@ for user-defined purpose without allocating any real GPU memory. See documentation chapter: \ref statistics. */ +#include "drivers/vulkan/godot_vulkan.h" #ifdef __cplusplus extern "C" { #endif -#ifdef USE_VOLK - #include <volk.h> -#else - #include <vulkan/vulkan.h> -#endif #if !defined(VMA_VULKAN_VERSION) #if defined(VK_VERSION_1_3) |