diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6803d9afd52..893566e831a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug report -description: Report a bug in Godot +description: Report a bug in Redot body: - type: markdown @@ -8,8 +8,8 @@ body: When reporting bugs, please follow the guidelines in this template. This helps identify the problem precisely and thus enables contributors to fix it faster. - Write a descriptive issue title above. - The golden rule is to **always open *one* issue for *one* bug**. If you notice several bugs and want to report them, make sure to create one new issue for each of them. - - Search [open](https://github.com/godotengine/godot/issues) and [closed](https://github.com/godotengine/godot/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported. If you don't find a relevant match or if you're unsure, don't hesitate to **open a new issue**. The bugsquad will handle it from there if it's a duplicate. - - Verify that you are using a [supported Godot version](https://docs.godotengine.org/en/latest/about/release_policy.html). Please always check if your issue is reproducible in the latest version – it may already have been fixed! + - Search [open](https://github.com/Redot-Engine/redot-engine/issues) and [closed](https://github.com/Redot-Engine/redot-engine/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported. If you don't find a relevant match or if you're unsure, don't hesitate to **open a new issue**. The bugsquad will handle it from there if it's a duplicate. + - Verify that you are using a [supported Godot version](https://docs.redotengine.org/en/latest/about/release_policy.html). Please always check if your issue is reproducible in the latest version – it may already have been fixed! - If you use a custom build, please test if your issue is reproducible in official builds too. Likewise if you use any C++ modules, GDExtensions, or editor plugins, you should check if the bug is reproducible in a project without these. - type: textarea @@ -17,8 +17,8 @@ body: label: Tested versions description: | To properly fix a bug, we need to identify if the bug was recently introduced in the engine, or if it was always present. - - Please specify the Godot version you found the issue in, including the **Git commit hash** if using a development or non-official build. The exact Godot version (including the commit hash) can be copied by clicking the version shown in the editor (bottom bar) or in the project manager (top bar). - - If you can, **please test earlier Godot versions** (previous stable branch, and development snapshots of the current feature release) and, if applicable, newer versions (development snapshots for the next feature release). Mention whether the bug is reproducible or not in the versions you tested. You can find all Godot releases in our [download archive](https://godotengine.org/download/archive/). + - Please specify the Redot version you found the issue in, including the **Git commit hash** if using a development or non-official build. The exact Redot version (including the commit hash) can be copied by clicking the version shown in the editor (bottom bar) or in the project manager (top bar). + - If you can, **please test earlier Redot versions** (previous stable branch, and development snapshots of the current feature release) and, if applicable, newer versions (development snapshots for the next feature release). Mention whether the bug is reproducible or not in the versions you tested. You can find all Redot releases in our [download archive](https://redotengine.org/download/archive/). - The aim is for us to identify whether a bug is a **regression**, i.e. an issue that didn't exist in a previous version, but was introduced later on, breaking existing functionality. For example, if a bug is reproducible in 4.2.stable but not in 4.1.stable, we would like you to test intermediate 4.2 dev and beta snapshots to find which snapshot is the first one where the issue can be reproduced. placeholder: | @@ -35,8 +35,8 @@ body: - For issues that are likely OS-specific and/or graphics-related, please specify the CPU model and architecture. - For graphics-related issues, specify the GPU model, driver version, and the rendering backend (GLES2, GLES3, Vulkan). - **Bug reports not including the required information may be closed at the maintainers' discretion.** If in doubt, always include all the requested information; it's better to include too much information than not enough information. - - **Starting from Godot 4.1, you can copy this information to your clipboard by using *Help > Copy System Info* at the top of the editor window.** - placeholder: Windows 10 - Godot v4.0.3.stable - Vulkan (Forward+) - dedicated NVIDIA GeForce GTX 970 (nvidia, 510.85.02) - Intel Core i7-10700KF CPU @ 3.80GHz (16 Threads) + - **Starting from Redot 4.1, you can copy this information to your clipboard by using *Help > Copy System Info* at the top of the editor window.** + placeholder: Windows 10 - Redot v4.0.3.stable - Vulkan (Forward+) - dedicated NVIDIA GeForce GTX 970 (nvidia, 510.85.02) - Intel Core i7-10700KF CPU @ 3.80GHz (16 Threads) validations: required: true @@ -63,7 +63,7 @@ body: attributes: label: Minimal reproduction project (MRP) description: | - - A small Godot project which reproduces the issue, with no unnecessary files included. Be sure to not include the `.godot` folder in the archive (but keep `project.godot`). + - A small Redot project which reproduces the issue, with no unnecessary files included. Be sure to not include the `.godot` folder in the archive (but keep `project.godot`). - Having an MRP is very important for contributors to be able to reproduce the bug in the same way that you are experiencing it. When testing a potential fix for the issue, contributors will use the MRP to validate that the fix is working as intended. - If the reproduction steps are not project dependent (e.g. the bug is visible in a brand new project), you can write "N/A" in the field. - Drag and drop a ZIP archive to upload it (max 10 MB). **Do not select another field until the project is done uploading.** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f787bec00e9..730f333b581 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,14 +1,14 @@ blank_issues_enabled: false contact_links: - - name: Godot proposals - url: https://github.com/godotengine/godot-proposals - about: Please submit feature proposals on the Godot proposals repository, not here. + - name: Redot proposals + url: https://github.com/Redot-Engine/redot-proposals + about: Please submit feature proposals on the Redot proposals repository, not here. - - name: Godot documentation repository - url: https://github.com/godotengine/godot-docs - about: Please report issues with documentation on the Godot documentation repository, not here. + - name: Redot documentation repository + url: https://github.com/Redot-Engine/redot-docs + about: Please report issues with documentation on the Redot documentation repository, not here. - - name: Godot community channels - url: https://godotengine.org/community + - name: Redot community channels + url: https://redotengine.org/community about: Please ask for technical support on one of the other community channels, not here. diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index 562bde978e3..9fe54e57a7a 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -348,7 +348,6 @@ bool ProjectSettings::_get(const StringName &p_name, Variant &r_ret) const { _THREAD_SAFE_METHOD_ if (!props.has(p_name)) { - WARN_PRINT("Property not found: " + String(p_name)); return false; } r_ret = props[p_name].variant; diff --git a/core/extension/gdextension_interface.cpp b/core/extension/gdextension_interface.cpp index ddf90f61307..66b01611602 100644 --- a/core/extension/gdextension_interface.cpp +++ b/core/extension/gdextension_interface.cpp @@ -507,6 +507,14 @@ static GDExtensionBool gdextension_variant_has_key(GDExtensionConstVariantPtr p_ return ret; } +static GDObjectInstanceID gdextension_variant_get_object_instance_id(GDExtensionConstVariantPtr p_self) { + const Variant *self = (const Variant *)p_self; + if (likely(self->get_type() == Variant::OBJECT)) { + return self->operator ObjectID(); + } + return 0; +} + static void gdextension_variant_get_type_name(GDExtensionVariantType p_type, GDExtensionUninitializedVariantPtr r_ret) { String name = Variant::get_type_name((Variant::Type)p_type); memnew_placement(r_ret, String(name)); @@ -1610,6 +1618,7 @@ void gdextension_setup_interface() { REGISTER_INTERFACE_FUNC(variant_has_method); REGISTER_INTERFACE_FUNC(variant_has_member); REGISTER_INTERFACE_FUNC(variant_has_key); + REGISTER_INTERFACE_FUNC(variant_get_object_instance_id); REGISTER_INTERFACE_FUNC(variant_get_type_name); REGISTER_INTERFACE_FUNC(variant_can_convert); REGISTER_INTERFACE_FUNC(variant_can_convert_strict); diff --git a/core/extension/gdextension_interface.h b/core/extension/gdextension_interface.h index 9e3ce25698e..374dbfd0711 100644 --- a/core/extension/gdextension_interface.h +++ b/core/extension/gdextension_interface.h @@ -1307,6 +1307,21 @@ typedef GDExtensionBool (*GDExtensionInterfaceVariantHasMember)(GDExtensionVaria */ typedef GDExtensionBool (*GDExtensionInterfaceVariantHasKey)(GDExtensionConstVariantPtr p_self, GDExtensionConstVariantPtr p_key, GDExtensionBool *r_valid); +/** + * @name variant_get_object_instance_id + * @since 4.4 + * + * Gets the object instance ID from a variant of type GDEXTENSION_VARIANT_TYPE_OBJECT. + * + * If the variant isn't of type GDEXTENSION_VARIANT_TYPE_OBJECT, then zero will be returned. + * The instance ID will be returned even if the object is no longer valid - use `object_get_instance_by_id()` to check if the object is still valid. + * + * @param p_self A pointer to the Variant. + * + * @return The instance ID for the contained object. + */ +typedef GDObjectInstanceID (*GDExtensionInterfaceVariantGetObjectInstanceId)(GDExtensionConstVariantPtr p_self); + /** * @name variant_get_type_name * @since 4.1 diff --git a/core/io/resource.cpp b/core/io/resource.cpp index 5f8a4b85a4e..0ff4fbe490c 100644 --- a/core/io/resource.cpp +++ b/core/io/resource.cpp @@ -99,31 +99,42 @@ void Resource::set_path_cache(const String &p_path) { GDVIRTUAL_CALL(_set_path_cache, p_path); } +static thread_local RandomPCG unique_id_gen(0, RandomPCG::DEFAULT_INC); + +void Resource::seed_scene_unique_id(uint32_t p_seed) { + unique_id_gen.seed(p_seed); +} + String Resource::generate_scene_unique_id() { // Generate a unique enough hash, but still user-readable. // If it's not unique it does not matter because the saver will try again. - OS::DateTime dt = OS::get_singleton()->get_datetime(); - uint32_t hash = hash_murmur3_one_32(OS::get_singleton()->get_ticks_usec()); - hash = hash_murmur3_one_32(dt.year, hash); - hash = hash_murmur3_one_32(dt.month, hash); - hash = hash_murmur3_one_32(dt.day, hash); - hash = hash_murmur3_one_32(dt.hour, hash); - hash = hash_murmur3_one_32(dt.minute, hash); - hash = hash_murmur3_one_32(dt.second, hash); - hash = hash_murmur3_one_32(Math::rand(), hash); + if (unique_id_gen.get_seed() == 0) { + OS::DateTime dt = OS::get_singleton()->get_datetime(); + uint32_t hash = hash_murmur3_one_32(OS::get_singleton()->get_ticks_usec()); + hash = hash_murmur3_one_32(dt.year, hash); + hash = hash_murmur3_one_32(dt.month, hash); + hash = hash_murmur3_one_32(dt.day, hash); + hash = hash_murmur3_one_32(dt.hour, hash); + hash = hash_murmur3_one_32(dt.minute, hash); + hash = hash_murmur3_one_32(dt.second, hash); + hash = hash_murmur3_one_32(Math::rand(), hash); + unique_id_gen.seed(hash); + } + + uint32_t random_num = unique_id_gen.rand(); static constexpr uint32_t characters = 5; static constexpr uint32_t char_count = ('z' - 'a'); static constexpr uint32_t base = char_count + ('9' - '0'); String id; for (uint32_t i = 0; i < characters; i++) { - uint32_t c = hash % base; + uint32_t c = random_num % base; if (c < char_count) { id += String::chr('a' + c); } else { id += String::chr('0' + (c - char_count)); } - hash /= base; + random_num /= base; } return id; diff --git a/core/io/resource.h b/core/io/resource.h index 8966c0233ca..015f7ad197b 100644 --- a/core/io/resource.h +++ b/core/io/resource.h @@ -114,6 +114,7 @@ class Resource : public RefCounted { virtual void set_path_cache(const String &p_path); // Set raw path without involving resource cache. _FORCE_INLINE_ bool is_built_in() const { return path_cache.is_empty() || path_cache.contains("::") || path_cache.begins_with("local://"); } + static void seed_scene_unique_id(uint32_t p_seed); static String generate_scene_unique_id(); void set_scene_unique_id(const String &p_id); String get_scene_unique_id() const; diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index b4826c356e0..109999d6125 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -845,29 +845,27 @@ Error ResourceLoaderBinary::load() { } } - if (ClassDB::has_property(res->get_class_name(), name)) { - if (value.get_type() == Variant::ARRAY) { - Array set_array = value; - bool is_get_valid = false; - Variant get_value = res->get(name, &is_get_valid); - if (is_get_valid && get_value.get_type() == Variant::ARRAY) { - Array get_array = get_value; - if (!set_array.is_same_typed(get_array)) { - value = Array(set_array, get_array.get_typed_builtin(), get_array.get_typed_class_name(), get_array.get_typed_script()); - } + if (value.get_type() == Variant::ARRAY) { + Array set_array = value; + bool is_get_valid = false; + Variant get_value = res->get(name, &is_get_valid); + if (is_get_valid && get_value.get_type() == Variant::ARRAY) { + Array get_array = get_value; + if (!set_array.is_same_typed(get_array)) { + value = Array(set_array, get_array.get_typed_builtin(), get_array.get_typed_class_name(), get_array.get_typed_script()); } } + } - if (value.get_type() == Variant::DICTIONARY) { - Dictionary set_dict = value; - bool is_get_valid = false; - Variant get_value = res->get(name, &is_get_valid); - if (is_get_valid && get_value.get_type() == Variant::DICTIONARY) { - Dictionary get_dict = get_value; - if (!set_dict.is_same_typed(get_dict)) { - value = Dictionary(set_dict, get_dict.get_typed_key_builtin(), get_dict.get_typed_key_class_name(), get_dict.get_typed_key_script(), - get_dict.get_typed_value_builtin(), get_dict.get_typed_value_class_name(), get_dict.get_typed_value_script()); - } + if (value.get_type() == Variant::DICTIONARY) { + Dictionary set_dict = value; + bool is_get_valid = false; + Variant get_value = res->get(name, &is_get_valid); + if (is_get_valid && get_value.get_type() == Variant::DICTIONARY) { + Dictionary get_dict = get_value; + if (!set_dict.is_same_typed(get_dict)) { + value = Dictionary(set_dict, get_dict.get_typed_key_builtin(), get_dict.get_typed_key_class_name(), get_dict.get_typed_key_script(), + get_dict.get_typed_value_builtin(), get_dict.get_typed_value_class_name(), get_dict.get_typed_value_script()); } } } @@ -2136,6 +2134,8 @@ static String _resource_get_class(Ref p_resource) { } Error ResourceFormatSaverBinaryInstance::save(const String &p_path, const Ref &p_resource, uint32_t p_flags) { + Resource::seed_scene_unique_id(p_path.hash()); + Error err; Ref f; if (p_flags & ResourceSaver::FLAG_COMPRESS) { diff --git a/core/string/translation_domain.cpp b/core/string/translation_domain.cpp index b44eb40366a..6a5e1b2af85 100644 --- a/core/string/translation_domain.cpp +++ b/core/string/translation_domain.cpp @@ -33,6 +33,170 @@ #include "core/string/translation.h" #include "core/string/translation_server.h" +struct _character_accent_pair { + const char32_t character; + const char32_t *accented_character; +}; + +static _character_accent_pair _character_to_accented[] = { + { 'A', U"Å" }, + { 'B', U"ß" }, + { 'C', U"Ç" }, + { 'D', U"Ð" }, + { 'E', U"É" }, + { 'F', U"F́" }, + { 'G', U"Ĝ" }, + { 'H', U"Ĥ" }, + { 'I', U"Ĩ" }, + { 'J', U"Ĵ" }, + { 'K', U"ĸ" }, + { 'L', U"Ł" }, + { 'M', U"Ḿ" }, + { 'N', U"й" }, + { 'O', U"Ö" }, + { 'P', U"Ṕ" }, + { 'Q', U"Q́" }, + { 'R', U"Ř" }, + { 'S', U"Ŝ" }, + { 'T', U"Ŧ" }, + { 'U', U"Ũ" }, + { 'V', U"Ṽ" }, + { 'W', U"Ŵ" }, + { 'X', U"X́" }, + { 'Y', U"Ÿ" }, + { 'Z', U"Ž" }, + { 'a', U"á" }, + { 'b', U"ḅ" }, + { 'c', U"ć" }, + { 'd', U"d́" }, + { 'e', U"é" }, + { 'f', U"f́" }, + { 'g', U"ǵ" }, + { 'h', U"h̀" }, + { 'i', U"í" }, + { 'j', U"ǰ" }, + { 'k', U"ḱ" }, + { 'l', U"ł" }, + { 'm', U"m̀" }, + { 'n', U"ή" }, + { 'o', U"ô" }, + { 'p', U"ṕ" }, + { 'q', U"q́" }, + { 'r', U"ŕ" }, + { 's', U"š" }, + { 't', U"ŧ" }, + { 'u', U"ü" }, + { 'v', U"ṽ" }, + { 'w', U"ŵ" }, + { 'x', U"x́" }, + { 'y', U"ý" }, + { 'z', U"ź" }, +}; + +String TranslationDomain::_get_override_string(const String &p_message) const { + String res; + for (int i = 0; i < p_message.length(); i++) { + if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += p_message[i]; + res += p_message[i + 1]; + i++; + continue; + } + res += '*'; + } + return res; +} + +String TranslationDomain::_double_vowels(const String &p_message) const { + String res; + for (int i = 0; i < p_message.length(); i++) { + if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += p_message[i]; + res += p_message[i + 1]; + i++; + continue; + } + res += p_message[i]; + if (p_message[i] == 'a' || p_message[i] == 'e' || p_message[i] == 'i' || p_message[i] == 'o' || p_message[i] == 'u' || + p_message[i] == 'A' || p_message[i] == 'E' || p_message[i] == 'I' || p_message[i] == 'O' || p_message[i] == 'U') { + res += p_message[i]; + } + } + return res; +}; + +String TranslationDomain::_replace_with_accented_string(const String &p_message) const { + String res; + for (int i = 0; i < p_message.length(); i++) { + if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += p_message[i]; + res += p_message[i + 1]; + i++; + continue; + } + const char32_t *accented = _get_accented_version(p_message[i]); + if (accented) { + res += accented; + } else { + res += p_message[i]; + } + } + return res; +} + +String TranslationDomain::_wrap_with_fakebidi_characters(const String &p_message) const { + String res; + char32_t fakebidiprefix = U'\u202e'; + char32_t fakebidisuffix = U'\u202c'; + res += fakebidiprefix; + // The fake bidi unicode gets popped at every newline so pushing it back at every newline. + for (int i = 0; i < p_message.length(); i++) { + if (p_message[i] == '\n') { + res += fakebidisuffix; + res += p_message[i]; + res += fakebidiprefix; + } else if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += fakebidisuffix; + res += p_message[i]; + res += p_message[i + 1]; + res += fakebidiprefix; + i++; + } else { + res += p_message[i]; + } + } + res += fakebidisuffix; + return res; +} + +String TranslationDomain::_add_padding(const String &p_message, int p_length) const { + String underscores = String("_").repeat(p_length * pseudolocalization.expansion_ratio / 2); + String prefix = pseudolocalization.prefix + underscores; + String suffix = underscores + pseudolocalization.suffix; + + return prefix + p_message + suffix; +} + +const char32_t *TranslationDomain::_get_accented_version(char32_t p_character) const { + if (!is_ascii_alphabet_char(p_character)) { + return nullptr; + } + + for (unsigned int i = 0; i < sizeof(_character_to_accented) / sizeof(_character_to_accented[0]); i++) { + if (_character_to_accented[i].character == p_character) { + return _character_to_accented[i].accented_character; + } + } + + return nullptr; +} + +bool TranslationDomain::_is_placeholder(const String &p_message, int p_index) const { + return p_index < p_message.length() - 1 && p_message[p_index] == '%' && + (p_message[p_index + 1] == 's' || p_message[p_index + 1] == 'c' || p_message[p_index + 1] == 'd' || + p_message[p_index + 1] == 'o' || p_message[p_index + 1] == 'x' || p_message[p_index + 1] == 'X' || p_message[p_index + 1] == 'f'); +} + StringName TranslationDomain::get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_context) const { StringName res; int best_score = 0; @@ -129,9 +293,9 @@ StringName TranslationDomain::translate(const StringName &p_message, const Strin } if (!res) { - return p_message; + return pseudolocalization.enabled ? pseudolocalize(p_message) : p_message; } - return res; + return pseudolocalization.enabled ? pseudolocalize(res) : res; } StringName TranslationDomain::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { @@ -152,6 +316,100 @@ StringName TranslationDomain::translate_plural(const StringName &p_message, cons return res; } +bool TranslationDomain::is_pseudolocalization_enabled() const { + return pseudolocalization.enabled; +} + +void TranslationDomain::set_pseudolocalization_enabled(bool p_enabled) { + pseudolocalization.enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_accents_enabled() const { + return pseudolocalization.accents_enabled; +} + +void TranslationDomain::set_pseudolocalization_accents_enabled(bool p_enabled) { + pseudolocalization.accents_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_double_vowels_enabled() const { + return pseudolocalization.double_vowels_enabled; +} + +void TranslationDomain::set_pseudolocalization_double_vowels_enabled(bool p_enabled) { + pseudolocalization.double_vowels_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_fake_bidi_enabled() const { + return pseudolocalization.fake_bidi_enabled; +} + +void TranslationDomain::set_pseudolocalization_fake_bidi_enabled(bool p_enabled) { + pseudolocalization.fake_bidi_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_override_enabled() const { + return pseudolocalization.override_enabled; +} + +void TranslationDomain::set_pseudolocalization_override_enabled(bool p_enabled) { + pseudolocalization.override_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_skip_placeholders_enabled() const { + return pseudolocalization.skip_placeholders_enabled; +} + +void TranslationDomain::set_pseudolocalization_skip_placeholders_enabled(bool p_enabled) { + pseudolocalization.skip_placeholders_enabled = p_enabled; +} + +float TranslationDomain::get_pseudolocalization_expansion_ratio() const { + return pseudolocalization.expansion_ratio; +} + +void TranslationDomain::set_pseudolocalization_expansion_ratio(float p_ratio) { + pseudolocalization.expansion_ratio = p_ratio; +} + +String TranslationDomain::get_pseudolocalization_prefix() const { + return pseudolocalization.prefix; +} + +void TranslationDomain::set_pseudolocalization_prefix(const String &p_prefix) { + pseudolocalization.prefix = p_prefix; +} + +String TranslationDomain::get_pseudolocalization_suffix() const { + return pseudolocalization.suffix; +} + +void TranslationDomain::set_pseudolocalization_suffix(const String &p_suffix) { + pseudolocalization.suffix = p_suffix; +} + +StringName TranslationDomain::pseudolocalize(const StringName &p_message) const { + String message = p_message; + int length = message.length(); + if (pseudolocalization.override_enabled) { + message = _get_override_string(message); + } + + if (pseudolocalization.double_vowels_enabled) { + message = _double_vowels(message); + } + + if (pseudolocalization.accents_enabled) { + message = _replace_with_accented_string(message); + } + + if (pseudolocalization.fake_bidi_enabled) { + message = _wrap_with_fakebidi_characters(message); + } + + return _add_padding(message, length); +} + void TranslationDomain::_bind_methods() { ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationDomain::get_translation_object); ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationDomain::add_translation); @@ -159,6 +417,36 @@ void TranslationDomain::_bind_methods() { ClassDB::bind_method(D_METHOD("clear"), &TranslationDomain::clear); ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationDomain::translate, DEFVAL(StringName())); ClassDB::bind_method(D_METHOD("translate_plural", "message", "message_plural", "n", "context"), &TranslationDomain::translate_plural, DEFVAL(StringName())); + + ClassDB::bind_method(D_METHOD("is_pseudolocalization_enabled"), &TranslationDomain::is_pseudolocalization_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_accents_enabled"), &TranslationDomain::is_pseudolocalization_accents_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_accents_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_accents_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_double_vowels_enabled"), &TranslationDomain::is_pseudolocalization_double_vowels_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_double_vowels_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_double_vowels_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_fake_bidi_enabled"), &TranslationDomain::is_pseudolocalization_fake_bidi_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_fake_bidi_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_fake_bidi_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_override_enabled"), &TranslationDomain::is_pseudolocalization_override_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_override_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_override_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_skip_placeholders_enabled"), &TranslationDomain::is_pseudolocalization_skip_placeholders_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_skip_placeholders_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_skip_placeholders_enabled); + ClassDB::bind_method(D_METHOD("get_pseudolocalization_expansion_ratio"), &TranslationDomain::get_pseudolocalization_expansion_ratio); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_expansion_ratio", "ratio"), &TranslationDomain::set_pseudolocalization_expansion_ratio); + ClassDB::bind_method(D_METHOD("get_pseudolocalization_prefix"), &TranslationDomain::get_pseudolocalization_prefix); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_prefix", "prefix"), &TranslationDomain::set_pseudolocalization_prefix); + ClassDB::bind_method(D_METHOD("get_pseudolocalization_suffix"), &TranslationDomain::get_pseudolocalization_suffix); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_suffix", "suffix"), &TranslationDomain::set_pseudolocalization_suffix); + ClassDB::bind_method(D_METHOD("pseudolocalize", "message"), &TranslationDomain::pseudolocalize); + + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_accents_enabled"), "set_pseudolocalization_accents_enabled", "is_pseudolocalization_accents_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_double_vowels_enabled"), "set_pseudolocalization_double_vowels_enabled", "is_pseudolocalization_double_vowels_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_fake_bidi_enabled"), "set_pseudolocalization_fake_bidi_enabled", "is_pseudolocalization_fake_bidi_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_override_enabled"), "set_pseudolocalization_override_enabled", "is_pseudolocalization_override_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_skip_placeholders_enabled"), "set_pseudolocalization_skip_placeholders_enabled", "is_pseudolocalization_skip_placeholders_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::FLOAT, "pseudolocalization_expansion_ratio"), "set_pseudolocalization_expansion_ratio", "get_pseudolocalization_expansion_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::Type::STRING, "pseudolocalization_prefix"), "set_pseudolocalization_prefix", "get_pseudolocalization_prefix"); + ADD_PROPERTY(PropertyInfo(Variant::Type::STRING, "pseudolocalization_suffix"), "set_pseudolocalization_suffix", "get_pseudolocalization_suffix"); } TranslationDomain::TranslationDomain() { diff --git a/core/string/translation_domain.h b/core/string/translation_domain.h index 61399672174..55592d3b357 100644 --- a/core/string/translation_domain.h +++ b/core/string/translation_domain.h @@ -38,7 +38,28 @@ class Translation; class TranslationDomain : public RefCounted { GDCLASS(TranslationDomain, RefCounted); + struct PseudolocalizationConfig { + bool enabled = false; + bool accents_enabled = true; + bool double_vowels_enabled = false; + bool fake_bidi_enabled = false; + bool override_enabled = false; + bool skip_placeholders_enabled = true; + float expansion_ratio = 0.0; + String prefix = "["; + String suffix = "]"; + }; + HashSet> translations; + PseudolocalizationConfig pseudolocalization; + + String _get_override_string(const String &p_message) const; + String _double_vowels(const String &p_message) const; + String _replace_with_accented_string(const String &p_message) const; + String _wrap_with_fakebidi_characters(const String &p_message) const; + String _add_padding(const String &p_message, int p_length) const; + const char32_t *_get_accented_version(char32_t p_character) const; + bool _is_placeholder(const String &p_message, int p_index) const; protected: static void _bind_methods(); @@ -59,6 +80,27 @@ class TranslationDomain : public RefCounted { StringName translate(const StringName &p_message, const StringName &p_context) const; StringName translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const; + bool is_pseudolocalization_enabled() const; + void set_pseudolocalization_enabled(bool p_enabled); + bool is_pseudolocalization_accents_enabled() const; + void set_pseudolocalization_accents_enabled(bool p_enabled); + bool is_pseudolocalization_double_vowels_enabled() const; + void set_pseudolocalization_double_vowels_enabled(bool p_enabled); + bool is_pseudolocalization_fake_bidi_enabled() const; + void set_pseudolocalization_fake_bidi_enabled(bool p_enabled); + bool is_pseudolocalization_override_enabled() const; + void set_pseudolocalization_override_enabled(bool p_enabled); + bool is_pseudolocalization_skip_placeholders_enabled() const; + void set_pseudolocalization_skip_placeholders_enabled(bool p_enabled); + float get_pseudolocalization_expansion_ratio() const; + void set_pseudolocalization_expansion_ratio(float p_ratio); + String get_pseudolocalization_prefix() const; + void set_pseudolocalization_prefix(const String &p_prefix); + String get_pseudolocalization_suffix() const; + void set_pseudolocalization_suffix(const String &p_suffix); + + StringName pseudolocalize(const StringName &p_message) const; + TranslationDomain(); }; diff --git a/core/string/translation_server.cpp b/core/string/translation_server.cpp index c6b818a49b3..89b37d0b8a2 100644 --- a/core/string/translation_server.cpp +++ b/core/string/translation_server.cpp @@ -39,66 +39,6 @@ #include "main/main.h" #endif -struct _character_accent_pair { - const char32_t character; - const char32_t *accented_character; -}; - -static _character_accent_pair _character_to_accented[] = { - { 'A', U"Å" }, - { 'B', U"ß" }, - { 'C', U"Ç" }, - { 'D', U"Ð" }, - { 'E', U"É" }, - { 'F', U"F́" }, - { 'G', U"Ĝ" }, - { 'H', U"Ĥ" }, - { 'I', U"Ĩ" }, - { 'J', U"Ĵ" }, - { 'K', U"ĸ" }, - { 'L', U"Ł" }, - { 'M', U"Ḿ" }, - { 'N', U"й" }, - { 'O', U"Ö" }, - { 'P', U"Ṕ" }, - { 'Q', U"Q́" }, - { 'R', U"Ř" }, - { 'S', U"Ŝ" }, - { 'T', U"Ŧ" }, - { 'U', U"Ũ" }, - { 'V', U"Ṽ" }, - { 'W', U"Ŵ" }, - { 'X', U"X́" }, - { 'Y', U"Ÿ" }, - { 'Z', U"Ž" }, - { 'a', U"á" }, - { 'b', U"ḅ" }, - { 'c', U"ć" }, - { 'd', U"d́" }, - { 'e', U"é" }, - { 'f', U"f́" }, - { 'g', U"ǵ" }, - { 'h', U"h̀" }, - { 'i', U"í" }, - { 'j', U"ǰ" }, - { 'k', U"ḱ" }, - { 'l', U"ł" }, - { 'm', U"m̀" }, - { 'n', U"ή" }, - { 'o', U"ô" }, - { 'p', U"ṕ" }, - { 'q', U"q́" }, - { 'r', U"ŕ" }, - { 's', U"š" }, - { 't', U"ŧ" }, - { 'u', U"ü" }, - { 'v', U"ṽ" }, - { 'w', U"ŵ" }, - { 'x', U"x́" }, - { 'y', U"ý" }, - { 'z', U"ź" }, -}; - Vector TranslationServer::locale_script_info; HashMap TranslationServer::language_map; @@ -433,8 +373,7 @@ StringName TranslationServer::translate(const StringName &p_message, const Strin return p_message; } - const StringName res = main_domain->translate(p_message, p_context); - return pseudolocalization_enabled ? pseudolocalize(res) : res; + return main_domain->translate(p_message, p_context); } StringName TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { @@ -510,15 +449,15 @@ void TranslationServer::setup() { } fallback = GLOBAL_DEF("internationalization/locale/fallback", "en"); - pseudolocalization_enabled = GLOBAL_DEF("internationalization/pseudolocalization/use_pseudolocalization", false); - pseudolocalization_accents_enabled = GLOBAL_DEF("internationalization/pseudolocalization/replace_with_accents", true); - pseudolocalization_double_vowels_enabled = GLOBAL_DEF("internationalization/pseudolocalization/double_vowels", false); - pseudolocalization_fake_bidi_enabled = GLOBAL_DEF("internationalization/pseudolocalization/fake_bidi", false); - pseudolocalization_override_enabled = GLOBAL_DEF("internationalization/pseudolocalization/override", false); - expansion_ratio = GLOBAL_DEF("internationalization/pseudolocalization/expansion_ratio", 0.0); - pseudolocalization_prefix = GLOBAL_DEF("internationalization/pseudolocalization/prefix", "["); - pseudolocalization_suffix = GLOBAL_DEF("internationalization/pseudolocalization/suffix", "]"); - pseudolocalization_skip_placeholders_enabled = GLOBAL_DEF("internationalization/pseudolocalization/skip_placeholders", true); + main_domain->set_pseudolocalization_enabled(GLOBAL_DEF("internationalization/pseudolocalization/use_pseudolocalization", false)); + main_domain->set_pseudolocalization_accents_enabled(GLOBAL_DEF("internationalization/pseudolocalization/replace_with_accents", true)); + main_domain->set_pseudolocalization_double_vowels_enabled(GLOBAL_DEF("internationalization/pseudolocalization/double_vowels", false)); + main_domain->set_pseudolocalization_fake_bidi_enabled(GLOBAL_DEF("internationalization/pseudolocalization/fake_bidi", false)); + main_domain->set_pseudolocalization_override_enabled(GLOBAL_DEF("internationalization/pseudolocalization/override", false)); + main_domain->set_pseudolocalization_expansion_ratio(GLOBAL_DEF("internationalization/pseudolocalization/expansion_ratio", 0.0)); + main_domain->set_pseudolocalization_prefix(GLOBAL_DEF("internationalization/pseudolocalization/prefix", "[")); + main_domain->set_pseudolocalization_suffix(GLOBAL_DEF("internationalization/pseudolocalization/suffix", "]")); + main_domain->set_pseudolocalization_skip_placeholders_enabled(GLOBAL_DEF("internationalization/pseudolocalization/skip_placeholders", true)); #ifdef TOOLS_ENABLED ProjectSettings::get_singleton()->set_custom_property_info(PropertyInfo(Variant::STRING, "internationalization/locale/fallback", PROPERTY_HINT_LOCALE_ID, "")); @@ -567,11 +506,11 @@ StringName TranslationServer::doc_translate_plural(const StringName &p_message, } bool TranslationServer::is_pseudolocalization_enabled() const { - return pseudolocalization_enabled; + return main_domain->is_pseudolocalization_enabled(); } void TranslationServer::set_pseudolocalization_enabled(bool p_enabled) { - pseudolocalization_enabled = p_enabled; + main_domain->set_pseudolocalization_enabled(p_enabled); ResourceLoader::reload_translation_remaps(); @@ -581,14 +520,14 @@ void TranslationServer::set_pseudolocalization_enabled(bool p_enabled) { } void TranslationServer::reload_pseudolocalization() { - pseudolocalization_accents_enabled = GLOBAL_GET("internationalization/pseudolocalization/replace_with_accents"); - pseudolocalization_double_vowels_enabled = GLOBAL_GET("internationalization/pseudolocalization/double_vowels"); - pseudolocalization_fake_bidi_enabled = GLOBAL_GET("internationalization/pseudolocalization/fake_bidi"); - pseudolocalization_override_enabled = GLOBAL_GET("internationalization/pseudolocalization/override"); - expansion_ratio = GLOBAL_GET("internationalization/pseudolocalization/expansion_ratio"); - pseudolocalization_prefix = GLOBAL_GET("internationalization/pseudolocalization/prefix"); - pseudolocalization_suffix = GLOBAL_GET("internationalization/pseudolocalization/suffix"); - pseudolocalization_skip_placeholders_enabled = GLOBAL_GET("internationalization/pseudolocalization/skip_placeholders"); + main_domain->set_pseudolocalization_accents_enabled(GLOBAL_GET("internationalization/pseudolocalization/replace_with_accents")); + main_domain->set_pseudolocalization_double_vowels_enabled(GLOBAL_GET("internationalization/pseudolocalization/double_vowels")); + main_domain->set_pseudolocalization_fake_bidi_enabled(GLOBAL_GET("internationalization/pseudolocalization/fake_bidi")); + main_domain->set_pseudolocalization_override_enabled(GLOBAL_GET("internationalization/pseudolocalization/override")); + main_domain->set_pseudolocalization_expansion_ratio(GLOBAL_GET("internationalization/pseudolocalization/expansion_ratio")); + main_domain->set_pseudolocalization_prefix(GLOBAL_GET("internationalization/pseudolocalization/prefix")); + main_domain->set_pseudolocalization_suffix(GLOBAL_GET("internationalization/pseudolocalization/suffix")); + main_domain->set_pseudolocalization_skip_placeholders_enabled(GLOBAL_GET("internationalization/pseudolocalization/skip_placeholders")); ResourceLoader::reload_translation_remaps(); @@ -598,138 +537,7 @@ void TranslationServer::reload_pseudolocalization() { } StringName TranslationServer::pseudolocalize(const StringName &p_message) const { - String message = p_message; - int length = message.length(); - if (pseudolocalization_override_enabled) { - message = get_override_string(message); - } - - if (pseudolocalization_double_vowels_enabled) { - message = double_vowels(message); - } - - if (pseudolocalization_accents_enabled) { - message = replace_with_accented_string(message); - } - - if (pseudolocalization_fake_bidi_enabled) { - message = wrap_with_fakebidi_characters(message); - } - - StringName res = add_padding(message, length); - return res; -} - -StringName TranslationServer::tool_pseudolocalize(const StringName &p_message) const { - String message = p_message; - message = double_vowels(message); - message = replace_with_accented_string(message); - StringName res = "[!!! " + message + " !!!]"; - return res; -} - -String TranslationServer::get_override_string(String &p_message) const { - String res; - for (int i = 0; i < p_message.length(); i++) { - if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += p_message[i]; - res += p_message[i + 1]; - i++; - continue; - } - res += '*'; - } - return res; -} - -String TranslationServer::double_vowels(String &p_message) const { - String res; - for (int i = 0; i < p_message.length(); i++) { - if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += p_message[i]; - res += p_message[i + 1]; - i++; - continue; - } - res += p_message[i]; - if (p_message[i] == 'a' || p_message[i] == 'e' || p_message[i] == 'i' || p_message[i] == 'o' || p_message[i] == 'u' || - p_message[i] == 'A' || p_message[i] == 'E' || p_message[i] == 'I' || p_message[i] == 'O' || p_message[i] == 'U') { - res += p_message[i]; - } - } - return res; -}; - -String TranslationServer::replace_with_accented_string(String &p_message) const { - String res; - for (int i = 0; i < p_message.length(); i++) { - if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += p_message[i]; - res += p_message[i + 1]; - i++; - continue; - } - const char32_t *accented = get_accented_version(p_message[i]); - if (accented) { - res += accented; - } else { - res += p_message[i]; - } - } - return res; -} - -String TranslationServer::wrap_with_fakebidi_characters(String &p_message) const { - String res; - char32_t fakebidiprefix = U'\u202e'; - char32_t fakebidisuffix = U'\u202c'; - res += fakebidiprefix; - // The fake bidi unicode gets popped at every newline so pushing it back at every newline. - for (int i = 0; i < p_message.length(); i++) { - if (p_message[i] == '\n') { - res += fakebidisuffix; - res += p_message[i]; - res += fakebidiprefix; - } else if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += fakebidisuffix; - res += p_message[i]; - res += p_message[i + 1]; - res += fakebidiprefix; - i++; - } else { - res += p_message[i]; - } - } - res += fakebidisuffix; - return res; -} - -String TranslationServer::add_padding(const String &p_message, int p_length) const { - String underscores = String("_").repeat(p_length * expansion_ratio / 2); - String prefix = pseudolocalization_prefix + underscores; - String suffix = underscores + pseudolocalization_suffix; - - return prefix + p_message + suffix; -} - -const char32_t *TranslationServer::get_accented_version(char32_t p_character) const { - if (!is_ascii_alphabet_char(p_character)) { - return nullptr; - } - - for (unsigned int i = 0; i < sizeof(_character_to_accented) / sizeof(_character_to_accented[0]); i++) { - if (_character_to_accented[i].character == p_character) { - return _character_to_accented[i].accented_character; - } - } - - return nullptr; -} - -bool TranslationServer::is_placeholder(String &p_message, int p_index) const { - return p_index < p_message.length() - 1 && p_message[p_index] == '%' && - (p_message[p_index + 1] == 's' || p_message[p_index + 1] == 'c' || p_message[p_index + 1] == 'd' || - p_message[p_index + 1] == 'o' || p_message[p_index + 1] == 'x' || p_message[p_index + 1] == 'X' || p_message[p_index + 1] == 'f'); + return main_domain->pseudolocalize(p_message); } #ifdef TOOLS_ENABLED diff --git a/core/string/translation_server.h b/core/string/translation_server.h index 272fa1f11cc..a09230c0192 100644 --- a/core/string/translation_server.h +++ b/core/string/translation_server.h @@ -48,25 +48,6 @@ class TranslationServer : public Object { bool enabled = true; - bool pseudolocalization_enabled = false; - bool pseudolocalization_accents_enabled = false; - bool pseudolocalization_double_vowels_enabled = false; - bool pseudolocalization_fake_bidi_enabled = false; - bool pseudolocalization_override_enabled = false; - bool pseudolocalization_skip_placeholders_enabled = false; - float expansion_ratio = 0.0; - String pseudolocalization_prefix; - String pseudolocalization_suffix; - - StringName tool_pseudolocalize(const StringName &p_message) const; - String get_override_string(String &p_message) const; - String double_vowels(String &p_message) const; - String replace_with_accented_string(String &p_message) const; - String wrap_with_fakebidi_characters(String &p_message) const; - String add_padding(const String &p_message, int p_length) const; - const char32_t *get_accented_version(char32_t p_character) const; - bool is_placeholder(String &p_message, int p_index) const; - static TranslationServer *singleton; bool _load_translations(const String &p_from); String _standardize_locale(const String &p_locale, bool p_add_defaults) const; @@ -93,6 +74,8 @@ class TranslationServer : public Object { public: _FORCE_INLINE_ static TranslationServer *get_singleton() { return singleton; } + Ref get_editor_domain() const { return editor_domain; } + void set_enabled(bool p_enabled) { enabled = p_enabled; } _FORCE_INLINE_ bool is_enabled() const { return enabled; } diff --git a/core/templates/hash_map.cpp b/core/templates/hash_map.cpp new file mode 100644 index 00000000000..93664dd2e10 --- /dev/null +++ b/core/templates/hash_map.cpp @@ -0,0 +1,43 @@ +/**************************************************************************/ +/* hash_map.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 "hash_map.h" + +#include "core/variant/variant.h" + +bool _hashmap_variant_less_than(const Variant &p_left, const Variant &p_right) { + bool valid = false; + Variant res; + Variant::evaluate(Variant::OP_LESS, p_left, p_right, res, valid); + if (!valid) { + res = false; + } + return res; +} diff --git a/core/templates/hash_map.h b/core/templates/hash_map.h index a3e8c2c788c..329952e8d4f 100644 --- a/core/templates/hash_map.h +++ b/core/templates/hash_map.h @@ -61,6 +61,8 @@ struct HashMapElement { data(p_key, p_value) {} }; +bool _hashmap_variant_less_than(const Variant &p_left, const Variant &p_right); + template , @@ -271,6 +273,47 @@ class HashMap { num_elements = 0; } + void sort() { + if (elements == nullptr || num_elements < 2) { + return; // An empty or single element HashMap is already sorted. + } + // Use insertion sort because we want this operation to be fast for the + // common case where the input is already sorted or nearly sorted. + HashMapElement *inserting = head_element->next; + while (inserting != nullptr) { + HashMapElement *after = nullptr; + for (HashMapElement *current = inserting->prev; current != nullptr; current = current->prev) { + if (_hashmap_variant_less_than(inserting->data.key, current->data.key)) { + after = current; + } else { + break; + } + } + HashMapElement *next = inserting->next; + if (after != nullptr) { + // Modify the elements around `inserting` to remove it from its current position. + inserting->prev->next = next; + if (next == nullptr) { + tail_element = inserting->prev; + } else { + next->prev = inserting->prev; + } + // Modify `before` and `after` to insert `inserting` between them. + HashMapElement *before = after->prev; + if (before == nullptr) { + head_element = inserting; + } else { + before->next = inserting; + } + after->prev = inserting; + // Point `inserting` to its new surroundings. + inserting->prev = before; + inserting->next = after; + } + inserting = next; + } + } + TValue &get(const TKey &p_key) { uint32_t pos = 0; bool exists = _lookup_pos(p_key, pos); diff --git a/core/templates/rid_owner.h b/core/templates/rid_owner.h index 537413e2ba5..42001590546 100644 --- a/core/templates/rid_owner.h +++ b/core/templates/rid_owner.h @@ -32,7 +32,7 @@ #define RID_OWNER_H #include "core/os/memory.h" -#include "core/os/spin_lock.h" +#include "core/os/mutex.h" #include "core/string/print_string.h" #include "core/templates/hash_set.h" #include "core/templates/list.h" @@ -69,42 +69,54 @@ class RID_AllocBase { template class RID_Alloc : public RID_AllocBase { - T **chunks = nullptr; + struct Chunk { + T data; + uint32_t validator; + }; + Chunk **chunks = nullptr; uint32_t **free_list_chunks = nullptr; - uint32_t **validator_chunks = nullptr; uint32_t elements_in_chunk; uint32_t max_alloc = 0; uint32_t alloc_count = 0; + uint32_t chunk_limit = 0; const char *description = nullptr; - mutable SpinLock spin_lock; + mutable Mutex mutex; _FORCE_INLINE_ RID _allocate_rid() { if constexpr (THREAD_SAFE) { - spin_lock.lock(); + mutex.lock(); } if (alloc_count == max_alloc) { //allocate a new chunk uint32_t chunk_count = alloc_count == 0 ? 0 : (max_alloc / elements_in_chunk); + if (THREAD_SAFE && chunk_count == chunk_limit) { + mutex.unlock(); + if (description != nullptr) { + ERR_FAIL_V_MSG(RID(), vformat("Element limit for RID of type '%s' reached.", String(description))); + } else { + ERR_FAIL_V_MSG(RID(), "Element limit reached."); + } + } //grow chunks - chunks = (T **)memrealloc(chunks, sizeof(T *) * (chunk_count + 1)); - chunks[chunk_count] = (T *)memalloc(sizeof(T) * elements_in_chunk); //but don't initialize - - //grow validators - validator_chunks = (uint32_t **)memrealloc(validator_chunks, sizeof(uint32_t *) * (chunk_count + 1)); - validator_chunks[chunk_count] = (uint32_t *)memalloc(sizeof(uint32_t) * elements_in_chunk); + if constexpr (!THREAD_SAFE) { + chunks = (Chunk **)memrealloc(chunks, sizeof(Chunk *) * (chunk_count + 1)); + } + chunks[chunk_count] = (Chunk *)memalloc(sizeof(Chunk) * elements_in_chunk); //but don't initialize //grow free lists - free_list_chunks = (uint32_t **)memrealloc(free_list_chunks, sizeof(uint32_t *) * (chunk_count + 1)); + if constexpr (!THREAD_SAFE) { + free_list_chunks = (uint32_t **)memrealloc(free_list_chunks, sizeof(uint32_t *) * (chunk_count + 1)); + } free_list_chunks[chunk_count] = (uint32_t *)memalloc(sizeof(uint32_t) * elements_in_chunk); //initialize for (uint32_t i = 0; i < elements_in_chunk; i++) { // Don't initialize chunk. - validator_chunks[chunk_count][i] = 0xFFFFFFFF; + chunks[chunk_count][i].validator = 0xFFFFFFFF; free_list_chunks[chunk_count][i] = alloc_count + i; } @@ -122,14 +134,13 @@ class RID_Alloc : public RID_AllocBase { id <<= 32; id |= free_index; - validator_chunks[free_chunk][free_element] = validator; - - validator_chunks[free_chunk][free_element] |= 0x80000000; //mark uninitialized bit + chunks[free_chunk][free_element].validator = validator; + chunks[free_chunk][free_element].validator |= 0x80000000; //mark uninitialized bit alloc_count++; if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } return _make_from_id(id); @@ -156,16 +167,10 @@ class RID_Alloc : public RID_AllocBase { if (p_rid == RID()) { return nullptr; } - if constexpr (THREAD_SAFE) { - spin_lock.lock(); - } uint64_t id = p_rid.get_id(); uint32_t idx = uint32_t(id & 0xFFFFFFFF); if (unlikely(idx >= max_alloc)) { - if constexpr (THREAD_SAFE) { - spin_lock.unlock(); - } return nullptr; } @@ -174,38 +179,26 @@ class RID_Alloc : public RID_AllocBase { uint32_t validator = uint32_t(id >> 32); + Chunk &c = chunks[idx_chunk][idx_element]; if (unlikely(p_initialize)) { - if (unlikely(!(validator_chunks[idx_chunk][idx_element] & 0x80000000))) { - if constexpr (THREAD_SAFE) { - spin_lock.unlock(); - } + if (unlikely(!(c.validator & 0x80000000))) { ERR_FAIL_V_MSG(nullptr, "Initializing already initialized RID"); } - if (unlikely((validator_chunks[idx_chunk][idx_element] & 0x7FFFFFFF) != validator)) { - if constexpr (THREAD_SAFE) { - spin_lock.unlock(); - } + if (unlikely((c.validator & 0x7FFFFFFF) != validator)) { ERR_FAIL_V_MSG(nullptr, "Attempting to initialize the wrong RID"); } - validator_chunks[idx_chunk][idx_element] &= 0x7FFFFFFF; //initialized + c.validator &= 0x7FFFFFFF; //initialized - } else if (unlikely(validator_chunks[idx_chunk][idx_element] != validator)) { - if constexpr (THREAD_SAFE) { - spin_lock.unlock(); - } - if ((validator_chunks[idx_chunk][idx_element] & 0x80000000) && validator_chunks[idx_chunk][idx_element] != 0xFFFFFFFF) { + } else if (unlikely(c.validator != validator)) { + if ((c.validator & 0x80000000) && c.validator != 0xFFFFFFFF) { ERR_FAIL_V_MSG(nullptr, "Attempting to use an uninitialized RID"); } return nullptr; } - T *ptr = &chunks[idx_chunk][idx_element]; - - if constexpr (THREAD_SAFE) { - spin_lock.unlock(); - } + T *ptr = &c.data; return ptr; } @@ -222,14 +215,14 @@ class RID_Alloc : public RID_AllocBase { _FORCE_INLINE_ bool owns(const RID &p_rid) const { if constexpr (THREAD_SAFE) { - spin_lock.lock(); + mutex.lock(); } uint64_t id = p_rid.get_id(); uint32_t idx = uint32_t(id & 0xFFFFFFFF); if (unlikely(idx >= max_alloc)) { if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } return false; } @@ -239,10 +232,10 @@ class RID_Alloc : public RID_AllocBase { uint32_t validator = uint32_t(id >> 32); - bool owned = (validator != 0x7FFFFFFF) && (validator_chunks[idx_chunk][idx_element] & 0x7FFFFFFF) == validator; + bool owned = (validator != 0x7FFFFFFF) && (chunks[idx_chunk][idx_element].validator & 0x7FFFFFFF) == validator; if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } return owned; @@ -250,14 +243,14 @@ class RID_Alloc : public RID_AllocBase { _FORCE_INLINE_ void free(const RID &p_rid) { if constexpr (THREAD_SAFE) { - spin_lock.lock(); + mutex.lock(); } uint64_t id = p_rid.get_id(); uint32_t idx = uint32_t(id & 0xFFFFFFFF); if (unlikely(idx >= max_alloc)) { if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } ERR_FAIL(); } @@ -266,26 +259,26 @@ class RID_Alloc : public RID_AllocBase { uint32_t idx_element = idx % elements_in_chunk; uint32_t validator = uint32_t(id >> 32); - if (unlikely(validator_chunks[idx_chunk][idx_element] & 0x80000000)) { + if (unlikely(chunks[idx_chunk][idx_element].validator & 0x80000000)) { if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } - ERR_FAIL_MSG("Attempted to free an uninitialized or invalid RID."); - } else if (unlikely(validator_chunks[idx_chunk][idx_element] != validator)) { + ERR_FAIL_MSG("Attempted to free an uninitialized or invalid RID"); + } else if (unlikely(chunks[idx_chunk][idx_element].validator != validator)) { if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } ERR_FAIL(); } - chunks[idx_chunk][idx_element].~T(); - validator_chunks[idx_chunk][idx_element] = 0xFFFFFFFF; // go invalid + chunks[idx_chunk][idx_element].data.~T(); + chunks[idx_chunk][idx_element].validator = 0xFFFFFFFF; // go invalid alloc_count--; free_list_chunks[alloc_count / elements_in_chunk][alloc_count % elements_in_chunk] = idx; if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } } @@ -294,34 +287,35 @@ class RID_Alloc : public RID_AllocBase { } void get_owned_list(List *p_owned) const { if constexpr (THREAD_SAFE) { - spin_lock.lock(); + mutex.lock(); } for (size_t i = 0; i < max_alloc; i++) { - uint64_t validator = validator_chunks[i / elements_in_chunk][i % elements_in_chunk]; + uint64_t validator = chunks[i / elements_in_chunk][i % elements_in_chunk].validator; if (validator != 0xFFFFFFFF) { p_owned->push_back(_make_from_id((validator << 32) | i)); } } if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } } //used for fast iteration in the elements or RIDs void fill_owned_buffer(RID *p_rid_buffer) const { if constexpr (THREAD_SAFE) { - spin_lock.lock(); + mutex.lock(); } uint32_t idx = 0; for (size_t i = 0; i < max_alloc; i++) { - uint64_t validator = validator_chunks[i / elements_in_chunk][i % elements_in_chunk]; + uint64_t validator = chunks[i / elements_in_chunk][i % elements_in_chunk].validator; if (validator != 0xFFFFFFFF) { p_rid_buffer[idx] = _make_from_id((validator << 32) | i); idx++; } } + if constexpr (THREAD_SAFE) { - spin_lock.unlock(); + mutex.unlock(); } } @@ -329,8 +323,13 @@ class RID_Alloc : public RID_AllocBase { description = p_descrption; } - RID_Alloc(uint32_t p_target_chunk_byte_size = 65536) { + RID_Alloc(uint32_t p_target_chunk_byte_size = 65536, uint32_t p_maximum_number_of_elements = 262144) { elements_in_chunk = sizeof(T) > p_target_chunk_byte_size ? 1 : (p_target_chunk_byte_size / sizeof(T)); + if constexpr (THREAD_SAFE) { + chunk_limit = (p_maximum_number_of_elements / elements_in_chunk) + 1; + chunks = (Chunk **)memalloc(sizeof(Chunk *) * chunk_limit); + free_list_chunks = (uint32_t **)memalloc(sizeof(uint32_t *) * chunk_limit); + } } ~RID_Alloc() { @@ -339,12 +338,12 @@ class RID_Alloc : public RID_AllocBase { alloc_count, description ? description : typeid(T).name())); for (size_t i = 0; i < max_alloc; i++) { - uint64_t validator = validator_chunks[i / elements_in_chunk][i % elements_in_chunk]; + uint64_t validator = chunks[i / elements_in_chunk][i % elements_in_chunk].validator; if (validator & 0x80000000) { continue; //uninitialized } if (validator != 0xFFFFFFFF) { - chunks[i / elements_in_chunk][i % elements_in_chunk].~T(); + chunks[i / elements_in_chunk][i % elements_in_chunk].data.~T(); } } } @@ -352,14 +351,12 @@ class RID_Alloc : public RID_AllocBase { uint32_t chunk_count = max_alloc / elements_in_chunk; for (uint32_t i = 0; i < chunk_count; i++) { memfree(chunks[i]); - memfree(validator_chunks[i]); memfree(free_list_chunks[i]); } if (chunks) { memfree(chunks); memfree(free_list_chunks); - memfree(validator_chunks); } } }; @@ -419,8 +416,8 @@ class RID_PtrOwner { alloc.set_description(p_descrption); } - RID_PtrOwner(uint32_t p_target_chunk_byte_size = 65536) : - alloc(p_target_chunk_byte_size) {} + RID_PtrOwner(uint32_t p_target_chunk_byte_size = 65536, uint32_t p_maximum_number_of_elements = 262144) : + alloc(p_target_chunk_byte_size, p_maximum_number_of_elements) {} }; template @@ -473,8 +470,8 @@ class RID_Owner { void set_description(const char *p_descrption) { alloc.set_description(p_descrption); } - RID_Owner(uint32_t p_target_chunk_byte_size = 65536) : - alloc(p_target_chunk_byte_size) {} + RID_Owner(uint32_t p_target_chunk_byte_size = 65536, uint32_t p_maximum_number_of_elements = 262144) : + alloc(p_target_chunk_byte_size, p_maximum_number_of_elements) {} }; #endif // RID_OWNER_H diff --git a/core/variant/dictionary.cpp b/core/variant/dictionary.cpp index 2db754438ff..501ca692055 100644 --- a/core/variant/dictionary.cpp +++ b/core/variant/dictionary.cpp @@ -294,6 +294,11 @@ void Dictionary::clear() { _p->variant_map.clear(); } +void Dictionary::sort() { + ERR_FAIL_COND_MSG(_p->read_only, "Dictionary is in read-only state."); + _p->variant_map.sort(); +} + void Dictionary::merge(const Dictionary &p_dictionary, bool p_overwrite) { ERR_FAIL_COND_MSG(_p->read_only, "Dictionary is in read-only state."); for (const KeyValue &E : p_dictionary._p->variant_map) { diff --git a/core/variant/dictionary.h b/core/variant/dictionary.h index 5f3ce40219b..bbfb5b30837 100644 --- a/core/variant/dictionary.h +++ b/core/variant/dictionary.h @@ -64,6 +64,7 @@ class Dictionary { int size() const; bool is_empty() const; void clear(); + void sort(); void merge(const Dictionary &p_dictionary, bool p_overwrite = false); Dictionary merged(const Dictionary &p_dictionary, bool p_overwrite = false) const; diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index ab33c9db555..29e11462c92 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -2272,6 +2272,7 @@ static void _register_variant_builtin_methods_misc() { bind_method(Dictionary, is_empty, sarray(), varray()); bind_method(Dictionary, clear, sarray(), varray()); bind_method(Dictionary, assign, sarray("dictionary"), varray()); + bind_method(Dictionary, sort, sarray(), varray()); bind_method(Dictionary, merge, sarray("dictionary", "overwrite"), varray(false)); bind_method(Dictionary, merged, sarray("dictionary", "overwrite"), varray(false)); bind_method(Dictionary, has, sarray("key"), varray()); diff --git a/doc/classes/Dictionary.xml b/doc/classes/Dictionary.xml index 4441c3cb795..5c9b22fe4a1 100644 --- a/doc/classes/Dictionary.xml +++ b/doc/classes/Dictionary.xml @@ -474,6 +474,12 @@ Returns the number of entries in the dictionary. Empty dictionaries ([code]{ }[/code]) always return [code]0[/code]. See also [method is_empty]. + + + + Sorts the dictionary in-place by key. This can be used to ensure dictionaries with the same contents produce equivalent results when getting the [method keys], getting the [method values], and converting to a string. This is also useful when wanting a JSON representation consistent with what is in memory, and useful for storing on a database that requires dictionaries to be sorted. + + diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 79064a88ba6..37bf265d20c 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -893,6 +893,13 @@ Returns [code]true[/code] if the specified [param feature] is supported by the current [DisplayServer], [code]false[/code] otherwise. + + + + Returns [code]true[/code] if hardware keyboard is connected. + [b]Note:[/b] This method is implemented on Android and iOS, on other platforms this method always returns [code]true[/code]. + + diff --git a/doc/classes/EditorContextMenuPlugin.xml b/doc/classes/EditorContextMenuPlugin.xml index 71c4ca0f9b9..fb90a2a5cd6 100644 --- a/doc/classes/EditorContextMenuPlugin.xml +++ b/doc/classes/EditorContextMenuPlugin.xml @@ -47,6 +47,24 @@ [/codeblock] + + + + + + + Add a submenu to the context menu of the plugin's specified slot. The submenu is not automatically handled, you need to connect to its signals yourself. Also the submenu is freed on every popup, so provide a new [PopupMenu] every time. + [codeblock] + func _popup_menu(paths): + var popup_menu = PopupMenu.new() + popup_menu.add_item("Blue") + popup_menu.add_item("White") + popup_menu.id_pressed.connect(_on_color_submenu_option) + + add_context_menu_item("Set Node Color", popup_menu) + [/codeblock] + + diff --git a/doc/classes/EditorInterface.xml b/doc/classes/EditorInterface.xml index 795c5c1c2f0..43059db8b2e 100644 --- a/doc/classes/EditorInterface.xml +++ b/doc/classes/EditorInterface.xml @@ -343,6 +343,14 @@ [/codeblock] + + + + + + Pops up an editor dialog for quick selecting a resource file. The [param callback] must take a single argument of type [String] which will contain the path of the selected resource or be empty if the dialog is canceled. If [param base_types] is provided, the dialog will only show resources that match these types. Only types deriving from [Resource] are supported. + + diff --git a/doc/classes/FileDialog.xml b/doc/classes/FileDialog.xml index 1ae889adc13..9529fac77ec 100644 --- a/doc/classes/FileDialog.xml +++ b/doc/classes/FileDialog.xml @@ -29,6 +29,12 @@ [param default_value_index] should be an index of the value in the [param values]. If [param values] is empty it should be either [code]1[/code] (checked), or [code]0[/code] (unchecked). + + + + Clear the filter for file names. + + @@ -134,6 +140,10 @@ The dialog's open or save mode, which affects the selection behavior. See [enum FileMode]. + + The filter for file names (case-insensitive). When set to a non-empty string, only files that contains the substring will be shown. [member filename_filter] can be edited by the user with the filter button at the top of the file dialog. + See also [member filters], which should be used to restrict the file types that can be selected instead of [member filename_filter] which is meant to be set by the user. + The available file type filters. Each filter string in the array should be formatted like this: [code]*.txt,*.doc;Text Files[/code]. The description text of the filter is optional and can be omitted. @@ -173,6 +183,12 @@ Emitted when the user selects a file by double-clicking it or pressing the [b]OK[/b] button. + + + + Emitted when the filter for file names changes. + + @@ -237,6 +253,9 @@ Custom icon for the reload button. + + Custom icon for the toggle button for the filter for file names. + Custom icon for the toggle hidden button. diff --git a/doc/classes/ItemList.xml b/doc/classes/ItemList.xml index 2d898673c19..fdaeb54bdfa 100644 --- a/doc/classes/ItemList.xml +++ b/doc/classes/ItemList.xml @@ -379,6 +379,9 @@ If [code]true[/code], the control will automatically resize the height to fit its content. + + If [code]true[/code], the control will automatically resize the width to fit its content. + The width all columns will be adjusted to. diff --git a/doc/classes/LineEdit.xml b/doc/classes/LineEdit.xml index 41f42392dea..3e0c328dcb9 100644 --- a/doc/classes/LineEdit.xml +++ b/doc/classes/LineEdit.xml @@ -8,7 +8,7 @@ - When the [LineEdit] control is focused using the keyboard arrow keys, it will only gain focus and not enter edit mode. - To enter edit mode, click on the control with the mouse or press the [code]ui_text_submit[/code] action (by default [kbd]Enter[/kbd] or [kbd]Kp Enter[/kbd]). - To exit edit mode, press [code]ui_text_submit[/code] or [code]ui_cancel[/code] (by default [kbd]Escape[/kbd]) actions. - - Check [method is_editing] and [signal editing_toggled] for more information. + - Check [method edit], [method unedit], [method is_editing], and [signal editing_toggled] for more information. [b]Important:[/b] - Focusing the [LineEdit] with [code]ui_focus_next[/code] (by default [kbd]Tab[/kbd]) or [code]ui_focus_prev[/code] (by default [kbd]Shift + Tab[/kbd]) or [method Control.grab_focus] still enters edit mode (for compatibility). [LineEdit] features many built-in shortcuts that are always available ([kbd]Ctrl[/kbd] here maps to [kbd]Cmd[/kbd] on macOS): @@ -75,6 +75,13 @@ Clears the current selection. + + + + Allows entering edit mode whether the [LineEdit] is focused or not. + Use [method Callable.call_deferred] if you want to enter edit mode on [signal text_submitted]. + + @@ -223,6 +230,12 @@ Selects the whole [String]. + + + + Allows exiting edit mode while preserving focus. + + diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index aed041c5ad1..d76e65b6184 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -229,6 +229,11 @@ [code]points[/code] - [PackedVector3Array], containing outline points. [code]x[/code] and [code]y[/code] are point coordinates. [code]z[/code] is the type of the point, using the [enum ContourPointTag] values. [code]contours[/code] - [PackedInt32Array], containing indices the end points of each contour. [code]orientation[/code] - [bool], contour orientation. If [code]true[/code], clockwise contours must be filled. + - Two successive [constant CONTOUR_CURVE_TAG_ON] points indicate a line segment. + - One [constant CONTOUR_CURVE_TAG_OFF_CONIC] point between two [constant CONTOUR_CURVE_TAG_ON] points indicates a single conic (quadratic) Bézier arc. + - Two [constant CONTOUR_CURVE_TAG_OFF_CUBIC] points between two [constant CONTOUR_CURVE_TAG_ON] points indicate a single cubic Bézier arc. + - Two successive [constant CONTOUR_CURVE_TAG_OFF_CONIC] points indicate two successive conic (quadratic) Bézier arcs with a virtual [constant CONTOUR_CURVE_TAG_ON] point at their middle. + - Each contour is closed. The last point of a contour uses the first point of a contour as its next point, and vice versa. The first point can be [constant CONTOUR_CURVE_TAG_OFF_CONIC] point. diff --git a/doc/classes/TranslationDomain.xml b/doc/classes/TranslationDomain.xml index da6f2704bf3..5045f86260c 100644 --- a/doc/classes/TranslationDomain.xml +++ b/doc/classes/TranslationDomain.xml @@ -30,6 +30,13 @@ Returns the [Translation] instance that best matches [param locale]. Returns [code]null[/code] if there are no matches. + + + + + Returns the pseudolocalized string based on the [param message] passed in. + + @@ -57,4 +64,42 @@ + + + Replace all characters with their accented variants during pseudolocalization. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Double vowels in strings during pseudolocalization to simulate the lengthening of text due to localization. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + If [code]true[/code], enables pseudolocalization for the project. This can be used to spot untranslatable strings or layout issues that may occur once the project is localized to languages that have longer strings than the source language. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + The expansion ratio to use during pseudolocalization. A value of [code]0.3[/code] is sufficient for most practical purposes, and will increase the length of each string by 30%. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + If [code]true[/code], emulate bidirectional (right-to-left) text when pseudolocalization is enabled. This can be used to spot issues with RTL layout and UI mirroring that will crop up if the project is localized to RTL languages such as Arabic or Hebrew. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Replace all characters in the string with [code]*[/code]. Useful for finding non-localizable strings. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Prefix that will be prepended to the pseudolocalized string. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Skip placeholders for string formatting like [code]%s[/code] or [code]%f[/code] during pseudolocalization. Useful to identify strings which need additional control characters to display correctly. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Suffix that will be appended to the pseudolocalized string. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index 0a4965c36c1..69ca984f67d 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -125,12 +125,13 @@ Returns the pseudolocalized string based on the [param message] passed in. + [b]Note:[/b] This method always uses the main translation domain. - Reparses the pseudolocalization options and reloads the translation. + Reparses the pseudolocalization options and reloads the translation for the main translation domain. @@ -187,7 +188,7 @@ - If [code]true[/code], enables the use of pseudolocalization. See [member ProjectSettings.internationalization/pseudolocalization/use_pseudolocalization] for details. + If [code]true[/code], enables the use of pseudolocalization on the main translation domain. See [member ProjectSettings.internationalization/pseudolocalization/use_pseudolocalization] for details. diff --git a/drivers/SCsub b/drivers/SCsub index 219c4451eef..e0bfa138f58 100644 --- a/drivers/SCsub +++ b/drivers/SCsub @@ -1,6 +1,8 @@ #!/usr/bin/env python from misc.utility.scons_hints import * +from methods import print_error + Import("env") env.drivers_sources = [] @@ -20,7 +22,7 @@ if env["platform"] == "windows": SConscript("backtrace/SCsub") if env["xaudio2"]: if "xaudio2" not in supported: - print("Target platform '{}' does not support the XAudio2 audio driver. Aborting.".format(env["platform"])) + print_error("Target platform '{}' does not support the XAudio2 audio driver".format(env["platform"])) Exit(255) SConscript("xaudio2/SCsub") @@ -34,7 +36,7 @@ if env["vulkan"]: SConscript("vulkan/SCsub") if env["d3d12"]: if "d3d12" not in supported: - print("Target platform '{}' does not support the D3D12 rendering driver. Aborting.".format(env["platform"])) + print_error("Target platform '{}' does not support the D3D12 rendering driver".format(env["platform"])) Exit(255) SConscript("d3d12/SCsub") if env["opengl3"]: @@ -43,7 +45,7 @@ if env["opengl3"]: SConscript("egl/SCsub") if env["metal"]: if "metal" not in supported: - print("Target platform '{}' does not support the Metal rendering driver. Aborting.".format(env["platform"])) + print_error("Target platform '{}' does not support the Metal rendering driver".format(env["platform"])) Exit(255) SConscript("metal/SCsub") diff --git a/drivers/metal/rendering_device_driver_metal.mm b/drivers/metal/rendering_device_driver_metal.mm index 9d691a0d230..0f7faaddf08 100644 --- a/drivers/metal/rendering_device_driver_metal.mm +++ b/drivers/metal/rendering_device_driver_metal.mm @@ -358,7 +358,11 @@ _FORCE_INLINE_ MTLSize mipmapLevelSizeFromSize(MTLSize p_size, NSUInteger p_leve } RDD::TextureID RenderingDeviceDriverMetal::texture_create_from_extension(uint64_t p_native_texture, TextureType p_type, DataFormat p_format, uint32_t p_array_layers, bool p_depth_stencil) { - ERR_FAIL_V_MSG(RDD::TextureID(), "not implemented"); + id obj = (__bridge id)(void *)(uintptr_t)p_native_texture; + + // We only need to create a RDD::TextureID for an existing, natively-provided texture. + + return rid::make(obj); } RDD::TextureID RenderingDeviceDriverMetal::texture_create_shared(TextureID p_original_texture, const TextureView &p_view) { diff --git a/drivers/unix/dir_access_unix.cpp b/drivers/unix/dir_access_unix.cpp index e4bad880837..816ffd7a322 100644 --- a/drivers/unix/dir_access_unix.cpp +++ b/drivers/unix/dir_access_unix.cpp @@ -410,7 +410,17 @@ Error DirAccessUnix::rename(String p_path, String p_new_path) { p_new_path = p_new_path.left(-1); } - return ::rename(p_path.utf8().get_data(), p_new_path.utf8().get_data()) == 0 ? OK : FAILED; + int res = ::rename(p_path.utf8().get_data(), p_new_path.utf8().get_data()); + if (res != 0 && errno == EXDEV) { // Cross-device move, use copy and remove. + Error err = OK; + err = copy(p_path, p_new_path); + if (err != OK) { + return err; + } + return remove(p_path); + } else { + return (res == 0) ? OK : FAILED; + } } Error DirAccessUnix::remove(String p_path) { diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 07547fee8a0..88a32b1a6da 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -644,6 +644,8 @@ void FindReplaceBar::_search_text_submitted(const String &p_text) { } else { search_next(); } + + callable_mp(search_text, &LineEdit::edit).call_deferred(); } void FindReplaceBar::_replace_text_submitted(const String &p_text) { diff --git a/editor/debugger/debug_adapter/debug_adapter_parser.cpp b/editor/debugger/debug_adapter/debug_adapter_parser.cpp index 4210baeed2d..2af629676ab 100644 --- a/editor/debugger/debug_adapter/debug_adapter_parser.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_parser.cpp @@ -30,6 +30,7 @@ #include "debug_adapter_parser.h" +#include "editor/debugger/debug_adapter/debug_adapter_types.h" #include "editor/debugger/editor_debugger_node.h" #include "editor/debugger/script_editor_debugger.h" #include "editor/export/editor_export_platform.h" @@ -442,26 +443,34 @@ Dictionary DebugAdapterParser::req_variables(const Dictionary &p_params) const { return Dictionary(); } - Dictionary response = prepare_success_response(p_params), body; - response["body"] = body; - Dictionary args = p_params["arguments"]; int variable_id = args["variablesReference"]; - HashMap::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id); + if (HashMap::Iterator E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id); E) { + Dictionary response = prepare_success_response(p_params); + Dictionary body; + response["body"] = body; - if (E) { if (!DebugAdapterProtocol::get_singleton()->get_current_peer()->supportsVariableType) { for (int i = 0; i < E->value.size(); i++) { Dictionary variable = E->value[i]; variable.erase("type"); } } + body["variables"] = E ? E->value : Array(); return response; } else { - return Dictionary(); + // If the requested variable is an object, it needs to be requested from the debuggee. + ObjectID object_id = DebugAdapterProtocol::get_singleton()->search_object_id(variable_id); + + if (object_id.is_null()) { + return prepare_error_response(p_params, DAP::ErrorType::UNKNOWN); + } + + DebugAdapterProtocol::get_singleton()->request_remote_object(object_id); } + return Dictionary(); } Dictionary DebugAdapterParser::req_next(const Dictionary &p_params) const { @@ -479,16 +488,27 @@ Dictionary DebugAdapterParser::req_stepIn(const Dictionary &p_params) const { } Dictionary DebugAdapterParser::req_evaluate(const Dictionary &p_params) const { - Dictionary response = prepare_success_response(p_params), body; - response["body"] = body; - Dictionary args = p_params["arguments"]; + String expression = args["expression"]; + int frame_id = args.has("frameId") ? static_cast(args["frameId"]) : DebugAdapterProtocol::get_singleton()->_current_frame; - String value = EditorDebuggerNode::get_singleton()->get_var_value(args["expression"]); - body["result"] = value; - body["variablesReference"] = 0; + if (HashMap::Iterator E = DebugAdapterProtocol::get_singleton()->eval_list.find(expression); E) { + Dictionary response = prepare_success_response(p_params); + Dictionary body; + response["body"] = body; - return response; + DAP::Variable var = E->value; + + body["result"] = var.value; + body["variablesReference"] = var.variablesReference; + + // Since an evaluation can alter the state of the debuggee, they are volatile, and should only be used once + DebugAdapterProtocol::get_singleton()->eval_list.erase(E->key); + return response; + } else { + DebugAdapterProtocol::get_singleton()->request_remote_evaluate(expression, frame_id); + } + return Dictionary(); } Dictionary DebugAdapterParser::req_godot_put_msg(const Dictionary &p_params) const { diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp index f847d3be7b1..066cf633014 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -33,8 +33,8 @@ #include "core/config/project_settings.h" #include "core/debugger/debugger_marshalls.h" #include "core/io/json.h" +#include "core/io/marshalls.h" #include "editor/debugger/script_editor_debugger.h" -#include "editor/doc_tools.h" #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" @@ -186,6 +186,8 @@ void DebugAdapterProtocol::reset_stack_info() { stackframe_list.clear(); variable_list.clear(); + object_list.clear(); + object_pending_set.clear(); } int DebugAdapterProtocol::parse_variant(const Variant &p_var) { @@ -671,12 +673,194 @@ int DebugAdapterProtocol::parse_variant(const Variant &p_var) { variable_list.insert(id, arr); return id; } + case Variant::OBJECT: { + // Objects have to be requested from the debuggee. This has do be done + // in a lazy way, as retrieving object properties takes time. + EncodedObjectAsID *encoded_obj = Object::cast_to(p_var); + + // Object may be null; in that case, return early. + if (!encoded_obj) { + return 0; + } + + // Object may have been already requested. + ObjectID object_id = encoded_obj->get_object_id(); + if (object_list.has(object_id)) { + return object_list[object_id]; + } + + // Queue requesting the object. + int id = variable_id++; + object_list.insert(object_id, id); + return id; + } default: // Simple atomic stuff, or too complex to be manipulated return 0; } } +void DebugAdapterProtocol::parse_object(SceneDebuggerObject &p_obj) { + // If the object is not on the pending list, we weren't expecting it. Ignore it. + ObjectID object_id = p_obj.id; + if (!object_pending_set.erase(object_id)) { + return; + } + + // Populate DAP::Variable's with the object's properties. These properties will be divided by categories. + Array properties; + Array script_members; + Array script_constants; + Array script_node; + DAP::Variable node_type; + Array node_properties; + + for (SceneDebuggerObject::SceneDebuggerProperty &property : p_obj.properties) { + PropertyInfo &info = property.first; + + // Script members ("Members/" prefix) + if (info.name.begins_with("Members/")) { + info.name = info.name.trim_prefix("Members/"); + script_members.push_back(parse_object_variable(property)); + } + + // Script constants ("Constants/" prefix) + else if (info.name.begins_with("Constants/")) { + info.name = info.name.trim_prefix("Constants/"); + script_constants.push_back(parse_object_variable(property)); + } + + // Script node ("Node/" prefix) + else if (info.name.begins_with("Node/")) { + info.name = info.name.trim_prefix("Node/"); + script_node.push_back(parse_object_variable(property)); + } + + // Regular categories (with type Variant::NIL) + else if (info.type == Variant::NIL) { + if (!node_properties.is_empty()) { + node_type.value = itos(node_properties.size()); + variable_list.insert(node_type.variablesReference, node_properties.duplicate()); + properties.push_back(node_type.to_json()); + } + + node_type.name = info.name; + node_type.type = "Category"; + node_type.variablesReference = variable_id++; + node_properties.clear(); + } + + // Regular properties. + else { + node_properties.push_back(parse_object_variable(property)); + } + } + + // Add the last category. + if (!node_properties.is_empty()) { + node_type.value = itos(node_properties.size()); + variable_list.insert(node_type.variablesReference, node_properties.duplicate()); + properties.push_back(node_type.to_json()); + } + + // Add the script categories, in reverse order to be at the front of the array: + // ( [members; constants; node; category1; category2; ...] ) + if (!script_node.is_empty()) { + DAP::Variable node; + node.name = "Node"; + node.type = "Category"; + node.value = itos(script_node.size()); + node.variablesReference = variable_id++; + variable_list.insert(node.variablesReference, script_node); + properties.push_front(node.to_json()); + } + + if (!script_constants.is_empty()) { + DAP::Variable constants; + constants.name = "Constants"; + constants.type = "Category"; + constants.value = itos(script_constants.size()); + constants.variablesReference = variable_id++; + variable_list.insert(constants.variablesReference, script_constants); + properties.push_front(constants.to_json()); + } + + if (!script_members.is_empty()) { + DAP::Variable members; + members.name = "Members"; + members.type = "Category"; + members.value = itos(script_members.size()); + members.variablesReference = variable_id++; + variable_list.insert(members.variablesReference, script_members); + properties.push_front(members.to_json()); + } + + ERR_FAIL_COND(!object_list.has(object_id)); + variable_list.insert(object_list[object_id], properties); +} + +void DebugAdapterProtocol::parse_evaluation(DebuggerMarshalls::ScriptStackVariable &p_var) { + // If the eval is not on the pending list, we weren't expecting it. Ignore it. + String eval = p_var.name; + if (!eval_pending_list.erase(eval)) { + return; + } + + DAP::Variable variable; + variable.name = p_var.name; + variable.value = p_var.value; + variable.type = Variant::get_type_name(p_var.value.get_type()); + variable.variablesReference = parse_variant(p_var.value); + + eval_list.insert(variable.name, variable); +} + +const Variant DebugAdapterProtocol::parse_object_variable(const SceneDebuggerObject::SceneDebuggerProperty &p_property) { + const PropertyInfo &info = p_property.first; + const Variant &value = p_property.second; + + DAP::Variable var; + var.name = info.name; + var.type = Variant::get_type_name(info.type); + var.value = value; + var.variablesReference = parse_variant(value); + + return var.to_json(); +} + +ObjectID DebugAdapterProtocol::search_object_id(DAPVarID p_var_id) { + for (const KeyValue &E : object_list) { + if (E.value == p_var_id) { + return E.key; + } + } + return ObjectID(); +} + +bool DebugAdapterProtocol::request_remote_object(const ObjectID &p_object_id) { + // If the object is already on the pending list, we don't need to request it again. + if (object_pending_set.has(p_object_id)) { + return false; + } + + EditorDebuggerNode::get_singleton()->get_default_debugger()->request_remote_object(p_object_id); + object_pending_set.insert(p_object_id); + + return true; +} + +bool DebugAdapterProtocol::request_remote_evaluate(const String &p_eval, int p_stack_frame) { + // If the eval is already on the pending list, we don't need to request it again + if (eval_pending_list.has(p_eval)) { + return false; + } + + EditorDebuggerNode::get_singleton()->get_default_debugger()->request_remote_evaluate(p_eval, p_stack_frame); + eval_pending_list.insert(p_eval); + + return true; +} + bool DebugAdapterProtocol::process_message(const String &p_text) { JSON json; ERR_FAIL_COND_V_MSG(json.parse(p_text) != OK, true, "Malformed message!"); @@ -986,6 +1170,20 @@ void DebugAdapterProtocol::on_debug_data(const String &p_msg, const Array &p_dat return; } + if (p_msg == "scene:inspect_object") { + // An object was requested from the debuggee; parse it. + SceneDebuggerObject remote_obj; + remote_obj.deserialize(p_data); + + parse_object(remote_obj); + } else if (p_msg == "evaluation_return") { + // An evaluation was requested from the debuggee; parse it. + DebuggerMarshalls::ScriptStackVariable remote_evaluation; + remote_evaluation.deserialize(p_data); + + parse_evaluation(remote_evaluation); + } + notify_custom_data(p_msg, p_data); } diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.h b/editor/debugger/debug_adapter/debug_adapter_protocol.h index caff0f9c7ff..1d1e1838504 100644 --- a/editor/debugger/debug_adapter/debug_adapter_protocol.h +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.h @@ -31,12 +31,13 @@ #ifndef DEBUG_ADAPTER_PROTOCOL_H #define DEBUG_ADAPTER_PROTOCOL_H -#include "core/io/stream_peer.h" +#include "core/debugger/debugger_marshalls.h" #include "core/io/stream_peer_tcp.h" #include "core/io/tcp_server.h" #include "debug_adapter_parser.h" #include "debug_adapter_types.h" +#include "scene/debugger/scene_debugger.h" #define DAP_MAX_BUFFER_SIZE 4194304 // 4MB #define DAP_MAX_CLIENTS 8 @@ -75,6 +76,8 @@ class DebugAdapterProtocol : public Object { friend class DebugAdapterParser; + using DAPVarID = int; + private: static DebugAdapterProtocol *singleton; DebugAdapterParser *parser = nullptr; @@ -99,6 +102,13 @@ class DebugAdapterProtocol : public Object { void reset_stack_info(); int parse_variant(const Variant &p_var); + void parse_object(SceneDebuggerObject &p_obj); + const Variant parse_object_variable(const SceneDebuggerObject::SceneDebuggerProperty &p_property); + void parse_evaluation(DebuggerMarshalls::ScriptStackVariable &p_var); + + ObjectID search_object_id(DAPVarID p_var_id); + bool request_remote_object(const ObjectID &p_object_id); + bool request_remote_evaluate(const String &p_eval, int p_stack_frame); bool _initialized = false; bool _processing_breakpoint = false; @@ -106,7 +116,7 @@ class DebugAdapterProtocol : public Object { bool _processing_stackdump = false; int _remaining_vars = 0; int _current_frame = 0; - uint64_t _request_timeout = 1000; + uint64_t _request_timeout = 5000; bool _sync_breakpoints = false; String _current_request; @@ -114,10 +124,16 @@ class DebugAdapterProtocol : public Object { int breakpoint_id = 0; int stackframe_id = 0; - int variable_id = 0; + DAPVarID variable_id = 0; List breakpoint_list; HashMap, DAP::StackFrame> stackframe_list; - HashMap variable_list; + HashMap variable_list; + + HashMap object_list; + HashSet object_pending_set; + + HashMap eval_list; + HashSet eval_pending_list; public: friend class DebugAdapterServer; diff --git a/editor/debugger/editor_expression_evaluator.cpp b/editor/debugger/editor_expression_evaluator.cpp index e8b1e33d205..25e9c9eac37 100644 --- a/editor/debugger/editor_expression_evaluator.cpp +++ b/editor/debugger/editor_expression_evaluator.cpp @@ -64,10 +64,7 @@ void EditorExpressionEvaluator::_evaluate() { 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); + editor_debugger->request_remote_evaluate(expression, editor_debugger->get_stack_script_frame()); expression_input->clear(); } diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 642244ebeb1..cbe7910518c 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -253,6 +253,13 @@ const SceneDebuggerTree *ScriptEditorDebugger::get_remote_tree() { return scene_tree; } +void ScriptEditorDebugger::request_remote_evaluate(const String &p_expression, int p_stack_frame) { + Array msg; + msg.push_back(p_expression); + msg.push_back(p_stack_frame); + _put_msg("evaluate", msg); +} + void ScriptEditorDebugger::update_remote_object(ObjectID p_obj_id, const String &p_prop, const Variant &p_value) { Array msg; msg.push_back(p_obj_id); diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h index 26106849f90..1908b1e5a7f 100644 --- a/editor/debugger/script_editor_debugger.h +++ b/editor/debugger/script_editor_debugger.h @@ -254,6 +254,8 @@ class ScriptEditorDebugger : public MarginContainer { void request_remote_tree(); const SceneDebuggerTree *get_remote_tree(); + void request_remote_evaluate(const String &p_expression, int p_stack_frame); + void start(Ref p_peer); void stop(); diff --git a/editor/editor_interface.cpp b/editor/editor_interface.cpp index fa6198f6950..264c80dcbf5 100644 --- a/editor/editor_interface.cpp +++ b/editor/editor_interface.cpp @@ -40,6 +40,7 @@ #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" #include "editor/filesystem_dock.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/scene_tree_editor.h" @@ -336,6 +337,24 @@ void EditorInterface::popup_property_selector(Object *p_object, const Callable & property_selector->connect(SNAME("canceled"), canceled_callback, CONNECT_DEFERRED); } +void EditorInterface::popup_quick_open(const Callable &p_callback, const TypedArray &p_base_types) { + StringName required_type = SNAME("Resource"); + Vector base_types; + if (p_base_types.is_empty()) { + base_types.append(required_type); + } else { + for (int i = 0; i < p_base_types.size(); i++) { + StringName type = p_base_types[i]; + ERR_FAIL_COND_MSG(!(ClassDB::is_parent_class(type, required_type) || EditorNode::get_editor_data().script_class_is_parent(type, required_type)), "Only types deriving from Resource are supported in the quick open dialog."); + base_types.append(type); + } + } + + EditorQuickOpenDialog *quick_open = EditorNode::get_singleton()->get_quick_open_dialog(); + quick_open->connect(SNAME("canceled"), callable_mp(this, &EditorInterface::_quick_open).bind(String(), p_callback)); + quick_open->popup_dialog(base_types, callable_mp(this, &EditorInterface::_quick_open).bind(p_callback)); +} + void EditorInterface::_node_selected(const NodePath &p_node_path, const Callable &p_callback) { const NodePath path = get_edited_scene_root()->get_path().rel_path_to(p_node_path); _call_dialog_callback(p_callback, path, "node selected"); @@ -353,6 +372,12 @@ void EditorInterface::_property_selection_canceled(const Callable &p_callback) { _call_dialog_callback(p_callback, NodePath(), "property selection canceled"); } +void EditorInterface::_quick_open(const String &p_file_path, const Callable &p_callback) { + EditorQuickOpenDialog *quick_open = EditorNode::get_singleton()->get_quick_open_dialog(); + quick_open->disconnect(SNAME("canceled"), callable_mp(this, &EditorInterface::_quick_open)); + _call_dialog_callback(p_callback, p_file_path, "quick open"); +} + void EditorInterface::_call_dialog_callback(const Callable &p_callback, const Variant &p_selected, const String &p_context) { Callable::CallError ce; Variant ret; @@ -568,6 +593,7 @@ void EditorInterface::_bind_methods() { ClassDB::bind_method(D_METHOD("popup_node_selector", "callback", "valid_types", "current_value"), &EditorInterface::popup_node_selector, DEFVAL(TypedArray()), DEFVAL(Variant())); ClassDB::bind_method(D_METHOD("popup_property_selector", "object", "callback", "type_filter", "current_value"), &EditorInterface::popup_property_selector, DEFVAL(PackedInt32Array()), DEFVAL(String())); + ClassDB::bind_method(D_METHOD("popup_quick_open", "callback", "base_types"), &EditorInterface::popup_quick_open, DEFVAL(TypedArray())); // Editor docks. diff --git a/editor/editor_interface.h b/editor/editor_interface.h index 20d66d71f53..4877444dac4 100644 --- a/editor/editor_interface.h +++ b/editor/editor_interface.h @@ -72,6 +72,7 @@ class EditorInterface : public Object { void _node_selection_canceled(const Callable &p_callback); void _property_selected(const String &p_property_name, const Callable &p_callback); void _property_selection_canceled(const Callable &p_callback); + void _quick_open(const String &p_file_path, const Callable &p_callback); void _call_dialog_callback(const Callable &p_callback, const Variant &p_selected, const String &p_context); // Editor tools. @@ -138,6 +139,7 @@ class EditorInterface : public Object { void popup_node_selector(const Callable &p_callback, const TypedArray &p_valid_types = TypedArray(), Node *p_current_value = nullptr); // Must use Vector because exposing Vector is not supported. void popup_property_selector(Object *p_object, const Callable &p_callback, const PackedInt32Array &p_type_filter = PackedInt32Array(), const String &p_current_value = String()); + void popup_quick_open(const Callable &p_callback, const TypedArray &p_base_types = TypedArray()); // Editor docks. diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 2853ebc4994..d88fb134f16 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -1983,7 +1983,7 @@ void EditorNode::try_autosave() { editor_data.save_editor_external_data(); } -void EditorNode::restart_editor() { +void EditorNode::restart_editor(bool p_goto_project_manager) { exiting = true; if (project_run_bar->is_playing()) { @@ -1991,22 +1991,25 @@ void EditorNode::restart_editor() { } String to_reopen; - if (get_tree()->get_edited_scene_root()) { + if (!p_goto_project_manager && get_tree()->get_edited_scene_root()) { to_reopen = get_tree()->get_edited_scene_root()->get_scene_file_path(); } _exit_editor(EXIT_SUCCESS); List args; - for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_TOOL)) { args.push_back(a); } - args.push_back("--path"); - args.push_back(ProjectSettings::get_singleton()->get_resource_path()); + if (p_goto_project_manager) { + args.push_back("--project-manager"); + } else { + args.push_back("--path"); + args.push_back(ProjectSettings::get_singleton()->get_resource_path()); - args.push_back("-e"); + args.push_back("-e"); + } if (!to_reopen.is_empty()) { args.push_back(to_reopen); @@ -2387,7 +2390,7 @@ void EditorNode::hide_unused_editors(const Object *p_editing_owner) { // This is to sweep properties that were removed from the inspector. List to_remove; for (KeyValue> &kv : active_plugins) { - const Object *context = ObjectDB::get_instance(kv.key); + Object *context = ObjectDB::get_instance(kv.key); if (context) { // In case of self-owning plugins, they are disabled here if they can auto hide. const EditorPlugin *self_owning = Object::cast_to(context); @@ -2396,7 +2399,7 @@ void EditorNode::hide_unused_editors(const Object *p_editing_owner) { } } - if (!context) { + if (!context || context->call(SNAME("_should_stop_editing"))) { to_remove.push_back(kv.key); for (EditorPlugin *plugin : kv.value) { if (plugin->can_auto_hide()) { @@ -3402,23 +3405,7 @@ void EditorNode::_discard_changes(const String &p_str) { } break; case RUN_PROJECT_MANAGER: { - project_run_bar->stop_playing(); - _exit_editor(EXIT_SUCCESS); - String exec = OS::get_singleton()->get_executable_path(); - - List args; - for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_TOOL)) { - args.push_back(a); - } - - String exec_base_dir = exec.get_base_dir(); - if (!exec_base_dir.is_empty()) { - args.push_back("--path"); - args.push_back(exec_base_dir); - } - args.push_back("--project-manager"); - - OS::get_singleton()->set_restart_on_exit(true, args); + restart_editor(true); } break; case RELOAD_CURRENT_PROJECT: { restart_editor(); diff --git a/editor/editor_node.h b/editor/editor_node.h index 55caed4bb41..696caf857c8 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -926,7 +926,7 @@ class EditorNode : public Node { void save_scene_list(const HashSet &p_scene_paths); void save_before_run(); void try_autosave(); - void restart_editor(); + void restart_editor(bool p_goto_project_manager = false); void unload_editor_addons(); void dim_editor(bool p_dimming); diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp index 897e7835fdd..c5a35e466c9 100644 --- a/editor/editor_properties.cpp +++ b/editor/editor_properties.cpp @@ -3179,6 +3179,10 @@ void EditorPropertyResource::_update_preferred_shader() { } } +bool EditorPropertyResource::_should_stop_editing() const { + return !resource_picker->is_toggle_pressed(); +} + void EditorPropertyResource::_viewport_selected(const NodePath &p_path) { Node *to_node = get_node(p_path); if (!Object::cast_to(to_node)) { @@ -3353,13 +3357,18 @@ void EditorPropertyResource::_notification(int p_what) { switch (p_what) { case NOTIFICATION_EXIT_TREE: { const EditorInspector *ei = get_parent_inspector(); - if (ei && !ei->is_main_editor_inspector()) { + const EditorInspector *main_ei = InspectorDock::get_inspector_singleton(); + if (ei && main_ei && ei != main_ei && !main_ei->is_ancestor_of(ei)) { fold_resource(); } } break; } } +void EditorPropertyResource::_bind_methods() { + ClassDB::bind_method(D_METHOD("_should_stop_editing"), &EditorPropertyResource::_should_stop_editing); +} + EditorPropertyResource::EditorPropertyResource() { use_sub_inspector = bool(EDITOR_GET("interface/inspector/open_resources_in_current_inspector")); has_borders = true; diff --git a/editor/editor_properties.h b/editor/editor_properties.h index 2ec78cdb448..004630da3e2 100644 --- a/editor/editor_properties.h +++ b/editor/editor_properties.h @@ -683,10 +683,12 @@ class EditorPropertyResource : public EditorProperty { void _open_editor_pressed(); void _update_preferred_shader(); + bool _should_stop_editing() const; protected: virtual void _set_read_only(bool p_read_only) override; void _notification(int p_what); + static void _bind_methods(); public: virtual void update_property() override; diff --git a/editor/editor_resource_picker.cpp b/editor/editor_resource_picker.cpp index e4ae2a62027..0f0287718c6 100644 --- a/editor/editor_resource_picker.cpp +++ b/editor/editor_resource_picker.cpp @@ -132,6 +132,11 @@ void EditorResourcePicker::_resource_selected() { emit_signal(SNAME("resource_selected"), edited_resource, false); } +void EditorResourcePicker::_resource_changed() { + emit_signal(SNAME("resource_changed"), edited_resource); + _update_resource(); +} + void EditorResourcePicker::_file_selected(const String &p_path) { Ref loaded_resource = ResourceLoader::load(p_path); ERR_FAIL_COND_MSG(loaded_resource.is_null(), "Cannot load resource from path '" + p_path + "'."); @@ -167,8 +172,7 @@ void EditorResourcePicker::_file_selected(const String &p_path) { } edited_resource = loaded_resource; - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } void EditorResourcePicker::_resource_saved(Object *p_resource) { @@ -353,8 +357,7 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { case OBJ_MENU_CLEAR: { edited_resource = Ref(); - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } break; case OBJ_MENU_MAKE_UNIQUE: { @@ -366,8 +369,7 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { ERR_FAIL_COND(unique_resource.is_null()); // duplicate() may fail. edited_resource = unique_resource; - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } break; case OBJ_MENU_MAKE_UNIQUE_RECURSIVE: { @@ -432,9 +434,7 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { _edit_menu_cbk(OBJ_MENU_MAKE_UNIQUE); return; } - - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } break; case OBJ_MENU_SHOW_IN_FILE_SYSTEM: { @@ -453,8 +453,7 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { ERR_FAIL_INDEX(to_type, conversions.size()); edited_resource = conversions[to_type]->convert(edited_resource); - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); break; } @@ -481,8 +480,7 @@ void EditorResourcePicker::_edit_menu_cbk(int p_which) { // Prevent freeing of the object until the end of the update of the resource (GH-88286). Ref old_edited_resource = edited_resource; edited_resource = Ref(resp); - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } break; } } @@ -778,8 +776,7 @@ void EditorResourcePicker::drop_data_fw(const Point2 &p_point, const Variant &p_ } edited_resource = dropped_resource; - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } } @@ -952,6 +949,10 @@ void EditorResourcePicker::set_toggle_pressed(bool p_pressed) { assign_button->set_pressed(p_pressed); } +bool EditorResourcePicker::is_toggle_pressed() const { + return assign_button->is_pressed(); +} + void EditorResourcePicker::set_editable(bool p_editable) { editable = p_editable; assign_button->set_disabled(!editable && !edited_resource.is_valid()); @@ -1046,8 +1047,7 @@ void EditorResourcePicker::_duplicate_selected_resources() { if (meta.size() == 1) { // Root. edited_resource = unique_resource; - emit_signal(SNAME("resource_changed"), edited_resource); - _update_resource(); + _resource_changed(); } else { Array parent_meta = item->get_parent()->get_metadata(0); Ref parent = parent_meta[0]; diff --git a/editor/editor_resource_picker.h b/editor/editor_resource_picker.h index c39d9af764f..0a32dea3ed2 100644 --- a/editor/editor_resource_picker.h +++ b/editor/editor_resource_picker.h @@ -86,6 +86,7 @@ class EditorResourcePicker : public HBoxContainer { void _update_resource_preview(const String &p_path, const Ref &p_preview, const Ref &p_small_preview, ObjectID p_obj); void _resource_selected(); + void _resource_changed(); void _file_selected(const String &p_path); void _resource_saved(Object *p_resource); @@ -134,6 +135,7 @@ class EditorResourcePicker : public HBoxContainer { void set_toggle_mode(bool p_enable); bool is_toggle_mode() const; void set_toggle_pressed(bool p_pressed); + bool is_toggle_pressed() const; void set_editable(bool p_editable); bool is_editable() const; diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index ceaffb64c49..ee06f08a2d7 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -221,7 +221,6 @@ bool EditorSettings::_get(const StringName &p_name, Variant &r_ret) const { const VariantContainer *v = props.getptr(p_name); if (!v) { - WARN_PRINT("EditorSettings::_get - Property not found: " + String(p_name)); return false; } r_ret = v->variant; diff --git a/editor/gui/editor_file_dialog.cpp b/editor/gui/editor_file_dialog.cpp index a63c3f7848e..77d0ba7a60a 100644 --- a/editor/gui/editor_file_dialog.cpp +++ b/editor/gui/editor_file_dialog.cpp @@ -1580,7 +1580,7 @@ void EditorFileDialog::_favorite_move_down() { } void EditorFileDialog::_update_favorites() { - bool res = (access == ACCESS_RESOURCES); + bool access_resources = (access == ACCESS_RESOURCES); String current = get_current_dir(); favorites->clear(); @@ -1596,8 +1596,11 @@ void EditorFileDialog::_update_favorites() { for (int i = 0; i < favorited.size(); i++) { String name = favorited[i]; - bool cres = name.begins_with("res://"); - if (cres != res || !name.ends_with("/")) { + if (access_resources != name.begins_with("res://")) { + continue; + } + + if (!name.ends_with("/")) { continue; } @@ -1609,7 +1612,7 @@ void EditorFileDialog::_update_favorites() { } // Compute favorite display text. - if (res && name == "res://") { + if (access_resources && name == "res://") { if (name == current) { current_favorite = favorited_paths.size(); } @@ -1620,7 +1623,7 @@ void EditorFileDialog::_update_favorites() { if (name == current || name == current + "/") { current_favorite = favorited_paths.size(); } - name = name.substr(0, name.length() - 1); + name = name.trim_suffix("/"); name = name.get_file(); favorited_paths.append(favorited[i]); favorited_names.append(name); @@ -1647,7 +1650,7 @@ void EditorFileDialog::_update_favorites() { } void EditorFileDialog::_favorite_pressed() { - bool res = (access == ACCESS_RESOURCES); + bool access_resources = (access == ACCESS_RESOURCES); String cd = get_current_dir(); if (!cd.ends_with("/")) { @@ -1657,13 +1660,12 @@ void EditorFileDialog::_favorite_pressed() { Vector favorited = EditorSettings::get_singleton()->get_favorites(); bool found = false; - for (int i = 0; i < favorited.size(); i++) { - bool cres = favorited[i].begins_with("res://"); - if (cres != res) { + for (const String &name : favorited) { + if (access_resources != name.begins_with("res://")) { continue; } - if (favorited[i] == cd) { + if (name == cd) { found = true; break; } @@ -1683,31 +1685,30 @@ void EditorFileDialog::_favorite_pressed() { void EditorFileDialog::_update_recent() { recent->clear(); - bool res = (access == ACCESS_RESOURCES); + bool access_resources = (access == ACCESS_RESOURCES); Vector recentd = EditorSettings::get_singleton()->get_recent_dirs(); Vector recentd_paths; Vector recentd_names; + bool modified = false; for (int i = 0; i < recentd.size(); i++) { - bool cres = recentd[i].begins_with("res://"); - if (cres != res) { + String name = recentd[i]; + if (access_resources != name.begins_with("res://")) { continue; } - if (!dir_access->dir_exists(recentd[i])) { + if (!dir_access->dir_exists(name)) { // Remove invalid directory from the list of Recent directories. recentd.remove_at(i--); + modified = true; continue; } // Compute recent directory display text. - String name = recentd[i]; - if (res && name == "res://") { + if (access_resources && name == "res://") { name = "/"; } else { - if (name.ends_with("/")) { - name = name.substr(0, name.length() - 1); - } + name = name.trim_suffix("/"); name = name.get_file(); } recentd_paths.append(recentd[i]); @@ -1721,7 +1722,10 @@ void EditorFileDialog::_update_recent() { recent->set_item_metadata(-1, recentd_paths[i]); recent->set_item_icon_modulate(-1, get_dir_icon_color(recentd_paths[i])); } - EditorSettings::get_singleton()->set_recent_dirs(recentd); + + if (modified) { + EditorSettings::get_singleton()->set_recent_dirs(recentd); + } } void EditorFileDialog::_recent_selected(int p_idx) { diff --git a/editor/gui/editor_quick_open_dialog.cpp b/editor/gui/editor_quick_open_dialog.cpp index 83b11e70221..94a5ff94a31 100644 --- a/editor/gui/editor_quick_open_dialog.cpp +++ b/editor/gui/editor_quick_open_dialog.cpp @@ -709,7 +709,7 @@ void QuickOpenResultContainer::_notification(int p_what) { 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))); + panel_container->add_theme_style_override(SceneStringName(panel), get_theme_stylebox(SceneStringName(panel), SNAME("Tree"))); if (content_display_mode == QuickOpenDisplayMode::LIST) { display_mode_toggle->set_icon(get_editor_theme_icon(SNAME("FileThumbnail"))); diff --git a/editor/plugins/editor_context_menu_plugin.cpp b/editor/plugins/editor_context_menu_plugin.cpp index 0648327faba..b635816bd97 100644 --- a/editor/plugins/editor_context_menu_plugin.cpp +++ b/editor/plugins/editor_context_menu_plugin.cpp @@ -67,10 +67,21 @@ void EditorContextMenuPlugin::add_context_menu_item_from_shortcut(const String & context_menu_items.insert(p_name, item); } +void EditorContextMenuPlugin::add_context_submenu_item(const String &p_name, PopupMenu *p_menu, const Ref &p_texture) { + ERR_FAIL_NULL(p_menu); + + ContextMenuItem item; + item.item_name = p_name; + item.icon = p_texture; + item.submenu = p_menu; + context_menu_items.insert(p_name, item); +} + void EditorContextMenuPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("add_menu_shortcut", "shortcut", "callback"), &EditorContextMenuPlugin::add_menu_shortcut); ClassDB::bind_method(D_METHOD("add_context_menu_item", "name", "callback", "icon"), &EditorContextMenuPlugin::add_context_menu_item, DEFVAL(Ref())); ClassDB::bind_method(D_METHOD("add_context_menu_item_from_shortcut", "name", "shortcut", "icon"), &EditorContextMenuPlugin::add_context_menu_item_from_shortcut, DEFVAL(Ref())); + ClassDB::bind_method(D_METHOD("add_context_submenu_item", "name", "menu", "icon"), &EditorContextMenuPlugin::add_context_submenu_item, DEFVAL(Ref())); GDVIRTUAL_BIND(_popup_menu, "paths"); @@ -117,12 +128,17 @@ void EditorContextMenuPluginManager::add_options_from_plugins(PopupMenu *p_popup EditorContextMenuPlugin::ContextMenuItem &item = E.value; item.id = id; - if (item.icon.is_valid()) { - p_popup->add_icon_item(item.icon, item.item_name, id); - p_popup->set_item_icon_max_width(-1, icon_size); + if (item.submenu) { + p_popup->add_submenu_node_item(item.item_name, item.submenu, id); } else { p_popup->add_item(item.item_name, id); } + + if (item.icon.is_valid()) { + p_popup->set_item_icon(-1, item.icon); + p_popup->set_item_icon_max_width(-1, icon_size); + } + if (item.shortcut.is_valid()) { p_popup->set_item_shortcut(-1, item.shortcut, true); } diff --git a/editor/plugins/editor_context_menu_plugin.h b/editor/plugins/editor_context_menu_plugin.h index 0232d254ba5..86c67dedda1 100644 --- a/editor/plugins/editor_context_menu_plugin.h +++ b/editor/plugins/editor_context_menu_plugin.h @@ -65,6 +65,7 @@ class EditorContextMenuPlugin : public RefCounted { Callable callable; Ref icon; Ref shortcut; + PopupMenu *submenu = nullptr; }; HashMap context_menu_items; HashMap, Callable> context_menu_shortcuts; @@ -80,6 +81,7 @@ class EditorContextMenuPlugin : public RefCounted { void add_menu_shortcut(const Ref &p_shortcut, const Callable &p_callable); void add_context_menu_item(const String &p_name, const Callable &p_callable, const Ref &p_texture); void add_context_menu_item_from_shortcut(const String &p_name, const Ref &p_shortcut, const Ref &p_texture); + void add_context_submenu_item(const String &p_name, PopupMenu *p_menu, const Ref &p_texture); }; VARIANT_ENUM_CAST(EditorContextMenuPlugin::ContextMenuSlot); diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index dc86acd884d..0cf194b7fe1 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -4255,8 +4255,31 @@ Vector3 Node3DEditorViewport::_get_instance_position(const Point2 &p_pos) const ray_params.to = world_pos + world_ray * camera->get_far(); PhysicsDirectSpaceState3D::RayResult result; - if (ss->intersect_ray(ray_params, result)) { - return result.position; + if (ss->intersect_ray(ray_params, result) && preview_node->get_child_count() > 0) { + // Calculate an offset for the `preview_node` such that the its bounding box is on top of and touching the contact surface's plane. + + // Use the Gram-Schmidt process to get an orthonormal Basis aligned with the surface normal. + const Vector3 bb_basis_x = result.normal; + Vector3 bb_basis_y = Vector3(0, 1, 0); + bb_basis_y = bb_basis_y - bb_basis_y.project(bb_basis_x); + if (bb_basis_y.is_zero_approx()) { + bb_basis_y = Vector3(0, 0, 1); + bb_basis_y = bb_basis_y - bb_basis_y.project(bb_basis_x); + } + bb_basis_y = bb_basis_y.normalized(); + const Vector3 bb_basis_z = bb_basis_x.cross(bb_basis_y); + const Basis bb_basis = Basis(bb_basis_x, bb_basis_y, bb_basis_z); + + // This normal-aligned Basis allows us to create an AABB that can fit on the surface plane as snugly as possible. + const Transform3D bb_transform = Transform3D(bb_basis, preview_node->get_transform().origin); + const AABB preview_node_bb = _calculate_spatial_bounds(preview_node, true, &bb_transform); + // The x-axis's alignment with the surface normal also makes it trivial to get the distance from `preview_node`'s origin at (0, 0, 0) to the correct AABB face. + const float offset_distance = -preview_node_bb.position.x; + + // `result_offset` is in global space. + const Vector3 result_offset = result.position + result.normal * offset_distance; + + return result_offset; } const bool is_orthogonal = camera->get_projection() == Camera3D::PROJECTION_ORTHOGONAL; @@ -4284,18 +4307,21 @@ Vector3 Node3DEditorViewport::_get_instance_position(const Point2 &p_pos) const return world_pos + world_ray * FALLBACK_DISTANCE; } -AABB Node3DEditorViewport::_calculate_spatial_bounds(const Node3D *p_parent, const Node3D *p_top_level_parent) { +AABB Node3DEditorViewport::_calculate_spatial_bounds(const Node3D *p_parent, bool p_omit_top_level, const Transform3D *p_bounds_orientation) { AABB bounds; - if (!p_top_level_parent) { - p_top_level_parent = p_parent; + Transform3D bounds_orientation; + if (p_bounds_orientation) { + bounds_orientation = *p_bounds_orientation; + } else { + bounds_orientation = p_parent->get_global_transform(); } if (!p_parent) { return AABB(Vector3(-0.2, -0.2, -0.2), Vector3(0.4, 0.4, 0.4)); } - Transform3D xform_to_top_level_parent_space = p_top_level_parent->get_global_transform().affine_inverse() * p_parent->get_global_transform(); + const Transform3D xform_to_top_level_parent_space = bounds_orientation.affine_inverse() * p_parent->get_global_transform(); const VisualInstance3D *visual_instance = Object::cast_to(p_parent); if (visual_instance) { @@ -4306,9 +4332,9 @@ AABB Node3DEditorViewport::_calculate_spatial_bounds(const Node3D *p_parent, con bounds = xform_to_top_level_parent_space.xform(bounds); for (int i = 0; i < p_parent->get_child_count(); i++) { - Node3D *child = Object::cast_to(p_parent->get_child(i)); - if (child) { - AABB child_bounds = _calculate_spatial_bounds(child, p_top_level_parent); + const Node3D *child = Object::cast_to(p_parent->get_child(i)); + if (child && !(p_omit_top_level && child->is_set_as_top_level())) { + const AABB child_bounds = _calculate_spatial_bounds(child, p_omit_top_level, &bounds_orientation); bounds.merge_with(child_bounds); } } @@ -4359,6 +4385,10 @@ void Node3DEditorViewport::_create_preview_node(const Vector &files) con if (instance) { instance = _sanitize_preview_node(instance); preview_node->add_child(instance); + Node3D *node_3d = Object::cast_to(instance); + if (node_3d) { + node_3d->set_as_top_level(false); + } } add_preview = true; } @@ -4579,8 +4609,12 @@ bool Node3DEditorViewport::_create_instance(Node *p_parent, const String &p_path } Transform3D new_tf = node3d->get_transform(); - new_tf.origin = parent_tf.affine_inverse().xform(preview_node_pos + node3d->get_position()); - new_tf.basis = parent_tf.affine_inverse().basis * new_tf.basis; + if (node3d->is_set_as_top_level()) { + new_tf.origin += preview_node_pos; + } else { + new_tf.origin = parent_tf.affine_inverse().xform(preview_node_pos + node3d->get_position()); + new_tf.basis = parent_tf.affine_inverse().basis * new_tf.basis; + } undo_redo->add_do_method(instantiated_scene, "set_transform", new_tf); } @@ -9003,8 +9037,7 @@ Node3DEditor::Node3DEditor() { current_hover_gizmo_handle = -1; current_hover_gizmo_handle_secondary = false; { - //sun popup - + // Sun/preview environment popup. sun_environ_popup = memnew(PopupPanel); add_child(sun_environ_popup); @@ -9018,7 +9051,7 @@ Node3DEditor::Node3DEditor() { sun_vb->hide(); sun_title = memnew(Label); - sun_title->set_theme_type_variation("HeaderSmall"); + sun_title->set_theme_type_variation("HeaderMedium"); sun_vb->add_child(sun_title); sun_title->set_text(TTR("Preview Sun")); sun_title->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); @@ -9056,11 +9089,14 @@ void fragment() { sun_direction->set_material(sun_direction_material); HBoxContainer *sun_angle_hbox = memnew(HBoxContainer); + sun_angle_hbox->set_h_size_flags(SIZE_EXPAND_FILL); VBoxContainer *sun_angle_altitude_vbox = memnew(VBoxContainer); + sun_angle_altitude_vbox->set_h_size_flags(SIZE_EXPAND_FILL); Label *sun_angle_altitude_label = memnew(Label); sun_angle_altitude_label->set_text(TTR("Angular Altitude")); sun_angle_altitude_vbox->add_child(sun_angle_altitude_label); sun_angle_altitude = memnew(EditorSpinSlider); + sun_angle_altitude->set_suffix(U"\u00B0"); sun_angle_altitude->set_max(90); sun_angle_altitude->set_min(-90); sun_angle_altitude->set_step(0.1); @@ -9068,11 +9104,13 @@ void fragment() { sun_angle_altitude_vbox->add_child(sun_angle_altitude); sun_angle_hbox->add_child(sun_angle_altitude_vbox); VBoxContainer *sun_angle_azimuth_vbox = memnew(VBoxContainer); + sun_angle_azimuth_vbox->set_h_size_flags(SIZE_EXPAND_FILL); sun_angle_azimuth_vbox->set_custom_minimum_size(Vector2(100, 0)); Label *sun_angle_azimuth_label = memnew(Label); sun_angle_azimuth_label->set_text(TTR("Azimuth")); sun_angle_azimuth_vbox->add_child(sun_angle_azimuth_label); sun_angle_azimuth = memnew(EditorSpinSlider); + sun_angle_azimuth->set_suffix(U"\u00B0"); sun_angle_azimuth->set_max(180); sun_angle_azimuth->set_min(-180); sun_angle_azimuth->set_step(0.1); @@ -9117,7 +9155,7 @@ void fragment() { sun_state->set_h_size_flags(SIZE_EXPAND_FILL); VSeparator *sc = memnew(VSeparator); - sc->set_custom_minimum_size(Size2(50 * EDSCALE, 0)); + sc->set_custom_minimum_size(Size2(10 * EDSCALE, 0)); sc->set_v_size_flags(SIZE_EXPAND_FILL); sun_environ_hb->add_child(sc); @@ -9127,7 +9165,7 @@ void fragment() { environ_vb->hide(); environ_title = memnew(Label); - environ_title->set_theme_type_variation("HeaderSmall"); + environ_title->set_theme_type_variation("HeaderMedium"); environ_vb->add_child(environ_title); environ_title->set_text(TTR("Preview Environment")); @@ -9154,21 +9192,25 @@ void fragment() { environ_ao_button = memnew(Button); environ_ao_button->set_text(TTR("AO")); + environ_ao_button->set_h_size_flags(SIZE_EXPAND_FILL); environ_ao_button->set_toggle_mode(true); environ_ao_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_preview_settings_changed), CONNECT_DEFERRED); fx_vb->add_child(environ_ao_button); environ_glow_button = memnew(Button); environ_glow_button->set_text(TTR("Glow")); + environ_glow_button->set_h_size_flags(SIZE_EXPAND_FILL); environ_glow_button->set_toggle_mode(true); environ_glow_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_preview_settings_changed), CONNECT_DEFERRED); fx_vb->add_child(environ_glow_button); environ_tonemap_button = memnew(Button); environ_tonemap_button->set_text(TTR("Tonemap")); + environ_tonemap_button->set_h_size_flags(SIZE_EXPAND_FILL); environ_tonemap_button->set_toggle_mode(true); environ_tonemap_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_preview_settings_changed), CONNECT_DEFERRED); fx_vb->add_child(environ_tonemap_button); environ_gi_button = memnew(Button); environ_gi_button->set_text(TTR("GI")); + environ_gi_button->set_h_size_flags(SIZE_EXPAND_FILL); environ_gi_button->set_toggle_mode(true); environ_gi_button->connect(SceneStringName(pressed), callable_mp(this, &Node3DEditor::_preview_settings_changed), CONNECT_DEFERRED); fx_vb->add_child(environ_gi_button); diff --git a/editor/plugins/node_3d_editor_plugin.h b/editor/plugins/node_3d_editor_plugin.h index c7e6420875e..a8cade36fd1 100644 --- a/editor/plugins/node_3d_editor_plugin.h +++ b/editor/plugins/node_3d_editor_plugin.h @@ -472,7 +472,7 @@ class Node3DEditorViewport : public Control { Point2 _get_warped_mouse_motion(const Ref &p_ev_mouse_motion) const; Vector3 _get_instance_position(const Point2 &p_pos) const; - static AABB _calculate_spatial_bounds(const Node3D *p_parent, const Node3D *p_top_level_parent = nullptr); + static AABB _calculate_spatial_bounds(const Node3D *p_parent, bool p_omit_top_level = false, const Transform3D *p_bounds_orientation = nullptr); Node *_sanitize_preview_node(Node *p_node) const; diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index 8f646a76212..cc488ff3402 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -3596,6 +3596,13 @@ void ThemeEditor::_theme_close_button_cbk() { } } +void ThemeEditor::_scene_closed(const String &p_path) { + if (theme.is_valid() && theme->is_built_in() && theme->get_path().get_slice("::", 0) == p_path) { + theme = Ref(); + EditorNode::get_singleton()->hide_unused_editors(plugin); + } +} + void ThemeEditor::_add_preview_button_cbk() { preview_scene_dialog->popup_file_dialog(); } @@ -3679,7 +3686,10 @@ void ThemeEditor::_preview_control_picked(String p_class_name) { void ThemeEditor::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_READY: { + EditorNode::get_singleton()->connect("scene_closed", callable_mp(this, &ThemeEditor::_scene_closed)); + } break; + case NOTIFICATION_THEME_CHANGED: { preview_tabs->add_theme_style_override("tab_selected", get_theme_stylebox(SNAME("ThemeEditorPreviewFG"), EditorStringName(EditorStyles))); preview_tabs->add_theme_style_override("tab_unselected", get_theme_stylebox(SNAME("ThemeEditorPreviewBG"), EditorStringName(EditorStyles))); @@ -3807,71 +3817,7 @@ void ThemeEditorPlugin::make_visible(bool p_visible) { } bool ThemeEditorPlugin::can_auto_hide() const { - Ref edited_theme = theme_editor->theme; - if (edited_theme.is_null()) { - return true; - } - - Ref edited_resource = Ref(InspectorDock::get_inspector_singleton()->get_next_edited_object()); - if (edited_resource.is_null()) { - return true; - } - - // Don't hide if edited resource used by this theme. - Ref sbox = edited_resource; - if (sbox.is_valid()) { - List type_list; - edited_theme->get_stylebox_type_list(&type_list); - - for (const StringName &E : type_list) { - List list; - edited_theme->get_stylebox_list(E, &list); - - for (const StringName &F : list) { - if (edited_theme->get_stylebox(F, E) == sbox) { - return false; - } - } - } - return true; - } - - Ref tex = edited_resource; - if (tex.is_valid()) { - List type_list; - edited_theme->get_icon_type_list(&type_list); - - for (const StringName &E : type_list) { - List list; - edited_theme->get_icon_list(E, &list); - - for (const StringName &F : list) { - if (edited_theme->get_icon(F, E) == tex) { - return false; - } - } - } - return true; - } - - Ref fnt = edited_resource; - if (fnt.is_valid()) { - List type_list; - edited_theme->get_font_type_list(&type_list); - - for (const StringName &E : type_list) { - List list; - edited_theme->get_font_list(E, &list); - - for (const StringName &F : list) { - if (edited_theme->get_font(F, E) == fnt) { - return false; - } - } - } - return true; - } - return true; + return theme_editor->theme.is_null(); } ThemeEditorPlugin::ThemeEditorPlugin() { diff --git a/editor/plugins/theme_editor_plugin.h b/editor/plugins/theme_editor_plugin.h index 1d009637b7a..39dc8d154bb 100644 --- a/editor/plugins/theme_editor_plugin.h +++ b/editor/plugins/theme_editor_plugin.h @@ -445,6 +445,7 @@ class ThemeEditor : public VBoxContainer { void _theme_save_button_cbk(bool p_save_as); void _theme_edit_button_cbk(); void _theme_close_button_cbk(); + void _scene_closed(const String &p_path); void _add_preview_button_cbk(); void _preview_scene_dialog_cbk(const String &p_path); diff --git a/editor/plugins/tiles/tile_data_editors.cpp b/editor/plugins/tiles/tile_data_editors.cpp index 12b9761fd2f..c6921699a49 100644 --- a/editor/plugins/tiles/tile_data_editors.cpp +++ b/editor/plugins/tiles/tile_data_editors.cpp @@ -874,6 +874,11 @@ void GenericTilePolygonEditor::_notification(int p_what) { button_expand->set_pressed_no_signal(false); } } break; + + case NOTIFICATION_READY: { + get_parent()->connect(SceneStringName(tree_exited), callable_mp(TileSetEditor::get_singleton(), &TileSetEditor::remove_expanded_editor)); + } break; + case NOTIFICATION_THEME_CHANGED: { button_expand->set_icon(get_editor_theme_icon(SNAME("DistractionFree"))); button_create->set_icon(get_editor_theme_icon(SNAME("CurveCreate"))); diff --git a/editor/project_settings_editor.cpp b/editor/project_settings_editor.cpp index 418f4e4932f..f973367beda 100644 --- a/editor/project_settings_editor.cpp +++ b/editor/project_settings_editor.cpp @@ -104,7 +104,8 @@ void ProjectSettingsEditor::_update_advanced(bool p_is_advanced) { } void ProjectSettingsEditor::_advanced_toggled(bool p_button_pressed) { - EditorSettings::get_singleton()->set_project_metadata("project_settings", "advanced_mode", p_button_pressed); + EditorSettings::get_singleton()->set("_project_settings_advanced_mode", p_button_pressed); + EditorSettings::get_singleton()->save(); _update_advanced(p_button_pressed); } @@ -768,8 +769,7 @@ ProjectSettingsEditor::ProjectSettingsEditor(EditorData *p_data) { set_ok_button_text(TTR("Close")); set_hide_on_ok(true); - bool use_advanced = EditorSettings::get_singleton()->get_project_metadata("project_settings", "advanced_mode", false); - + bool use_advanced = EDITOR_DEF("_project_settings_advanced_mode", false); if (use_advanced) { advanced->set_pressed(true); } diff --git a/editor/themes/editor_fonts.cpp b/editor/themes/editor_fonts.cpp index c50d1237e00..da35ed332ee 100644 --- a/editor/themes/editor_fonts.cpp +++ b/editor/themes/editor_fonts.cpp @@ -156,6 +156,18 @@ void editor_register_fonts(const Ref &p_theme) { Ref default_font = load_internal_font(_font_NotoSans_Regular, _font_NotoSans_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false); Ref default_font_msdf = load_internal_font(_font_NotoSans_Regular, _font_NotoSans_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, font_allow_msdf); + String noto_cjk_path; + String noto_cjk_bold_path; + String var_suffix[] = { "HK", "KR", "SC", "TC", "JP" }; // Note: All Noto Sans CJK versions support all glyph variations, it should not match current locale. + for (size_t i = 0; i < sizeof(var_suffix) / sizeof(String); i++) { + if (noto_cjk_path.is_empty()) { + noto_cjk_path = OS::get_singleton()->get_system_font_path("Noto Sans CJK " + var_suffix[i], 400, 100); + } + if (noto_cjk_bold_path.is_empty()) { + noto_cjk_bold_path = OS::get_singleton()->get_system_font_path("Noto Sans CJK " + var_suffix[i], 800, 100); + } + } + TypedArray fallbacks; Ref arabic_font = load_internal_font(_font_NotoNaskhArabicUI_Regular, _font_NotoNaskhArabicUI_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); Ref bengali_font = load_internal_font(_font_NotoSansBengaliUI_Regular, _font_NotoSansBengaliUI_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); @@ -168,6 +180,9 @@ void editor_register_fonts(const Ref &p_theme) { Ref tamil_font = load_internal_font(_font_NotoSansTamilUI_Regular, _font_NotoSansTamilUI_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); Ref telugu_font = load_internal_font(_font_NotoSansTeluguUI_Regular, _font_NotoSansTeluguUI_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); Ref thai_font = load_internal_font(_font_NotoSansThai_Regular, _font_NotoSansThai_Regular_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); + if (!noto_cjk_path.is_empty()) { + load_external_font(noto_cjk_path, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); + } Ref fallback_font = load_internal_font(_font_DroidSansFallback, _font_DroidSansFallback_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); Ref japanese_font = load_internal_font(_font_DroidSansJapanese, _font_DroidSansJapanese_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks); default_font->set_fallbacks(fallbacks); @@ -188,6 +203,9 @@ void editor_register_fonts(const Ref &p_theme) { Ref tamil_font_bold = load_internal_font(_font_NotoSansTamilUI_Bold, _font_NotoSansTamilUI_Bold_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks_bold); Ref telugu_font_bold = load_internal_font(_font_NotoSansTeluguUI_Bold, _font_NotoSansTeluguUI_Bold_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks_bold); Ref thai_font_bold = load_internal_font(_font_NotoSansThai_Bold, _font_NotoSansThai_Bold_size, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks_bold); + if (!noto_cjk_bold_path.is_empty()) { + load_external_font(noto_cjk_bold_path, font_hinting, font_antialiasing, true, font_subpixel_positioning, font_disable_embedded_bitmaps, false, &fallbacks_bold); + } Ref fallback_font_bold = make_bold_font(fallback_font, embolden_strength, &fallbacks_bold); Ref japanese_font_bold = make_bold_font(japanese_font, embolden_strength, &fallbacks_bold); diff --git a/main/main.cpp b/main/main.cpp index 36912c4fa39..5206e9b84c5 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -245,6 +245,7 @@ static MovieWriter *movie_writer = nullptr; static bool disable_vsync = false; static bool print_fps = false; #ifdef TOOLS_ENABLED +static bool editor_pseudolocalization = false; static bool dump_gdextension_interface = false; static bool dump_extension_api = false; static bool include_docs_in_extension_api_dump = false; @@ -649,6 +650,9 @@ void Main::print_help(const char *p_binary) { print_help_option("--fixed-fps ", "Force a fixed number of frames per second. This setting disables real-time synchronization.\n"); print_help_option("--delta-smoothing ", "Enable or disable frame delta smoothing [\"enable\", \"disable\"].\n"); print_help_option("--print-fps", "Print the frames per second to the stdout.\n"); +#ifdef TOOLS_ENABLED + print_help_option("--editor-pseudolocalization", "Enable pseudolocalization for the editor and the project manager.\n"); +#endif print_help_title("Standalone tools"); print_help_option("-s, --script