summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/web_builds.yml30
-rw-r--r--SConstruct8
-rw-r--r--core/debugger/remote_debugger.cpp31
-rw-r--r--core/debugger/remote_debugger.h1
-rw-r--r--core/input/input_builders.py4
-rw-r--r--core/math/geometry_2d.h26
-rw-r--r--core/object/class_db.cpp7
-rw-r--r--core/object/object.cpp2
-rw-r--r--core/object/script_language.h1
-rw-r--r--core/object/script_language_extension.h1
-rw-r--r--core/object/worker_thread_pool.cpp6
-rw-r--r--core/os/condition_variable.h14
-rw-r--r--core/os/mutex.cpp4
-rw-r--r--core/os/mutex.h40
-rw-r--r--core/os/os.cpp6
-rw-r--r--core/os/semaphore.h17
-rw-r--r--core/os/thread.cpp9
-rw-r--r--core/os/thread.h64
-rw-r--r--doc/classes/AABB.xml217
-rw-r--r--doc/classes/GraphEdit.xml39
-rw-r--r--doc/classes/ProjectSettings.xml1
-rwxr-xr-xdoc/tools/make_rst.py18
-rw-r--r--drivers/d3d12/rendering_device_driver_d3d12.cpp2
-rw-r--r--drivers/unix/os_unix.cpp2
-rw-r--r--editor/SCsub1
-rw-r--r--editor/connections_dialog.cpp21
-rw-r--r--editor/connections_dialog.h2
-rw-r--r--editor/debugger/editor_debugger_node.cpp10
-rw-r--r--editor/debugger/editor_debugger_node.h3
-rw-r--r--editor/debugger/editor_performance_profiler.cpp4
-rw-r--r--editor/debugger/script_editor_debugger.cpp8
-rw-r--r--editor/debugger/script_editor_debugger.h3
-rw-r--r--editor/editor_audio_buses.cpp7
-rw-r--r--editor/editor_file_system.cpp8
-rw-r--r--editor/editor_file_system.h2
-rw-r--r--editor/editor_help_search.cpp148
-rw-r--r--editor/editor_help_search.h16
-rw-r--r--editor/editor_node.cpp5
-rw-r--r--editor/editor_settings.cpp8
-rw-r--r--editor/editor_settings.h2
-rw-r--r--editor/filesystem_dock.cpp18
-rw-r--r--editor/icons/TileMapLayer.svg1
-rw-r--r--editor/import/resource_importer_texture.cpp6
-rw-r--r--editor/plugins/bone_map_editor_plugin.cpp3
-rw-r--r--editor/plugins/canvas_item_editor_plugin.cpp3
-rw-r--r--editor/plugins/particle_process_material_editor_plugin.cpp3
-rw-r--r--editor/plugins/script_editor_plugin.cpp26
-rw-r--r--editor/plugins/script_editor_plugin.h5
-rw-r--r--editor/plugins/script_text_editor.cpp3
-rw-r--r--editor/project_manager.cpp2946
-rw-r--r--editor/project_manager.h422
-rw-r--r--editor/project_manager/SCsub5
-rw-r--r--editor/project_manager/project_dialog.cpp977
-rw-r--r--editor/project_manager/project_dialog.h136
-rw-r--r--editor/project_manager/project_list.cpp1074
-rw-r--r--editor/project_manager/project_list.h264
-rw-r--r--editor/project_manager/project_tag.cpp74
-rw-r--r--editor/project_manager/project_tag.h56
-rw-r--r--editor/scene_tree_dock.cpp1
-rw-r--r--editor/themes/editor_fonts.cpp3
-rw-r--r--editor/themes/editor_icons.cpp23
-rw-r--r--editor/themes/editor_icons.h2
-rw-r--r--editor/themes/editor_theme.h13
-rw-r--r--editor/themes/editor_theme_manager.cpp223
-rw-r--r--editor/themes/editor_theme_manager.h36
-rw-r--r--main/main.cpp20
-rw-r--r--methods.py31
-rw-r--r--misc/dist/html/editor.html8
-rw-r--r--misc/dist/html/full-size.html5
-rw-r--r--misc/dist/html/service-worker.js12
-rw-r--r--misc/extension_api_validation/4.2-stable.expected15
-rw-r--r--modules/etcpak/image_compress_etcpak.cpp2
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml10
-rw-r--r--modules/gdscript/editor/gdscript_highlighter.cpp3
-rw-r--r--modules/gdscript/gdscript.cpp151
-rw-r--r--modules/gdscript/gdscript.h7
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp15
-rw-r--r--modules/gdscript/gdscript_byte_codegen.h2
-rw-r--r--modules/gdscript/gdscript_compiler.cpp3
-rw-r--r--modules/gdscript/gdscript_editor.cpp89
-rw-r--r--modules/gdscript/gdscript_parser.cpp38
-rw-r--r--modules/gdscript/gdscript_parser.h4
-rw-r--r--modules/gdscript/language_server/gdscript_text_document.cpp2
-rw-r--r--modules/gdscript/tests/README.md41
-rw-r--r--modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_invalid.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/uid_too_late.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/uid.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/features/uid.out1
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd10
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/free_is_callable.out3
-rw-r--r--modules/gdscript/tests/test_completion.h18
-rw-r--r--modules/gdscript/tests/test_gdscript_uid.h115
-rw-r--r--modules/gltf/structures/gltf_buffer_view.cpp1
-rw-r--r--modules/mono/csharp_script.cpp29
-rw-r--r--modules/mono/csharp_script.h1
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln6
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs14
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj1
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs164
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs56
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs20
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj2
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs20
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs22
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs15
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs71
-rw-r--r--modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs27
-rw-r--r--modules/mono/editor/editor_internal_calls.cpp4
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs6
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs4
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs4
-rw-r--r--modules/mono/glue/runtime_interop.cpp2
-rw-r--r--modules/mono/utils/path_utils.cpp2
-rw-r--r--modules/openxr/doc_classes/OpenXRHand.xml13
-rw-r--r--modules/openxr/editor/openxr_editor_plugin.cpp5
-rw-r--r--modules/openxr/editor/openxr_editor_plugin.h4
-rw-r--r--modules/openxr/editor/openxr_select_runtime.cpp132
-rw-r--r--modules/openxr/editor/openxr_select_runtime.h51
-rw-r--r--modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp2
-rw-r--r--modules/openxr/extensions/openxr_opengl_extension.h6
-rw-r--r--modules/openxr/extensions/openxr_vulkan_extension.h2
-rw-r--r--modules/openxr/scene/openxr_hand.cpp26
-rw-r--r--modules/openxr/scene/openxr_hand.h11
-rw-r--r--modules/svg/register_types.cpp9
-rw-r--r--platform/android/export/export.cpp2
-rw-r--r--platform/android/export/export_plugin.cpp54
-rw-r--r--platform/android/export/export_plugin.h2
-rw-r--r--platform/android/java/app/build.gradle9
-rw-r--r--platform/android/java/app/config.gradle2
-rw-r--r--platform/linuxbsd/x11/display_server_x11.cpp46
-rw-r--r--platform/linuxbsd/x11/display_server_x11.h2
-rw-r--r--platform/macos/detect.py41
-rw-r--r--platform/web/.eslintrc.html.js2
-rw-r--r--platform/web/SCsub4
-rw-r--r--platform/web/audio_driver_web.cpp52
-rw-r--r--platform/web/audio_driver_web.h51
-rw-r--r--platform/web/detect.py24
-rw-r--r--platform/web/doc_classes/EditorExportPlatformWeb.xml4
-rw-r--r--platform/web/emscripten_helpers.py41
-rw-r--r--platform/web/export/export_plugin.cpp26
-rw-r--r--platform/web/export/export_plugin.h5
-rw-r--r--platform/web/js/engine/features.js22
-rw-r--r--platform/web/js/libs/audio.worklet.js4
-rw-r--r--scene/2d/audio_stream_player_2d.cpp262
-rw-r--r--scene/2d/audio_stream_player_2d.h29
-rw-r--r--scene/2d/tile_map.compat.inc10
-rw-r--r--scene/2d/tile_map.cpp808
-rw-r--r--scene/2d/tile_map.h16
-rw-r--r--scene/2d/tile_map_layer.cpp646
-rw-r--r--scene/2d/tile_map_layer.h60
-rw-r--r--scene/3d/audio_stream_player_3d.cpp260
-rw-r--r--scene/3d/audio_stream_player_3d.h33
-rw-r--r--scene/3d/collision_object_3d.cpp3
-rw-r--r--scene/audio/audio_stream_player.cpp261
-rw-r--r--scene/audio/audio_stream_player.h30
-rw-r--r--scene/audio/audio_stream_player_internal.cpp321
-rw-r--r--scene/audio/audio_stream_player_internal.h105
-rw-r--r--scene/gui/button.cpp481
-rw-r--r--scene/gui/button.h4
-rw-r--r--scene/gui/graph_edit.compat.inc5
-rw-r--r--scene/gui/graph_edit.cpp710
-rw-r--r--scene/gui/graph_edit.h82
-rw-r--r--scene/gui/graph_edit_arranger.cpp64
-rw-r--r--scene/gui/option_button.cpp4
-rw-r--r--scene/main/canvas_item.cpp1
-rw-r--r--scene/main/canvas_item.h4
-rw-r--r--scene/main/http_request.cpp2
-rw-r--r--scene/main/node.cpp4
-rw-r--r--scene/register_scene_types.cpp5
-rw-r--r--scene/resources/tile_set.cpp626
-rw-r--r--scene/resources/tile_set.h7
-rw-r--r--scene/theme/default_theme.cpp19
-rw-r--r--servers/register_server_types.cpp8
-rw-r--r--servers/rendering/rendering_server_default.cpp10
-rw-r--r--servers/rendering/rendering_server_default.h2
-rw-r--r--servers/text_server.cpp35
-rw-r--r--tests/scene/test_audio_stream_wav.h2
-rw-r--r--tests/scene/test_node_2d.h28
183 files changed, 8305 insertions, 5520 deletions
diff --git a/.github/workflows/web_builds.yml b/.github/workflows/web_builds.yml
index d314991fee..9524b5260b 100644
--- a/.github/workflows/web_builds.yml
+++ b/.github/workflows/web_builds.yml
@@ -17,7 +17,24 @@ concurrency:
jobs:
web-template:
runs-on: "ubuntu-22.04"
- name: Template (target=template_release)
+ name: ${{ matrix.name }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: Template w/ threads (target=template_release, threads=yes)
+ cache-name: web-template
+ target: template_release
+ sconsflags: threads=yes
+ tests: false
+ artifact: true
+
+ - name: Template w/o threads (target=template_release, threads=no)
+ cache-name: web-nothreads-template
+ target: template_release
+ sconsflags: threads=no
+ tests: false
+ artifact: true
steps:
- uses: actions/checkout@v4
@@ -34,6 +51,8 @@ jobs:
- name: Setup Godot build cache
uses: ./.github/actions/godot-cache
+ with:
+ cache-name: ${{ matrix.cache-name }}
continue-on-error: true
- name: Setup python and scons
@@ -42,10 +61,13 @@ jobs:
- name: Compilation
uses: ./.github/actions/godot-build
with:
- sconsflags: ${{ env.SCONSFLAGS }}
+ sconsflags: ${{ env.SCONSFLAGS }} ${{ matrix.sconsflags }}
platform: web
- target: template_release
- tests: false
+ target: ${{ matrix.target }}
+ tests: ${{ matrix.tests }}
- name: Upload artifact
uses: ./.github/actions/upload-artifact
+ if: ${{ matrix.artifact }}
+ with:
+ name: ${{ matrix.cache-name }}
diff --git a/SConstruct b/SConstruct
index c7b9d5bc86..6a4dea2c09 100644
--- a/SConstruct
+++ b/SConstruct
@@ -183,6 +183,7 @@ opts.Add(BoolVariable("separate_debug_symbols", "Extract debugging symbols to a
opts.Add(EnumVariable("lto", "Link-time optimization (production builds)", "none", ("none", "auto", "thin", "full")))
opts.Add(BoolVariable("production", "Set defaults to build Godot for use in production", False))
opts.Add(BoolVariable("generate_apk", "Generate an APK/AAB after building Android library by calling Gradle", False))
+opts.Add(BoolVariable("threads", "Enable threading support", True))
# Components
opts.Add(BoolVariable("deprecated", "Enable compatibility code for deprecated and removed features", True))
@@ -832,6 +833,10 @@ if selected_platform in platform_list:
suffix += ".double"
suffix += "." + env["arch"]
+
+ if not env["threads"]:
+ suffix += ".nothreads"
+
suffix += env.extra_suffix
sys.path.remove(tmppath)
@@ -972,6 +977,9 @@ if selected_platform in platform_list:
env.Tool("compilation_db")
env.Alias("compiledb", env.CompilationDatabase())
+ if env["threads"]:
+ env.Append(CPPDEFINES=["THREADS_ENABLED"])
+
Export("env")
# Build subdirs, the build order is dependent on link order.
diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp
index ce675d6b06..d3b0039e72 100644
--- a/core/debugger/remote_debugger.cpp
+++ b/core/debugger/remote_debugger.cpp
@@ -36,6 +36,7 @@
#include "core/debugger/engine_profiler.h"
#include "core/debugger/script_debugger.h"
#include "core/input/input.h"
+#include "core/io/resource_loader.h"
#include "core/object/script_language.h"
#include "core/os/os.h"
@@ -513,8 +514,9 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) {
_send_stack_vars(globals, globals_vals, 2);
} else if (command == "reload_scripts") {
+ script_paths_to_reload = data;
+ } else if (command == "reload_all_scripts") {
reload_all_scripts = true;
-
} else if (command == "breakpoint") {
ERR_FAIL_COND(data.size() < 3);
bool set = data[2];
@@ -589,19 +591,36 @@ void RemoteDebugger::poll_events(bool p_is_idle) {
}
// Reload scripts during idle poll only.
- if (p_is_idle && reload_all_scripts) {
- for (int i = 0; i < ScriptServer::get_language_count(); i++) {
- ScriptServer::get_language(i)->reload_all_scripts();
+ if (p_is_idle) {
+ if (reload_all_scripts) {
+ for (int i = 0; i < ScriptServer::get_language_count(); i++) {
+ ScriptServer::get_language(i)->reload_all_scripts();
+ }
+ reload_all_scripts = false;
+ } else if (!script_paths_to_reload.is_empty()) {
+ Array scripts_to_reload;
+ for (int i = 0; i < script_paths_to_reload.size(); ++i) {
+ String path = script_paths_to_reload[i];
+ Error err = OK;
+ Ref<Script> script = ResourceLoader::load(path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);
+ ERR_CONTINUE_MSG(err != OK, vformat("Could not reload script '%s': %s", path, error_names[err]));
+ ERR_CONTINUE_MSG(script.is_null(), vformat("Could not reload script '%s': Not a script!", path, error_names[err]));
+ scripts_to_reload.push_back(script);
+ }
+ for (int i = 0; i < ScriptServer::get_language_count(); i++) {
+ ScriptServer::get_language(i)->reload_scripts(scripts_to_reload, true);
+ }
}
- reload_all_scripts = false;
+ script_paths_to_reload.clear();
}
}
Error RemoteDebugger::_core_capture(const String &p_cmd, const Array &p_data, bool &r_captured) {
r_captured = true;
if (p_cmd == "reload_scripts") {
+ script_paths_to_reload = p_data;
+ } else if (p_cmd == "reload_all_scripts") {
reload_all_scripts = true;
-
} else if (p_cmd == "breakpoint") {
ERR_FAIL_COND_V(p_data.size() < 3, ERR_INVALID_DATA);
bool set = p_data[2];
diff --git a/core/debugger/remote_debugger.h b/core/debugger/remote_debugger.h
index 7c399178c6..519a90e7cc 100644
--- a/core/debugger/remote_debugger.h
+++ b/core/debugger/remote_debugger.h
@@ -74,6 +74,7 @@ private:
int warn_count = 0;
int last_reset = 0;
bool reload_all_scripts = false;
+ Array script_paths_to_reload;
// Make handlers and send_message thread safe.
Mutex mutex;
diff --git a/core/input/input_builders.py b/core/input/input_builders.py
index e98e2441e2..94c566493e 100644
--- a/core/input/input_builders.py
+++ b/core/input/input_builders.py
@@ -45,10 +45,10 @@ def make_default_controller_mappings(target, source, env):
platform_mappings[current_platform][guid] = line
platform_variables = {
- "Linux": "#if LINUXBSD_ENABLED",
+ "Linux": "#ifdef LINUXBSD_ENABLED",
"Windows": "#ifdef WINDOWS_ENABLED",
"Mac OS X": "#ifdef MACOS_ENABLED",
- "Android": "#if defined(__ANDROID__)",
+ "Android": "#ifdef ANDROID_ENABLED",
"iOS": "#ifdef IOS_ENABLED",
"Web": "#ifdef WEB_ENABLED",
}
diff --git a/core/math/geometry_2d.h b/core/math/geometry_2d.h
index b37fce9e9c..9907d579a5 100644
--- a/core/math/geometry_2d.h
+++ b/core/math/geometry_2d.h
@@ -119,6 +119,10 @@ public:
}
}
+ static real_t get_distance_to_segment(const Vector2 &p_point, const Vector2 *p_segment) {
+ return p_point.distance_to(get_closest_point_to_segment(p_point, p_segment));
+ }
+
static bool is_point_in_triangle(const Vector2 &s, const Vector2 &a, const Vector2 &b, const Vector2 &c) {
Vector2 an = a - s;
Vector2 bn = b - s;
@@ -249,6 +253,28 @@ public:
return -1;
}
+ static bool segment_intersects_rect(const Vector2 &p_from, const Vector2 &p_to, const Rect2 &p_rect) {
+ if (p_rect.has_point(p_from) || p_rect.has_point(p_to)) {
+ return true;
+ }
+
+ const Vector2 rect_points[4] = {
+ p_rect.position,
+ p_rect.position + Vector2(p_rect.size.x, 0),
+ p_rect.position + p_rect.size,
+ p_rect.position + Vector2(0, p_rect.size.y)
+ };
+
+ // Check if any of the rect's edges intersect the segment.
+ for (int i = 0; i < 4; i++) {
+ if (segment_intersects_segment(p_from, p_to, rect_points[i], rect_points[(i + 1) % 4], nullptr)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
enum PolyBooleanOperation {
OPERATION_UNION,
OPERATION_DIFFERENCE,
diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp
index bf1bd0de93..45fbb19f88 100644
--- a/core/object/class_db.cpp
+++ b/core/object/class_db.cpp
@@ -31,6 +31,7 @@
#include "class_db.h"
#include "core/config/engine.h"
+#include "core/core_string_names.h"
#include "core/io/resource_loader.h"
#include "core/object/script_language.h"
#include "core/os/mutex.h"
@@ -1299,6 +1300,12 @@ bool ClassDB::get_property(Object *p_object, const StringName &p_property, Varia
check = check->inherits_ptr;
}
+ // The "free()" method is special, so we assume it exists and return a Callable.
+ if (p_property == CoreStringNames::get_singleton()->_free) {
+ r_value = Callable(p_object, p_property);
+ return true;
+ }
+
return false;
}
diff --git a/core/object/object.cpp b/core/object/object.cpp
index 5a776e2106..3901c4835d 100644
--- a/core/object/object.cpp
+++ b/core/object/object.cpp
@@ -728,7 +728,7 @@ Variant Object::callp(const StringName &p_method, const Variant **p_args, int p_
r_error.expected = 0;
return Variant();
}
- if (Object::cast_to<RefCounted>(this)) {
+ if (is_ref_counted()) {
r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD;
ERR_FAIL_V_MSG(Variant(), "Can't 'free' a reference.");
}
diff --git a/core/object/script_language.h b/core/object/script_language.h
index 66106bf139..bb714d5bc3 100644
--- a/core/object/script_language.h
+++ b/core/object/script_language.h
@@ -371,6 +371,7 @@ public:
virtual Vector<StackInfo> debug_get_current_stack_info() { return Vector<StackInfo>(); }
virtual void reload_all_scripts() = 0;
+ virtual void reload_scripts(const Array &p_scripts, bool p_soft_reload) = 0;
virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) = 0;
/* LOADER FUNCTIONS */
diff --git a/core/object/script_language_extension.h b/core/object/script_language_extension.h
index 8b01667519..5b10739486 100644
--- a/core/object/script_language_extension.h
+++ b/core/object/script_language_extension.h
@@ -562,6 +562,7 @@ public:
}
EXBIND0(reload_all_scripts)
+ EXBIND2(reload_scripts, const Array &, bool)
EXBIND2(reload_tool_script, const Ref<Script> &, bool)
/* LOADER FUNCTIONS */
diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp
index 8e8a2ef06b..e2ab473b01 100644
--- a/core/object/worker_thread_pool.cpp
+++ b/core/object/worker_thread_pool.cpp
@@ -47,6 +47,7 @@ WorkerThreadPool *WorkerThreadPool::singleton = nullptr;
thread_local CommandQueueMT *WorkerThreadPool::flushing_cmd_queue = nullptr;
void WorkerThreadPool::_process_task(Task *p_task) {
+#ifdef THREADS_ENABLED
int pool_thread_index = thread_ids[Thread::get_caller_id()];
ThreadData &curr_thread = threads[pool_thread_index];
Task *prev_task = nullptr; // In case this is recursively called.
@@ -69,6 +70,7 @@ void WorkerThreadPool::_process_task(Task *p_task) {
curr_thread.current_task = p_task;
task_mutex.unlock();
}
+#endif
if (p_task->group) {
// Handling a group
@@ -143,6 +145,7 @@ void WorkerThreadPool::_process_task(Task *p_task) {
}
}
+#ifdef THREADS_ENABLED
{
curr_thread.current_task = prev_task;
if (p_task->low_priority) {
@@ -159,6 +162,7 @@ void WorkerThreadPool::_process_task(Task *p_task) {
}
set_current_thread_safe_for_nodes(safe_for_nodes_backup);
+#endif
}
void WorkerThreadPool::_thread_function(void *p_user) {
@@ -542,6 +546,7 @@ bool WorkerThreadPool::is_group_task_completed(GroupID p_group) const {
}
void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) {
+#ifdef THREADS_ENABLED
task_mutex.lock();
Group **groupp = groups.getptr(p_group);
task_mutex.unlock();
@@ -574,6 +579,7 @@ void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) {
task_mutex.lock(); // This mutex is needed when Physics 2D and/or 3D is selected to run on a separate thread.
groups.erase(p_group);
task_mutex.unlock();
+#endif
}
int WorkerThreadPool::get_thread_index() {
diff --git a/core/os/condition_variable.h b/core/os/condition_variable.h
index 6a49ced31b..2b6b272e18 100644
--- a/core/os/condition_variable.h
+++ b/core/os/condition_variable.h
@@ -33,6 +33,8 @@
#include "core/os/mutex.h"
+#ifdef THREADS_ENABLED
+
#ifdef MINGW_ENABLED
#define MINGW_STDTHREAD_REDUNDANCY_WARNING
#include "thirdparty/mingw-std-threads/mingw.condition_variable.h"
@@ -66,4 +68,16 @@ public:
}
};
+#else // No threads.
+
+class ConditionVariable {
+public:
+ template <class BinaryMutexT>
+ void wait(const MutexLock<BinaryMutexT> &p_lock) const {}
+ void notify_one() const {}
+ void notify_all() const {}
+};
+
+#endif // THREADS_ENABLED
+
#endif // CONDITION_VARIABLE_H
diff --git a/core/os/mutex.cpp b/core/os/mutex.cpp
index 5d4e457c5f..9a8a2a2961 100644
--- a/core/os/mutex.cpp
+++ b/core/os/mutex.cpp
@@ -40,7 +40,11 @@ void _global_unlock() {
_global_mutex.unlock();
}
+#ifdef THREADS_ENABLED
+
template class MutexImpl<THREADING_NAMESPACE::recursive_mutex>;
template class MutexImpl<THREADING_NAMESPACE::mutex>;
template class MutexLock<MutexImpl<THREADING_NAMESPACE::recursive_mutex>>;
template class MutexLock<MutexImpl<THREADING_NAMESPACE::mutex>>;
+
+#endif
diff --git a/core/os/mutex.h b/core/os/mutex.h
index 03af48ca7c..69f494d9cd 100644
--- a/core/os/mutex.h
+++ b/core/os/mutex.h
@@ -43,6 +43,8 @@
#define THREADING_NAMESPACE std
#endif
+#ifdef THREADS_ENABLED
+
template <class MutexT>
class MutexLock;
@@ -125,8 +127,8 @@ class MutexLock {
THREADING_NAMESPACE::unique_lock<typename MutexT::StdMutexType> lock;
public:
- _ALWAYS_INLINE_ explicit MutexLock(const MutexT &p_mutex) :
- lock(p_mutex.mutex){};
+ explicit MutexLock(const MutexT &p_mutex) :
+ lock(p_mutex.mutex) {}
};
// This specialization is needed so manual locking and MutexLock can be used
@@ -155,4 +157,38 @@ extern template class MutexImpl<THREADING_NAMESPACE::mutex>;
extern template class MutexLock<MutexImpl<THREADING_NAMESPACE::recursive_mutex>>;
extern template class MutexLock<MutexImpl<THREADING_NAMESPACE::mutex>>;
+#else // No threads.
+
+class MutexImpl {
+ mutable THREADING_NAMESPACE::mutex mutex;
+
+public:
+ void lock() const {}
+ void unlock() const {}
+ bool try_lock() const { return true; }
+};
+
+template <int Tag>
+class SafeBinaryMutex : public MutexImpl {
+ static thread_local uint32_t count;
+};
+
+template <class MutexT>
+class MutexLock {
+public:
+ MutexLock(const MutexT &p_mutex) {}
+};
+
+template <int Tag>
+class MutexLock<SafeBinaryMutex<Tag>> {
+public:
+ MutexLock(const SafeBinaryMutex<Tag> &p_mutex) {}
+ ~MutexLock() {}
+};
+
+using Mutex = MutexImpl;
+using BinaryMutex = MutexImpl;
+
+#endif // THREADS_ENABLED
+
#endif // MUTEX_H
diff --git a/core/os/os.cpp b/core/os/os.cpp
index 26ae286979..d5d9988cc1 100644
--- a/core/os/os.cpp
+++ b/core/os/os.cpp
@@ -504,6 +504,12 @@ bool OS::has_feature(const String &p_feature) {
}
#endif
+#ifdef THREADS_ENABLED
+ if (p_feature == "threads") {
+ return true;
+ }
+#endif
+
if (_check_internal_feature_support(p_feature)) {
return true;
}
diff --git a/core/os/semaphore.h b/core/os/semaphore.h
index b8ae35b86b..19ef1dedc0 100644
--- a/core/os/semaphore.h
+++ b/core/os/semaphore.h
@@ -31,6 +31,10 @@
#ifndef SEMAPHORE_H
#define SEMAPHORE_H
+#include <cstdint>
+
+#ifdef THREADS_ENABLED
+
#include "core/error/error_list.h"
#include "core/typedefs.h"
#ifdef DEBUG_ENABLED
@@ -132,4 +136,17 @@ public:
#endif
};
+#else // No threads.
+
+class Semaphore {
+public:
+ void post(uint32_t p_count = 1) const {}
+ void wait() const {}
+ bool try_wait() const {
+ return true;
+ }
+};
+
+#endif // THREADS_ENABLED
+
#endif // SEMAPHORE_H
diff --git a/core/os/thread.cpp b/core/os/thread.cpp
index 2ba90ba42c..afc74364f6 100644
--- a/core/os/thread.cpp
+++ b/core/os/thread.cpp
@@ -33,19 +33,22 @@
#include "thread.h"
+#ifdef THREADS_ENABLED
#include "core/object/script_language.h"
#include "core/templates/safe_refcount.h"
-Thread::PlatformFunctions Thread::platform_functions;
-
SafeNumeric<uint64_t> Thread::id_counter(1); // The first value after .increment() is 2, hence by default the main thread ID should be 1.
thread_local Thread::ID Thread::caller_id = Thread::UNASSIGNED_ID;
+#endif
+
+Thread::PlatformFunctions Thread::platform_functions;
void Thread::_set_platform_functions(const PlatformFunctions &p_functions) {
platform_functions = p_functions;
}
+#ifdef THREADS_ENABLED
void Thread::callback(ID p_caller_id, const Settings &p_settings, Callback p_callback, void *p_userdata) {
Thread::caller_id = p_caller_id;
if (platform_functions.set_priority) {
@@ -107,4 +110,6 @@ Thread::~Thread() {
}
}
+#endif // THREADS_ENABLED
+
#endif // PLATFORM_THREAD_OVERRIDE
diff --git a/core/os/thread.h b/core/os/thread.h
index cc954678f9..a0ecc24c91 100644
--- a/core/os/thread.h
+++ b/core/os/thread.h
@@ -53,6 +53,8 @@
class String;
+#ifdef THREADS_ENABLED
+
class Thread {
public:
typedef void (*Callback)(void *p_userdata);
@@ -86,6 +88,8 @@ public:
private:
friend class Main;
+ static PlatformFunctions platform_functions;
+
ID id = UNASSIGNED_ID;
static SafeNumeric<uint64_t> id_counter;
static thread_local ID caller_id;
@@ -93,8 +97,6 @@ private:
static void callback(ID p_caller_id, const Settings &p_settings, Thread::Callback p_callback, void *p_userdata);
- static PlatformFunctions platform_functions;
-
static void make_main_thread() { caller_id = MAIN_ID; }
static void release_main_thread() { caller_id = UNASSIGNED_ID; }
@@ -125,6 +127,64 @@ public:
~Thread();
};
+#else // No threads.
+
+class Thread {
+public:
+ typedef void (*Callback)(void *p_userdata);
+
+ typedef uint64_t ID;
+
+ enum : ID {
+ UNASSIGNED_ID = 0,
+ MAIN_ID = 1
+ };
+
+ enum Priority {
+ PRIORITY_LOW,
+ PRIORITY_NORMAL,
+ PRIORITY_HIGH
+ };
+
+ struct Settings {
+ Priority priority;
+ Settings() { priority = PRIORITY_NORMAL; }
+ };
+
+ struct PlatformFunctions {
+ Error (*set_name)(const String &) = nullptr;
+ void (*set_priority)(Thread::Priority) = nullptr;
+ void (*init)() = nullptr;
+ void (*wrapper)(Thread::Callback, void *) = nullptr;
+ void (*term)() = nullptr;
+ };
+
+private:
+ friend class Main;
+
+ static PlatformFunctions platform_functions;
+
+ static void make_main_thread() {}
+ static void release_main_thread() {}
+
+public:
+ static void _set_platform_functions(const PlatformFunctions &p_functions);
+
+ _FORCE_INLINE_ ID get_id() const { return 0; }
+ _FORCE_INLINE_ static ID get_caller_id() { return MAIN_ID; }
+ _FORCE_INLINE_ static ID get_main_id() { return MAIN_ID; }
+
+ _FORCE_INLINE_ static bool is_main_thread() { return true; }
+
+ static Error set_name(const String &p_name) { return ERR_UNAVAILABLE; }
+
+ void start(Thread::Callback p_callback, void *p_user, const Settings &p_settings = Settings()) {}
+ bool is_started() const { return false; }
+ void wait_to_finish() {}
+};
+
+#endif // THREADS_ENABLED
+
#endif // THREAD_H
#endif // PLATFORM_THREAD_OVERRIDE
diff --git a/doc/classes/AABB.xml b/doc/classes/AABB.xml
index c1c637d2d6..427d38d421 100644
--- a/doc/classes/AABB.xml
+++ b/doc/classes/AABB.xml
@@ -4,10 +4,10 @@
A 3D axis-aligned bounding box.
</brief_description>
<description>
- [AABB] consists of a position, a size, and several utility functions. It is typically used for fast overlap tests.
- It uses floating-point coordinates. The 2D counterpart to [AABB] is [Rect2].
- Negative values for [member size] are not supported and will not work for most methods. Use [method abs] to get an AABB with a positive size.
- [b]Note:[/b] Unlike [Rect2], [AABB] does not have a variant that uses integer coordinates.
+ The [AABB] built-in [Variant] type represents an axis-aligned bounding box in a 3D space. It is defined by its [member position] and [member size], which are [Vector3]. It is frequently used for fast overlap tests (see [method intersects]). Although [AABB] itself is axis-aligned, it can be combined with [Transform3D] to represent a rotated or skewed bounding box.
+ It uses floating-point coordinates. The 2D counterpart to [AABB] is [Rect2]. There is no version of [AABB] that uses integer coordinates.
+ [b]Note:[/b] Negative values for [member size] are not supported. With negative size, most [AABB] methods do not work correctly. Use [method abs] to get an equivalent [AABB] with a non-negative size.
+ [b]Note:[/b] In a boolean context, a [AABB] evaluates to [code]false[/code] if both [member position] and [member size] are zero (equal to [constant Vector3.ZERO]). Otherwise, it always evaluates to [code]true[/code].
</description>
<tutorials>
<link title="Math documentation index">$DOCS_URL/tutorials/math/index.html</link>
@@ -18,7 +18,7 @@
<constructor name="AABB">
<return type="AABB" />
<description>
- Constructs a default-initialized [AABB] with default (zero) values of [member position] and [member size].
+ Constructs an [AABB] with its [member position] and [member size] set to [constant Vector3.ZERO].
</description>
</constructor>
<constructor name="AABB">
@@ -33,7 +33,7 @@
<param index="0" name="position" type="Vector3" />
<param index="1" name="size" type="Vector3" />
<description>
- Constructs an [AABB] from a position and size.
+ Constructs an [AABB] by [param position] and [param size].
</description>
</constructor>
</constructors>
@@ -41,34 +41,78 @@
<method name="abs" qualifiers="const">
<return type="AABB" />
<description>
- Returns an AABB with equivalent position and size, modified so that the most-negative corner is the origin and the size is positive.
+ Returns an [AABB] equivalent to this bounding box, with its width, height, and depth modified to be non-negative values.
+ [codeblocks]
+ [gdscript]
+ var box = AABB(Vector3(5, 0, 5), Vector3(-20, -10, -5))
+ var absolute = box.abs()
+ print(absolute.position) # Prints (-15, -10, 0)
+ print(absolute.size) # Prints (20, 10, 5)
+ [/gdscript]
+ [csharp]
+ var box = new Aabb(new Vector3(5, 0, 5), new Vector3(-20, -10, -5));
+ var absolute = box.Abs();
+ GD.Print(absolute.Position); // Prints (-15, -10, 0)
+ GD.Print(absolute.Size); // Prints (20, 10, 5)
+ [/csharp]
+ [/codeblocks]
+ [b]Note:[/b] It's recommended to use this method when [member size] is negative, as most other methods in Godot assume that the [member size]'s components are greater than [code]0[/code].
</description>
</method>
<method name="encloses" qualifiers="const">
<return type="bool" />
<param index="0" name="with" type="AABB" />
<description>
- Returns [code]true[/code] if this [AABB] completely encloses another one.
+ Returns [code]true[/code] if this bounding box [i]completely[/i] encloses the [param with] box. The edges of both boxes are included.
+ [codeblocks]
+ [gdscript]
+ var a = AABB(Vector3(0, 0, 0), Vector3(4, 4, 4))
+ var b = AABB(Vector3(1, 1, 1), Vector3(3, 3, 3))
+ var c = AABB(Vector3(2, 2, 2), Vector3(8, 8, 8))
+
+ print(a.encloses(a)) # Prints true
+ print(a.encloses(b)) # Prints true
+ print(a.encloses(c)) # Prints false
+ [/gdscript]
+ [csharp]
+ var a = new Aabb(new Vector3(0, 0, 0), new Vector3(4, 4, 4));
+ var b = new Aabb(new Vector3(1, 1, 1), new Vector3(3, 3, 3));
+ var c = new Aabb(new Vector3(2, 2, 2), new Vector3(8, 8, 8));
+
+ GD.Print(a.Encloses(a)); // Prints True
+ GD.Print(a.Encloses(b)); // Prints True
+ GD.Print(a.Encloses(c)); // Prints False
+ [/csharp]
+ [/codeblocks]
</description>
</method>
<method name="expand" qualifiers="const">
<return type="AABB" />
<param index="0" name="to_point" type="Vector3" />
<description>
- Returns a copy of this [AABB] expanded to include a given point.
- [b]Example:[/b]
+ Returns a copy of this bounding box expanded to align the edges with the given [param to_point], if necessary.
[codeblocks]
[gdscript]
- # position (-3, 2, 0), size (1, 1, 1)
- var box = AABB(Vector3(-3, 2, 0), Vector3(1, 1, 1))
- # position (-3, -1, 0), size (3, 4, 2), so we fit both the original AABB and Vector3(0, -1, 2)
- var box2 = box.expand(Vector3(0, -1, 2))
+ var box = AABB(Vector3(0, 0, 0), Vector3(5, 2, 5))
+
+ box = box.expand(Vector3(10, 0, 0))
+ print(box.position) # Prints (0, 0, 0)
+ print(box.size) # Prints (10, 2, 5)
+
+ box = box.expand(Vector3(-5, 0, 5))
+ print(box.position) # Prints (-5, 0, 0)
+ print(box.size) # Prints (15, 2, 5)
[/gdscript]
[csharp]
- // position (-3, 2, 0), size (1, 1, 1)
- var box = new Aabb(new Vector3(-3, 2, 0), new Vector3(1, 1, 1));
- // position (-3, -1, 0), size (3, 4, 2), so we fit both the original AABB and Vector3(0, -1, 2)
- var box2 = box.Expand(new Vector3(0, -1, 2));
+ var box = new Aabb(new Vector3(0, 0, 0), new Vector3(5, 2, 5));
+
+ box = box.Expand(new Vector3(10, 0, 0));
+ GD.Print(box.Position); // Prints (0, 0, 0)
+ GD.Print(box.Size); // Prints (10, 2, 5)
+
+ box = box.Expand(new Vector3(-5, 0, 5));
+ GD.Print(box.Position); // Prints (-5, 0, 0)
+ GD.Print(box.Size); // Prints (15, 2, 5)
[/csharp]
[/codeblocks]
</description>
@@ -76,111 +120,188 @@
<method name="get_center" qualifiers="const">
<return type="Vector3" />
<description>
- Returns the center of the [AABB], which is equal to [member position] + ([member size] / 2).
+ Returns the center point of the bounding box. This is the same as [code]position + (size / 2.0)[/code].
</description>
</method>
<method name="get_endpoint" qualifiers="const">
<return type="Vector3" />
<param index="0" name="idx" type="int" />
<description>
- Gets the position of the 8 endpoints of the [AABB] in space.
+ Returns the position of one of the 8 vertices that compose this bounding box. With a [param idx] of [code]0[/code] this is the same as [member position], and a [param idx] of [code]7[/code] is the same as [member end].
</description>
</method>
<method name="get_longest_axis" qualifiers="const">
<return type="Vector3" />
<description>
- Returns the normalized longest axis of the [AABB].
+ Returns the longest normalized axis of this bounding box's [member size], as a [Vector3] ([constant Vector3.RIGHT], [constant Vector3.UP], or [constant Vector3.BACK]).
+ [codeblocks]
+ [gdscript]
+ var box = AABB(Vector3(0, 0, 0), Vector3(2, 4, 8))
+
+ print(box.get_longest_axis()) # Prints (0, 0, 1)
+ print(box.get_longest_axis_index()) # Prints 2
+ print(box.get_longest_axis_size()) # Prints 8
+ [/gdscript]
+ [csharp]
+ var box = new Aabb(new Vector3(0, 0, 0), new Vector3(2, 4, 8));
+
+ GD.Print(box.GetLongestAxis()); // Prints (0, 0, 1)
+ GD.Print(box.GetLongestAxisIndex()); // Prints 2
+ GD.Print(box.GetLongestAxisSize()); // Prints 8
+ [/csharp]
+ [/codeblocks]
+ See also [method get_longest_axis_index] and [method get_longest_axis_size].
</description>
</method>
<method name="get_longest_axis_index" qualifiers="const">
<return type="int" />
<description>
- Returns the index of the longest axis of the [AABB] (according to [Vector3]'s [code]AXIS_*[/code] constants).
+ Returns the index to the longest axis of this bounding box's [member size] (see [constant Vector3.AXIS_X], [constant Vector3.AXIS_Y], and [constant Vector3.AXIS_Z]).
+ For an example, see [method get_longest_axis].
</description>
</method>
<method name="get_longest_axis_size" qualifiers="const">
<return type="float" />
<description>
- Returns the scalar length of the longest axis of the [AABB].
+ Returns the longest dimension of this bounding box's [member size].
+ For an example, see [method get_longest_axis].
</description>
</method>
<method name="get_shortest_axis" qualifiers="const">
<return type="Vector3" />
<description>
- Returns the normalized shortest axis of the [AABB].
+ Returns the shortest normaalized axis of this bounding box's [member size], as a [Vector3] ([constant Vector3.RIGHT], [constant Vector3.UP], or [constant Vector3.BACK]).
+ [codeblocks]
+ [gdscript]
+ var box = AABB(Vector3(0, 0, 0), Vector3(2, 4, 8))
+
+ print(box.get_shortest_axis()) # Prints (1, 0, 0)
+ print(box.get_shortest_axis_index()) # Prints 0
+ print(box.get_shortest_axis_size()) # Prints 2
+ [/gdscript]
+ [csharp]
+ var box = new Aabb(new Vector3(0, 0, 0), new Vector3(2, 4, 8));
+
+ GD.Print(box.GetShortestAxis()); // Prints (1, 0, 0)
+ GD.Print(box.GetShortestAxisIndex()); // Prints 0
+ GD.Print(box.GetShortestAxisSize()); // Prints 2
+ [/csharp]
+ [/codeblocks]
+ See also [method get_shortest_axis_index] and [method get_shortest_axis_size].
</description>
</method>
<method name="get_shortest_axis_index" qualifiers="const">
<return type="int" />
<description>
- Returns the index of the shortest axis of the [AABB] (according to [Vector3]::AXIS* enum).
+ Returns the index to the shortest axis of this bounding box's [member size] (see [constant Vector3.AXIS_X], [constant Vector3.AXIS_Y], and [constant Vector3.AXIS_Z]).
+ For an example, see [method get_shortest_axis].
</description>
</method>
<method name="get_shortest_axis_size" qualifiers="const">
<return type="float" />
<description>
- Returns the scalar length of the shortest axis of the [AABB].
+ Returns the shortest dimension of this bounding box's [member size].
+ For an example, see [method get_shortest_axis].
</description>
</method>
<method name="get_support" qualifiers="const">
<return type="Vector3" />
<param index="0" name="dir" type="Vector3" />
<description>
- Returns the vertex of the AABB that's the farthest in a given direction. This point is commonly known as the support point in collision detection algorithms.
+ Returns the vertex's position of this bounding box that's the farthest in the given direction. This point is commonly known as the support point in collision detection algorithms.
</description>
</method>
<method name="get_volume" qualifiers="const">
<return type="float" />
<description>
- Returns the volume of the [AABB].
+ Returns the bounding box's volume. This is equivalent to [code]size.x * size.y * size.z[/code]. See also [method has_volume].
</description>
</method>
<method name="grow" qualifiers="const">
<return type="AABB" />
<param index="0" name="by" type="float" />
<description>
- Returns a copy of the [AABB] grown a given number of units towards all the sides.
+ Returns a copy of this bounding box extended on all sides by the given amount [param by]. A negative amount shrinks the box instead.
+ [codeblocks]
+ [gdscript]
+ var a = AABB(Vector3(4, 4, 4), Vector3(8, 8, 8)).grow(4)
+ print(a.position) # Prints (0, 0, 0)
+ print(a.size) # Prints (16, 16, 16)
+
+ var b = AABB(Vector3(0, 0, 0), Vector3(8, 4, 2)).grow(2)
+ print(b.position) # Prints (-2, -2, -2)
+ print(b.size) # Prints (12, 8, 6)
+ [/gdscript]
+ [csharp]
+ var a = new Aabb(new Vector3(4, 4, 4), new Vector3(8, 8, 8)).Grow(4);
+ GD.Print(a.Position); // Prints (0, 0, 0)
+ GD.Print(a.Size); // Prints (16, 16, 16)
+
+ var b = new Aabb(new Vector3(0, 0, 0), new Vector3(8, 4, 2)).Grow(2);
+ GD.Print(b.Position); // Prints (-2, -2, -2)
+ GD.Print(b.Size); // Prints (12, 8, 6)
+ [/csharp]
+ [/codeblocks]
</description>
</method>
<method name="has_point" qualifiers="const">
<return type="bool" />
<param index="0" name="point" type="Vector3" />
<description>
- Returns [code]true[/code] if the [AABB] contains a point. Points on the faces of the AABB are considered included, though float-point precision errors may impact the accuracy of such checks.
- [b]Note:[/b] This method is not reliable for [AABB] with a [i]negative size[/i]. Use [method abs] to get a positive sized equivalent [AABB] to check for contained points.
+ Returns [code]true[/code] if the bounding box contains the given [param point]. By convention, points exactly on the right, top, and front sides are [b]not[/b] included.
+ [b]Note:[/b] This method is not reliable for [AABB] with a [i]negative[/i] [member size]. Use [method abs] first to get a valid bounding box.
</description>
</method>
<method name="has_surface" qualifiers="const">
<return type="bool" />
<description>
- Returns [code]true[/code] if the [AABB] has a surface or a length, and [code]false[/code] if the [AABB] is empty (all components of [member size] are zero or negative).
+ Returns [code]true[/code] if this bounding box has a surface or a length, that is, at least one component of [member size] is greater than [code]0[/code]. Otherwise, returns [code]false[/code].
</description>
</method>
<method name="has_volume" qualifiers="const">
<return type="bool" />
<description>
- Returns [code]true[/code] if the [AABB] has a volume, and [code]false[/code] if the [AABB] is flat, empty, or has a negative [member size].
+ Returns [code]true[/code] if this bounding box's width, height, and depth are all positive. See also [method get_volume].
</description>
</method>
<method name="intersection" qualifiers="const">
<return type="AABB" />
<param index="0" name="with" type="AABB" />
<description>
- Returns the intersection between two [AABB]. An empty AABB (size [code](0, 0, 0)[/code]) is returned on failure.
+ Returns the intersection between this bounding box and [param with]. If the boxes do not intersect, returns an empty [AABB]. If the boxes intersect at the edge, returns a flat [AABB] with no volume (see [method has_surface] and [method has_volume]).
+ [codeblocks]
+ [gdscript]
+ var box1 = AABB(Vector3(0, 0, 0), Vector3(5, 2, 8))
+ var box2 = AABB(Vector3(2, 0, 2), Vector3(8, 4, 4))
+
+ var intersection = box1.intersection(box2)
+ print(intersection.position) # Prints (2, 0, 2)
+ print(intersection.size) # Prints (3, 2, 4)
+ [/gdscript]
+ [csharp]
+ var box1 = new Aabb(new Vector3(0, 0, 0), new Vector3(5, 2, 8));
+ var box2 = new Aabb(new Vector3(2, 0, 2), new Vector3(8, 4, 4));
+
+ var intersection = box1.Intersection(box2);
+ GD.Print(intersection.Position); // Prints (2, 0, 2)
+ GD.Print(intersection.Size); // Prints (3, 2, 4)
+ [/csharp]
+ [/codeblocks]
+ [b]Note:[/b] If you only need to know whether two bounding boxes are intersecting, use [method intersects], instead.
</description>
</method>
<method name="intersects" qualifiers="const">
<return type="bool" />
<param index="0" name="with" type="AABB" />
<description>
- Returns [code]true[/code] if the [AABB] overlaps with another.
+ Returns [code]true[/code] if this bounding box overlaps with the box [param with]. The edges of both boxes are [i]always[/i] excluded.
</description>
</method>
<method name="intersects_plane" qualifiers="const">
<return type="bool" />
<param index="0" name="plane" type="Plane" />
<description>
- Returns [code]true[/code] if the [AABB] is on both sides of a plane.
+ Returns [code]true[/code] if this bounding box is on both sides of the given [param plane].
</description>
</method>
<method name="intersects_ray" qualifiers="const">
@@ -188,7 +309,8 @@
<param index="0" name="from" type="Vector3" />
<param index="1" name="dir" type="Vector3" />
<description>
- Returns the point of intersection of the given ray with this [AABB] or [code]null[/code] if there is no intersection. Ray length is infinite.
+ Returns the first point where this bounding box and the given ray intersect, as a [Vector3]. If no intersection occurs, returns [code]null[/code].
+ The ray begin at [param from], faces [param dir] and extends towards infinity.
</description>
</method>
<method name="intersects_segment" qualifiers="const">
@@ -196,40 +318,41 @@
<param index="0" name="from" type="Vector3" />
<param index="1" name="to" type="Vector3" />
<description>
- Returns the point of intersection between [param from] and [param to] with this [AABB] or [code]null[/code] if there is no intersection.
+ Returns the first point where this bounding box and the given segment intersect, as a [Vector3]. If no intersection occurs, returns [code]null[/code].
+ The segment begins at [param from] and ends at [param to].
</description>
</method>
<method name="is_equal_approx" qualifiers="const">
<return type="bool" />
<param index="0" name="aabb" type="AABB" />
<description>
- Returns [code]true[/code] if this [AABB] and [param aabb] are approximately equal, by calling [method @GlobalScope.is_equal_approx] on each component.
+ Returns [code]true[/code] if this bounding box and [param aabb] are approximately equal, by calling [method Vector2.is_equal_approx] on the [member position] and the [member size].
</description>
</method>
<method name="is_finite" qualifiers="const">
<return type="bool" />
<description>
- Returns [code]true[/code] if this [AABB] is finite, by calling [method @GlobalScope.is_finite] on each component.
+ Returns [code]true[/code] if this bounding box's values are finite, by calling [method Vector2.is_finite] on the [member position] and the [member size].
</description>
</method>
<method name="merge" qualifiers="const">
<return type="AABB" />
<param index="0" name="with" type="AABB" />
<description>
- Returns a larger [AABB] that contains both this [AABB] and [param with].
+ Returns an [AABB] that encloses both this bounding box and [param with] around the edges. See also [method encloses].
</description>
</method>
</methods>
<members>
<member name="end" type="Vector3" setter="" getter="" default="Vector3(0, 0, 0)">
- Ending corner. This is calculated as [code]position + size[/code]. Setting this value will change the size.
+ The ending point. This is usually the corner on the top-right and forward of the bounding box, and is equivalent to [code]position + size[/code]. Setting this point affects the [member size].
</member>
<member name="position" type="Vector3" setter="" getter="" default="Vector3(0, 0, 0)">
- Beginning corner. Typically has values lower than [member end].
+ The origin point. This is usually the corner on the bottom-left and back of the bounding box.
</member>
<member name="size" type="Vector3" setter="" getter="" default="Vector3(0, 0, 0)">
- Size from [member position] to [member end]. Typically, all components are positive.
- If the size is negative, you can use [method abs] to fix it.
+ The bounding box's width, height, and depth starting from [member position]. Setting this value also affects the [member end] point.
+ [b]Note:[/b] It's recommended setting the width, height, and depth to non-negative values. This is because most methods in Godot assume that the [member position] is the bottom-left-back corner, and the [member end] is the top-right-forward corner. To get an equivalent bounding box with non-negative size, use [method abs].
</member>
</members>
<operators>
@@ -237,7 +360,7 @@
<return type="bool" />
<param index="0" name="right" type="AABB" />
<description>
- Returns [code]true[/code] if the AABBs are not equal.
+ Returns [code]true[/code] if the [member position] or [member size] of both bounding boxes are not equal.
[b]Note:[/b] Due to floating-point precision errors, consider using [method is_equal_approx] instead, which is more reliable.
</description>
</operator>
@@ -254,7 +377,7 @@
<return type="bool" />
<param index="0" name="right" type="AABB" />
<description>
- Returns [code]true[/code] if the AABBs are exactly equal.
+ Returns [code]true[/code] if both [member position] and [member size] of the bounding boxes are exactly equal, respectively.
[b]Note:[/b] Due to floating-point precision errors, consider using [method is_equal_approx] instead, which is more reliable.
</description>
</operator>
diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml
index 95e760be9f..e5952d9f71 100644
--- a/doc/classes/GraphEdit.xml
+++ b/doc/classes/GraphEdit.xml
@@ -143,7 +143,22 @@
[b]Note:[/b] This method suppresses any other connection request signals apart from [signal connection_drag_ended].
</description>
</method>
- <method name="get_connection_line">
+ <method name="get_closest_connection_at_point" qualifiers="const">
+ <return type="Dictionary" />
+ <param index="0" name="point" type="Vector2" />
+ <param index="1" name="max_distance" type="float" default="4.0" />
+ <description>
+ Returns the closest connection to the given point in screen space. If no connection is found within [param max_distance] pixels, an empty [Dictionary] is returned.
+ A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
+ For example, getting a connection at a given mouse position can be achieved like this:
+ [codeblocks]
+ [gdscript]
+ var connection = get_closest_connection_at_point(mouse_event.get_position())
+ [/gdscript]
+ [/codeblocks]
+ </description>
+ </method>
+ <method name="get_connection_line" qualifiers="const">
<return type="PackedVector2Array" />
<param index="0" name="from_node" type="Vector2" />
<param index="1" name="to_node" type="Vector2" />
@@ -154,7 +169,14 @@
<method name="get_connection_list" qualifiers="const">
<return type="Dictionary[]" />
<description>
- Returns an Array containing the list of connections. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
+ Returns an [Array] containing the list of connections. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
+ </description>
+ </method>
+ <method name="get_connections_intersecting_with_rect" qualifiers="const">
+ <return type="Dictionary[]" />
+ <param index="0" name="rect" type="Rect2" />
+ <description>
+ Returns an [Array] containing the list of connections that intersect with the given [Rect2]. A connection consists in a structure of the form [code]{ from_port: 0, from_node: "GraphNode name 0", to_port: 1, to_node: "GraphNode name 1" }[/code].
</description>
</method>
<method name="get_menu_hbox">
@@ -233,7 +255,7 @@
<member name="connection_lines_curvature" type="float" setter="set_connection_lines_curvature" getter="get_connection_lines_curvature" default="0.5">
The curvature of the lines between the nodes. 0 results in straight lines.
</member>
- <member name="connection_lines_thickness" type="float" setter="set_connection_lines_thickness" getter="get_connection_lines_thickness" default="2.0">
+ <member name="connection_lines_thickness" type="float" setter="set_connection_lines_thickness" getter="get_connection_lines_thickness" default="4.0">
The thickness of the lines between the nodes.
</member>
<member name="focus_mode" type="int" setter="set_focus_mode" getter="get_focus_mode" overrides="Control" enum="Control.FocusMode" default="2" />
@@ -417,7 +439,16 @@
</constants>
<theme_items>
<theme_item name="activity" data_type="color" type="Color" default="Color(1, 1, 1, 1)">
- Color of the connection's activity (see [method set_connection_activity]).
+ Color the connection line is interpolated to based on the activity value of a connection (see [method set_connection_activity]).
+ </theme_item>
+ <theme_item name="connection_hover_tint_color" data_type="color" type="Color" default="Color(0, 0, 0, 0.3)">
+ Color which is blended with the connection line when the mouse is hovering over it.
+ </theme_item>
+ <theme_item name="connection_rim_color" data_type="color" type="Color" default="Color(0.1, 0.1, 0.1, 0.6)">
+ Color of the rim around each connection line used for making intersecting lines more distinguishable.
+ </theme_item>
+ <theme_item name="connection_valid_target_tint_color" data_type="color" type="Color" default="Color(1, 1, 1, 0.4)">
+ Color which is blended with the connection line when the currently dragged connection is hovering over a valid target port.
</theme_item>
<theme_item name="grid_major" data_type="color" type="Color" default="Color(1, 1, 1, 0.2)">
Color of major grid lines/dots.
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index acff58dadf..e0d41ab90a 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -2551,6 +2551,7 @@
[b]Note:[/b] This setting is only effective when using the Compatibility rendering method, not Forward+ and Mobile.
</member>
<member name="rendering/limits/spatial_indexer/threaded_cull_minimum_instances" type="int" setter="" getter="" default="1000">
+ The minimum number of instances that must be present in a scene to enable culling computations on multiple threads. If a scene has fewer instances than this number, culling is done on a single thread.
</member>
<member name="rendering/limits/spatial_indexer/update_iterations_per_frame" type="int" setter="" getter="" default="10">
</member>
diff --git a/doc/tools/make_rst.py b/doc/tools/make_rst.py
index 2f6340e572..4435d52527 100755
--- a/doc/tools/make_rst.py
+++ b/doc/tools/make_rst.py
@@ -68,6 +68,8 @@ BASE_STRINGS = [
"This value is an integer composed as a bitmask of the following flags.",
"There is currently no description for this class. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
"There is currently no description for this signal. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
+ "There is currently no description for this enum. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
+ "There is currently no description for this constant. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
"There is currently no description for this annotation. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
"There is currently no description for this property. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
"There is currently no description for this constructor. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!",
@@ -1102,6 +1104,14 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
if value.text is not None and value.text.strip() != "":
f.write(f"{format_text_block(value.text.strip(), value, state)}")
+ else:
+ f.write(".. container:: contribute\n\n\t")
+ f.write(
+ translate(
+ "There is currently no description for this enum. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!"
+ )
+ + "\n\n"
+ )
f.write("\n\n")
@@ -1125,6 +1135,14 @@ def make_rst_class(class_def: ClassDef, state: State, dry_run: bool, output_dir:
if constant.text is not None and constant.text.strip() != "":
f.write(f"{format_text_block(constant.text.strip(), constant, state)}")
+ else:
+ f.write(".. container:: contribute\n\n\t")
+ f.write(
+ translate(
+ "There is currently no description for this constant. Please help us by :ref:`contributing one <doc_updating_the_class_reference>`!"
+ )
+ + "\n\n"
+ )
f.write("\n\n")
diff --git a/drivers/d3d12/rendering_device_driver_d3d12.cpp b/drivers/d3d12/rendering_device_driver_d3d12.cpp
index 1d1dc6bec8..aed82b4ce8 100644
--- a/drivers/d3d12/rendering_device_driver_d3d12.cpp
+++ b/drivers/d3d12/rendering_device_driver_d3d12.cpp
@@ -5170,7 +5170,7 @@ void RenderingDeviceDriverD3D12::command_timestamp_write(CommandBufferID p_cmd_b
TimestampQueryPoolInfo *tqp_info = (TimestampQueryPoolInfo *)p_pool_id.id;
ID3D12Resource *results_buffer = tqp_info->results_buffer_allocation->GetResource();
cmd_buf_info->cmd_list->EndQuery(tqp_info->query_heap.Get(), D3D12_QUERY_TYPE_TIMESTAMP, p_index);
- cmd_buf_info->cmd_list->ResolveQueryData(tqp_info->query_heap.Get(), D3D12_QUERY_TYPE_TIMESTAMP, p_index, tqp_info->query_count, results_buffer, p_index * sizeof(uint64_t));
+ cmd_buf_info->cmd_list->ResolveQueryData(tqp_info->query_heap.Get(), D3D12_QUERY_TYPE_TIMESTAMP, p_index, 1, results_buffer, p_index * sizeof(uint64_t));
}
void RenderingDeviceDriverD3D12::command_begin_label(CommandBufferID p_cmd_buffer, const char *p_label_name, const Color &p_color) {
diff --git a/drivers/unix/os_unix.cpp b/drivers/unix/os_unix.cpp
index 51ea9234d4..8126f74332 100644
--- a/drivers/unix/os_unix.cpp
+++ b/drivers/unix/os_unix.cpp
@@ -153,7 +153,9 @@ int OS_Unix::unix_initialize_audio(int p_audio_driver) {
}
void OS_Unix::initialize_core() {
+#ifdef THREADS_ENABLED
init_thread_posix();
+#endif
FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_RESOURCES);
FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA);
diff --git a/editor/SCsub b/editor/SCsub
index 5b36bca81a..aa240a1e01 100644
--- a/editor/SCsub
+++ b/editor/SCsub
@@ -113,6 +113,7 @@ if env.editor_build:
SConscript("icons/SCsub")
SConscript("import/SCsub")
SConscript("plugins/SCsub")
+ SConscript("project_manager/SCsub")
SConscript("themes/SCsub")
lib = env.add_library("editor", env.editor_sources)
diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp
index a5af7ef134..0285692ab7 100644
--- a/editor/connections_dialog.cpp
+++ b/editor/connections_dialog.cpp
@@ -1202,7 +1202,22 @@ void ConnectionsDock::_slot_menu_about_to_popup() {
slot_menu->set_item_disabled(slot_menu->get_item_index(SLOT_MENU_DISCONNECT), connection_is_inherited);
}
-void ConnectionsDock::_rmb_pressed(const Ref<InputEvent> &p_event) {
+void ConnectionsDock::_tree_gui_input(const Ref<InputEvent> &p_event) {
+ // Handle Delete press.
+ if (ED_IS_SHORTCUT("connections_editor/disconnect", p_event)) {
+ TreeItem *item = tree->get_selected();
+ if (item && _get_item_type(*item) == TREE_ITEM_TYPE_CONNECTION) {
+ Connection connection = item->get_metadata(0);
+ _disconnect(connection);
+ update_tree();
+
+ // Stop the Delete input from propagating elsewhere.
+ accept_event();
+ return;
+ }
+ }
+
+ // Handle RMB press.
const Ref<InputEventMouseButton> &mb_event = p_event;
if (mb_event.is_null() || !mb_event->is_pressed() || mb_event->get_button_index() != MouseButton::RIGHT) {
return;
@@ -1536,13 +1551,13 @@ ConnectionsDock::ConnectionsDock() {
slot_menu->connect("about_to_popup", callable_mp(this, &ConnectionsDock::_slot_menu_about_to_popup));
slot_menu->add_item(TTR("Edit..."), SLOT_MENU_EDIT);
slot_menu->add_item(TTR("Go to Method"), SLOT_MENU_GO_TO_METHOD);
- slot_menu->add_item(TTR("Disconnect"), SLOT_MENU_DISCONNECT);
+ slot_menu->add_shortcut(ED_SHORTCUT("connections_editor/disconnect", TTR("Disconnect"), Key::KEY_DELETE), SLOT_MENU_DISCONNECT);
add_child(slot_menu);
connect_dialog->connect("connected", callable_mp(this, &ConnectionsDock::_make_or_edit_connection));
tree->connect("item_selected", callable_mp(this, &ConnectionsDock::_tree_item_selected));
tree->connect("item_activated", callable_mp(this, &ConnectionsDock::_tree_item_activated));
- tree->connect("gui_input", callable_mp(this, &ConnectionsDock::_rmb_pressed));
+ tree->connect("gui_input", callable_mp(this, &ConnectionsDock::_tree_gui_input));
add_theme_constant_override("separation", 3 * EDSCALE);
}
diff --git a/editor/connections_dialog.h b/editor/connections_dialog.h
index 2fd4778389..a99f0dd0fe 100644
--- a/editor/connections_dialog.h
+++ b/editor/connections_dialog.h
@@ -252,7 +252,7 @@ class ConnectionsDock : public VBoxContainer {
void _signal_menu_about_to_popup();
void _handle_slot_menu_option(int p_option);
void _slot_menu_about_to_popup();
- void _rmb_pressed(const Ref<InputEvent> &p_event);
+ void _tree_gui_input(const Ref<InputEvent> &p_event);
void _close();
protected:
diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp
index 372d558aab..6471bd449e 100644
--- a/editor/debugger/editor_debugger_node.cpp
+++ b/editor/debugger/editor_debugger_node.cpp
@@ -593,9 +593,15 @@ void EditorDebuggerNode::set_breakpoints(const String &p_path, Array p_lines) {
}
}
-void EditorDebuggerNode::reload_scripts() {
+void EditorDebuggerNode::reload_all_scripts() {
_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
- dbg->reload_scripts();
+ dbg->reload_all_scripts();
+ });
+}
+
+void EditorDebuggerNode::reload_scripts(const Vector<String> &p_script_paths) {
+ _for_all(tabs, [&](ScriptEditorDebugger *dbg) {
+ dbg->reload_scripts(p_script_paths);
});
}
diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h
index 4338f144b8..d30f29c7c6 100644
--- a/editor/debugger/editor_debugger_node.h
+++ b/editor/debugger/editor_debugger_node.h
@@ -187,7 +187,8 @@ public:
bool is_skip_breakpoints() const;
void set_breakpoint(const String &p_path, int p_line, bool p_enabled);
void set_breakpoints(const String &p_path, Array p_lines);
- void reload_scripts();
+ void reload_all_scripts();
+ void reload_scripts(const Vector<String> &p_script_paths);
// Remote inspector/edit.
void request_remote_tree();
diff --git a/editor/debugger/editor_performance_profiler.cpp b/editor/debugger/editor_performance_profiler.cpp
index af723cc731..37e13b59cc 100644
--- a/editor/debugger/editor_performance_profiler.cpp
+++ b/editor/debugger/editor_performance_profiler.cpp
@@ -31,9 +31,9 @@
#include "editor_performance_profiler.h"
#include "editor/editor_property_name_processor.h"
-#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "main/performance.h"
EditorPerformanceProfiler::Monitor::Monitor() {}
@@ -122,7 +122,7 @@ void EditorPerformanceProfiler::_monitor_draw() {
}
Size2i cell_size = Size2i(monitor_draw->get_size()) / Size2i(columns, rows);
float spacing = float(POINT_SEPARATION) / float(columns);
- float value_multiplier = EditorSettings::get_singleton()->is_dark_theme() ? 1.4f : 0.55f;
+ float value_multiplier = EditorThemeManager::is_dark_theme() ? 1.4f : 0.55f;
float hue_shift = 1.0f / float(monitors.size());
for (int i = 0; i < active.size(); i++) {
diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp
index 6cc3769976..3c863bdc19 100644
--- a/editor/debugger/script_editor_debugger.cpp
+++ b/editor/debugger/script_editor_debugger.cpp
@@ -1518,8 +1518,12 @@ void ScriptEditorDebugger::set_breakpoint(const String &p_path, int p_line, bool
}
}
-void ScriptEditorDebugger::reload_scripts() {
- _put_msg("reload_scripts", Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID);
+void ScriptEditorDebugger::reload_all_scripts() {
+ _put_msg("reload_all_scripts", Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID);
+}
+
+void ScriptEditorDebugger::reload_scripts(const Vector<String> &p_script_paths) {
+ _put_msg("reload_scripts", Variant(p_script_paths).operator Array(), debugging_thread_id != Thread::UNASSIGNED_ID ? debugging_thread_id : Thread::MAIN_ID);
}
bool ScriptEditorDebugger::is_skip_breakpoints() {
diff --git a/editor/debugger/script_editor_debugger.h b/editor/debugger/script_editor_debugger.h
index 79224061ff..589e82ef25 100644
--- a/editor/debugger/script_editor_debugger.h
+++ b/editor/debugger/script_editor_debugger.h
@@ -300,7 +300,8 @@ public:
void update_live_edit_root();
- void reload_scripts();
+ void reload_all_scripts();
+ void reload_scripts(const Vector<String> &p_script_paths);
bool is_skip_breakpoints();
diff --git a/editor/editor_audio_buses.cpp b/editor/editor_audio_buses.cpp
index 50845b4458..61a4b341b9 100644
--- a/editor/editor_audio_buses.cpp
+++ b/editor/editor_audio_buses.cpp
@@ -41,6 +41,7 @@
#include "editor/filesystem_dock.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/separator.h"
#include "scene/resources/font.h"
#include "servers/audio_server.h"
@@ -84,9 +85,9 @@ void EditorAudioBus::_notification(int p_what) {
disabled_vu = get_editor_theme_icon(SNAME("BusVuFrozen"));
- Color solo_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44);
- Color mute_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44);
- Color bypass_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0);
+ Color solo_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.89, 0.22) : Color(1.0, 0.92, 0.44);
+ Color mute_color = EditorThemeManager::is_dark_theme() ? Color(1.0, 0.16, 0.16) : Color(1.0, 0.44, 0.44);
+ Color bypass_color = EditorThemeManager::is_dark_theme() ? Color(0.13, 0.8, 1.0) : Color(0.44, 0.87, 1.0);
solo->set_icon(get_editor_theme_icon(SNAME("AudioBusSolo")));
solo->add_theme_color_override("icon_pressed_color", solo_color);
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index aab433ac27..9fbe7ba655 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -2354,7 +2354,11 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
reimport_files.sort();
+#ifdef THREADS_ENABLED
bool use_multiple_threads = GLOBAL_GET("editor/import/use_multiple_threads");
+#else
+ bool use_multiple_threads = false;
+#endif
int from = 0;
for (int i = 0; i < reimport_files.size(); i++) {
@@ -2680,6 +2684,10 @@ void EditorFileSystem::remove_import_format_support_query(Ref<EditorFileSystemIm
}
EditorFileSystem::EditorFileSystem() {
+#ifdef THREADS_ENABLED
+ use_threads = true;
+#endif
+
ResourceLoader::import = _resource_import;
reimport_on_missing_imported_files = GLOBAL_GET("editor/import/reimport_missing_imported_files");
singleton = this;
diff --git a/editor/editor_file_system.h b/editor/editor_file_system.h
index 2a34c06b0d..2f5cd88a55 100644
--- a/editor/editor_file_system.h
+++ b/editor/editor_file_system.h
@@ -165,7 +165,7 @@ class EditorFileSystem : public Node {
EditorFileSystemDirectory::FileInfo *new_file = nullptr;
};
- bool use_threads = true;
+ bool use_threads = false;
Thread thread;
static void _thread_func(void *_userdata);
diff --git a/editor/editor_help_search.cpp b/editor/editor_help_search.cpp
index 190e8c2445..229eb79e11 100644
--- a/editor/editor_help_search.cpp
+++ b/editor/editor_help_search.cpp
@@ -48,7 +48,7 @@ void EditorHelpSearch::_update_results() {
search_flags |= SEARCH_SHOW_HIERARCHY;
}
- search = Ref<Runner>(memnew(Runner(results_tree, results_tree, term, search_flags)));
+ search = Ref<Runner>(memnew(Runner(results_tree, results_tree, &tree_cache, term, search_flags)));
set_process(true);
}
@@ -96,6 +96,7 @@ void EditorHelpSearch::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_VISIBILITY_CHANGED: {
if (!is_visible()) {
+ tree_cache.clear();
callable_mp(results_tree, &Tree::clear).call_deferred(); // Wait for the Tree's mouse event propagation.
get_ok_button()->set_disabled(true);
EditorSettings::get_singleton()->set_project_metadata("dialog_bounds", "search_help", Rect2(get_position(), get_size()));
@@ -258,6 +259,13 @@ EditorHelpSearch::EditorHelpSearch() {
vbox->add_child(results_tree, true);
}
+void EditorHelpSearch::TreeCache::clear() {
+ for (const KeyValue<String, TreeItem *> &E : item_cache) {
+ memdelete(E.value);
+ }
+ item_cache.clear();
+}
+
bool EditorHelpSearch::Runner::_is_class_disabled_by_feature_profile(const StringName &p_class) {
Ref<EditorFeatureProfile> profile = EditorFeatureProfileManager::get_singleton()->get_current_profile();
if (profile.is_null()) {
@@ -436,11 +444,42 @@ bool EditorHelpSearch::Runner::_phase_match_classes() {
return iterator_stack.is_empty();
}
+void EditorHelpSearch::Runner::_populate_cache() {
+ root_item = results_tree->get_root();
+
+ if (root_item) {
+ LocalVector<TreeItem *> stack;
+
+ // Add children of root item to stack.
+ for (TreeItem *child = root_item->get_first_child(); child; child = child->get_next()) {
+ stack.push_back(child);
+ }
+
+ // Traverse stack and cache items.
+ while (!stack.is_empty()) {
+ TreeItem *cur_item = stack[stack.size() - 1];
+ stack.resize(stack.size() - 1);
+
+ // Add to the cache.
+ tree_cache->item_cache.insert(cur_item->get_metadata(0).operator String(), cur_item);
+
+ // Add any children to the stack.
+ for (TreeItem *child = cur_item->get_first_child(); child; child = child->get_next()) {
+ stack.push_back(child);
+ }
+
+ // Remove from parent.
+ cur_item->get_parent()->remove_child(cur_item);
+ }
+ } else {
+ root_item = results_tree->create_item();
+ }
+}
+
bool EditorHelpSearch::Runner::_phase_class_items_init() {
iterator_match = matches.begin();
- results_tree->clear();
- root_item = results_tree->create_item();
+ _populate_cache();
class_items.clear();
return true;
@@ -618,27 +657,54 @@ TreeItem *EditorHelpSearch::Runner::_create_class_hierarchy(const ClassMatch &p_
return class_item;
}
+bool EditorHelpSearch::Runner::_find_or_create_item(TreeItem *p_parent, const String &p_item_meta, TreeItem *&r_item) {
+ // Attempt to find in cache.
+ if (tree_cache->item_cache.has(p_item_meta)) {
+ r_item = tree_cache->item_cache[p_item_meta];
+
+ // Remove from cache.
+ tree_cache->item_cache.erase(p_item_meta);
+
+ // Add to tree.
+ p_parent->add_child(r_item);
+
+ return false;
+ } else {
+ // Otherwise create item.
+ r_item = results_tree->create_item(p_parent);
+
+ return true;
+ }
+}
+
TreeItem *EditorHelpSearch::Runner::_create_class_item(TreeItem *p_parent, const DocData::ClassDoc *p_doc, bool p_gray) {
String tooltip = DTR(p_doc->brief_description.strip_edges());
- TreeItem *item = results_tree->create_item(p_parent);
- item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_doc->name));
- item->set_text(0, p_doc->name);
- item->set_text(1, TTR("Class"));
- item->set_tooltip_text(0, tooltip);
- item->set_tooltip_text(1, tooltip);
- item->set_metadata(0, "class_name:" + p_doc->name);
+ const String item_meta = "class_name:" + p_doc->name;
+
+ TreeItem *item = nullptr;
+ if (_find_or_create_item(p_parent, item_meta, item)) {
+ item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_doc->name));
+ item->set_text(0, p_doc->name);
+ item->set_text(1, TTR("Class"));
+ item->set_tooltip_text(0, tooltip);
+ item->set_tooltip_text(1, tooltip);
+ item->set_metadata(0, item_meta);
+ if (p_doc->is_deprecated) {
+ Ref<Texture2D> error_icon = ui_service->get_editor_theme_icon(SNAME("StatusError"));
+ item->add_button(0, error_icon, 0, false, TTR("This class is marked as deprecated."));
+ } else if (p_doc->is_experimental) {
+ Ref<Texture2D> warning_icon = ui_service->get_editor_theme_icon(SNAME("NodeWarning"));
+ item->add_button(0, warning_icon, 0, false, TTR("This class is marked as experimental."));
+ }
+ }
+
if (p_gray) {
item->set_custom_color(0, disabled_color);
item->set_custom_color(1, disabled_color);
- }
-
- if (p_doc->is_deprecated) {
- Ref<Texture2D> error_icon = ui_service->get_editor_theme_icon("StatusError");
- item->add_button(0, error_icon, 0, false, TTR("This class is marked as deprecated."));
- } else if (p_doc->is_experimental) {
- Ref<Texture2D> warning_icon = ui_service->get_editor_theme_icon("NodeWarning");
- item->add_button(0, warning_icon, 0, false, TTR("This class is marked as experimental."));
+ } else {
+ item->clear_custom_color(0);
+ item->clear_custom_color(1);
}
_match_item(item, p_doc->name);
@@ -679,30 +745,29 @@ TreeItem *EditorHelpSearch::Runner::_create_theme_property_item(TreeItem *p_pare
}
TreeItem *EditorHelpSearch::Runner::_create_member_item(TreeItem *p_parent, const String &p_class_name, const String &p_icon, const String &p_name, const String &p_text, const String &p_type, const String &p_metatype, const String &p_tooltip, bool is_deprecated, bool is_experimental) {
- Ref<Texture2D> icon;
- String text;
- if (search_flags & SEARCH_SHOW_HIERARCHY) {
- icon = ui_service->get_editor_theme_icon(p_icon);
- text = p_text;
- } else {
- icon = ui_service->get_editor_theme_icon(p_icon);
- text = p_class_name + "." + p_text;
+ const String item_meta = "class_" + p_metatype + ":" + p_class_name + ":" + p_name;
+
+ TreeItem *item = nullptr;
+ if (_find_or_create_item(p_parent, item_meta, item)) {
+ item->set_icon(0, ui_service->get_editor_theme_icon(p_icon));
+ item->set_text(1, TTRGET(p_type));
+ item->set_tooltip_text(0, p_tooltip);
+ item->set_tooltip_text(1, p_tooltip);
+ item->set_metadata(0, item_meta);
+
+ if (is_deprecated) {
+ Ref<Texture2D> error_icon = ui_service->get_editor_theme_icon(SNAME("StatusError"));
+ item->add_button(0, error_icon, 0, false, TTR("This member is marked as deprecated."));
+ } else if (is_experimental) {
+ Ref<Texture2D> warning_icon = ui_service->get_editor_theme_icon(SNAME("NodeWarning"));
+ item->add_button(0, warning_icon, 0, false, TTR("This member is marked as experimental."));
+ }
}
- TreeItem *item = results_tree->create_item(p_parent);
- item->set_icon(0, icon);
- item->set_text(0, text);
- item->set_text(1, TTRGET(p_type));
- item->set_tooltip_text(0, p_tooltip);
- item->set_tooltip_text(1, p_tooltip);
- item->set_metadata(0, "class_" + p_metatype + ":" + p_class_name + ":" + p_name);
-
- if (is_deprecated) {
- Ref<Texture2D> error_icon = ui_service->get_editor_theme_icon("StatusError");
- item->add_button(0, error_icon, 0, false, TTR("This member is marked as deprecated."));
- } else if (is_experimental) {
- Ref<Texture2D> warning_icon = ui_service->get_editor_theme_icon("NodeWarning");
- item->add_button(0, warning_icon, 0, false, TTR("This member is marked as experimental."));
+ if (search_flags & SEARCH_SHOW_HIERARCHY) {
+ item->set_text(0, p_text);
+ } else {
+ item->set_text(0, p_class_name + "." + p_text);
}
_match_item(item, p_name);
@@ -721,9 +786,10 @@ bool EditorHelpSearch::Runner::work(uint64_t slot) {
return true;
}
-EditorHelpSearch::Runner::Runner(Control *p_icon_service, Tree *p_results_tree, const String &p_term, int p_search_flags) :
+EditorHelpSearch::Runner::Runner(Control *p_icon_service, Tree *p_results_tree, TreeCache *p_tree_cache, const String &p_term, int p_search_flags) :
ui_service(p_icon_service),
results_tree(p_results_tree),
+ tree_cache(p_tree_cache),
term((p_search_flags & SEARCH_CASE_SENSITIVE) == 0 ? p_term.strip_edges().to_lower() : p_term.strip_edges()),
search_flags(p_search_flags),
disabled_color(ui_service->get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor))) {
diff --git a/editor/editor_help_search.h b/editor/editor_help_search.h
index 1d7f2b81df..e4980d6ff7 100644
--- a/editor/editor_help_search.h
+++ b/editor/editor_help_search.h
@@ -67,6 +67,16 @@ class EditorHelpSearch : public ConfirmationDialog {
class Runner;
Ref<Runner> search;
+ struct TreeCache {
+ HashMap<String, TreeItem *> item_cache;
+
+ void clear();
+
+ ~TreeCache() {
+ clear();
+ }
+ } tree_cache;
+
void _update_results();
void _search_box_gui_input(const Ref<InputEvent> &p_event);
@@ -117,6 +127,7 @@ class EditorHelpSearch::Runner : public RefCounted {
Control *ui_service = nullptr;
Tree *results_tree = nullptr;
+ TreeCache *tree_cache = nullptr;
String term;
Vector<String> terms;
int search_flags;
@@ -134,6 +145,9 @@ class EditorHelpSearch::Runner : public RefCounted {
bool _is_class_disabled_by_feature_profile(const StringName &p_class);
+ void _populate_cache();
+ bool _find_or_create_item(TreeItem *p_parent, const String &p_item_meta, TreeItem *&r_item);
+
bool _slice();
bool _phase_match_classes_init();
bool _phase_match_classes();
@@ -162,7 +176,7 @@ class EditorHelpSearch::Runner : public RefCounted {
public:
bool work(uint64_t slot = 100000);
- Runner(Control *p_icon_service, Tree *p_results_tree, const String &p_term, int p_search_flags);
+ Runner(Control *p_icon_service, Tree *p_results_tree, TreeCache *p_tree_cache, const String &p_term, int p_search_flags);
};
#endif // EDITOR_HELP_SEARCH_H
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 521477d470..8cffc2b4b0 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -808,7 +808,7 @@ void EditorNode::_update_update_spinner() {
// as this feature should only be enabled for troubleshooting purposes.
// Make the icon modulate color overbright because icons are not completely white on a dark theme.
// On a light theme, icons are dark, so we need to modulate them with an even brighter color.
- const bool dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ const bool dark_theme = EditorThemeManager::is_dark_theme();
update_spinner->set_self_modulate(theme->get_color(SNAME("error_color"), EditorStringName(Editor)) * (dark_theme ? Color(1.1, 1.1, 1.1) : Color(4.25, 4.25, 4.25)));
} else {
update_spinner->set_tooltip_text(TTR("Spins when the editor window redraws."));
@@ -6936,10 +6936,9 @@ EditorNode::EditorNode() {
renderer = memnew(OptionButton);
renderer->set_visible(true);
renderer->set_flat(true);
+ renderer->set_theme_type_variation("TopBarOptionButton");
renderer->set_fit_to_longest_item(false);
renderer->set_focus_mode(Control::FOCUS_NONE);
- renderer->add_theme_font_override("font", theme->get_font(SNAME("bold"), EditorStringName(EditorFonts)));
- renderer->add_theme_font_size_override("font_size", theme->get_font_size(SNAME("bold_size"), EditorStringName(EditorFonts)));
renderer->set_tooltip_text(TTR("Choose a rendering method.\n\nNotes:\n- On mobile platforms, the Mobile rendering method is used if Forward+ is selected here.\n- On the web platform, the Compatibility rendering method is always used."));
right_menu_hb->add_child(renderer);
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index b565431185..25510122f4 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -1278,14 +1278,6 @@ void EditorSettings::load_favorites_and_recent_dirs() {
}
}
-bool EditorSettings::is_dark_theme() {
- int AUTO_COLOR = 0;
- int LIGHT_COLOR = 2;
- Color base_color = get("interface/theme/base_color");
- int icon_font_color_setting = get("interface/theme/icon_and_font_color");
- return (icon_font_color_setting == AUTO_COLOR && base_color.get_luminance() < 0.5) || icon_font_color_setting == LIGHT_COLOR;
-}
-
void EditorSettings::list_text_editor_themes() {
String themes = "Default,Godot 2,Custom";
diff --git a/editor/editor_settings.h b/editor/editor_settings.h
index c3ce790e0e..2e280ac9d6 100644
--- a/editor/editor_settings.h
+++ b/editor/editor_settings.h
@@ -158,8 +158,6 @@ public:
Vector<String> get_recent_dirs() const;
void load_favorites_and_recent_dirs();
- bool is_dark_theme();
-
void list_text_editor_themes();
void load_text_editor_theme();
bool import_text_editor_theme(String p_file);
diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp
index a46dffb1f3..ecbfe4bec5 100644
--- a/editor/filesystem_dock.cpp
+++ b/editor/filesystem_dock.cpp
@@ -53,6 +53,7 @@
#include "editor/scene_tree_dock.h"
#include "editor/shader_create_dialog.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/item_list.h"
#include "scene/gui/label.h"
#include "scene/gui/line_edit.h"
@@ -625,7 +626,7 @@ void FileSystemDock::_notification(int p_what) {
// Update editor dark theme & always show folders states from editor settings, redraw if needed.
bool do_redraw = false;
- bool new_editor_is_dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ bool new_editor_is_dark_theme = EditorThemeManager::is_dark_theme();
if (new_editor_is_dark_theme != editor_is_dark_theme) {
editor_is_dark_theme = new_editor_is_dark_theme;
do_redraw = true;
@@ -865,18 +866,7 @@ void FileSystemDock::_search(EditorFileSystemDirectory *p_path, List<FileInfo> *
struct FileSystemDock::FileInfoTypeComparator {
bool operator()(const FileInfo &p_a, const FileInfo &p_b) const {
- // Uses the extension, then the icon name to distinguish file types.
- String icon_path_a = "";
- String icon_path_b = "";
- Ref<Texture2D> icon_a = EditorNode::get_singleton()->get_class_icon(p_a.type);
- if (icon_a.is_valid()) {
- icon_path_a = icon_a->get_name();
- }
- Ref<Texture2D> icon_b = EditorNode::get_singleton()->get_class_icon(p_b.type);
- if (icon_b.is_valid()) {
- icon_path_b = icon_b->get_name();
- }
- return NaturalNoCaseComparator()(p_a.name.get_extension() + icon_path_a + p_a.name.get_basename(), p_b.name.get_extension() + icon_path_b + p_b.name.get_basename());
+ return NaturalNoCaseComparator()(p_a.name.get_extension() + p_a.type + p_a.name.get_basename(), p_b.name.get_extension() + p_b.type + p_b.name.get_basename());
}
};
@@ -3763,7 +3753,7 @@ FileSystemDock::FileSystemDock() {
assigned_folder_colors = ProjectSettings::get_singleton()->get_setting("file_customization/folder_colors");
- editor_is_dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ editor_is_dark_theme = EditorThemeManager::is_dark_theme();
VBoxContainer *top_vbc = memnew(VBoxContainer);
add_child(top_vbc);
diff --git a/editor/icons/TileMapLayer.svg b/editor/icons/TileMapLayer.svg
new file mode 100644
index 0000000000..1903a87e3b
--- /dev/null
+++ b/editor/icons/TileMapLayer.svg
@@ -0,0 +1 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m1 7v2h2v-2zm3 0v2h2v-2zm3 0v2h2v-2zm3 0v2h2v-2zm3 0v2h2v-2z" fill="#8da5f3"/></svg> \ No newline at end of file
diff --git a/editor/import/resource_importer_texture.cpp b/editor/import/resource_importer_texture.cpp
index cdfc85cf6f..8cf104725a 100644
--- a/editor/import/resource_importer_texture.cpp
+++ b/editor/import/resource_importer_texture.cpp
@@ -36,10 +36,10 @@
#include "core/version.h"
#include "editor/editor_file_system.h"
#include "editor/editor_node.h"
-#include "editor/editor_settings.h"
#include "editor/gui/editor_toaster.h"
#include "editor/import/resource_importer_texture_settings.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/resources/compressed_texture.h"
void ResourceImporterTexture::_texture_reimport_roughness(const Ref<CompressedTexture2D> &p_tex, const String &p_normal_path, RS::TextureDetectRoughnessChannel p_channel) {
@@ -696,7 +696,7 @@ Error ResourceImporterTexture::import(const String &p_source_file, const String
editor_meta["editor_scale"] = EDSCALE;
}
if (convert_editor_colors) {
- editor_meta["editor_dark_theme"] = EditorSettings::get_singleton()->is_dark_theme();
+ editor_meta["editor_dark_theme"] = EditorThemeManager::is_dark_theme();
}
_save_editor_meta(editor_meta, p_save_path + ".editor.meta");
@@ -755,7 +755,7 @@ bool ResourceImporterTexture::are_import_settings_valid(const String &p_path) co
if (editor_meta.has("editor_scale") && (float)editor_meta["editor_scale"] != EDSCALE) {
return false;
}
- if (editor_meta.has("editor_dark_theme") && (bool)editor_meta["editor_dark_theme"] != EditorSettings::get_singleton()->is_dark_theme()) {
+ if (editor_meta.has("editor_dark_theme") && (bool)editor_meta["editor_dark_theme"] != EditorThemeManager::is_dark_theme()) {
return false;
}
}
diff --git a/editor/plugins/bone_map_editor_plugin.cpp b/editor/plugins/bone_map_editor_plugin.cpp
index 38573fbaa7..3256b90aba 100644
--- a/editor/plugins/bone_map_editor_plugin.cpp
+++ b/editor/plugins/bone_map_editor_plugin.cpp
@@ -36,6 +36,7 @@
#include "editor/import/3d/post_import_plugin_skeleton_track_organizer.h"
#include "editor/import/3d/scene_import_settings.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/aspect_ratio_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/texture_rect.h"
@@ -52,7 +53,7 @@ void BoneMapperButton::fetch_textures() {
set_offset(SIDE_BOTTOM, 0);
// Hack to avoid handle color darkening...
- set_modulate(EditorSettings::get_singleton()->is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25));
+ set_modulate(EditorThemeManager::is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25));
circle = memnew(TextureRect);
circle->set_texture(get_editor_theme_icon(SNAME("BoneMapperHandleCircle")));
diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp
index 6c776ad9b3..3722d6beba 100644
--- a/editor/plugins/canvas_item_editor_plugin.cpp
+++ b/editor/plugins/canvas_item_editor_plugin.cpp
@@ -45,6 +45,7 @@
#include "editor/plugins/script_editor_plugin.h"
#include "editor/scene_tree_dock.h"
#include "editor/themes/editor_scale.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/2d/polygon_2d.h"
#include "scene/2d/skeleton_2d.h"
#include "scene/2d/sprite_2d.h"
@@ -3898,7 +3899,7 @@ void CanvasItemEditor::_update_editor_settings() {
// to distinguish from the other key icons at the top. On a light theme,
// the icon will be dark, so we need to lighten it before blending it
// with the red color.
- const Color key_auto_color = EditorSettings::get_singleton()->is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25);
+ const Color key_auto_color = EditorThemeManager::is_dark_theme() ? Color(1, 1, 1) : Color(4.25, 4.25, 4.25);
key_auto_insert_button->add_theme_color_override("icon_pressed_color", key_auto_color.lerp(Color(1, 0, 0), 0.55));
animation_menu->set_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));
diff --git a/editor/plugins/particle_process_material_editor_plugin.cpp b/editor/plugins/particle_process_material_editor_plugin.cpp
index e696da3f5e..d6ec3921d5 100644
--- a/editor/plugins/particle_process_material_editor_plugin.cpp
+++ b/editor/plugins/particle_process_material_editor_plugin.cpp
@@ -34,6 +34,7 @@
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/gui/editor_spin_slider.h"
+#include "editor/themes/editor_theme_manager.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/label.h"
@@ -352,7 +353,7 @@ void ParticleProcessMaterialMinMaxPropertyEditor::_notification(int p_what) {
min_edit->add_theme_color_override(SNAME("label_color"), get_theme_color(SNAME("property_color_x"), EditorStringName(Editor)));
max_edit->add_theme_color_override(SNAME("label_color"), get_theme_color(SNAME("property_color_y"), EditorStringName(Editor)));
- const bool dark_theme = EditorSettings::get_singleton()->is_dark_theme();
+ const bool dark_theme = EditorThemeManager::is_dark_theme();
const Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
background_color = dark_theme ? Color(0.3, 0.3, 0.3) : Color(0.7, 0.7, 0.7);
normal_color = dark_theme ? Color(0.5, 0.5, 0.5) : Color(0.8, 0.8, 0.8);
diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp
index ad82801692..55191f44d4 100644
--- a/editor/plugins/script_editor_plugin.cpp
+++ b/editor/plugins/script_editor_plugin.cpp
@@ -1001,7 +1001,10 @@ void ScriptEditor::_res_saved_callback(const Ref<Resource> &p_res) {
}
_update_script_names();
- trigger_live_script_reload();
+ Ref<Script> scr = p_res;
+ if (scr.is_valid()) {
+ trigger_live_script_reload(scr->get_path());
+ }
}
void ScriptEditor::_scene_saved_callback(const String &p_path) {
@@ -1029,16 +1032,33 @@ void ScriptEditor::_scene_saved_callback(const String &p_path) {
}
}
-void ScriptEditor::trigger_live_script_reload() {
+void ScriptEditor::trigger_live_script_reload(const String &p_script_path) {
+ if (!script_paths_to_reload.has(p_script_path)) {
+ script_paths_to_reload.append(p_script_path);
+ }
if (!pending_auto_reload && auto_reload_running_scripts) {
callable_mp(this, &ScriptEditor::_live_auto_reload_running_scripts).call_deferred();
pending_auto_reload = true;
}
}
+void ScriptEditor::trigger_live_script_reload_all() {
+ if (!pending_auto_reload && auto_reload_running_scripts) {
+ call_deferred(SNAME("_live_auto_reload_running_scripts"));
+ pending_auto_reload = true;
+ reload_all_scripts = true;
+ }
+}
+
void ScriptEditor::_live_auto_reload_running_scripts() {
pending_auto_reload = false;
- EditorDebuggerNode::get_singleton()->reload_scripts();
+ if (reload_all_scripts) {
+ EditorDebuggerNode::get_singleton()->reload_all_scripts();
+ } else {
+ EditorDebuggerNode::get_singleton()->reload_scripts(script_paths_to_reload);
+ }
+ reload_all_scripts = false;
+ script_paths_to_reload.clear();
}
bool ScriptEditor::_test_script_times_on_disk(Ref<Resource> p_for_script) {
diff --git a/editor/plugins/script_editor_plugin.h b/editor/plugins/script_editor_plugin.h
index 4a814ea1bc..68eb23c838 100644
--- a/editor/plugins/script_editor_plugin.h
+++ b/editor/plugins/script_editor_plugin.h
@@ -382,6 +382,8 @@ class ScriptEditor : public PanelContainer {
bool pending_auto_reload;
bool auto_reload_running_scripts;
+ bool reload_all_scripts = false;
+ Vector<String> script_paths_to_reload;
void _live_auto_reload_running_scripts();
void _update_selected_editor_menu();
@@ -542,7 +544,8 @@ public:
void clear_docs_from_script(const Ref<Script> &p_script);
void update_docs_from_script(const Ref<Script> &p_script);
- void trigger_live_script_reload();
+ void trigger_live_script_reload(const String &p_script_path);
+ void trigger_live_script_reload_all();
bool can_take_away_focus() const;
diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp
index 45fb531d37..370144a427 100644
--- a/editor/plugins/script_text_editor.cpp
+++ b/editor/plugins/script_text_editor.cpp
@@ -146,6 +146,7 @@ void ScriptTextEditor::set_edited_resource(const Ref<Resource> &p_res) {
ERR_FAIL_COND(p_res.is_null());
script = p_res;
+ script->connect_changed(callable_mp((ScriptEditorBase *)this, &ScriptEditorBase::reload_text));
code_editor->get_text_editor()->set_text(script->get_source_code());
code_editor->get_text_editor()->clear_undo_history();
@@ -824,7 +825,7 @@ void ScriptEditor::_update_modified_scripts_for_external_editor(Ref<Script> p_fo
scr->set_last_modified_time(rel_scr->get_last_modified_time());
scr->update_exports();
- trigger_live_script_reload();
+ trigger_live_script_reload(scr->get_path());
}
}
}
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index e7277bad6a..fff7b89c01 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -36,17 +36,18 @@
#include "core/io/file_access.h"
#include "core/io/resource_saver.h"
#include "core/io/stream_peer_tls.h"
-#include "core/io/zip_io.h"
#include "core/os/keyboard.h"
#include "core/os/os.h"
#include "core/string/translation.h"
#include "core/version.h"
-#include "editor/editor_paths.h"
+#include "editor/editor_about.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
-#include "editor/editor_vcs_interface.h"
#include "editor/gui/editor_file_dialog.h"
#include "editor/plugins/asset_library_editor_plugin.h"
+#include "editor/project_manager/project_dialog.h"
+#include "editor/project_manager/project_list.h"
+#include "editor/project_manager/project_tag.h"
#include "editor/themes/editor_icons.h"
#include "editor/themes/editor_scale.h"
#include "editor/themes/editor_theme_manager.h"
@@ -57,1948 +58,21 @@
#include "scene/gui/flow_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h"
+#include "scene/gui/option_button.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/texture_rect.h"
#include "scene/main/window.h"
-#include "scene/resources/image_texture.h"
#include "servers/display_server.h"
#include "servers/navigation_server_3d.h"
#include "servers/physics_server_2d.h"
constexpr int GODOT4_CONFIG_VERSION = 5;
-/// Project Dialog.
-
-void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) {
- msg->set_text(p_msg);
- Ref<Texture2D> current_path_icon = status_rect->get_texture();
- Ref<Texture2D> current_install_icon = install_status_rect->get_texture();
- Ref<Texture2D> new_icon;
-
- switch (p_type) {
- case MESSAGE_ERROR: {
- msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
- msg->set_modulate(Color(1, 1, 1, 1));
- new_icon = get_editor_theme_icon(SNAME("StatusError"));
-
- } break;
- case MESSAGE_WARNING: {
- msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
- msg->set_modulate(Color(1, 1, 1, 1));
- new_icon = get_editor_theme_icon(SNAME("StatusWarning"));
-
- } break;
- case MESSAGE_SUCCESS: {
- msg->remove_theme_color_override("font_color");
- msg->set_modulate(Color(1, 1, 1, 0));
- new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));
-
- } break;
- }
-
- if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
- status_rect->set_texture(new_icon);
- } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
- install_status_rect->set_texture(new_icon);
- }
-}
-
-static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) {
- return p_path.ends_with(".zip") && p_d->file_exists(p_path);
-}
-
-String ProjectDialog::_test_path() {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- const String base_path = project_path->get_text();
- String valid_path, valid_install_path;
- bool is_zip = false;
- if (d->change_dir(base_path) == OK) {
- valid_path = base_path;
- } else if (is_zip_file(d, base_path)) {
- valid_path = base_path;
- is_zip = true;
- } else if (d->change_dir(base_path.strip_edges()) == OK) {
- valid_path = base_path.strip_edges();
- } else if (is_zip_file(d, base_path.strip_edges())) {
- valid_path = base_path.strip_edges();
- is_zip = true;
- }
-
- if (valid_path.is_empty()) {
- _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- if (mode == MODE_IMPORT && is_zip) {
- if (d->change_dir(install_path->get_text()) == OK) {
- valid_install_path = install_path->get_text();
- } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
- valid_install_path = install_path->get_text().strip_edges();
- }
-
- if (valid_install_path.is_empty()) {
- _set_message(TTR("The install path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH);
- get_ok_button()->set_disabled(true);
- return "";
- }
- }
-
- if (mode == MODE_IMPORT || mode == MODE_RENAME) {
- if (!d->file_exists("project.godot")) {
- if (is_zip) {
- Ref<FileAccess> io_fa;
- zlib_filefunc_def io = zipio_create_io(&io_fa);
-
- unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
- if (!pkg) {
- _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- unzClose(pkg);
- return "";
- }
-
- int ret = unzGoToFirstFile(pkg);
- while (ret == UNZ_OK) {
- unz_file_info info;
- char fname[16384];
- ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
- if (ret != UNZ_OK) {
- break;
- }
-
- if (String::utf8(fname).ends_with("project.godot")) {
- break;
- }
-
- ret = unzGoToNextFile(pkg);
- }
-
- if (ret == UNZ_END_OF_LIST_OF_FILE) {
- _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- unzClose(pkg);
- return "";
- }
-
- unzClose(pkg);
-
- // check if the specified install folder is empty, even though this is not an error, it is good to check here
- d->list_dir_begin();
- is_folder_empty = true;
- String n = d->get_next();
- while (!n.is_empty()) {
- if (!n.begins_with(".")) {
- // Allow `.`, `..` (reserved current/parent folder names)
- // and hidden files/folders to be present.
- // For instance, this lets users initialize a Git repository
- // and still be able to create a project in the directory afterwards.
- is_folder_empty = false;
- break;
- }
- n = d->get_next();
- }
- d->list_dir_end();
-
- if (!is_folder_empty) {
- _set_message(TTR("Please choose an empty install folder."), MESSAGE_WARNING, INSTALL_PATH);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- } else {
- _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR);
- install_path_container->hide();
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- } else if (is_zip) {
- _set_message(TTR("The install directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- } else {
- // Check if the specified folder is empty, even though this is not an error, it is good to check here.
- d->list_dir_begin();
- is_folder_empty = true;
- String n = d->get_next();
- while (!n.is_empty()) {
- if (!n.begins_with(".")) {
- // Allow `.`, `..` (reserved current/parent folder names)
- // and hidden files/folders to be present.
- // For instance, this lets users initialize a Git repository
- // and still be able to create a project in the directory afterwards.
- is_folder_empty = false;
- break;
- }
- n = d->get_next();
- }
- d->list_dir_end();
-
- if (!is_folder_empty) {
- if (valid_path == OS::get_singleton()->get_environment("HOME") || valid_path == OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS) || valid_path == OS::get_singleton()->get_executable_path().get_base_dir()) {
- _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- return "";
- }
-
- _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING);
- get_ok_button()->set_disabled(false);
- return valid_path;
- }
- }
-
- _set_message("");
- _set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
- get_ok_button()->set_disabled(false);
- return valid_path;
-}
-
-void ProjectDialog::_update_path(const String &p_path) {
- String sp = _test_path();
- if (!sp.is_empty()) {
- // If the project name is empty or default, infer the project name from the selected folder name
- if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) {
- sp = sp.replace("\\", "/");
- int lidx = sp.rfind("/");
-
- if (lidx != -1) {
- sp = sp.substr(lidx + 1, sp.length()).capitalize();
- }
- if (sp.is_empty() && mode == MODE_IMPORT) {
- sp = TTR("Imported Project");
- }
-
- project_name->set_text(sp);
- _text_changed(sp);
- }
- }
-
- if (!created_folder_path.is_empty() && created_folder_path != p_path) {
- _remove_created_folder();
- }
-}
-
-void ProjectDialog::_path_text_changed(const String &p_path) {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- if (mode == MODE_IMPORT && is_zip_file(d, p_path)) {
- install_path->set_text(p_path.get_base_dir());
- install_path_container->show();
- } else if (mode == MODE_IMPORT && is_zip_file(d, p_path.strip_edges())) {
- install_path->set_text(p_path.strip_edges().get_base_dir());
- install_path_container->show();
- } else {
- install_path_container->hide();
- }
-
- _update_path(p_path.simplify_path());
-}
-
-void ProjectDialog::_file_selected(const String &p_path) {
- // If not already shown.
- show_dialog();
-
- String p = p_path;
- if (mode == MODE_IMPORT) {
- if (p.ends_with("project.godot")) {
- p = p.get_base_dir();
- install_path_container->hide();
- get_ok_button()->set_disabled(false);
- } else if (p.ends_with(".zip")) {
- install_path->set_text(p.get_base_dir());
- install_path_container->show();
- get_ok_button()->set_disabled(false);
- } else {
- _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
- get_ok_button()->set_disabled(true);
- return;
- }
- }
-
- String sp = p.simplify_path();
- project_path->set_text(sp);
- _update_path(sp);
- if (p.ends_with(".zip")) {
- callable_mp((Control *)install_path, &Control::grab_focus).call_deferred();
- } else {
- callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
- }
-}
-
-void ProjectDialog::_path_selected(const String &p_path) {
- // If not already shown.
- show_dialog();
-
- String sp = p_path.simplify_path();
- project_path->set_text(sp);
- _update_path(sp);
- callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
-}
-
-void ProjectDialog::_install_path_selected(const String &p_path) {
- String sp = p_path.simplify_path();
- install_path->set_text(sp);
- _update_path(sp);
- callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
-}
-
-void ProjectDialog::_browse_path() {
- fdialog->set_current_dir(project_path->get_text());
-
- if (mode == MODE_IMPORT) {
- fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);
- fdialog->clear_filters();
- fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project")));
- fdialog->add_filter("*.zip", TTR("ZIP File"));
- } else {
- fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
- }
- fdialog->popup_file_dialog();
-}
-
-void ProjectDialog::_browse_install_path() {
- fdialog_install->set_current_dir(install_path->get_text());
- fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
- fdialog_install->popup_file_dialog();
-}
-
-void ProjectDialog::_create_folder() {
- const String project_name_no_edges = project_name->get_text().strip_edges();
- if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) {
- _set_message(TTR("Invalid project name."), MESSAGE_WARNING);
- return;
- }
-
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- if (d->change_dir(project_path->get_text()) == OK) {
- if (!d->dir_exists(project_name_no_edges)) {
- if (d->make_dir(project_name_no_edges) == OK) {
- d->change_dir(project_name_no_edges);
- String dir_str = d->get_current_dir();
- project_path->set_text(dir_str);
- _update_path(dir_str);
- created_folder_path = d->get_current_dir();
- create_dir->set_disabled(true);
- } else {
- dialog_error->set_text(TTR("Couldn't create folder."));
- dialog_error->popup_centered();
- }
- } else {
- dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
- dialog_error->popup_centered();
- }
- }
-}
-
-void ProjectDialog::_text_changed(const String &p_text) {
- if (mode != MODE_NEW) {
- return;
- }
-
- _test_path();
-
- if (p_text.strip_edges().is_empty()) {
- _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR);
- }
-}
-
-void ProjectDialog::_nonempty_confirmation_ok_pressed() {
- is_folder_empty = true;
- ok_pressed();
-}
-
-void ProjectDialog::_renderer_selected() {
- ERR_FAIL_NULL(renderer_button_group->get_pressed_button());
-
- String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
-
- if (renderer_type == "forward_plus") {
- renderer_info->set_text(
- String::utf8("• ") + TTR("Supports desktop platforms only.") +
- String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +
- String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +
- String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
- String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));
- } else if (renderer_type == "mobile") {
- renderer_info->set_text(
- String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +
- String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +
- String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +
- String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
- String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));
- } else if (renderer_type == "gl_compatibility") {
- renderer_info->set_text(
- String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +
- String::utf8("\n• ") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
- String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +
- String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
- String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));
- } else {
- WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
- }
-}
-
-void ProjectDialog::_remove_created_folder() {
- if (!created_folder_path.is_empty()) {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- d->remove(created_folder_path);
-
- create_dir->set_disabled(false);
- created_folder_path = "";
- }
-}
-
-void ProjectDialog::ok_pressed() {
- String dir = project_path->get_text();
-
- if (mode == MODE_RENAME) {
- String dir2 = _test_path();
- if (dir2.is_empty()) {
- _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
- return;
- }
-
- // Load project.godot as ConfigFile to set the new name.
- ConfigFile cfg;
- String project_godot = dir2.path_join("project.godot");
- Error err = cfg.load(project_godot);
- if (err != OK) {
- _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
- } else {
- cfg.set_value("application", "config/name", project_name->get_text().strip_edges());
- err = cfg.save(project_godot);
- if (err != OK) {
- _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR);
- }
- }
-
- hide();
- emit_signal(SNAME("projects_updated"));
-
- } else {
- if (mode == MODE_IMPORT) {
- if (project_path->get_text().ends_with(".zip")) {
- mode = MODE_INSTALL;
- ok_pressed();
-
- return;
- }
-
- } else {
- if (mode == MODE_NEW) {
- // Before we create a project, check that the target folder is empty.
- // If not, we need to ask the user if they're sure they want to do this.
- if (!is_folder_empty) {
- ConfirmationDialog *cd = memnew(ConfirmationDialog);
- cd->set_title(TTR("Warning: This folder is not empty"));
- cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?"));
- cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
- get_parent()->add_child(cd);
- cd->popup_centered();
- cd->grab_focus();
- return;
- }
- PackedStringArray project_features = ProjectSettings::get_required_features();
- ProjectSettings::CustomMap initial_settings;
-
- // Be sure to change this code if/when renderers are changed.
- // Default values are "forward_plus" for the main setting, "mobile" for the mobile override,
- // and "gl_compatibility" for the web override.
- String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
- initial_settings["rendering/renderer/rendering_method"] = renderer_type;
-
- EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);
- EditorSettings::get_singleton()->save();
-
- if (renderer_type == "forward_plus") {
- project_features.push_back("Forward Plus");
- } else if (renderer_type == "mobile") {
- project_features.push_back("Mobile");
- } else if (renderer_type == "gl_compatibility") {
- project_features.push_back("GL Compatibility");
- // Also change the default rendering method for the mobile override.
- initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";
- } else {
- WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
- }
-
- project_features.sort();
- initial_settings["application/config/features"] = project_features;
- initial_settings["application/config/name"] = project_name->get_text().strip_edges();
- initial_settings["application/config/icon"] = "res://icon.svg";
-
- if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) {
- _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
- } else {
- // Store default project icon in SVG format.
- Error err;
- Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err);
- fa_icon->store_string(get_default_project_icon());
-
- if (err != OK) {
- _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
- }
-
- EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir);
- }
- } else if (mode == MODE_INSTALL) {
- if (project_path->get_text().ends_with(".zip")) {
- dir = install_path->get_text();
- zip_path = project_path->get_text();
- }
-
- Ref<FileAccess> io_fa;
- zlib_filefunc_def io = zipio_create_io(&io_fa);
-
- unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
- if (!pkg) {
- dialog_error->set_text(TTR("Error opening package file, not in ZIP format."));
- dialog_error->popup_centered();
- return;
- }
-
- // Find the zip_root
- String zip_root;
- int ret = unzGoToFirstFile(pkg);
- while (ret == UNZ_OK) {
- unz_file_info info;
- char fname[16384];
- unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
-
- String name = String::utf8(fname);
- if (name.ends_with("project.godot")) {
- zip_root = name.substr(0, name.rfind("project.godot"));
- break;
- }
-
- ret = unzGoToNextFile(pkg);
- }
-
- ret = unzGoToFirstFile(pkg);
-
- Vector<String> failed_files;
-
- while (ret == UNZ_OK) {
- //get filename
- unz_file_info info;
- char fname[16384];
- ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
- if (ret != UNZ_OK) {
- break;
- }
-
- String path = String::utf8(fname);
-
- if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) {
- //
- } else if (path.ends_with("/")) { // a dir
- path = path.substr(0, path.length() - 1);
- String rel_path = path.substr(zip_root.length());
-
- Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- da->make_dir(dir.path_join(rel_path));
- } else {
- Vector<uint8_t> uncomp_data;
- uncomp_data.resize(info.uncompressed_size);
- String rel_path = path.substr(zip_root.length());
-
- //read
- unzOpenCurrentFile(pkg);
- ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
- ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));
- unzCloseCurrentFile(pkg);
-
- Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE);
- if (f.is_valid()) {
- f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
- } else {
- failed_files.push_back(rel_path);
- }
- }
-
- ret = unzGoToNextFile(pkg);
- }
-
- unzClose(pkg);
-
- if (failed_files.size()) {
- String err_msg = TTR("The following files failed extraction from package:") + "\n\n";
- for (int i = 0; i < failed_files.size(); i++) {
- if (i > 15) {
- err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
- break;
- }
- err_msg += failed_files[i] + "\n";
- }
-
- dialog_error->set_text(err_msg);
- dialog_error->popup_centered();
-
- } else if (!project_path->get_text().ends_with(".zip")) {
- dialog_error->set_text(TTR("Package installed successfully!"));
- dialog_error->popup_centered();
- }
- }
- }
-
- dir = dir.replace("\\", "/");
- if (dir.ends_with("/")) {
- dir = dir.substr(0, dir.length() - 1);
- }
-
- hide();
- emit_signal(SNAME("project_created"), dir);
- }
-}
-
-void ProjectDialog::cancel_pressed() {
- _remove_created_folder();
-
- project_path->clear();
- _update_path("");
- project_name->clear();
- _text_changed("");
-
- if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
- msg->show();
- }
-
- if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
- msg->show();
- }
-}
-
-void ProjectDialog::set_zip_path(const String &p_path) {
- zip_path = p_path;
-}
-
-void ProjectDialog::set_zip_title(const String &p_title) {
- zip_title = p_title;
-}
-
-void ProjectDialog::set_mode(Mode p_mode) {
- mode = p_mode;
-}
-
-void ProjectDialog::set_project_path(const String &p_path) {
- project_path->set_text(p_path);
-}
-
-void ProjectDialog::ask_for_path_and_show() {
- // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog.
- show_dialog();
- _set_message("");
-
- _browse_path();
-}
-
-void ProjectDialog::show_dialog() {
- if (mode == MODE_RENAME) {
- project_path->set_editable(false);
- browse->hide();
- install_browse->hide();
-
- set_title(TTR("Rename Project"));
- set_ok_button_text(TTR("Rename"));
- name_container->show();
- status_rect->hide();
- msg->hide();
- install_path_container->hide();
- install_status_rect->hide();
- renderer_container->hide();
- default_files_container->hide();
- get_ok_button()->set_disabled(false);
-
- // Fetch current name from project.godot to prefill the text input.
- ConfigFile cfg;
- String project_godot = project_path->get_text().path_join("project.godot");
- Error err = cfg.load(project_godot);
- if (err != OK) {
- _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
- status_rect->show();
- msg->show();
- get_ok_button()->set_disabled(true);
- } else {
- String cur_name = cfg.get_value("application", "config/name", "");
- project_name->set_text(cur_name);
- _text_changed(cur_name);
- }
-
- callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
-
- create_dir->hide();
-
- } else {
- fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
- if (!fav_dir.is_empty()) {
- project_path->set_text(fav_dir);
- fdialog->set_current_dir(fav_dir);
- } else {
- Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- project_path->set_text(d->get_current_dir());
- fdialog->set_current_dir(d->get_current_dir());
- }
-
- if (project_name->get_text().is_empty()) {
- String proj = TTR("New Game Project");
- project_name->set_text(proj);
- _text_changed(proj);
- }
-
- project_path->set_editable(true);
- browse->set_disabled(false);
- browse->show();
- install_browse->set_disabled(false);
- install_browse->show();
- create_dir->show();
- status_rect->show();
- install_status_rect->show();
- msg->show();
-
- if (mode == MODE_IMPORT) {
- set_title(TTR("Import Existing Project"));
- set_ok_button_text(TTR("Import & Edit"));
- name_container->hide();
- install_path_container->hide();
- renderer_container->hide();
- default_files_container->hide();
- project_path->grab_focus();
-
- } else if (mode == MODE_NEW) {
- set_title(TTR("Create New Project"));
- set_ok_button_text(TTR("Create & Edit"));
- name_container->show();
- install_path_container->hide();
- renderer_container->show();
- default_files_container->show();
- callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
- callable_mp(project_name, &LineEdit::select_all).call_deferred();
-
- } else if (mode == MODE_INSTALL) {
- set_title(TTR("Install Project:") + " " + zip_title);
- set_ok_button_text(TTR("Install & Edit"));
- project_name->set_text(zip_title);
- name_container->show();
- install_path_container->hide();
- renderer_container->hide();
- default_files_container->hide();
- project_path->grab_focus();
- }
-
- _test_path();
- }
-
- popup_centered(Size2(500, 0) * EDSCALE);
-}
-
-void ProjectDialog::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_WM_CLOSE_REQUEST: {
- _remove_created_folder();
- } break;
- }
-}
-
-void ProjectDialog::_bind_methods() {
- ADD_SIGNAL(MethodInfo("project_created"));
- ADD_SIGNAL(MethodInfo("projects_updated"));
-}
-
-ProjectDialog::ProjectDialog() {
- VBoxContainer *vb = memnew(VBoxContainer);
- add_child(vb);
-
- name_container = memnew(VBoxContainer);
- vb->add_child(name_container);
-
- Label *l = memnew(Label);
- l->set_text(TTR("Project Name:"));
- name_container->add_child(l);
-
- HBoxContainer *pnhb = memnew(HBoxContainer);
- name_container->add_child(pnhb);
-
- project_name = memnew(LineEdit);
- project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- pnhb->add_child(project_name);
-
- create_dir = memnew(Button);
- pnhb->add_child(create_dir);
- create_dir->set_text(TTR("Create Folder"));
- create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder));
-
- path_container = memnew(VBoxContainer);
- vb->add_child(path_container);
-
- l = memnew(Label);
- l->set_text(TTR("Project Path:"));
- path_container->add_child(l);
-
- HBoxContainer *pphb = memnew(HBoxContainer);
- path_container->add_child(pphb);
-
- project_path = memnew(LineEdit);
- project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
- pphb->add_child(project_path);
-
- install_path_container = memnew(VBoxContainer);
- vb->add_child(install_path_container);
-
- l = memnew(Label);
- l->set_text(TTR("Project Installation Path:"));
- install_path_container->add_child(l);
-
- HBoxContainer *iphb = memnew(HBoxContainer);
- install_path_container->add_child(iphb);
-
- install_path = memnew(LineEdit);
- install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
- iphb->add_child(install_path);
-
- // status icon
- status_rect = memnew(TextureRect);
- status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
- pphb->add_child(status_rect);
-
- browse = memnew(Button);
- browse->set_text(TTR("Browse"));
- browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path));
- pphb->add_child(browse);
-
- // install status icon
- install_status_rect = memnew(TextureRect);
- install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
- iphb->add_child(install_status_rect);
-
- install_browse = memnew(Button);
- install_browse->set_text(TTR("Browse"));
- install_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_install_path));
- iphb->add_child(install_browse);
-
- msg = memnew(Label);
- msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
- msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
- vb->add_child(msg);
-
- // Renderer selection.
- renderer_container = memnew(VBoxContainer);
- vb->add_child(renderer_container);
- l = memnew(Label);
- l->set_text(TTR("Renderer:"));
- renderer_container->add_child(l);
- HBoxContainer *rshc = memnew(HBoxContainer);
- renderer_container->add_child(rshc);
- renderer_button_group.instantiate();
-
- // Left hand side, used for checkboxes to select renderer.
- Container *rvb = memnew(VBoxContainer);
- rshc->add_child(rvb);
-
- String default_renderer_type = "forward_plus";
- if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
- default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
- }
-
- Button *rs_button = memnew(CheckBox);
- rs_button->set_button_group(renderer_button_group);
- rs_button->set_text(TTR("Forward+"));
-#if defined(WEB_ENABLED)
- rs_button->set_disabled(true);
-#endif
- rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
- rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
- rvb->add_child(rs_button);
- if (default_renderer_type == "forward_plus") {
- rs_button->set_pressed(true);
- }
- rs_button = memnew(CheckBox);
- rs_button->set_button_group(renderer_button_group);
- rs_button->set_text(TTR("Mobile"));
-#if defined(WEB_ENABLED)
- rs_button->set_disabled(true);
-#endif
- rs_button->set_meta(SNAME("rendering_method"), "mobile");
- rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
- rvb->add_child(rs_button);
- if (default_renderer_type == "mobile") {
- rs_button->set_pressed(true);
- }
- rs_button = memnew(CheckBox);
- rs_button->set_button_group(renderer_button_group);
- rs_button->set_text(TTR("Compatibility"));
-#if !defined(GLES3_ENABLED)
- rs_button->set_disabled(true);
-#endif
- rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
- rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
- rvb->add_child(rs_button);
-#if defined(GLES3_ENABLED)
- if (default_renderer_type == "gl_compatibility") {
- rs_button->set_pressed(true);
- }
-#endif
- rshc->add_child(memnew(VSeparator));
-
- // Right hand side, used for text explaining each choice.
- rvb = memnew(VBoxContainer);
- rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- rshc->add_child(rvb);
- renderer_info = memnew(Label);
- renderer_info->set_modulate(Color(1, 1, 1, 0.7));
- rvb->add_child(renderer_info);
- _renderer_selected();
-
- l = memnew(Label);
- l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
- // Add some extra spacing to separate it from the list above and the buttons below.
- l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
- l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
- l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
- l->set_modulate(Color(1, 1, 1, 0.7));
- renderer_container->add_child(l);
-
- default_files_container = memnew(HBoxContainer);
- vb->add_child(default_files_container);
- l = memnew(Label);
- l->set_text(TTR("Version Control Metadata:"));
- default_files_container->add_child(l);
- vcs_metadata_selection = memnew(OptionButton);
- vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
- vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
- vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
- vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
- default_files_container->add_child(vcs_metadata_selection);
- Control *spacer = memnew(Control);
- spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- default_files_container->add_child(spacer);
-
- fdialog = memnew(EditorFileDialog);
- fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
- fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
- fdialog_install = memnew(EditorFileDialog);
- fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
- fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
- add_child(fdialog);
- add_child(fdialog_install);
-
- project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed));
- project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
- install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_update_path));
- fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected));
- fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected));
- fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
- fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
-
- set_hide_on_ok(false);
-
- dialog_error = memnew(AcceptDialog);
- add_child(dialog_error);
-}
-
-/// Project List and friends.
-
-void ProjectListItemControl::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_THEME_CHANGED: {
- if (icon_needs_reload) {
- // The project icon may not be loaded by the time the control is displayed,
- // so use a loading placeholder.
- project_icon->set_texture(get_editor_theme_icon(SNAME("ProjectIconLoading")));
- }
-
- project_title->begin_bulk_theme_override();
- project_title->add_theme_font_override("font", get_theme_font(SNAME("title"), EditorStringName(EditorFonts)));
- project_title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
- project_title->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
- project_title->end_bulk_theme_override();
-
- project_path->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
- project_unsupported_features->set_texture(get_editor_theme_icon(SNAME("NodeWarning")));
-
- favorite_button->set_texture_normal(get_editor_theme_icon(SNAME("Favorites")));
- if (project_is_missing) {
- explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
- } else {
- explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
- }
- } break;
-
- case NOTIFICATION_MOUSE_ENTER: {
- is_hovering = true;
- queue_redraw();
- } break;
-
- case NOTIFICATION_MOUSE_EXIT: {
- is_hovering = false;
- queue_redraw();
- } break;
-
- case NOTIFICATION_DRAW: {
- if (is_selected) {
- draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
- }
- if (is_hovering) {
- draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
- }
-
- draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
- } break;
- }
-}
-
-void ProjectListItemControl::set_project_title(const String &p_title) {
- project_title->set_text(p_title);
-}
-
-void ProjectListItemControl::set_project_path(const String &p_path) {
- project_path->set_text(p_path);
-}
-
-void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) {
- for (const String &tag : p_tags) {
- ProjectTag *tag_control = memnew(ProjectTag(tag));
- tag_container->add_child(tag_control);
- tag_control->connect_button_to(callable_mp(p_parent_list, &ProjectList::add_search_tag).bind(tag));
- }
-}
-
-void ProjectListItemControl::set_project_icon(const Ref<Texture2D> &p_icon) {
- icon_needs_reload = false;
-
- // The default project icon is 128×128 to look crisp on hiDPI displays,
- // but we want the actual displayed size to be 64×64 on loDPI displays.
- project_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
- project_icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
- project_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
-
- project_icon->set_texture(p_icon);
-}
-
-bool _project_feature_looks_like_version(const String &p_feature) {
- return p_feature.contains(".") && p_feature.substr(0, 3).is_numeric();
-}
-
-void ProjectListItemControl::set_unsupported_features(PackedStringArray p_features) {
- if (p_features.size() > 0) {
- String tooltip_text = "";
- for (int i = 0; i < p_features.size(); i++) {
- if (_project_feature_looks_like_version(p_features[i])) {
- tooltip_text += TTR("This project was last edited in a different Godot version: ") + p_features[i] + "\n";
- p_features.remove_at(i);
- i--;
- }
- }
- if (p_features.size() > 0) {
- String unsupported_features_str = String(", ").join(p_features);
- tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str;
- }
- project_unsupported_features->set_tooltip_text(tooltip_text);
- project_unsupported_features->show();
- } else {
- project_unsupported_features->hide();
- }
-}
-
-bool ProjectListItemControl::should_load_project_icon() const {
- return icon_needs_reload;
-}
-
-void ProjectListItemControl::set_selected(bool p_selected) {
- is_selected = p_selected;
- queue_redraw();
-}
-
-void ProjectListItemControl::set_is_favorite(bool p_favorite) {
- favorite_button->set_modulate(p_favorite ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
-}
-
-void ProjectListItemControl::set_is_missing(bool p_missing) {
- if (project_is_missing == p_missing) {
- return;
- }
- project_is_missing = p_missing;
-
- if (project_is_missing) {
- project_icon->set_modulate(Color(1, 1, 1, 0.5));
-
- explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
- explore_button->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
- } else {
- project_icon->set_modulate(Color(1, 1, 1, 1.0));
-
- explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
-#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
- explore_button->set_tooltip_text(TTR("Show in File Manager"));
-#else
- // Opening the system file manager is not supported on the Android and web editors.
- explore_button->hide();
-#endif
- }
-}
-
-void ProjectListItemControl::set_is_grayed(bool p_grayed) {
- if (p_grayed) {
- main_vbox->set_modulate(Color(1, 1, 1, 0.5));
- // Don't make the icon less prominent if the parent is already grayed out.
- explore_button->set_modulate(Color(1, 1, 1, 1.0));
- } else {
- main_vbox->set_modulate(Color(1, 1, 1, 1.0));
- explore_button->set_modulate(Color(1, 1, 1, 0.5));
- }
-}
-
-void ProjectListItemControl::_favorite_button_pressed() {
- emit_signal(SNAME("favorite_pressed"));
-}
-
-void ProjectListItemControl::_explore_button_pressed() {
- emit_signal(SNAME("explore_pressed"));
-}
-
-void ProjectListItemControl::_bind_methods() {
- ADD_SIGNAL(MethodInfo("favorite_pressed"));
- ADD_SIGNAL(MethodInfo("explore_pressed"));
-}
-
-ProjectListItemControl::ProjectListItemControl() {
- set_focus_mode(FocusMode::FOCUS_ALL);
-
- VBoxContainer *favorite_box = memnew(VBoxContainer);
- favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
- add_child(favorite_box);
-
- favorite_button = memnew(TextureButton);
- favorite_button->set_name("FavoriteButton");
- // This makes the project's "hover" style display correctly when hovering the favorite icon.
- favorite_button->set_mouse_filter(MOUSE_FILTER_PASS);
- favorite_box->add_child(favorite_button);
- favorite_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_favorite_button_pressed));
-
- project_icon = memnew(TextureRect);
- project_icon->set_name("ProjectIcon");
- project_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
- add_child(project_icon);
-
- main_vbox = memnew(VBoxContainer);
- main_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- add_child(main_vbox);
-
- Control *ec = memnew(Control);
- ec->set_custom_minimum_size(Size2(0, 1));
- ec->set_mouse_filter(MOUSE_FILTER_PASS);
- main_vbox->add_child(ec);
-
- // Top half, title, tags and unsupported features labels.
- {
- HBoxContainer *title_hb = memnew(HBoxContainer);
- main_vbox->add_child(title_hb);
-
- project_title = memnew(Label);
- project_title->set_auto_translate(false);
- project_title->set_name("ProjectName");
- project_title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- project_title->set_clip_text(true);
- title_hb->add_child(project_title);
-
- tag_container = memnew(HBoxContainer);
- title_hb->add_child(tag_container);
-
- Control *spacer = memnew(Control);
- spacer->set_custom_minimum_size(Size2(10, 10));
- title_hb->add_child(spacer);
- }
-
- // Bottom half, containing the path and view folder button.
- {
- HBoxContainer *path_hb = memnew(HBoxContainer);
- path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- main_vbox->add_child(path_hb);
-
- explore_button = memnew(Button);
- explore_button->set_name("ExploreButton");
- explore_button->set_flat(true);
- path_hb->add_child(explore_button);
- explore_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_explore_button_pressed));
-
- project_path = memnew(Label);
- project_path->set_name("ProjectPath");
- project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
- project_path->set_clip_text(true);
- project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- project_path->set_modulate(Color(1, 1, 1, 0.5));
- path_hb->add_child(project_path);
-
- project_unsupported_features = memnew(TextureRect);
- project_unsupported_features->set_name("ProjectUnsupportedFeatures");
- project_unsupported_features->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
- path_hb->add_child(project_unsupported_features);
- project_unsupported_features->hide();
-
- Control *spacer = memnew(Control);
- spacer->set_custom_minimum_size(Size2(10, 10));
- path_hb->add_child(spacer);
- }
-}
-
-struct ProjectListComparator {
- ProjectList::FilterOption order_option = ProjectList::FilterOption::EDIT_DATE;
-
- // operator<
- _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
- if (a.favorite && !b.favorite) {
- return true;
- }
- if (b.favorite && !a.favorite) {
- return false;
- }
- switch (order_option) {
- case ProjectList::PATH:
- return a.path < b.path;
- case ProjectList::EDIT_DATE:
- return a.last_edited > b.last_edited;
- case ProjectList::TAGS:
- return a.tag_sort_string < b.tag_sort_string;
- default:
- return a.project_name < b.project_name;
- }
- }
-};
-
-const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed";
-const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
-const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
-
-void ProjectList::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_PROCESS: {
- // Load icons as a coroutine to speed up launch when you have hundreds of projects
- if (_icon_load_index < _projects.size()) {
- Item &item = _projects.write[_icon_load_index];
- if (item.control->should_load_project_icon()) {
- _load_project_icon(_icon_load_index);
- }
- _icon_load_index++;
-
- } else {
- set_process(false);
- }
- } break;
- }
-}
-
-void ProjectList::_update_icons_async() {
- _icon_load_index = 0;
- set_process(true);
-}
-
-void ProjectList::_load_project_icon(int p_index) {
- Item &item = _projects.write[p_index];
-
- Ref<Texture2D> default_icon = get_editor_theme_icon(SNAME("DefaultProjectIcon"));
- Ref<Texture2D> icon;
- if (!item.icon.is_empty()) {
- Ref<Image> img;
- img.instantiate();
- Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
- if (err == OK) {
- img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
- icon = ImageTexture::create_from_image(img);
- }
- }
- if (icon.is_null()) {
- icon = default_icon;
- }
-
- item.control->set_project_icon(icon);
-}
-
-// Load project data from p_property_key and return it in a ProjectList::Item.
-// p_favorite is passed directly into the Item.
-ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
- String conf = p_path.path_join("project.godot");
- bool grayed = false;
- bool missing = false;
-
- Ref<ConfigFile> cf = memnew(ConfigFile);
- Error cf_err = cf->load(conf);
-
- int config_version = 0;
- String project_name = TTR("Unnamed Project");
- if (cf_err == OK) {
- String cf_project_name = cf->get_value("application", "config/name", "");
- if (!cf_project_name.is_empty()) {
- project_name = cf_project_name.xml_unescape();
- }
- config_version = (int)cf->get_value("", "config_version", 0);
- }
-
- if (config_version > ProjectSettings::CONFIG_VERSION) {
- // Comes from an incompatible (more recent) Godot version, gray it out.
- grayed = true;
- }
-
- const String description = cf->get_value("application", "config/description", "");
- const PackedStringArray tags = cf->get_value("application", "config/tags", PackedStringArray());
- const String icon = cf->get_value("application", "config/icon", "");
- const String main_scene = cf->get_value("application", "run/main_scene", "");
-
- PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
- PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
-
- uint64_t last_edited = 0;
- if (cf_err == OK) {
- // The modification date marks the date the project was last edited.
- // This is because the `project.godot` file will always be modified
- // when editing a project (but not when running it).
- last_edited = FileAccess::get_modified_time(conf);
-
- String fscache = p_path.path_join(".fscache");
- if (FileAccess::exists(fscache)) {
- uint64_t cache_modified = FileAccess::get_modified_time(fscache);
- if (cache_modified > last_edited) {
- last_edited = cache_modified;
- }
- }
- } else {
- grayed = true;
- missing = true;
- print_line("Project is missing: " + conf);
- }
-
- for (const String &tag : tags) {
- ProjectManager::get_singleton()->add_new_tag(tag);
- }
-
- return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
-}
-
-void ProjectList::_migrate_config() {
- // Proposal #1637 moved the project list from editor settings to a separate config file
- // If the new config file doesn't exist, populate it from EditorSettings
- if (FileAccess::exists(_config_path)) {
- return;
- }
-
- List<PropertyInfo> properties;
- EditorSettings::get_singleton()->get_property_list(&properties);
-
- for (const PropertyInfo &E : properties) {
- // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
- String property_key = E.name;
- if (!property_key.begins_with("projects/")) {
- continue;
- }
-
- String path = EDITOR_GET(property_key);
- print_line("Migrating legacy project '" + path + "'.");
-
- String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
- bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
- add_project(path, favorite);
- if (favorite) {
- EditorSettings::get_singleton()->erase(favoriteKey);
- }
- EditorSettings::get_singleton()->erase(property_key);
- }
-
- save_config();
-}
-
-void ProjectList::update_project_list() {
- // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
- // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
- // FIXME: Does it really have to be a full, hard reload? Runtime updates should be made much cheaper.
-
- // Clear whole list
- for (int i = 0; i < _projects.size(); ++i) {
- Item &project = _projects.write[i];
- CRASH_COND(project.control == nullptr);
- memdelete(project.control); // Why not queue_free()?
- }
- _projects.clear();
- _last_clicked = "";
- _selected_project_paths.clear();
-
- List<String> sections;
- _config.load(_config_path);
- _config.get_sections(&sections);
-
- for (const String &path : sections) {
- bool favorite = _config.get_value(path, "favorite", false);
- _projects.push_back(load_project_data(path, favorite));
- }
-
- // Create controls
- for (int i = 0; i < _projects.size(); ++i) {
- _create_project_item_control(i);
- }
-
- sort_projects();
- _update_icons_async();
- update_dock_menu();
-
- set_v_scroll(0);
- emit_signal(SNAME(SIGNAL_LIST_CHANGED));
-}
-
-void ProjectList::update_dock_menu() {
- if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) {
- return;
- }
- DisplayServer::get_singleton()->global_menu_clear("_dock");
-
- int favs_added = 0;
- int total_added = 0;
- for (int i = 0; i < _projects.size(); ++i) {
- if (!_projects[i].grayed && !_projects[i].missing) {
- if (_projects[i].favorite) {
- favs_added++;
- } else {
- if (favs_added != 0) {
- DisplayServer::get_singleton()->global_menu_add_separator("_dock");
- }
- favs_added = 0;
- }
- DisplayServer::get_singleton()->global_menu_add_item("_dock", _projects[i].project_name + " ( " + _projects[i].path + " )", callable_mp(this, &ProjectList::_global_menu_open_project), Callable(), i);
- total_added++;
- }
- }
- if (total_added != 0) {
- DisplayServer::get_singleton()->global_menu_add_separator("_dock");
- }
- DisplayServer::get_singleton()->global_menu_add_item("_dock", TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
-}
-
-void ProjectList::_global_menu_new_window(const Variant &p_tag) {
- List<String> args;
- args.push_back("-p");
- OS::get_singleton()->create_instance(args);
-}
-
-void ProjectList::_global_menu_open_project(const Variant &p_tag) {
- int idx = (int)p_tag;
-
- if (idx >= 0 && idx < _projects.size()) {
- String conf = _projects[idx].path.path_join("project.godot");
- List<String> args;
- args.push_back(conf);
- OS::get_singleton()->create_instance(args);
- }
-}
-
-void ProjectList::_create_project_item_control(int p_index) {
- // Will be added last in the list, so make sure indexes match
- ERR_FAIL_COND(p_index != _scroll_children->get_child_count());
-
- Item &item = _projects.write[p_index];
- ERR_FAIL_COND(item.control != nullptr); // Already created
-
- ProjectListItemControl *hb = memnew(ProjectListItemControl);
- hb->add_theme_constant_override("separation", 10 * EDSCALE);
-
- hb->set_project_title(!item.missing ? item.project_name : TTR("Missing Project"));
- hb->set_project_path(item.path);
- hb->set_tooltip_text(item.description);
- hb->set_tags(item.tags, this);
- hb->set_unsupported_features(item.unsupported_features.duplicate());
-
- hb->set_is_favorite(item.favorite);
- hb->set_is_missing(item.missing);
- hb->set_is_grayed(item.grayed);
-
- hb->connect("gui_input", callable_mp(this, &ProjectList::_panel_input).bind(hb));
- hb->connect("favorite_pressed", callable_mp(this, &ProjectList::_favorite_pressed).bind(hb));
-
-#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
- hb->connect("explore_pressed", callable_mp(this, &ProjectList::_show_project).bind(item.path));
-#endif
-
- _scroll_children->add_child(hb);
- item.control = hb;
-}
-
-void ProjectList::set_search_term(String p_search_term) {
- _search_term = p_search_term;
-}
-
-void ProjectList::set_order_option(int p_option) {
- FilterOption selected = (FilterOption)p_option;
- EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
- EditorSettings::get_singleton()->save();
- _order_option = selected;
-
- sort_projects();
-}
-
-void ProjectList::sort_projects() {
- SortArray<Item, ProjectListComparator> sorter;
- sorter.compare.order_option = _order_option;
- sorter.sort(_projects.ptrw(), _projects.size());
-
- String search_term;
- PackedStringArray tags;
-
- if (!_search_term.is_empty()) {
- PackedStringArray search_parts = _search_term.split(" ");
- if (search_parts.size() > 1 || search_parts[0].begins_with("tag:")) {
- PackedStringArray remaining;
- for (const String &part : search_parts) {
- if (part.begins_with("tag:")) {
- tags.push_back(part.get_slice(":", 1));
- } else {
- remaining.append(part);
- }
- }
- search_term = String(" ").join(remaining); // Search term without tags.
- } else {
- search_term = _search_term;
- }
- }
-
- for (int i = 0; i < _projects.size(); ++i) {
- Item &item = _projects.write[i];
-
- bool item_visible = true;
- if (!_search_term.is_empty()) {
- String search_path;
- if (search_term.contains("/")) {
- // Search path will match the whole path
- search_path = item.path;
- } else {
- // Search path will only match the last path component to make searching more strict
- search_path = item.path.get_file();
- }
-
- bool missing_tags = false;
- for (const String &tag : tags) {
- if (!item.tags.has(tag)) {
- missing_tags = true;
- break;
- }
- }
-
- // When searching, display projects whose name or path contain the search term and whose tags match the searched tags.
- item_visible = !missing_tags && (search_term.is_empty() || item.project_name.findn(search_term) != -1 || search_path.findn(search_term) != -1);
- }
-
- item.control->set_visible(item_visible);
- }
-
- for (int i = 0; i < _projects.size(); ++i) {
- Item &item = _projects.write[i];
- item.control->get_parent()->move_child(item.control, i);
- }
-
- // Rewind the coroutine because order of projects changed
- _update_icons_async();
- update_dock_menu();
-}
-
-const HashSet<String> &ProjectList::get_selected_project_keys() const {
- // Faster if that's all you need
- return _selected_project_paths;
-}
-
-Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
- Vector<Item> items;
- if (_selected_project_paths.size() == 0) {
- return items;
- }
- items.resize(_selected_project_paths.size());
- int j = 0;
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &item = _projects[i];
- if (_selected_project_paths.has(item.path)) {
- items.write[j++] = item;
- }
- }
- ERR_FAIL_COND_V(j != items.size(), items);
- return items;
-}
-
-void ProjectList::ensure_project_visible(int p_index) {
- const Item &item = _projects[p_index];
- ensure_control_visible(item.control);
-}
-
-int ProjectList::get_single_selected_index() const {
- if (_selected_project_paths.size() == 0) {
- // Default selection
- return 0;
- }
- String key;
- if (_selected_project_paths.size() == 1) {
- // Only one selected
- key = *_selected_project_paths.begin();
- } else {
- // Multiple selected, consider the last clicked one as "main"
- key = _last_clicked;
- }
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].path == key) {
- return i;
- }
- }
- return 0;
-}
-
-void ProjectList::_remove_project(int p_index, bool p_update_config) {
- const Item item = _projects[p_index]; // Take a copy
-
- _selected_project_paths.erase(item.path);
-
- if (_last_clicked == item.path) {
- _last_clicked = "";
- }
-
- memdelete(item.control);
- _projects.remove_at(p_index);
-
- if (p_update_config) {
- _config.erase_section(item.path);
- // Not actually saving the file, in case you are doing more changes to settings
- }
-
- update_dock_menu();
-}
-
-bool ProjectList::is_any_project_missing() const {
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].missing) {
- return true;
- }
- }
- return false;
-}
-
-void ProjectList::erase_missing_projects() {
- if (_projects.is_empty()) {
- return;
- }
-
- int deleted_count = 0;
- int remaining_count = 0;
-
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &item = _projects[i];
-
- if (item.missing) {
- _remove_project(i, true);
- --i;
- ++deleted_count;
-
- } else {
- ++remaining_count;
- }
- }
-
- print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
- save_config();
-}
-
-void ProjectList::_scan_folder_recursive(const String &p_path, List<String> *r_projects) {
- Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
- Error error = da->change_dir(p_path);
- ERR_FAIL_COND_MSG(error != OK, vformat("Failed to open the path \"%s\" for scanning (code %d).", p_path, error));
-
- da->list_dir_begin();
- String n = da->get_next();
- while (!n.is_empty()) {
- if (da->current_is_dir() && n[0] != '.') {
- _scan_folder_recursive(da->get_current_dir().path_join(n), r_projects);
- } else if (n == "project.godot") {
- r_projects->push_back(da->get_current_dir());
- }
- n = da->get_next();
- }
- da->list_dir_end();
-}
-
-void ProjectList::find_projects(const String &p_path) {
- PackedStringArray paths = { p_path };
- find_projects_multiple(paths);
-}
-
-void ProjectList::find_projects_multiple(const PackedStringArray &p_paths) {
- List<String> projects;
-
- for (int i = 0; i < p_paths.size(); i++) {
- const String &base_path = p_paths.get(i);
- print_verbose(vformat("Scanning for projects in \"%s\".", base_path));
-
- _scan_folder_recursive(base_path, &projects);
- print_verbose(vformat("Found %d project(s).", projects.size()));
- }
-
- for (const String &E : projects) {
- add_project(E, false);
- }
-
- save_config();
- update_project_list();
-}
-
-int ProjectList::refresh_project(const String &dir_path) {
- // Reloads information about a specific project.
- // If it wasn't loaded and should be in the list, it is added (i.e new project).
- // If it isn't in the list anymore, it is removed.
- // If it is in the list but doesn't exist anymore, it is marked as missing.
-
- bool should_be_in_list = _config.has_section(dir_path);
- bool is_favourite = _config.get_value(dir_path, "favorite", false);
-
- bool was_selected = _selected_project_paths.has(dir_path);
-
- // Remove item in any case
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &existing_item = _projects[i];
- if (existing_item.path == dir_path) {
- _remove_project(i, false);
- break;
- }
- }
-
- int index = -1;
- if (should_be_in_list) {
- // Recreate it with updated info
-
- Item item = load_project_data(dir_path, is_favourite);
-
- _projects.push_back(item);
- _create_project_item_control(_projects.size() - 1);
-
- sort_projects();
-
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].path == dir_path) {
- if (was_selected) {
- select_project(i);
- ensure_project_visible(i);
- }
- _load_project_icon(i);
-
- index = i;
- break;
- }
- }
- }
-
- return index;
-}
-
-void ProjectList::add_project(const String &dir_path, bool favorite) {
- if (!_config.has_section(dir_path)) {
- _config.set_value(dir_path, "favorite", favorite);
- }
-}
-
-void ProjectList::save_config() {
- _config.save(_config_path);
-}
-
-void ProjectList::set_project_version(const String &p_project_path, int p_version) {
- for (ProjectList::Item &E : _projects) {
- if (E.path == p_project_path) {
- E.version = p_version;
- break;
- }
- }
-}
-
-int ProjectList::get_project_count() const {
- return _projects.size();
-}
-
-void ProjectList::_clear_project_selection() {
- Vector<Item> previous_selected_items = get_selected_projects();
- _selected_project_paths.clear();
-
- for (int i = 0; i < previous_selected_items.size(); ++i) {
- previous_selected_items[i].control->set_selected(false);
- }
-}
-
-void ProjectList::_toggle_project(int p_index) {
- // This methods adds to the selection or removes from the
- // selection.
- Item &item = _projects.write[p_index];
-
- if (_selected_project_paths.has(item.path)) {
- _deselect_project_nocheck(p_index);
- } else {
- _select_project_nocheck(p_index);
- }
-}
-
-void ProjectList::_select_project_nocheck(int p_index) {
- Item &item = _projects.write[p_index];
- _selected_project_paths.insert(item.path);
- item.control->set_selected(true);
-}
-
-void ProjectList::_deselect_project_nocheck(int p_index) {
- Item &item = _projects.write[p_index];
- _selected_project_paths.erase(item.path);
- item.control->set_selected(false);
-}
-
-void ProjectList::select_project(int p_index) {
- // This method keeps only one project selected.
- _clear_project_selection();
- _select_project_nocheck(p_index);
-}
-
-void ProjectList::select_first_visible_project() {
- _clear_project_selection();
-
- for (int i = 0; i < _projects.size(); i++) {
- if (_projects[i].control->is_visible()) {
- _select_project_nocheck(i);
- break;
- }
- }
-}
-
-inline void _sort_project_range(int &a, int &b) {
- if (a > b) {
- int temp = a;
- a = b;
- b = temp;
- }
-}
-
-void ProjectList::_select_project_range(int p_begin, int p_end) {
- _clear_project_selection();
-
- _sort_project_range(p_begin, p_end);
- for (int i = p_begin; i <= p_end; ++i) {
- _select_project_nocheck(i);
- }
-}
-
-void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
- if (_selected_project_paths.size() == 0) {
- return;
- }
-
- for (int i = 0; i < _projects.size(); ++i) {
- Item &item = _projects.write[i];
- if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
- _config.erase_section(item.path);
-
- // Comment out for now until we have a better warning system to
- // ensure users delete their project only.
- //if (p_delete_project_contents) {
- // OS::get_singleton()->move_to_trash(item.path);
- //}
-
- memdelete(item.control);
- _projects.remove_at(i);
- --i;
- }
- }
-
- save_config();
- _selected_project_paths.clear();
- _last_clicked = "";
-
- update_dock_menu();
-}
-
-// Input for each item in the list.
-void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
- Ref<InputEventMouseButton> mb = p_ev;
- int clicked_index = p_hb->get_index();
- const Item &clicked_project = _projects[clicked_index];
-
- if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
- if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
- int anchor_index = -1;
- for (int i = 0; i < _projects.size(); ++i) {
- const Item &p = _projects[i];
- if (p.path == _last_clicked) {
- anchor_index = p.control->get_index();
- break;
- }
- }
- CRASH_COND(anchor_index == -1);
- _select_project_range(anchor_index, clicked_index);
-
- } else if (mb->is_command_or_control_pressed()) {
- _toggle_project(clicked_index);
-
- } else {
- _last_clicked = clicked_project.path;
- select_project(clicked_index);
- }
-
- emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
-
- // Do not allow opening a project more than once using a single project manager instance.
- // Opening the same project in several editor instances at once can lead to various issues.
- if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) {
- emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
- }
- }
-}
-
-void ProjectList::_favorite_pressed(Node *p_hb) {
- ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
-
- int index = control->get_index();
- Item item = _projects.write[index]; // Take copy
-
- item.favorite = !item.favorite;
-
- _config.set_value(item.path, "favorite", item.favorite);
- save_config();
-
- _projects.write[index] = item;
-
- control->set_is_favorite(item.favorite);
-
- sort_projects();
-
- if (item.favorite) {
- for (int i = 0; i < _projects.size(); ++i) {
- if (_projects[i].path == item.path) {
- ensure_project_visible(i);
- break;
- }
- }
- }
-
- update_dock_menu();
-}
-
-void ProjectList::_show_project(const String &p_path) {
- OS::get_singleton()->shell_show_in_file_manager(p_path, true);
-}
-
-void ProjectList::_bind_methods() {
- ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
- ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
- ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
-}
-
-ProjectList::ProjectList() {
- _scroll_children = memnew(VBoxContainer);
- _scroll_children->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- add_child(_scroll_children);
-
- _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
- _migrate_config();
-}
-
-/// Project Manager.
-
ProjectManager *ProjectManager::singleton = nullptr;
+// Notifications.
+
void ProjectManager::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_TRANSLATION_CHANGED:
@@ -2102,6 +176,8 @@ void ProjectManager::_notification(int p_what) {
}
}
+// Utility data.
+
Ref<Texture2D> ProjectManager::_file_dialog_get_icon(const String &p_path) {
if (p_path.get_extension().to_lower() == "godot") {
return singleton->icon_type_cache["GodotMonochrome"];
@@ -2129,6 +205,8 @@ void ProjectManager::_build_icon_type_cache(Ref<Theme> p_theme) {
}
}
+// Main layout.
+
void ProjectManager::_update_size_limits() {
const Size2 minimum_size = Size2(680, 450) * EDSCALE;
const Size2 default_size = Size2(1024, 600) * EDSCALE;
@@ -2159,154 +237,116 @@ void ProjectManager::_update_size_limits() {
}
}
-void ProjectManager::_dim_window() {
- // This method must be called before calling `get_tree()->quit()`.
- // Otherwise, its effect won't be visible
-
- // Dim the project manager window while it's quitting to make it clearer that it's busy.
- // No transition is applied, as the effect needs to be visible immediately
- float c = 0.5f;
- Color dim_color = Color(c, c, c);
- set_modulate(dim_color);
+void ProjectManager::_show_about() {
+ about->popup_centered(Size2(780, 500) * EDSCALE);
}
-void ProjectManager::_update_project_buttons() {
- Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
- bool empty_selection = selected_projects.is_empty();
+void ProjectManager::_version_button_pressed() {
+ DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
+}
- bool is_missing_project_selected = false;
- for (int i = 0; i < selected_projects.size(); ++i) {
- if (selected_projects[i].missing) {
- is_missing_project_selected = true;
- break;
- }
+void ProjectManager::_on_tab_changed(int p_tab) {
+#ifndef ANDROID_ENABLED
+ if (p_tab == 0) { // Projects
+ // Automatically grab focus when the user moves from the Templates tab
+ // back to the Projects tab.
+ search_box->grab_focus();
}
- erase_btn->set_disabled(empty_selection);
- open_btn->set_disabled(empty_selection || is_missing_project_selected);
- rename_btn->set_disabled(empty_selection || is_missing_project_selected);
- manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
- run_btn->set_disabled(empty_selection || is_missing_project_selected);
+ // The Templates tab's search field is focused on display in the asset
+ // library editor plugin code.
+#endif
+}
- erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
+void ProjectManager::_open_asset_library() {
+ asset_library->disable_community_support();
+ tabs->set_current_tab(1);
}
-void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
- ERR_FAIL_COND(p_ev.is_null());
+// Quick settings.
- Ref<InputEventKey> k = p_ev;
+void ProjectManager::_language_selected(int p_id) {
+ String lang = language_btn->get_item_metadata(p_id);
+ EditorSettings::get_singleton()->set("interface/editor/editor_language", lang);
- if (k.is_valid()) {
- if (!k->is_pressed()) {
- return;
- }
+ restart_required_dialog->popup_centered();
+}
- // Pressing Command + Q quits the Project Manager
- // This is handled by the platform implementation on macOS,
- // so only define the shortcut on other platforms
-#ifndef MACOS_ENABLED
- if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
- _dim_window();
- get_tree()->quit();
- }
-#endif
+void ProjectManager::_restart_confirm() {
+ List<String> args = OS::get_singleton()->get_cmdline_args();
+ Error err = OS::get_singleton()->create_instance(args);
+ ERR_FAIL_COND(err);
- if (tabs->get_current_tab() != 0) {
- return;
- }
+ _dim_window();
+ get_tree()->quit();
+}
- bool keycode_handled = true;
+void ProjectManager::_dim_window() {
+ // This method must be called before calling `get_tree()->quit()`.
+ // Otherwise, its effect won't be visible
- switch (k->get_keycode()) {
- case Key::ENTER: {
- _open_selected_projects_ask();
- } break;
- case Key::HOME: {
- if (_project_list->get_project_count() > 0) {
- _project_list->select_project(0);
- _update_project_buttons();
- }
+ // Dim the project manager window while it's quitting to make it clearer that it's busy.
+ // No transition is applied, as the effect needs to be visible immediately
+ float c = 0.5f;
+ Color dim_color = Color(c, c, c);
+ set_modulate(dim_color);
+}
- } break;
- case Key::END: {
- if (_project_list->get_project_count() > 0) {
- _project_list->select_project(_project_list->get_project_count() - 1);
- _update_project_buttons();
- }
+// Project list.
- } break;
- case Key::UP: {
- if (k->is_shift_pressed()) {
- break;
- }
+void ProjectManager::_scan_projects() {
+ scan_dir->popup_file_dialog();
+}
- int index = _project_list->get_single_selected_index();
- if (index > 0) {
- _project_list->select_project(index - 1);
- _project_list->ensure_project_visible(index - 1);
- _update_project_buttons();
- }
+void ProjectManager::_run_project() {
+ const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
- break;
- }
- case Key::DOWN: {
- if (k->is_shift_pressed()) {
- break;
- }
+ if (selected_list.size() < 1) {
+ return;
+ }
- int index = _project_list->get_single_selected_index();
- if (index + 1 < _project_list->get_project_count()) {
- _project_list->select_project(index + 1);
- _project_list->ensure_project_visible(index + 1);
- _update_project_buttons();
- }
+ if (selected_list.size() > 1) {
+ multi_run_ask->set_text(vformat(TTR("Are you sure to run %d projects at once?"), selected_list.size()));
+ multi_run_ask->popup_centered();
+ } else {
+ _run_project_confirm();
+ }
+}
- } break;
- case Key::F: {
- if (k->is_command_or_control_pressed()) {
- this->search_box->grab_focus();
- } else {
- keycode_handled = false;
- }
- } break;
- default: {
- keycode_handled = false;
- } break;
+void ProjectManager::_run_project_confirm() {
+ Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+
+ for (int i = 0; i < selected_list.size(); ++i) {
+ const String &selected_main = selected_list[i].main_scene;
+ if (selected_main.is_empty()) {
+ run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category."));
+ run_error_diag->popup_centered();
+ continue;
}
- if (keycode_handled) {
- accept_event();
+ const String &path = selected_list[i].path;
+
+ // `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
+ if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
+ run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
+ run_error_diag->popup_centered();
+ continue;
}
- }
-}
-void ProjectManager::_on_projects_updated() {
- Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
- int index = 0;
- for (int i = 0; i < selected_projects.size(); ++i) {
- index = _project_list->refresh_project(selected_projects[i].path);
- }
- if (index != -1) {
- _project_list->ensure_project_visible(index);
- }
+ print_line("Running project: " + path);
- _project_list->update_dock_menu();
-}
+ List<String> args;
-void ProjectManager::_on_project_created(const String &dir) {
- _project_list->add_project(dir, false);
- _project_list->save_config();
- search_box->clear();
- int i = _project_list->refresh_project(dir);
- _project_list->select_project(i);
- _project_list->ensure_project_visible(i);
- _open_selected_projects_ask();
+ for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
+ args.push_back(a);
+ }
- _project_list->update_dock_menu();
-}
+ args.push_back("--path");
+ args.push_back(path);
-void ProjectManager::_confirm_update_settings() {
- _open_selected_projects();
+ Error err = OS::get_singleton()->create_instance(args);
+ ERR_FAIL_COND(err);
+ }
}
void ProjectManager::_open_selected_projects() {
@@ -2417,7 +457,7 @@ void ProjectManager::_open_selected_projects_ask() {
warning_message += TTR("Warning: This project uses C#, but this build of Godot does not have\nthe Mono module. If you proceed you will not be able to use any C# scripts.\n\n");
unsupported_features.remove_at(i);
i--;
- } else if (_project_feature_looks_like_version(feature)) {
+ } else if (ProjectList::project_feature_looks_like_version(feature)) {
warning_message += vformat(TTR("Warning: This project was last edited in Godot %s. Opening will change it to Godot %s.\n\n"), Variant(feature), Variant(VERSION_BRANCH));
unsupported_features.remove_at(i);
i--;
@@ -2438,113 +478,148 @@ void ProjectManager::_open_selected_projects_ask() {
_open_selected_projects();
}
-void ProjectManager::_full_convert_button_pressed() {
- ask_update_settings->hide();
- ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 0));
- ask_full_convert_dialog->get_cancel_button()->grab_focus();
+void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
+ npdialog->set_mode(ProjectDialog::MODE_INSTALL);
+ npdialog->set_zip_path(p_zip_path);
+ npdialog->set_zip_title(p_title);
+ npdialog->show_dialog();
}
-void ProjectManager::_perform_full_project_conversion() {
- Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
- if (selected_list.is_empty()) {
- return;
- }
+void ProjectManager::_import_project() {
+ npdialog->set_mode(ProjectDialog::MODE_IMPORT);
+ npdialog->ask_for_path_and_show();
+}
- const String &path = selected_list[0].path;
+void ProjectManager::_new_project() {
+ npdialog->set_mode(ProjectDialog::MODE_NEW);
+ npdialog->show_dialog();
+}
- print_line("Converting project: " + path);
- List<String> args;
- args.push_back("--path");
- args.push_back(path);
- args.push_back("--convert-3to4");
- args.push_back("--rendering-driver");
- args.push_back(Main::get_rendering_driver_name());
+void ProjectManager::_rename_project() {
+ const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
- Error err = OS::get_singleton()->create_instance(args);
- ERR_FAIL_COND(err);
+ if (selected_list.size() == 0) {
+ return;
+ }
- _project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
+ for (const String &E : selected_list) {
+ npdialog->set_project_path(E);
+ npdialog->set_mode(ProjectDialog::MODE_RENAME);
+ npdialog->show_dialog();
+ }
}
-void ProjectManager::_run_project_confirm() {
- Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+void ProjectManager::_erase_project() {
+ const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
- for (int i = 0; i < selected_list.size(); ++i) {
- const String &selected_main = selected_list[i].main_scene;
- if (selected_main.is_empty()) {
- run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category."));
- run_error_diag->popup_centered();
- continue;
- }
+ if (selected_list.size() == 0) {
+ return;
+ }
- const String &path = selected_list[i].path;
+ String confirm_message;
+ if (selected_list.size() >= 2) {
+ confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size());
+ } else {
+ confirm_message = TTR("Remove this project from the list?");
+ }
- // `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
- if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
- run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
- run_error_diag->popup_centered();
- continue;
- }
+ erase_ask_label->set_text(confirm_message);
+ //delete_project_contents->set_pressed(false);
+ erase_ask->popup_centered();
+}
- print_line("Running project: " + path);
+void ProjectManager::_erase_missing_projects() {
+ erase_missing_ask->set_text(TTR("Remove all missing projects from the list?\nThe project folders' contents won't be modified."));
+ erase_missing_ask->popup_centered();
+}
- List<String> args;
+void ProjectManager::_erase_project_confirm() {
+ _project_list->erase_selected_projects(false);
+ _update_project_buttons();
+}
- for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
- args.push_back(a);
- }
+void ProjectManager::_erase_missing_projects_confirm() {
+ _project_list->erase_missing_projects();
+ _update_project_buttons();
+}
- args.push_back("--path");
- args.push_back(path);
+void ProjectManager::_update_project_buttons() {
+ Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
+ bool empty_selection = selected_projects.is_empty();
- Error err = OS::get_singleton()->create_instance(args);
- ERR_FAIL_COND(err);
+ bool is_missing_project_selected = false;
+ for (int i = 0; i < selected_projects.size(); ++i) {
+ if (selected_projects[i].missing) {
+ is_missing_project_selected = true;
+ break;
+ }
}
-}
-void ProjectManager::_run_project() {
- const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+ erase_btn->set_disabled(empty_selection);
+ open_btn->set_disabled(empty_selection || is_missing_project_selected);
+ rename_btn->set_disabled(empty_selection || is_missing_project_selected);
+ manage_tags_btn->set_disabled(empty_selection || is_missing_project_selected || selected_projects.size() > 1);
+ run_btn->set_disabled(empty_selection || is_missing_project_selected);
- if (selected_list.size() < 1) {
- return;
- }
+ erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
+}
- if (selected_list.size() > 1) {
- multi_run_ask->set_text(vformat(TTR("Are you sure to run %d projects at once?"), selected_list.size()));
- multi_run_ask->popup_centered();
- } else {
- _run_project_confirm();
+void ProjectManager::_on_projects_updated() {
+ Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
+ int index = 0;
+ for (int i = 0; i < selected_projects.size(); ++i) {
+ index = _project_list->refresh_project(selected_projects[i].path);
+ }
+ if (index != -1) {
+ _project_list->ensure_project_visible(index);
}
-}
-void ProjectManager::_scan_projects() {
- scan_dir->popup_file_dialog();
+ _project_list->update_dock_menu();
}
-void ProjectManager::_new_project() {
- npdialog->set_mode(ProjectDialog::MODE_NEW);
- npdialog->show_dialog();
+void ProjectManager::_on_project_created(const String &dir) {
+ _project_list->add_project(dir, false);
+ _project_list->save_config();
+ search_box->clear();
+ int i = _project_list->refresh_project(dir);
+ _project_list->select_project(i);
+ _project_list->ensure_project_visible(i);
+ _open_selected_projects_ask();
+
+ _project_list->update_dock_menu();
}
-void ProjectManager::_import_project() {
- npdialog->set_mode(ProjectDialog::MODE_IMPORT);
- npdialog->ask_for_path_and_show();
+void ProjectManager::_on_order_option_changed(int p_idx) {
+ if (is_inside_tree()) {
+ _project_list->set_order_option(p_idx);
+ }
}
-void ProjectManager::_rename_project() {
- const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+void ProjectManager::_on_search_term_changed(const String &p_term) {
+ _project_list->set_search_term(p_term);
+ _project_list->sort_projects();
- if (selected_list.size() == 0) {
+ // Select the first visible project in the list.
+ // This makes it possible to open a project without ever touching the mouse,
+ // as the search field is automatically focused on startup.
+ _project_list->select_first_visible_project();
+ _update_project_buttons();
+}
+
+void ProjectManager::_on_search_term_submitted(const String &p_text) {
+ if (tabs->get_current_tab() != 0) {
return;
}
- for (const String &E : selected_list) {
- npdialog->set_project_path(E);
- npdialog->set_mode(ProjectDialog::MODE_RENAME);
- npdialog->show_dialog();
- }
+ _open_selected_projects_ask();
}
+LineEdit *ProjectManager::get_search_box() {
+ return search_box;
+}
+
+// Project tag management.
+
void ProjectManager::_manage_project_tags() {
for (int i = 0; i < project_tags->get_child_count(); i++) {
project_tags->get_child(i)->queue_free();
@@ -2653,66 +728,135 @@ void ProjectManager::_create_new_tag() {
_add_project_tag(new_tag_name->get_text());
}
-void ProjectManager::_erase_project_confirm() {
- _project_list->erase_selected_projects(false);
- _update_project_buttons();
+void ProjectManager::add_new_tag(const String &p_tag) {
+ if (!tag_set.has(p_tag)) {
+ tag_set.insert(p_tag);
+ ProjectTag *tag_control = memnew(ProjectTag(p_tag));
+ all_tags->add_child(tag_control);
+ all_tags->move_child(tag_control, -2);
+ tag_control->connect_button_to(callable_mp(this, &ProjectManager::_add_project_tag).bind(p_tag));
+ }
}
-void ProjectManager::_erase_missing_projects_confirm() {
- _project_list->erase_missing_projects();
- _update_project_buttons();
-}
+// Project converter/migration tool.
-void ProjectManager::_erase_project() {
- const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
+void ProjectManager::_full_convert_button_pressed() {
+ ask_update_settings->hide();
+ ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 0));
+ ask_full_convert_dialog->get_cancel_button()->grab_focus();
+}
- if (selected_list.size() == 0) {
+void ProjectManager::_perform_full_project_conversion() {
+ Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
+ if (selected_list.is_empty()) {
return;
}
- String confirm_message;
- if (selected_list.size() >= 2) {
- confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size());
- } else {
- confirm_message = TTR("Remove this project from the list?");
- }
+ const String &path = selected_list[0].path;
- erase_ask_label->set_text(confirm_message);
- //delete_project_contents->set_pressed(false);
- erase_ask->popup_centered();
-}
+ print_line("Converting project: " + path);
+ List<String> args;
+ args.push_back("--path");
+ args.push_back(path);
+ args.push_back("--convert-3to4");
+ args.push_back("--rendering-driver");
+ args.push_back(Main::get_rendering_driver_name());
-void ProjectManager::_erase_missing_projects() {
- erase_missing_ask->set_text(TTR("Remove all missing projects from the list?\nThe project folders' contents won't be modified."));
- erase_missing_ask->popup_centered();
-}
+ Error err = OS::get_singleton()->create_instance(args);
+ ERR_FAIL_COND(err);
-void ProjectManager::_show_about() {
- about->popup_centered(Size2(780, 500) * EDSCALE);
+ _project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
}
-void ProjectManager::_language_selected(int p_id) {
- String lang = language_btn->get_item_metadata(p_id);
- EditorSettings::get_singleton()->set("interface/editor/editor_language", lang);
+// Input and I/O.
- language_restart_ask->set_text(TTR("Language changed.\nThe interface will update after restarting the editor or project manager."));
- language_restart_ask->popup_centered();
-}
+void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
+ ERR_FAIL_COND(p_ev.is_null());
-void ProjectManager::_restart_confirm() {
- List<String> args = OS::get_singleton()->get_cmdline_args();
- Error err = OS::get_singleton()->create_instance(args);
- ERR_FAIL_COND(err);
+ Ref<InputEventKey> k = p_ev;
- _dim_window();
- get_tree()->quit();
-}
+ if (k.is_valid()) {
+ if (!k->is_pressed()) {
+ return;
+ }
-void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
- npdialog->set_mode(ProjectDialog::MODE_INSTALL);
- npdialog->set_zip_path(p_zip_path);
- npdialog->set_zip_title(p_title);
- npdialog->show_dialog();
+ // Pressing Command + Q quits the Project Manager
+ // This is handled by the platform implementation on macOS,
+ // so only define the shortcut on other platforms
+#ifndef MACOS_ENABLED
+ if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
+ _dim_window();
+ get_tree()->quit();
+ }
+#endif
+
+ if (tabs->get_current_tab() != 0) {
+ return;
+ }
+
+ bool keycode_handled = true;
+
+ switch (k->get_keycode()) {
+ case Key::ENTER: {
+ _open_selected_projects_ask();
+ } break;
+ case Key::HOME: {
+ if (_project_list->get_project_count() > 0) {
+ _project_list->select_project(0);
+ _update_project_buttons();
+ }
+
+ } break;
+ case Key::END: {
+ if (_project_list->get_project_count() > 0) {
+ _project_list->select_project(_project_list->get_project_count() - 1);
+ _update_project_buttons();
+ }
+
+ } break;
+ case Key::UP: {
+ if (k->is_shift_pressed()) {
+ break;
+ }
+
+ int index = _project_list->get_single_selected_index();
+ if (index > 0) {
+ _project_list->select_project(index - 1);
+ _project_list->ensure_project_visible(index - 1);
+ _update_project_buttons();
+ }
+
+ break;
+ }
+ case Key::DOWN: {
+ if (k->is_shift_pressed()) {
+ break;
+ }
+
+ int index = _project_list->get_single_selected_index();
+ if (index + 1 < _project_list->get_project_count()) {
+ _project_list->select_project(index + 1);
+ _project_list->ensure_project_visible(index + 1);
+ _update_project_buttons();
+ }
+
+ } break;
+ case Key::F: {
+ if (k->is_command_or_control_pressed()) {
+ this->search_box->grab_focus();
+ } else {
+ keycode_handled = false;
+ }
+ } break;
+ default: {
+ keycode_handled = false;
+ } break;
+ }
+
+ if (keycode_handled) {
+ accept_event();
+ }
+ }
}
void ProjectManager::_files_dropped(PackedStringArray p_files) {
@@ -2738,99 +882,23 @@ void ProjectManager::_files_dropped(PackedStringArray p_files) {
_project_list->find_projects_multiple(folders);
}
-void ProjectManager::_on_order_option_changed(int p_idx) {
- if (is_inside_tree()) {
- _project_list->set_order_option(p_idx);
- }
-}
-
-void ProjectManager::_on_tab_changed(int p_tab) {
-#ifndef ANDROID_ENABLED
- if (p_tab == 0) { // Projects
- // Automatically grab focus when the user moves from the Templates tab
- // back to the Projects tab.
- search_box->grab_focus();
- }
-
- // The Templates tab's search field is focused on display in the asset
- // library editor plugin code.
-#endif
-}
-
-void ProjectManager::_on_search_term_changed(const String &p_term) {
- _project_list->set_search_term(p_term);
- _project_list->sort_projects();
-
- // Select the first visible project in the list.
- // This makes it possible to open a project without ever touching the mouse,
- // as the search field is automatically focused on startup.
- _project_list->select_first_visible_project();
- _update_project_buttons();
-}
-
-void ProjectManager::_on_search_term_submitted(const String &p_text) {
- if (tabs->get_current_tab() != 0) {
- return;
- }
-
- _open_selected_projects_ask();
-}
-
-void ProjectManager::_open_asset_library() {
- asset_library->disable_community_support();
- tabs->set_current_tab(1);
-}
-
-void ProjectManager::_version_button_pressed() {
- DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
-}
-
-LineEdit *ProjectManager::get_search_box() {
- return search_box;
-}
-
-void ProjectManager::add_new_tag(const String &p_tag) {
- if (!tag_set.has(p_tag)) {
- tag_set.insert(p_tag);
- ProjectTag *tag_control = memnew(ProjectTag(p_tag));
- all_tags->add_child(tag_control);
- all_tags->move_child(tag_control, -2);
- tag_control->connect_button_to(callable_mp(this, &ProjectManager::_add_project_tag).bind(p_tag));
- }
-}
-
-void ProjectList::add_search_tag(const String &p_tag) {
- const String tag_string = "tag:" + p_tag;
-
- int exists = _search_term.find(tag_string);
- if (exists > -1) {
- _search_term = _search_term.erase(exists, tag_string.length() + 1);
- } else if (_search_term.is_empty() || _search_term.ends_with(" ")) {
- _search_term += tag_string;
- } else {
- _search_term += " " + tag_string;
- }
- ProjectManager::get_singleton()->get_search_box()->set_text(_search_term);
-
- sort_projects();
-}
+// Object methods.
ProjectManager::ProjectManager() {
singleton = this;
- // load settings
- if (!EditorSettings::get_singleton()) {
- EditorSettings::create();
- }
-
// Turn off some servers we aren't going to be using in the Project Manager.
NavigationServer3D::get_singleton()->set_active(false);
PhysicsServer3D::get_singleton()->set_active(false);
PhysicsServer2D::get_singleton()->set_active(false);
- EditorSettings::get_singleton()->set_optimize_save(false); //just write settings as they came
-
+ // Initialize settings.
{
+ if (!EditorSettings::get_singleton()) {
+ EditorSettings::create();
+ }
+ EditorSettings::get_singleton()->set_optimize_save(false); // Just write settings as they come.
+
int display_scale = EDITOR_GET("interface/editor/display_scale");
switch (display_scale) {
@@ -2862,30 +930,41 @@ ProjectManager::ProjectManager() {
}
EditorFileDialog::get_icon_func = &ProjectManager::_file_dialog_get_icon;
EditorFileDialog::get_thumbnail_func = &ProjectManager::_file_dialog_get_thumbnail;
+
+ EditorFileDialog::set_default_show_hidden_files(EDITOR_GET("filesystem/file_dialog/show_hidden_files"));
+ EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EDITOR_GET("filesystem/file_dialog/display_mode").operator int());
+
+ int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
+ if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer.
+ // Swap on means OK first.
+ AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2);
+ }
+
+ OS::get_singleton()->set_low_processor_usage_mode(true);
}
// TRANSLATORS: This refers to the application where users manage their Godot projects.
DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager", "Application"));
- EditorFileDialog::set_default_show_hidden_files(EDITOR_GET("filesystem/file_dialog/show_hidden_files"));
- EditorFileDialog::set_default_display_mode((EditorFileDialog::DisplayMode)EDITOR_GET("filesystem/file_dialog/display_mode").operator int());
+ SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
- int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
- if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer.
- // Swap on means OK first.
- AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2);
- }
+ // Initialize UI.
+ {
+ int pm_root_dir = EDITOR_GET("interface/editor/ui_layout_direction");
+ Control::set_root_layout_direction(pm_root_dir);
+ Window::set_root_layout_direction(pm_root_dir);
- int pm_root_dir = EDITOR_GET("interface/editor/ui_layout_direction");
- Control::set_root_layout_direction(pm_root_dir);
- Window::set_root_layout_direction(pm_root_dir);
+ EditorThemeManager::initialize();
+ Ref<Theme> theme = EditorThemeManager::generate_theme();
+ DisplayServer::set_early_window_clear_color_override(true, theme->get_color(SNAME("background"), EditorStringName(Editor)));
- EditorThemeManager::initialize();
- Ref<Theme> theme = EditorThemeManager::generate_theme();
- DisplayServer::set_early_window_clear_color_override(true, theme->get_color(SNAME("background"), EditorStringName(Editor)));
+ set_theme(theme);
+ set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
- set_theme(theme);
- set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+ _build_icon_type_cache(theme);
+ }
+
+ // Project manager layout.
background_panel = memnew(Panel);
add_child(background_panel);
@@ -2904,139 +983,8 @@ ProjectManager::ProjectManager() {
center_box->add_child(tabs);
tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed));
- local_projects_vb = memnew(VBoxContainer);
- local_projects_vb->set_name(TTR("Local Projects"));
- tabs->add_child(local_projects_vb);
-
+ // Quick settings.
{
- // A bar at top with buttons and options.
- HBoxContainer *hb = memnew(HBoxContainer);
- hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- local_projects_vb->add_child(hb);
-
- create_btn = memnew(Button);
- create_btn->set_text(TTR("New"));
- create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
- create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
- hb->add_child(create_btn);
-
- import_btn = memnew(Button);
- import_btn->set_text(TTR("Import"));
- import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
- import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
- hb->add_child(import_btn);
-
- scan_btn = memnew(Button);
- scan_btn->set_text(TTR("Scan"));
- scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
- scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
- hb->add_child(scan_btn);
-
- loading_label = memnew(Label(TTR("Loading, please wait...")));
- loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- hb->add_child(loading_label);
- // The loading label is shown later.
- loading_label->hide();
-
- search_box = memnew(LineEdit);
- search_box->set_placeholder(TTR("Filter Projects"));
- search_box->set_tooltip_text(TTR("This field filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character."));
- search_box->set_clear_button_enabled(true);
- search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
- search_box->connect("text_submitted", callable_mp(this, &ProjectManager::_on_search_term_submitted));
- search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- hb->add_child(search_box);
-
- Label *sort_label = memnew(Label);
- sort_label->set_text(TTR("Sort:"));
- hb->add_child(sort_label);
-
- filter_option = memnew(OptionButton);
- filter_option->set_clip_text(true);
- filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- filter_option->set_stretch_ratio(0.3);
- filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
- hb->add_child(filter_option);
-
- Vector<String> sort_filter_titles;
- sort_filter_titles.push_back(TTR("Last Edited"));
- sort_filter_titles.push_back(TTR("Name"));
- sort_filter_titles.push_back(TTR("Path"));
- sort_filter_titles.push_back(TTR("Tags"));
-
- for (int i = 0; i < sort_filter_titles.size(); i++) {
- filter_option->add_item(sort_filter_titles[i]);
- }
- }
-
- {
- // A container for the project list and for the side bar with buttons.
- HBoxContainer *search_tree_hb = memnew(HBoxContainer);
- local_projects_vb->add_child(search_tree_hb);
- search_tree_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-
- search_panel = memnew(PanelContainer);
- search_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
- search_tree_hb->add_child(search_panel);
-
- _project_list = memnew(ProjectList);
- _project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
- search_panel->add_child(_project_list);
- _project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
- _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
- _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
-
- // The side bar with the edit, run, rename, etc. buttons.
- VBoxContainer *tree_vb = memnew(VBoxContainer);
- tree_vb->set_custom_minimum_size(Size2(120, 120));
- search_tree_hb->add_child(tree_vb);
-
- tree_vb->add_child(memnew(HSeparator));
-
- open_btn = memnew(Button);
- open_btn->set_text(TTR("Edit"));
- open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
- open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
- tree_vb->add_child(open_btn);
-
- run_btn = memnew(Button);
- run_btn->set_text(TTR("Run"));
- run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
- run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
- tree_vb->add_child(run_btn);
-
- rename_btn = memnew(Button);
- rename_btn->set_text(TTR("Rename"));
- // The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
- rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
- rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
- tree_vb->add_child(rename_btn);
-
- manage_tags_btn = memnew(Button);
- manage_tags_btn->set_text(TTR("Manage Tags"));
- tree_vb->add_child(manage_tags_btn);
-
- erase_btn = memnew(Button);
- erase_btn->set_text(TTR("Remove"));
- erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
- erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
- tree_vb->add_child(erase_btn);
-
- erase_missing_btn = memnew(Button);
- erase_missing_btn->set_text(TTR("Remove Missing"));
- erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
- tree_vb->add_child(erase_missing_btn);
-
- tree_vb->add_spacer();
-
- about_btn = memnew(Button);
- about_btn->set_text(TTR("About"));
- about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
- tree_vb->add_child(about_btn);
- }
-
- {
- // Version info and language options
settings_hb = memnew(HBoxContainer);
settings_hb->set_alignment(BoxContainer::ALIGNMENT_END);
settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
@@ -3106,6 +1054,140 @@ ProjectManager::ProjectManager() {
center_box->add_child(settings_hb);
}
+ // Project list view.
+ {
+ local_projects_vb = memnew(VBoxContainer);
+ local_projects_vb->set_name(TTR("Local Projects"));
+ tabs->add_child(local_projects_vb);
+
+ // Project list's top bar.
+ {
+ HBoxContainer *hb = memnew(HBoxContainer);
+ hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ local_projects_vb->add_child(hb);
+
+ create_btn = memnew(Button);
+ create_btn->set_text(TTR("New"));
+ create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
+ create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
+ hb->add_child(create_btn);
+
+ import_btn = memnew(Button);
+ import_btn->set_text(TTR("Import"));
+ import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
+ import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
+ hb->add_child(import_btn);
+
+ scan_btn = memnew(Button);
+ scan_btn->set_text(TTR("Scan"));
+ scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
+ scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
+ hb->add_child(scan_btn);
+
+ loading_label = memnew(Label(TTR("Loading, please wait...")));
+ loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ hb->add_child(loading_label);
+ // The loading label is shown later.
+ loading_label->hide();
+
+ search_box = memnew(LineEdit);
+ search_box->set_placeholder(TTR("Filter Projects"));
+ search_box->set_tooltip_text(TTR("This field filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character."));
+ search_box->set_clear_button_enabled(true);
+ search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
+ search_box->connect("text_submitted", callable_mp(this, &ProjectManager::_on_search_term_submitted));
+ search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ hb->add_child(search_box);
+
+ Label *sort_label = memnew(Label);
+ sort_label->set_text(TTR("Sort:"));
+ hb->add_child(sort_label);
+
+ filter_option = memnew(OptionButton);
+ filter_option->set_clip_text(true);
+ filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ filter_option->set_stretch_ratio(0.3);
+ filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
+ hb->add_child(filter_option);
+
+ Vector<String> sort_filter_titles;
+ sort_filter_titles.push_back(TTR("Last Edited"));
+ sort_filter_titles.push_back(TTR("Name"));
+ sort_filter_titles.push_back(TTR("Path"));
+ sort_filter_titles.push_back(TTR("Tags"));
+
+ for (int i = 0; i < sort_filter_titles.size(); i++) {
+ filter_option->add_item(sort_filter_titles[i]);
+ }
+ }
+
+ // Project list and its sidebar.
+ {
+ HBoxContainer *search_tree_hb = memnew(HBoxContainer);
+ local_projects_vb->add_child(search_tree_hb);
+ search_tree_hb->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+
+ search_panel = memnew(PanelContainer);
+ search_panel->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ search_tree_hb->add_child(search_panel);
+
+ _project_list = memnew(ProjectList);
+ _project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
+ search_panel->add_child(_project_list);
+ _project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+ _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+ _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
+
+ // The side bar with the edit, run, rename, etc. buttons.
+ VBoxContainer *tree_vb = memnew(VBoxContainer);
+ tree_vb->set_custom_minimum_size(Size2(120, 120));
+ search_tree_hb->add_child(tree_vb);
+
+ tree_vb->add_child(memnew(HSeparator));
+
+ open_btn = memnew(Button);
+ open_btn->set_text(TTR("Edit"));
+ open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
+ open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
+ tree_vb->add_child(open_btn);
+
+ run_btn = memnew(Button);
+ run_btn->set_text(TTR("Run"));
+ run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
+ run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
+ tree_vb->add_child(run_btn);
+
+ rename_btn = memnew(Button);
+ rename_btn->set_text(TTR("Rename"));
+ // The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
+ rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
+ rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
+ tree_vb->add_child(rename_btn);
+
+ manage_tags_btn = memnew(Button);
+ manage_tags_btn->set_text(TTR("Manage Tags"));
+ tree_vb->add_child(manage_tags_btn);
+
+ erase_btn = memnew(Button);
+ erase_btn->set_text(TTR("Remove"));
+ erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
+ erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
+ tree_vb->add_child(erase_btn);
+
+ erase_missing_btn = memnew(Button);
+ erase_missing_btn->set_text(TTR("Remove Missing"));
+ erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
+ tree_vb->add_child(erase_missing_btn);
+
+ tree_vb->add_spacer();
+
+ about_btn = memnew(Button);
+ about_btn->set_text(TTR("About"));
+ about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
+ tree_vb->add_child(about_btn);
+ }
+ }
+
if (AssetLibraryEditorPlugin::is_available()) {
asset_library = memnew(EditorAssetLibrary(true));
asset_library->set_name(TTR("Asset Library Projects"));
@@ -3115,13 +1197,14 @@ ProjectManager::ProjectManager() {
print_verbose("Asset Library not available (due to using Web editor, or SSL support disabled).");
}
+ // Dialogs.
{
- // Dialogs
- language_restart_ask = memnew(ConfirmationDialog);
- language_restart_ask->set_ok_button_text(TTR("Restart Now"));
- language_restart_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
- language_restart_ask->set_cancel_button_text(TTR("Continue"));
- add_child(language_restart_ask);
+ restart_required_dialog = memnew(ConfirmationDialog);
+ restart_required_dialog->set_ok_button_text(TTR("Restart Now"));
+ restart_required_dialog->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
+ restart_required_dialog->set_cancel_button_text(TTR("Continue"));
+ restart_required_dialog->set_text(TTR("Settings changed!\nThe project manager must be restarted for changes to take effect."));
+ add_child(restart_required_dialog);
scan_dir = memnew(EditorFileDialog);
scan_dir->set_previews_enabled(false);
@@ -3166,7 +1249,7 @@ ProjectManager::ProjectManager() {
ask_update_settings = memnew(ConfirmationDialog);
ask_update_settings->set_autowrap(true);
- ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings));
+ ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects));
full_convert_button = ask_update_settings->add_button(TTR("Convert Full Project"), !GLOBAL_GET("gui/common/swap_cancel_ok"));
full_convert_button->connect("pressed", callable_mp(this, &ProjectManager::_full_convert_button_pressed));
add_child(ask_update_settings);
@@ -3199,12 +1282,10 @@ ProjectManager::ProjectManager() {
about = memnew(EditorAbout);
add_child(about);
-
- _build_icon_type_cache(get_theme());
}
+ // Tag management.
{
- // Tag management.
tag_manage_dialog = memnew(ConfirmationDialog);
add_child(tag_manage_dialog);
tag_manage_dialog->set_title(TTR("Manage Project Tags"));
@@ -3304,10 +1385,6 @@ ProjectManager::ProjectManager() {
}
}
- SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
-
- OS::get_singleton()->set_low_processor_usage_mode(true);
-
_update_size_limits();
}
@@ -3319,42 +1396,3 @@ ProjectManager::~ProjectManager() {
EditorThemeManager::finalize();
}
-
-void ProjectTag::_notification(int p_what) {
- if (display_close && p_what == NOTIFICATION_THEME_CHANGED) {
- button->set_icon(get_theme_icon(SNAME("close"), SNAME("TabBar")));
- }
-}
-
-ProjectTag::ProjectTag(const String &p_text, bool p_display_close) {
- add_theme_constant_override(SNAME("separation"), 0);
- set_v_size_flags(SIZE_SHRINK_CENTER);
- tag_string = p_text;
- display_close = p_display_close;
-
- Color tag_color = Color(1, 0, 0);
- tag_color.set_ok_hsl_s(0.8);
- tag_color.set_ok_hsl_h(float(p_text.hash() * 10001 % UINT32_MAX) / float(UINT32_MAX));
- set_self_modulate(tag_color);
-
- ColorRect *cr = memnew(ColorRect);
- add_child(cr);
- cr->set_custom_minimum_size(Vector2(4, 0) * EDSCALE);
- cr->set_color(tag_color);
-
- button = memnew(Button);
- add_child(button);
- button->set_auto_translate(false);
- button->set_text(p_text.capitalize());
- button->set_focus_mode(FOCUS_NONE);
- button->set_icon_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
- button->set_theme_type_variation(SNAME("ProjectTag"));
-}
-
-void ProjectTag::connect_button_to(const Callable &p_callable) {
- button->connect(SNAME("pressed"), p_callable, CONNECT_DEFERRED);
-}
-
-const String ProjectTag::get_tag() const {
- return tag_string;
-}
diff --git a/editor/project_manager.h b/editor/project_manager.h
index 7b091050bd..7ed8df8a9d 100644
--- a/editor/project_manager.h
+++ b/editor/project_manager.h
@@ -31,315 +31,68 @@
#ifndef PROJECT_MANAGER_H
#define PROJECT_MANAGER_H
-#include "core/io/config_file.h"
-#include "editor/editor_about.h"
#include "scene/gui/dialogs.h"
-#include "scene/gui/file_dialog.h"
#include "scene/gui/scroll_container.h"
class CheckBox;
+class EditorAbout;
class EditorAssetLibrary;
class EditorFileDialog;
class HFlowContainer;
+class LineEdit;
+class LinkButton;
+class OptionButton;
class PanelContainer;
+class ProjectDialog;
class ProjectList;
+class TabContainer;
-class ProjectDialog : public ConfirmationDialog {
- GDCLASS(ProjectDialog, ConfirmationDialog);
-
-public:
- enum Mode {
- MODE_NEW,
- MODE_IMPORT,
- MODE_INSTALL,
- MODE_RENAME,
- };
-
-private:
- enum MessageType {
- MESSAGE_ERROR,
- MESSAGE_WARNING,
- MESSAGE_SUCCESS,
- };
-
- enum InputType {
- PROJECT_PATH,
- INSTALL_PATH,
- };
-
- Mode mode = MODE_NEW;
- bool is_folder_empty = true;
-
- Button *browse = nullptr;
- Button *install_browse = nullptr;
- Button *create_dir = nullptr;
- Container *name_container = nullptr;
- Container *path_container = nullptr;
- Container *install_path_container = nullptr;
-
- Container *renderer_container = nullptr;
- Label *renderer_info = nullptr;
- HBoxContainer *default_files_container = nullptr;
- Ref<ButtonGroup> renderer_button_group;
-
- Label *msg = nullptr;
- LineEdit *project_path = nullptr;
- LineEdit *project_name = nullptr;
- LineEdit *install_path = nullptr;
- TextureRect *status_rect = nullptr;
- TextureRect *install_status_rect = nullptr;
-
- OptionButton *vcs_metadata_selection = nullptr;
-
- EditorFileDialog *fdialog = nullptr;
- EditorFileDialog *fdialog_install = nullptr;
- AcceptDialog *dialog_error = nullptr;
-
- String zip_path;
- String zip_title;
- String fav_dir;
-
- String created_folder_path;
-
- void _set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH);
-
- String _test_path();
- void _update_path(const String &p_path);
- void _path_text_changed(const String &p_path);
- void _path_selected(const String &p_path);
- void _file_selected(const String &p_path);
- void _install_path_selected(const String &p_path);
-
- void _browse_path();
- void _browse_install_path();
- void _create_folder();
-
- void _text_changed(const String &p_text);
- void _nonempty_confirmation_ok_pressed();
- void _renderer_selected();
- void _remove_created_folder();
-
- void ok_pressed() override;
- void cancel_pressed() override;
-
-protected:
- void _notification(int p_what);
- static void _bind_methods();
-
-public:
- void set_zip_path(const String &p_path);
- void set_zip_title(const String &p_title);
- void set_mode(Mode p_mode);
- void set_project_path(const String &p_path);
-
- void ask_for_path_and_show();
- void show_dialog();
-
- ProjectDialog();
-};
-
-class ProjectListItemControl : public HBoxContainer {
- GDCLASS(ProjectListItemControl, HBoxContainer)
-
- VBoxContainer *main_vbox = nullptr;
- TextureButton *favorite_button = nullptr;
- Button *explore_button = nullptr;
-
- TextureRect *project_icon = nullptr;
- Label *project_title = nullptr;
- Label *project_path = nullptr;
- TextureRect *project_unsupported_features = nullptr;
- HBoxContainer *tag_container = nullptr;
-
- bool project_is_missing = false;
- bool icon_needs_reload = true;
- bool is_selected = false;
- bool is_hovering = false;
-
- void _favorite_button_pressed();
- void _explore_button_pressed();
-
-protected:
- void _notification(int p_what);
- static void _bind_methods();
-
-public:
- void set_project_title(const String &p_title);
- void set_project_path(const String &p_path);
- void set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list);
- void set_project_icon(const Ref<Texture2D> &p_icon);
- void set_unsupported_features(PackedStringArray p_features);
-
- bool should_load_project_icon() const;
- void set_selected(bool p_selected);
-
- void set_is_favorite(bool p_favorite);
- void set_is_missing(bool p_missing);
- void set_is_grayed(bool p_grayed);
-
- ProjectListItemControl();
-};
-
-class ProjectList : public ScrollContainer {
- GDCLASS(ProjectList, ScrollContainer)
+class ProjectManager : public Control {
+ GDCLASS(ProjectManager, Control);
- friend class ProjectManager;
+ static ProjectManager *singleton;
-public:
- enum FilterOption {
- EDIT_DATE,
- NAME,
- PATH,
- TAGS,
- };
-
- // Can often be passed by copy
- struct Item {
- String project_name;
- String description;
- PackedStringArray tags;
- String tag_sort_string;
- String path;
- String icon;
- String main_scene;
- PackedStringArray unsupported_features;
- uint64_t last_edited = 0;
- bool favorite = false;
- bool grayed = false;
- bool missing = false;
- int version = 0;
-
- ProjectListItemControl *control = nullptr;
-
- Item() {}
-
- Item(const String &p_name,
- const String &p_description,
- const PackedStringArray &p_tags,
- const String &p_path,
- const String &p_icon,
- const String &p_main_scene,
- const PackedStringArray &p_unsupported_features,
- uint64_t p_last_edited,
- bool p_favorite,
- bool p_grayed,
- bool p_missing,
- int p_version) {
- project_name = p_name;
- description = p_description;
- tags = p_tags;
- path = p_path;
- icon = p_icon;
- main_scene = p_main_scene;
- unsupported_features = p_unsupported_features;
- last_edited = p_last_edited;
- favorite = p_favorite;
- grayed = p_grayed;
- missing = p_missing;
- version = p_version;
- control = nullptr;
-
- PackedStringArray sorted_tags = tags;
- sorted_tags.sort();
- tag_sort_string = String().join(sorted_tags);
- }
-
- _FORCE_INLINE_ bool operator==(const Item &l) const {
- return path == l.path;
- }
- };
-
-private:
- bool project_opening_initiated = false;
-
- String _search_term;
- FilterOption _order_option = FilterOption::EDIT_DATE;
- HashSet<String> _selected_project_paths;
- String _last_clicked; // Project key
- VBoxContainer *_scroll_children = nullptr;
- int _icon_load_index = 0;
-
- Vector<Item> _projects;
-
- ConfigFile _config;
- String _config_path;
-
- void _panel_input(const Ref<InputEvent> &p_ev, Node *p_hb);
- void _favorite_pressed(Node *p_hb);
- void _show_project(const String &p_path);
-
- void _migrate_config();
- void _scan_folder_recursive(const String &p_path, List<String> *r_projects);
-
- void _clear_project_selection();
- void _toggle_project(int p_index);
- void _select_project_nocheck(int p_index);
- void _deselect_project_nocheck(int p_index);
- void _select_project_range(int p_begin, int p_end);
-
- void _create_project_item_control(int p_index);
- void _remove_project(int p_index, bool p_update_settings);
-
- static Item load_project_data(const String &p_property_key, bool p_favorite);
- void _update_icons_async();
- void _load_project_icon(int p_index);
-
- void _global_menu_new_window(const Variant &p_tag);
- void _global_menu_open_project(const Variant &p_tag);
+ // Utility data.
-protected:
- void _notification(int p_what);
- static void _bind_methods();
+ static Ref<Texture2D> _file_dialog_get_icon(const String &p_path);
+ static Ref<Texture2D> _file_dialog_get_thumbnail(const String &p_path);
-public:
- static const char *SIGNAL_LIST_CHANGED;
- static const char *SIGNAL_SELECTION_CHANGED;
- static const char *SIGNAL_PROJECT_ASK_OPEN;
+ HashMap<String, Ref<Texture2D>> icon_type_cache;
- void update_project_list();
- int get_project_count() const;
+ void _build_icon_type_cache(Ref<Theme> p_theme);
- void find_projects(const String &p_path);
- void find_projects_multiple(const PackedStringArray &p_paths);
- void sort_projects();
+ // Main layout.
- void add_project(const String &dir_path, bool favorite);
- void set_project_version(const String &p_project_path, int version);
- int refresh_project(const String &dir_path);
- void ensure_project_visible(int p_index);
+ void _update_size_limits();
- void select_project(int p_index);
- void select_first_visible_project();
- void erase_selected_projects(bool p_delete_project_contents);
- Vector<Item> get_selected_projects() const;
- const HashSet<String> &get_selected_project_keys() const;
- int get_single_selected_index() const;
+ Panel *background_panel = nullptr;
+ Button *about_btn = nullptr;
+ LinkButton *version_btn = nullptr;
- bool is_any_project_missing() const;
- void erase_missing_projects();
+ ConfirmationDialog *open_templates = nullptr;
+ EditorAbout *about = nullptr;
- void set_search_term(String p_search_term);
- void add_search_tag(const String &p_tag);
- void set_order_option(int p_option);
+ void _show_about();
+ void _version_button_pressed();
- void update_dock_menu();
- void save_config();
+ TabContainer *tabs = nullptr;
+ VBoxContainer *local_projects_vb = nullptr;
+ EditorAssetLibrary *asset_library = nullptr;
- ProjectList();
-};
+ void _on_tab_changed(int p_tab);
+ void _open_asset_library();
-class ProjectManager : public Control {
- GDCLASS(ProjectManager, Control);
+ // Quick settings.
- HashMap<String, Ref<Texture2D>> icon_type_cache;
- void _build_icon_type_cache(Ref<Theme> p_theme);
+ OptionButton *language_btn = nullptr;
+ ConfirmationDialog *restart_required_dialog = nullptr;
- static ProjectManager *singleton;
+ void _language_selected(int p_id);
+ void _restart_confirm();
+ void _dim_window();
- void _update_size_limits();
+ // Project list.
- Panel *background_panel = nullptr;
- TabContainer *tabs = nullptr;
ProjectList *_project_list = nullptr;
LineEdit *search_box = nullptr;
@@ -356,29 +109,17 @@ class ProjectManager : public Control {
Button *manage_tags_btn = nullptr;
Button *erase_btn = nullptr;
Button *erase_missing_btn = nullptr;
- Button *about_btn = nullptr;
-
- VBoxContainer *local_projects_vb = nullptr;
- EditorAssetLibrary *asset_library = nullptr;
-
- Ref<StyleBox> tag_stylebox;
EditorFileDialog *scan_dir = nullptr;
- ConfirmationDialog *language_restart_ask = nullptr;
ConfirmationDialog *erase_ask = nullptr;
Label *erase_ask_label = nullptr;
// Comment out for now until we have a better warning system to
// ensure users delete their project only.
//CheckBox *delete_project_contents = nullptr;
-
ConfirmationDialog *erase_missing_ask = nullptr;
ConfirmationDialog *multi_open_ask = nullptr;
ConfirmationDialog *multi_run_ask = nullptr;
- ConfirmationDialog *ask_full_convert_dialog = nullptr;
- ConfirmationDialog *ask_update_settings = nullptr;
- ConfirmationDialog *open_templates = nullptr;
- EditorAbout *about = nullptr;
HBoxContainer *settings_hb = nullptr;
@@ -386,30 +127,13 @@ class ProjectManager : public Control {
AcceptDialog *dialog_error = nullptr;
ProjectDialog *npdialog = nullptr;
- Button *full_convert_button = nullptr;
- OptionButton *language_btn = nullptr;
- LinkButton *version_btn = nullptr;
-
- HashSet<String> tag_set;
- PackedStringArray current_project_tags;
- PackedStringArray forbidden_tag_characters{ "/", "\\", "-" };
- ConfirmationDialog *tag_manage_dialog = nullptr;
- HFlowContainer *project_tags = nullptr;
- HFlowContainer *all_tags = nullptr;
- Label *tag_edit_error = nullptr;
- Button *create_tag_btn = nullptr;
- ConfirmationDialog *create_tag_dialog = nullptr;
- LineEdit *new_tag_name = nullptr;
- Label *tag_error = nullptr;
-
- void _open_asset_library();
void _scan_projects();
void _run_project();
void _run_project_confirm();
void _open_selected_projects();
void _open_selected_projects_ask();
- void _full_convert_button_pressed();
- void _perform_full_project_conversion();
+
+ void _install_project(const String &p_zip_path, const String &p_title);
void _import_project();
void _new_project();
void _rename_project();
@@ -417,11 +141,30 @@ class ProjectManager : public Control {
void _erase_missing_projects();
void _erase_project_confirm();
void _erase_missing_projects_confirm();
- void _show_about();
void _update_project_buttons();
- void _language_selected(int p_id);
- void _restart_confirm();
- void _confirm_update_settings();
+
+ void _on_project_created(const String &dir);
+ void _on_projects_updated();
+
+ void _on_order_option_changed(int p_idx);
+ void _on_search_term_changed(const String &p_term);
+ void _on_search_term_submitted(const String &p_text);
+
+ // Project tag management.
+
+ HashSet<String> tag_set;
+ PackedStringArray current_project_tags;
+ PackedStringArray forbidden_tag_characters{ "/", "\\", "-" };
+
+ ConfirmationDialog *tag_manage_dialog = nullptr;
+ HFlowContainer *project_tags = nullptr;
+ HFlowContainer *all_tags = nullptr;
+ Label *tag_edit_error = nullptr;
+
+ Button *create_tag_btn = nullptr;
+ ConfirmationDialog *create_tag_dialog = nullptr;
+ LineEdit *new_tag_name = nullptr;
+ Label *tag_error = nullptr;
void _manage_project_tags();
void _add_project_tag(const String &p_tag);
@@ -430,23 +173,20 @@ class ProjectManager : public Control {
void _set_new_tag_name(const String p_name);
void _create_new_tag();
- void _on_project_created(const String &dir);
- void _on_projects_updated();
+ // Project converter/migration tool.
- void _install_project(const String &p_zip_path, const String &p_title);
+ ConfirmationDialog *ask_full_convert_dialog = nullptr;
+ ConfirmationDialog *ask_update_settings = nullptr;
+ Button *full_convert_button = nullptr;
- void _dim_window();
- virtual void shortcut_input(const Ref<InputEvent> &p_ev) override;
- void _files_dropped(PackedStringArray p_files);
+ void _full_convert_button_pressed();
+ void _perform_full_project_conversion();
- void _version_button_pressed();
- void _on_order_option_changed(int p_idx);
- void _on_tab_changed(int p_tab);
- void _on_search_term_changed(const String &p_term);
- void _on_search_term_submitted(const String &p_text);
+ // Input and I/O.
- static Ref<Texture2D> _file_dialog_get_icon(const String &p_path);
- static Ref<Texture2D> _file_dialog_get_thumbnail(const String &p_path);
+ virtual void shortcut_input(const Ref<InputEvent> &p_ev) override;
+
+ void _files_dropped(PackedStringArray p_files);
protected:
void _notification(int p_what);
@@ -454,28 +194,16 @@ protected:
public:
static ProjectManager *get_singleton() { return singleton; }
+ // Project list.
+
LineEdit *get_search_box();
+
+ // Project tag management.
+
void add_new_tag(const String &p_tag);
ProjectManager();
~ProjectManager();
};
-class ProjectTag : public HBoxContainer {
- GDCLASS(ProjectTag, HBoxContainer);
-
- String tag_string;
- Button *button = nullptr;
- bool display_close = false;
-
-protected:
- void _notification(int p_what);
-
-public:
- ProjectTag(const String &p_text, bool p_display_close = false);
-
- void connect_button_to(const Callable &p_callable);
- const String get_tag() const;
-};
-
#endif // PROJECT_MANAGER_H
diff --git a/editor/project_manager/SCsub b/editor/project_manager/SCsub
new file mode 100644
index 0000000000..359d04e5df
--- /dev/null
+++ b/editor/project_manager/SCsub
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+Import("env")
+
+env.add_source_files(env.editor_sources, "*.cpp")
diff --git a/editor/project_manager/project_dialog.cpp b/editor/project_manager/project_dialog.cpp
new file mode 100644
index 0000000000..f773e6f696
--- /dev/null
+++ b/editor/project_manager/project_dialog.cpp
@@ -0,0 +1,977 @@
+/**************************************************************************/
+/* project_dialog.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "project_dialog.h"
+
+#include "core/config/project_settings.h"
+#include "core/io/dir_access.h"
+#include "core/io/zip_io.h"
+#include "core/version.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+#include "editor/editor_vcs_interface.h"
+#include "editor/gui/editor_file_dialog.h"
+#include "editor/themes/editor_icons.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/check_box.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/separator.h"
+#include "scene/gui/texture_rect.h"
+
+void ProjectDialog::_set_message(const String &p_msg, MessageType p_type, InputType input_type) {
+ msg->set_text(p_msg);
+ Ref<Texture2D> current_path_icon = status_rect->get_texture();
+ Ref<Texture2D> current_install_icon = install_status_rect->get_texture();
+ Ref<Texture2D> new_icon;
+
+ switch (p_type) {
+ case MESSAGE_ERROR: {
+ msg->add_theme_color_override("font_color", get_theme_color(SNAME("error_color"), EditorStringName(Editor)));
+ msg->set_modulate(Color(1, 1, 1, 1));
+ new_icon = get_editor_theme_icon(SNAME("StatusError"));
+
+ } break;
+ case MESSAGE_WARNING: {
+ msg->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
+ msg->set_modulate(Color(1, 1, 1, 1));
+ new_icon = get_editor_theme_icon(SNAME("StatusWarning"));
+
+ } break;
+ case MESSAGE_SUCCESS: {
+ msg->remove_theme_color_override("font_color");
+ msg->set_modulate(Color(1, 1, 1, 0));
+ new_icon = get_editor_theme_icon(SNAME("StatusSuccess"));
+
+ } break;
+ }
+
+ if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
+ status_rect->set_texture(new_icon);
+ } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
+ install_status_rect->set_texture(new_icon);
+ }
+}
+
+static bool is_zip_file(Ref<DirAccess> p_d, const String &p_path) {
+ return p_path.ends_with(".zip") && p_d->file_exists(p_path);
+}
+
+String ProjectDialog::_test_path() {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ const String base_path = project_path->get_text();
+ String valid_path, valid_install_path;
+ bool is_zip = false;
+ if (d->change_dir(base_path) == OK) {
+ valid_path = base_path;
+ } else if (is_zip_file(d, base_path)) {
+ valid_path = base_path;
+ is_zip = true;
+ } else if (d->change_dir(base_path.strip_edges()) == OK) {
+ valid_path = base_path.strip_edges();
+ } else if (is_zip_file(d, base_path.strip_edges())) {
+ valid_path = base_path.strip_edges();
+ is_zip = true;
+ }
+
+ if (valid_path.is_empty()) {
+ _set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ if (mode == MODE_IMPORT && is_zip) {
+ if (d->change_dir(install_path->get_text()) == OK) {
+ valid_install_path = install_path->get_text();
+ } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
+ valid_install_path = install_path->get_text().strip_edges();
+ }
+
+ if (valid_install_path.is_empty()) {
+ _set_message(TTR("The install path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+ }
+
+ if (mode == MODE_IMPORT || mode == MODE_RENAME) {
+ if (!d->file_exists("project.godot")) {
+ if (is_zip) {
+ Ref<FileAccess> io_fa;
+ zlib_filefunc_def io = zipio_create_io(&io_fa);
+
+ unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
+ if (!pkg) {
+ _set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ unzClose(pkg);
+ return "";
+ }
+
+ int ret = unzGoToFirstFile(pkg);
+ while (ret == UNZ_OK) {
+ unz_file_info info;
+ char fname[16384];
+ ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+ if (ret != UNZ_OK) {
+ break;
+ }
+
+ if (String::utf8(fname).ends_with("project.godot")) {
+ break;
+ }
+
+ ret = unzGoToNextFile(pkg);
+ }
+
+ if (ret == UNZ_END_OF_LIST_OF_FILE) {
+ _set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ unzClose(pkg);
+ return "";
+ }
+
+ unzClose(pkg);
+
+ // check if the specified install folder is empty, even though this is not an error, it is good to check here
+ d->list_dir_begin();
+ is_folder_empty = true;
+ String n = d->get_next();
+ while (!n.is_empty()) {
+ if (!n.begins_with(".")) {
+ // Allow `.`, `..` (reserved current/parent folder names)
+ // and hidden files/folders to be present.
+ // For instance, this lets users initialize a Git repository
+ // and still be able to create a project in the directory afterwards.
+ is_folder_empty = false;
+ break;
+ }
+ n = d->get_next();
+ }
+ d->list_dir_end();
+
+ if (!is_folder_empty) {
+ _set_message(TTR("Please choose an empty install folder."), MESSAGE_WARNING, INSTALL_PATH);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ } else {
+ _set_message(TTR("Please choose a \"project.godot\", a directory with it, or a \".zip\" file."), MESSAGE_ERROR);
+ install_path_container->hide();
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ } else if (is_zip) {
+ _set_message(TTR("The install directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ } else {
+ // Check if the specified folder is empty, even though this is not an error, it is good to check here.
+ d->list_dir_begin();
+ is_folder_empty = true;
+ String n = d->get_next();
+ while (!n.is_empty()) {
+ if (!n.begins_with(".")) {
+ // Allow `.`, `..` (reserved current/parent folder names)
+ // and hidden files/folders to be present.
+ // For instance, this lets users initialize a Git repository
+ // and still be able to create a project in the directory afterwards.
+ is_folder_empty = false;
+ break;
+ }
+ n = d->get_next();
+ }
+ d->list_dir_end();
+
+ if (!is_folder_empty) {
+ if (valid_path == OS::get_singleton()->get_environment("HOME") || valid_path == OS::get_singleton()->get_system_dir(OS::SYSTEM_DIR_DOCUMENTS) || valid_path == OS::get_singleton()->get_executable_path().get_base_dir()) {
+ _set_message(TTR("You cannot save a project in the selected path. Please make a new folder or choose a new path."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ return "";
+ }
+
+ _set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING);
+ get_ok_button()->set_disabled(false);
+ return valid_path;
+ }
+ }
+
+ _set_message("");
+ _set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
+ get_ok_button()->set_disabled(false);
+ return valid_path;
+}
+
+void ProjectDialog::_update_path(const String &p_path) {
+ String sp = _test_path();
+ if (!sp.is_empty()) {
+ // If the project name is empty or default, infer the project name from the selected folder name
+ if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) {
+ sp = sp.replace("\\", "/");
+ int lidx = sp.rfind("/");
+
+ if (lidx != -1) {
+ sp = sp.substr(lidx + 1, sp.length()).capitalize();
+ }
+ if (sp.is_empty() && mode == MODE_IMPORT) {
+ sp = TTR("Imported Project");
+ }
+
+ project_name->set_text(sp);
+ _text_changed(sp);
+ }
+ }
+
+ if (!created_folder_path.is_empty() && created_folder_path != p_path) {
+ _remove_created_folder();
+ }
+}
+
+void ProjectDialog::_path_text_changed(const String &p_path) {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ if (mode == MODE_IMPORT && is_zip_file(d, p_path)) {
+ install_path->set_text(p_path.get_base_dir());
+ install_path_container->show();
+ } else if (mode == MODE_IMPORT && is_zip_file(d, p_path.strip_edges())) {
+ install_path->set_text(p_path.strip_edges().get_base_dir());
+ install_path_container->show();
+ } else {
+ install_path_container->hide();
+ }
+
+ _update_path(p_path.simplify_path());
+}
+
+void ProjectDialog::_file_selected(const String &p_path) {
+ // If not already shown.
+ show_dialog();
+
+ String p = p_path;
+ if (mode == MODE_IMPORT) {
+ if (p.ends_with("project.godot")) {
+ p = p.get_base_dir();
+ install_path_container->hide();
+ get_ok_button()->set_disabled(false);
+ } else if (p.ends_with(".zip")) {
+ install_path->set_text(p.get_base_dir());
+ install_path_container->show();
+ get_ok_button()->set_disabled(false);
+ } else {
+ _set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
+ get_ok_button()->set_disabled(true);
+ return;
+ }
+ }
+
+ String sp = p.simplify_path();
+ project_path->set_text(sp);
+ _update_path(sp);
+ if (p.ends_with(".zip")) {
+ callable_mp((Control *)install_path, &Control::grab_focus).call_deferred();
+ } else {
+ callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
+ }
+}
+
+void ProjectDialog::_path_selected(const String &p_path) {
+ // If not already shown.
+ show_dialog();
+
+ String sp = p_path.simplify_path();
+ project_path->set_text(sp);
+ _update_path(sp);
+ callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
+}
+
+void ProjectDialog::_install_path_selected(const String &p_path) {
+ String sp = p_path.simplify_path();
+ install_path->set_text(sp);
+ _update_path(sp);
+ callable_mp((Control *)get_ok_button(), &Control::grab_focus).call_deferred();
+}
+
+void ProjectDialog::_browse_path() {
+ fdialog->set_current_dir(project_path->get_text());
+
+ if (mode == MODE_IMPORT) {
+ fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_ANY);
+ fdialog->clear_filters();
+ fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project")));
+ fdialog->add_filter("*.zip", TTR("ZIP File"));
+ } else {
+ fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
+ }
+ fdialog->popup_file_dialog();
+}
+
+void ProjectDialog::_browse_install_path() {
+ fdialog_install->set_current_dir(install_path->get_text());
+ fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
+ fdialog_install->popup_file_dialog();
+}
+
+void ProjectDialog::_create_folder() {
+ const String project_name_no_edges = project_name->get_text().strip_edges();
+ if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) {
+ _set_message(TTR("Invalid project name."), MESSAGE_WARNING);
+ return;
+ }
+
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ if (d->change_dir(project_path->get_text()) == OK) {
+ if (!d->dir_exists(project_name_no_edges)) {
+ if (d->make_dir(project_name_no_edges) == OK) {
+ d->change_dir(project_name_no_edges);
+ String dir_str = d->get_current_dir();
+ project_path->set_text(dir_str);
+ _update_path(dir_str);
+ created_folder_path = d->get_current_dir();
+ create_dir->set_disabled(true);
+ } else {
+ dialog_error->set_text(TTR("Couldn't create folder."));
+ dialog_error->popup_centered();
+ }
+ } else {
+ dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
+ dialog_error->popup_centered();
+ }
+ }
+}
+
+void ProjectDialog::_text_changed(const String &p_text) {
+ if (mode != MODE_NEW) {
+ return;
+ }
+
+ _test_path();
+
+ if (p_text.strip_edges().is_empty()) {
+ _set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR);
+ }
+}
+
+void ProjectDialog::_nonempty_confirmation_ok_pressed() {
+ is_folder_empty = true;
+ ok_pressed();
+}
+
+void ProjectDialog::_renderer_selected() {
+ ERR_FAIL_NULL(renderer_button_group->get_pressed_button());
+
+ String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
+
+ if (renderer_type == "forward_plus") {
+ renderer_info->set_text(
+ String::utf8("• ") + TTR("Supports desktop platforms only.") +
+ String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +
+ String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +
+ String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
+ String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));
+ } else if (renderer_type == "mobile") {
+ renderer_info->set_text(
+ String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +
+ String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +
+ String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +
+ String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
+ String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));
+ } else if (renderer_type == "gl_compatibility") {
+ renderer_info->set_text(
+ String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +
+ String::utf8("\n• ") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
+ String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +
+ String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
+ String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));
+ } else {
+ WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
+ }
+}
+
+void ProjectDialog::_remove_created_folder() {
+ if (!created_folder_path.is_empty()) {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ d->remove(created_folder_path);
+
+ create_dir->set_disabled(false);
+ created_folder_path = "";
+ }
+}
+
+void ProjectDialog::ok_pressed() {
+ String dir = project_path->get_text();
+
+ if (mode == MODE_RENAME) {
+ String dir2 = _test_path();
+ if (dir2.is_empty()) {
+ _set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
+ return;
+ }
+
+ // Load project.godot as ConfigFile to set the new name.
+ ConfigFile cfg;
+ String project_godot = dir2.path_join("project.godot");
+ Error err = cfg.load(project_godot);
+ if (err != OK) {
+ _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
+ } else {
+ cfg.set_value("application", "config/name", project_name->get_text().strip_edges());
+ err = cfg.save(project_godot);
+ if (err != OK) {
+ _set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR);
+ }
+ }
+
+ hide();
+ emit_signal(SNAME("projects_updated"));
+
+ } else {
+ if (mode == MODE_IMPORT) {
+ if (project_path->get_text().ends_with(".zip")) {
+ mode = MODE_INSTALL;
+ ok_pressed();
+
+ return;
+ }
+
+ } else {
+ if (mode == MODE_NEW) {
+ // Before we create a project, check that the target folder is empty.
+ // If not, we need to ask the user if they're sure they want to do this.
+ if (!is_folder_empty) {
+ ConfirmationDialog *cd = memnew(ConfirmationDialog);
+ cd->set_title(TTR("Warning: This folder is not empty"));
+ cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?"));
+ cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
+ get_parent()->add_child(cd);
+ cd->popup_centered();
+ cd->grab_focus();
+ return;
+ }
+ PackedStringArray project_features = ProjectSettings::get_required_features();
+ ProjectSettings::CustomMap initial_settings;
+
+ // Be sure to change this code if/when renderers are changed.
+ // Default values are "forward_plus" for the main setting, "mobile" for the mobile override,
+ // and "gl_compatibility" for the web override.
+ String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
+ initial_settings["rendering/renderer/rendering_method"] = renderer_type;
+
+ EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);
+ EditorSettings::get_singleton()->save();
+
+ if (renderer_type == "forward_plus") {
+ project_features.push_back("Forward Plus");
+ } else if (renderer_type == "mobile") {
+ project_features.push_back("Mobile");
+ } else if (renderer_type == "gl_compatibility") {
+ project_features.push_back("GL Compatibility");
+ // Also change the default rendering method for the mobile override.
+ initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";
+ } else {
+ WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
+ }
+
+ project_features.sort();
+ initial_settings["application/config/features"] = project_features;
+ initial_settings["application/config/name"] = project_name->get_text().strip_edges();
+ initial_settings["application/config/icon"] = "res://icon.svg";
+
+ if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) {
+ _set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
+ } else {
+ // Store default project icon in SVG format.
+ Error err;
+ Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err);
+ fa_icon->store_string(get_default_project_icon());
+
+ if (err != OK) {
+ _set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
+ }
+
+ EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir);
+ }
+ } else if (mode == MODE_INSTALL) {
+ if (project_path->get_text().ends_with(".zip")) {
+ dir = install_path->get_text();
+ zip_path = project_path->get_text();
+ }
+
+ Ref<FileAccess> io_fa;
+ zlib_filefunc_def io = zipio_create_io(&io_fa);
+
+ unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
+ if (!pkg) {
+ dialog_error->set_text(TTR("Error opening package file, not in ZIP format."));
+ dialog_error->popup_centered();
+ return;
+ }
+
+ // Find the zip_root
+ String zip_root;
+ int ret = unzGoToFirstFile(pkg);
+ while (ret == UNZ_OK) {
+ unz_file_info info;
+ char fname[16384];
+ unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+
+ String name = String::utf8(fname);
+ if (name.ends_with("project.godot")) {
+ zip_root = name.substr(0, name.rfind("project.godot"));
+ break;
+ }
+
+ ret = unzGoToNextFile(pkg);
+ }
+
+ ret = unzGoToFirstFile(pkg);
+
+ Vector<String> failed_files;
+
+ while (ret == UNZ_OK) {
+ //get filename
+ unz_file_info info;
+ char fname[16384];
+ ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
+ if (ret != UNZ_OK) {
+ break;
+ }
+
+ String path = String::utf8(fname);
+
+ if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) {
+ //
+ } else if (path.ends_with("/")) { // a dir
+ path = path.substr(0, path.length() - 1);
+ String rel_path = path.substr(zip_root.length());
+
+ Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ da->make_dir(dir.path_join(rel_path));
+ } else {
+ Vector<uint8_t> uncomp_data;
+ uncomp_data.resize(info.uncompressed_size);
+ String rel_path = path.substr(zip_root.length());
+
+ //read
+ unzOpenCurrentFile(pkg);
+ ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
+ ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));
+ unzCloseCurrentFile(pkg);
+
+ Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE);
+ if (f.is_valid()) {
+ f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
+ } else {
+ failed_files.push_back(rel_path);
+ }
+ }
+
+ ret = unzGoToNextFile(pkg);
+ }
+
+ unzClose(pkg);
+
+ if (failed_files.size()) {
+ String err_msg = TTR("The following files failed extraction from package:") + "\n\n";
+ for (int i = 0; i < failed_files.size(); i++) {
+ if (i > 15) {
+ err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
+ break;
+ }
+ err_msg += failed_files[i] + "\n";
+ }
+
+ dialog_error->set_text(err_msg);
+ dialog_error->popup_centered();
+
+ } else if (!project_path->get_text().ends_with(".zip")) {
+ dialog_error->set_text(TTR("Package installed successfully!"));
+ dialog_error->popup_centered();
+ }
+ }
+ }
+
+ dir = dir.replace("\\", "/");
+ if (dir.ends_with("/")) {
+ dir = dir.substr(0, dir.length() - 1);
+ }
+
+ hide();
+ emit_signal(SNAME("project_created"), dir);
+ }
+}
+
+void ProjectDialog::cancel_pressed() {
+ _remove_created_folder();
+
+ project_path->clear();
+ _update_path("");
+ project_name->clear();
+ _text_changed("");
+
+ if (status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
+ msg->show();
+ }
+
+ if (install_status_rect->get_texture() == get_editor_theme_icon(SNAME("StatusError"))) {
+ msg->show();
+ }
+}
+
+void ProjectDialog::set_zip_path(const String &p_path) {
+ zip_path = p_path;
+}
+
+void ProjectDialog::set_zip_title(const String &p_title) {
+ zip_title = p_title;
+}
+
+void ProjectDialog::set_mode(Mode p_mode) {
+ mode = p_mode;
+}
+
+void ProjectDialog::set_project_path(const String &p_path) {
+ project_path->set_text(p_path);
+}
+
+void ProjectDialog::ask_for_path_and_show() {
+ // Workaround: for the file selection dialog content to be rendered we need to show its parent dialog.
+ show_dialog();
+ _set_message("");
+
+ _browse_path();
+}
+
+void ProjectDialog::show_dialog() {
+ if (mode == MODE_RENAME) {
+ project_path->set_editable(false);
+ browse->hide();
+ install_browse->hide();
+
+ set_title(TTR("Rename Project"));
+ set_ok_button_text(TTR("Rename"));
+ name_container->show();
+ status_rect->hide();
+ msg->hide();
+ install_path_container->hide();
+ install_status_rect->hide();
+ renderer_container->hide();
+ default_files_container->hide();
+ get_ok_button()->set_disabled(false);
+
+ // Fetch current name from project.godot to prefill the text input.
+ ConfigFile cfg;
+ String project_godot = project_path->get_text().path_join("project.godot");
+ Error err = cfg.load(project_godot);
+ if (err != OK) {
+ _set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
+ status_rect->show();
+ msg->show();
+ get_ok_button()->set_disabled(true);
+ } else {
+ String cur_name = cfg.get_value("application", "config/name", "");
+ project_name->set_text(cur_name);
+ _text_changed(cur_name);
+ }
+
+ callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
+
+ create_dir->hide();
+
+ } else {
+ fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
+ if (!fav_dir.is_empty()) {
+ project_path->set_text(fav_dir);
+ fdialog->set_current_dir(fav_dir);
+ } else {
+ Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ project_path->set_text(d->get_current_dir());
+ fdialog->set_current_dir(d->get_current_dir());
+ }
+
+ if (project_name->get_text().is_empty()) {
+ String proj = TTR("New Game Project");
+ project_name->set_text(proj);
+ _text_changed(proj);
+ }
+
+ project_path->set_editable(true);
+ browse->set_disabled(false);
+ browse->show();
+ install_browse->set_disabled(false);
+ install_browse->show();
+ create_dir->show();
+ status_rect->show();
+ install_status_rect->show();
+ msg->show();
+
+ if (mode == MODE_IMPORT) {
+ set_title(TTR("Import Existing Project"));
+ set_ok_button_text(TTR("Import & Edit"));
+ name_container->hide();
+ install_path_container->hide();
+ renderer_container->hide();
+ default_files_container->hide();
+ project_path->grab_focus();
+
+ } else if (mode == MODE_NEW) {
+ set_title(TTR("Create New Project"));
+ set_ok_button_text(TTR("Create & Edit"));
+ name_container->show();
+ install_path_container->hide();
+ renderer_container->show();
+ default_files_container->show();
+ callable_mp((Control *)project_name, &Control::grab_focus).call_deferred();
+ callable_mp(project_name, &LineEdit::select_all).call_deferred();
+
+ } else if (mode == MODE_INSTALL) {
+ set_title(TTR("Install Project:") + " " + zip_title);
+ set_ok_button_text(TTR("Install & Edit"));
+ project_name->set_text(zip_title);
+ name_container->show();
+ install_path_container->hide();
+ renderer_container->hide();
+ default_files_container->hide();
+ project_path->grab_focus();
+ }
+
+ _test_path();
+ }
+
+ popup_centered(Size2(500, 0) * EDSCALE);
+}
+
+void ProjectDialog::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_WM_CLOSE_REQUEST: {
+ _remove_created_folder();
+ } break;
+ }
+}
+
+void ProjectDialog::_bind_methods() {
+ ADD_SIGNAL(MethodInfo("project_created"));
+ ADD_SIGNAL(MethodInfo("projects_updated"));
+}
+
+ProjectDialog::ProjectDialog() {
+ VBoxContainer *vb = memnew(VBoxContainer);
+ add_child(vb);
+
+ name_container = memnew(VBoxContainer);
+ vb->add_child(name_container);
+
+ Label *l = memnew(Label);
+ l->set_text(TTR("Project Name:"));
+ name_container->add_child(l);
+
+ HBoxContainer *pnhb = memnew(HBoxContainer);
+ name_container->add_child(pnhb);
+
+ project_name = memnew(LineEdit);
+ project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ pnhb->add_child(project_name);
+
+ create_dir = memnew(Button);
+ pnhb->add_child(create_dir);
+ create_dir->set_text(TTR("Create Folder"));
+ create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder));
+
+ path_container = memnew(VBoxContainer);
+ vb->add_child(path_container);
+
+ l = memnew(Label);
+ l->set_text(TTR("Project Path:"));
+ path_container->add_child(l);
+
+ HBoxContainer *pphb = memnew(HBoxContainer);
+ path_container->add_child(pphb);
+
+ project_path = memnew(LineEdit);
+ project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ pphb->add_child(project_path);
+
+ install_path_container = memnew(VBoxContainer);
+ vb->add_child(install_path_container);
+
+ l = memnew(Label);
+ l->set_text(TTR("Project Installation Path:"));
+ install_path_container->add_child(l);
+
+ HBoxContainer *iphb = memnew(HBoxContainer);
+ install_path_container->add_child(iphb);
+
+ install_path = memnew(LineEdit);
+ install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ iphb->add_child(install_path);
+
+ // status icon
+ status_rect = memnew(TextureRect);
+ status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ pphb->add_child(status_rect);
+
+ browse = memnew(Button);
+ browse->set_text(TTR("Browse"));
+ browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path));
+ pphb->add_child(browse);
+
+ // install status icon
+ install_status_rect = memnew(TextureRect);
+ install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ iphb->add_child(install_status_rect);
+
+ install_browse = memnew(Button);
+ install_browse->set_text(TTR("Browse"));
+ install_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_install_path));
+ iphb->add_child(install_browse);
+
+ msg = memnew(Label);
+ msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+ msg->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
+ vb->add_child(msg);
+
+ // Renderer selection.
+ renderer_container = memnew(VBoxContainer);
+ vb->add_child(renderer_container);
+ l = memnew(Label);
+ l->set_text(TTR("Renderer:"));
+ renderer_container->add_child(l);
+ HBoxContainer *rshc = memnew(HBoxContainer);
+ renderer_container->add_child(rshc);
+ renderer_button_group.instantiate();
+
+ // Left hand side, used for checkboxes to select renderer.
+ Container *rvb = memnew(VBoxContainer);
+ rshc->add_child(rvb);
+
+ String default_renderer_type = "forward_plus";
+ if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
+ default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
+ }
+
+ Button *rs_button = memnew(CheckBox);
+ rs_button->set_button_group(renderer_button_group);
+ rs_button->set_text(TTR("Forward+"));
+#if defined(WEB_ENABLED)
+ rs_button->set_disabled(true);
+#endif
+ rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
+ rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
+ rvb->add_child(rs_button);
+ if (default_renderer_type == "forward_plus") {
+ rs_button->set_pressed(true);
+ }
+ rs_button = memnew(CheckBox);
+ rs_button->set_button_group(renderer_button_group);
+ rs_button->set_text(TTR("Mobile"));
+#if defined(WEB_ENABLED)
+ rs_button->set_disabled(true);
+#endif
+ rs_button->set_meta(SNAME("rendering_method"), "mobile");
+ rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
+ rvb->add_child(rs_button);
+ if (default_renderer_type == "mobile") {
+ rs_button->set_pressed(true);
+ }
+ rs_button = memnew(CheckBox);
+ rs_button->set_button_group(renderer_button_group);
+ rs_button->set_text(TTR("Compatibility"));
+#if !defined(GLES3_ENABLED)
+ rs_button->set_disabled(true);
+#endif
+ rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
+ rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
+ rvb->add_child(rs_button);
+#if defined(GLES3_ENABLED)
+ if (default_renderer_type == "gl_compatibility") {
+ rs_button->set_pressed(true);
+ }
+#endif
+ rshc->add_child(memnew(VSeparator));
+
+ // Right hand side, used for text explaining each choice.
+ rvb = memnew(VBoxContainer);
+ rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ rshc->add_child(rvb);
+ renderer_info = memnew(Label);
+ renderer_info->set_modulate(Color(1, 1, 1, 0.7));
+ rvb->add_child(renderer_info);
+ _renderer_selected();
+
+ l = memnew(Label);
+ l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
+ // Add some extra spacing to separate it from the list above and the buttons below.
+ l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
+ l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
+ l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
+ l->set_modulate(Color(1, 1, 1, 0.7));
+ renderer_container->add_child(l);
+
+ default_files_container = memnew(HBoxContainer);
+ vb->add_child(default_files_container);
+ l = memnew(Label);
+ l->set_text(TTR("Version Control Metadata:"));
+ default_files_container->add_child(l);
+ vcs_metadata_selection = memnew(OptionButton);
+ vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
+ vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
+ vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
+ vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
+ default_files_container->add_child(vcs_metadata_selection);
+ Control *spacer = memnew(Control);
+ spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ default_files_container->add_child(spacer);
+
+ fdialog = memnew(EditorFileDialog);
+ fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
+ fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
+ fdialog_install = memnew(EditorFileDialog);
+ fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
+ fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
+ add_child(fdialog);
+ add_child(fdialog_install);
+
+ project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed));
+ project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
+ install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_update_path));
+ fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected));
+ fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected));
+ fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
+ fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
+
+ set_hide_on_ok(false);
+
+ dialog_error = memnew(AcceptDialog);
+ add_child(dialog_error);
+}
diff --git a/editor/project_manager/project_dialog.h b/editor/project_manager/project_dialog.h
new file mode 100644
index 0000000000..dcc5cf71f8
--- /dev/null
+++ b/editor/project_manager/project_dialog.h
@@ -0,0 +1,136 @@
+/**************************************************************************/
+/* project_dialog.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef PROJECT_DIALOG_H
+#define PROJECT_DIALOG_H
+
+#include "scene/gui/dialogs.h"
+
+class Button;
+class EditorFileDialog;
+class LineEdit;
+class OptionButton;
+class TextureRect;
+
+class ProjectDialog : public ConfirmationDialog {
+ GDCLASS(ProjectDialog, ConfirmationDialog);
+
+public:
+ enum Mode {
+ MODE_NEW,
+ MODE_IMPORT,
+ MODE_INSTALL,
+ MODE_RENAME,
+ };
+
+private:
+ enum MessageType {
+ MESSAGE_ERROR,
+ MESSAGE_WARNING,
+ MESSAGE_SUCCESS,
+ };
+
+ enum InputType {
+ PROJECT_PATH,
+ INSTALL_PATH,
+ };
+
+ Mode mode = MODE_NEW;
+ bool is_folder_empty = true;
+
+ Button *browse = nullptr;
+ Button *install_browse = nullptr;
+ Button *create_dir = nullptr;
+ VBoxContainer *name_container = nullptr;
+ VBoxContainer *path_container = nullptr;
+ VBoxContainer *install_path_container = nullptr;
+
+ VBoxContainer *renderer_container = nullptr;
+ Label *renderer_info = nullptr;
+ HBoxContainer *default_files_container = nullptr;
+ Ref<ButtonGroup> renderer_button_group;
+
+ Label *msg = nullptr;
+ LineEdit *project_path = nullptr;
+ LineEdit *project_name = nullptr;
+ LineEdit *install_path = nullptr;
+ TextureRect *status_rect = nullptr;
+ TextureRect *install_status_rect = nullptr;
+
+ OptionButton *vcs_metadata_selection = nullptr;
+
+ EditorFileDialog *fdialog = nullptr;
+ EditorFileDialog *fdialog_install = nullptr;
+ AcceptDialog *dialog_error = nullptr;
+
+ String zip_path;
+ String zip_title;
+ String fav_dir;
+
+ String created_folder_path;
+
+ void _set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH);
+
+ String _test_path();
+ void _update_path(const String &p_path);
+ void _path_text_changed(const String &p_path);
+ void _path_selected(const String &p_path);
+ void _file_selected(const String &p_path);
+ void _install_path_selected(const String &p_path);
+
+ void _browse_path();
+ void _browse_install_path();
+ void _create_folder();
+
+ void _text_changed(const String &p_text);
+ void _nonempty_confirmation_ok_pressed();
+ void _renderer_selected();
+ void _remove_created_folder();
+
+ void ok_pressed() override;
+ void cancel_pressed() override;
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void set_zip_path(const String &p_path);
+ void set_zip_title(const String &p_title);
+ void set_mode(Mode p_mode);
+ void set_project_path(const String &p_path);
+
+ void ask_for_path_and_show();
+ void show_dialog();
+
+ ProjectDialog();
+};
+
+#endif // PROJECT_DIALOG_H
diff --git a/editor/project_manager/project_list.cpp b/editor/project_manager/project_list.cpp
new file mode 100644
index 0000000000..67aaa85501
--- /dev/null
+++ b/editor/project_manager/project_list.cpp
@@ -0,0 +1,1074 @@
+/**************************************************************************/
+/* project_list.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 "project_list.h"
+
+#include "core/config/project_settings.h"
+#include "core/io/dir_access.h"
+#include "editor/editor_paths.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+#include "editor/project_manager.h"
+#include "editor/project_manager/project_tag.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/label.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/texture_button.h"
+#include "scene/gui/texture_rect.h"
+#include "scene/resources/image_texture.h"
+
+void ProjectListItemControl::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED: {
+ if (icon_needs_reload) {
+ // The project icon may not be loaded by the time the control is displayed,
+ // so use a loading placeholder.
+ project_icon->set_texture(get_editor_theme_icon(SNAME("ProjectIconLoading")));
+ }
+
+ project_title->begin_bulk_theme_override();
+ project_title->add_theme_font_override("font", get_theme_font(SNAME("title"), EditorStringName(EditorFonts)));
+ project_title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
+ project_title->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
+ project_title->end_bulk_theme_override();
+
+ project_path->add_theme_color_override("font_color", get_theme_color(SNAME("font_color"), SNAME("Tree")));
+ project_unsupported_features->set_texture(get_editor_theme_icon(SNAME("NodeWarning")));
+
+ favorite_button->set_texture_normal(get_editor_theme_icon(SNAME("Favorites")));
+ if (project_is_missing) {
+ explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
+ } else {
+ explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
+ }
+ } break;
+
+ case NOTIFICATION_MOUSE_ENTER: {
+ is_hovering = true;
+ queue_redraw();
+ } break;
+
+ case NOTIFICATION_MOUSE_EXIT: {
+ is_hovering = false;
+ queue_redraw();
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ if (is_selected) {
+ draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), get_size()));
+ }
+ if (is_hovering) {
+ draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
+ }
+
+ draw_line(Point2(0, get_size().y + 1), Point2(get_size().x, get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
+ } break;
+ }
+}
+
+void ProjectListItemControl::_favorite_button_pressed() {
+ emit_signal(SNAME("favorite_pressed"));
+}
+
+void ProjectListItemControl::_explore_button_pressed() {
+ emit_signal(SNAME("explore_pressed"));
+}
+
+void ProjectListItemControl::set_project_title(const String &p_title) {
+ project_title->set_text(p_title);
+}
+
+void ProjectListItemControl::set_project_path(const String &p_path) {
+ project_path->set_text(p_path);
+}
+
+void ProjectListItemControl::set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list) {
+ for (const String &tag : p_tags) {
+ ProjectTag *tag_control = memnew(ProjectTag(tag));
+ tag_container->add_child(tag_control);
+ tag_control->connect_button_to(callable_mp(p_parent_list, &ProjectList::add_search_tag).bind(tag));
+ }
+}
+
+void ProjectListItemControl::set_project_icon(const Ref<Texture2D> &p_icon) {
+ icon_needs_reload = false;
+
+ // The default project icon is 128×128 to look crisp on hiDPI displays,
+ // but we want the actual displayed size to be 64×64 on loDPI displays.
+ project_icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
+ project_icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
+ project_icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+
+ project_icon->set_texture(p_icon);
+}
+
+void ProjectListItemControl::set_unsupported_features(PackedStringArray p_features) {
+ if (p_features.size() > 0) {
+ String tooltip_text = "";
+ for (int i = 0; i < p_features.size(); i++) {
+ if (ProjectList::project_feature_looks_like_version(p_features[i])) {
+ tooltip_text += TTR("This project was last edited in a different Godot version: ") + p_features[i] + "\n";
+ p_features.remove_at(i);
+ i--;
+ }
+ }
+ if (p_features.size() > 0) {
+ String unsupported_features_str = String(", ").join(p_features);
+ tooltip_text += TTR("This project uses features unsupported by the current build:") + "\n" + unsupported_features_str;
+ }
+ project_unsupported_features->set_tooltip_text(tooltip_text);
+ project_unsupported_features->show();
+ } else {
+ project_unsupported_features->hide();
+ }
+}
+
+bool ProjectListItemControl::should_load_project_icon() const {
+ return icon_needs_reload;
+}
+
+void ProjectListItemControl::set_selected(bool p_selected) {
+ is_selected = p_selected;
+ queue_redraw();
+}
+
+void ProjectListItemControl::set_is_favorite(bool p_favorite) {
+ favorite_button->set_modulate(p_favorite ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
+}
+
+void ProjectListItemControl::set_is_missing(bool p_missing) {
+ if (project_is_missing == p_missing) {
+ return;
+ }
+ project_is_missing = p_missing;
+
+ if (project_is_missing) {
+ project_icon->set_modulate(Color(1, 1, 1, 0.5));
+
+ explore_button->set_icon(get_editor_theme_icon(SNAME("FileBroken")));
+ explore_button->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
+ } else {
+ project_icon->set_modulate(Color(1, 1, 1, 1.0));
+
+ explore_button->set_icon(get_editor_theme_icon(SNAME("Load")));
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ explore_button->set_tooltip_text(TTR("Show in File Manager"));
+#else
+ // Opening the system file manager is not supported on the Android and web editors.
+ explore_button->hide();
+#endif
+ }
+}
+
+void ProjectListItemControl::set_is_grayed(bool p_grayed) {
+ if (p_grayed) {
+ main_vbox->set_modulate(Color(1, 1, 1, 0.5));
+ // Don't make the icon less prominent if the parent is already grayed out.
+ explore_button->set_modulate(Color(1, 1, 1, 1.0));
+ } else {
+ main_vbox->set_modulate(Color(1, 1, 1, 1.0));
+ explore_button->set_modulate(Color(1, 1, 1, 0.5));
+ }
+}
+
+void ProjectListItemControl::_bind_methods() {
+ ADD_SIGNAL(MethodInfo("favorite_pressed"));
+ ADD_SIGNAL(MethodInfo("explore_pressed"));
+}
+
+ProjectListItemControl::ProjectListItemControl() {
+ set_focus_mode(FocusMode::FOCUS_ALL);
+
+ VBoxContainer *favorite_box = memnew(VBoxContainer);
+ favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
+ add_child(favorite_box);
+
+ favorite_button = memnew(TextureButton);
+ favorite_button->set_name("FavoriteButton");
+ // This makes the project's "hover" style display correctly when hovering the favorite icon.
+ favorite_button->set_mouse_filter(MOUSE_FILTER_PASS);
+ favorite_box->add_child(favorite_button);
+ favorite_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_favorite_button_pressed));
+
+ project_icon = memnew(TextureRect);
+ project_icon->set_name("ProjectIcon");
+ project_icon->set_v_size_flags(SIZE_SHRINK_CENTER);
+ add_child(project_icon);
+
+ main_vbox = memnew(VBoxContainer);
+ main_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ add_child(main_vbox);
+
+ Control *ec = memnew(Control);
+ ec->set_custom_minimum_size(Size2(0, 1));
+ ec->set_mouse_filter(MOUSE_FILTER_PASS);
+ main_vbox->add_child(ec);
+
+ // Top half, title, tags and unsupported features labels.
+ {
+ HBoxContainer *title_hb = memnew(HBoxContainer);
+ main_vbox->add_child(title_hb);
+
+ project_title = memnew(Label);
+ project_title->set_auto_translate(false);
+ project_title->set_name("ProjectName");
+ project_title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ project_title->set_clip_text(true);
+ title_hb->add_child(project_title);
+
+ tag_container = memnew(HBoxContainer);
+ title_hb->add_child(tag_container);
+
+ Control *spacer = memnew(Control);
+ spacer->set_custom_minimum_size(Size2(10, 10));
+ title_hb->add_child(spacer);
+ }
+
+ // Bottom half, containing the path and view folder button.
+ {
+ HBoxContainer *path_hb = memnew(HBoxContainer);
+ path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ main_vbox->add_child(path_hb);
+
+ explore_button = memnew(Button);
+ explore_button->set_name("ExploreButton");
+ explore_button->set_flat(true);
+ path_hb->add_child(explore_button);
+ explore_button->connect("pressed", callable_mp(this, &ProjectListItemControl::_explore_button_pressed));
+
+ project_path = memnew(Label);
+ project_path->set_name("ProjectPath");
+ project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
+ project_path->set_clip_text(true);
+ project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ project_path->set_modulate(Color(1, 1, 1, 0.5));
+ path_hb->add_child(project_path);
+
+ project_unsupported_features = memnew(TextureRect);
+ project_unsupported_features->set_name("ProjectUnsupportedFeatures");
+ project_unsupported_features->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
+ path_hb->add_child(project_unsupported_features);
+ project_unsupported_features->hide();
+
+ Control *spacer = memnew(Control);
+ spacer->set_custom_minimum_size(Size2(10, 10));
+ path_hb->add_child(spacer);
+ }
+}
+
+struct ProjectListComparator {
+ ProjectList::FilterOption order_option = ProjectList::FilterOption::EDIT_DATE;
+
+ // operator<
+ _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
+ if (a.favorite && !b.favorite) {
+ return true;
+ }
+ if (b.favorite && !a.favorite) {
+ return false;
+ }
+ switch (order_option) {
+ case ProjectList::PATH:
+ return a.path < b.path;
+ case ProjectList::EDIT_DATE:
+ return a.last_edited > b.last_edited;
+ case ProjectList::TAGS:
+ return a.tag_sort_string < b.tag_sort_string;
+ default:
+ return a.project_name < b.project_name;
+ }
+ }
+};
+
+const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed";
+const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
+const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
+
+// Helpers.
+
+bool ProjectList::project_feature_looks_like_version(const String &p_feature) {
+ return p_feature.contains(".") && p_feature.substr(0, 3).is_numeric();
+}
+
+// Notifications.
+
+void ProjectList::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_PROCESS: {
+ // Load icons as a coroutine to speed up launch when you have hundreds of projects
+ if (_icon_load_index < _projects.size()) {
+ Item &item = _projects.write[_icon_load_index];
+ if (item.control->should_load_project_icon()) {
+ _load_project_icon(_icon_load_index);
+ }
+ _icon_load_index++;
+
+ } else {
+ set_process(false);
+ }
+ } break;
+ }
+}
+
+// Initialization & loading.
+
+void ProjectList::_migrate_config() {
+ // Proposal #1637 moved the project list from editor settings to a separate config file
+ // If the new config file doesn't exist, populate it from EditorSettings
+ if (FileAccess::exists(_config_path)) {
+ return;
+ }
+
+ List<PropertyInfo> properties;
+ EditorSettings::get_singleton()->get_property_list(&properties);
+
+ for (const PropertyInfo &E : properties) {
+ // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
+ String property_key = E.name;
+ if (!property_key.begins_with("projects/")) {
+ continue;
+ }
+
+ String path = EDITOR_GET(property_key);
+ print_line("Migrating legacy project '" + path + "'.");
+
+ String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
+ bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
+ add_project(path, favorite);
+ if (favorite) {
+ EditorSettings::get_singleton()->erase(favoriteKey);
+ }
+ EditorSettings::get_singleton()->erase(property_key);
+ }
+
+ save_config();
+}
+
+void ProjectList::save_config() {
+ _config.save(_config_path);
+}
+
+// Load project data from p_property_key and return it in a ProjectList::Item.
+// p_favorite is passed directly into the Item.
+ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
+ String conf = p_path.path_join("project.godot");
+ bool grayed = false;
+ bool missing = false;
+
+ Ref<ConfigFile> cf = memnew(ConfigFile);
+ Error cf_err = cf->load(conf);
+
+ int config_version = 0;
+ String project_name = TTR("Unnamed Project");
+ if (cf_err == OK) {
+ String cf_project_name = cf->get_value("application", "config/name", "");
+ if (!cf_project_name.is_empty()) {
+ project_name = cf_project_name.xml_unescape();
+ }
+ config_version = (int)cf->get_value("", "config_version", 0);
+ }
+
+ if (config_version > ProjectSettings::CONFIG_VERSION) {
+ // Comes from an incompatible (more recent) Godot version, gray it out.
+ grayed = true;
+ }
+
+ const String description = cf->get_value("application", "config/description", "");
+ const PackedStringArray tags = cf->get_value("application", "config/tags", PackedStringArray());
+ const String icon = cf->get_value("application", "config/icon", "");
+ const String main_scene = cf->get_value("application", "run/main_scene", "");
+
+ PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
+ PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
+
+ uint64_t last_edited = 0;
+ if (cf_err == OK) {
+ // The modification date marks the date the project was last edited.
+ // This is because the `project.godot` file will always be modified
+ // when editing a project (but not when running it).
+ last_edited = FileAccess::get_modified_time(conf);
+
+ String fscache = p_path.path_join(".fscache");
+ if (FileAccess::exists(fscache)) {
+ uint64_t cache_modified = FileAccess::get_modified_time(fscache);
+ if (cache_modified > last_edited) {
+ last_edited = cache_modified;
+ }
+ }
+ } else {
+ grayed = true;
+ missing = true;
+ print_line("Project is missing: " + conf);
+ }
+
+ for (const String &tag : tags) {
+ ProjectManager::get_singleton()->add_new_tag(tag);
+ }
+
+ return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
+}
+
+void ProjectList::_update_icons_async() {
+ _icon_load_index = 0;
+ set_process(true);
+}
+
+void ProjectList::_load_project_icon(int p_index) {
+ Item &item = _projects.write[p_index];
+
+ Ref<Texture2D> default_icon = get_editor_theme_icon(SNAME("DefaultProjectIcon"));
+ Ref<Texture2D> icon;
+ if (!item.icon.is_empty()) {
+ Ref<Image> img;
+ img.instantiate();
+ Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
+ if (err == OK) {
+ img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
+ icon = ImageTexture::create_from_image(img);
+ }
+ }
+ if (icon.is_null()) {
+ icon = default_icon;
+ }
+
+ item.control->set_project_icon(icon);
+}
+
+// Project list updates.
+
+void ProjectList::update_project_list() {
+ // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
+ // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
+ // FIXME: Does it really have to be a full, hard reload? Runtime updates should be made much cheaper.
+
+ // Clear whole list
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &project = _projects.write[i];
+ CRASH_COND(project.control == nullptr);
+ memdelete(project.control); // Why not queue_free()?
+ }
+ _projects.clear();
+ _last_clicked = "";
+ _selected_project_paths.clear();
+
+ List<String> sections;
+ _config.load(_config_path);
+ _config.get_sections(&sections);
+
+ for (const String &path : sections) {
+ bool favorite = _config.get_value(path, "favorite", false);
+ _projects.push_back(load_project_data(path, favorite));
+ }
+
+ // Create controls
+ for (int i = 0; i < _projects.size(); ++i) {
+ _create_project_item_control(i);
+ }
+
+ sort_projects();
+ _update_icons_async();
+ update_dock_menu();
+
+ set_v_scroll(0);
+ emit_signal(SNAME(SIGNAL_LIST_CHANGED));
+}
+
+void ProjectList::sort_projects() {
+ SortArray<Item, ProjectListComparator> sorter;
+ sorter.compare.order_option = _order_option;
+ sorter.sort(_projects.ptrw(), _projects.size());
+
+ String search_term;
+ PackedStringArray tags;
+
+ if (!_search_term.is_empty()) {
+ PackedStringArray search_parts = _search_term.split(" ");
+ if (search_parts.size() > 1 || search_parts[0].begins_with("tag:")) {
+ PackedStringArray remaining;
+ for (const String &part : search_parts) {
+ if (part.begins_with("tag:")) {
+ tags.push_back(part.get_slice(":", 1));
+ } else {
+ remaining.append(part);
+ }
+ }
+ search_term = String(" ").join(remaining); // Search term without tags.
+ } else {
+ search_term = _search_term;
+ }
+ }
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &item = _projects.write[i];
+
+ bool item_visible = true;
+ if (!_search_term.is_empty()) {
+ String search_path;
+ if (search_term.contains("/")) {
+ // Search path will match the whole path
+ search_path = item.path;
+ } else {
+ // Search path will only match the last path component to make searching more strict
+ search_path = item.path.get_file();
+ }
+
+ bool missing_tags = false;
+ for (const String &tag : tags) {
+ if (!item.tags.has(tag)) {
+ missing_tags = true;
+ break;
+ }
+ }
+
+ // When searching, display projects whose name or path contain the search term and whose tags match the searched tags.
+ item_visible = !missing_tags && (search_term.is_empty() || item.project_name.findn(search_term) != -1 || search_path.findn(search_term) != -1);
+ }
+
+ item.control->set_visible(item_visible);
+ }
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &item = _projects.write[i];
+ item.control->get_parent()->move_child(item.control, i);
+ }
+
+ // Rewind the coroutine because order of projects changed
+ _update_icons_async();
+ update_dock_menu();
+}
+
+int ProjectList::get_project_count() const {
+ return _projects.size();
+}
+
+void ProjectList::find_projects(const String &p_path) {
+ PackedStringArray paths = { p_path };
+ find_projects_multiple(paths);
+}
+
+void ProjectList::find_projects_multiple(const PackedStringArray &p_paths) {
+ List<String> projects;
+
+ for (int i = 0; i < p_paths.size(); i++) {
+ const String &base_path = p_paths.get(i);
+ print_verbose(vformat("Scanning for projects in \"%s\".", base_path));
+
+ _scan_folder_recursive(base_path, &projects);
+ print_verbose(vformat("Found %d project(s).", projects.size()));
+ }
+
+ for (const String &E : projects) {
+ add_project(E, false);
+ }
+
+ save_config();
+ update_project_list();
+}
+
+void ProjectList::_scan_folder_recursive(const String &p_path, List<String> *r_projects) {
+ Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ Error error = da->change_dir(p_path);
+ ERR_FAIL_COND_MSG(error != OK, vformat("Failed to open the path \"%s\" for scanning (code %d).", p_path, error));
+
+ da->list_dir_begin();
+ String n = da->get_next();
+ while (!n.is_empty()) {
+ if (da->current_is_dir() && n[0] != '.') {
+ _scan_folder_recursive(da->get_current_dir().path_join(n), r_projects);
+ } else if (n == "project.godot") {
+ r_projects->push_back(da->get_current_dir());
+ }
+ n = da->get_next();
+ }
+ da->list_dir_end();
+}
+
+// Project list items.
+
+void ProjectList::add_project(const String &dir_path, bool favorite) {
+ if (!_config.has_section(dir_path)) {
+ _config.set_value(dir_path, "favorite", favorite);
+ }
+}
+
+void ProjectList::set_project_version(const String &p_project_path, int p_version) {
+ for (ProjectList::Item &E : _projects) {
+ if (E.path == p_project_path) {
+ E.version = p_version;
+ break;
+ }
+ }
+}
+
+int ProjectList::refresh_project(const String &dir_path) {
+ // Reloads information about a specific project.
+ // If it wasn't loaded and should be in the list, it is added (i.e new project).
+ // If it isn't in the list anymore, it is removed.
+ // If it is in the list but doesn't exist anymore, it is marked as missing.
+
+ bool should_be_in_list = _config.has_section(dir_path);
+ bool is_favourite = _config.get_value(dir_path, "favorite", false);
+
+ bool was_selected = _selected_project_paths.has(dir_path);
+
+ // Remove item in any case
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &existing_item = _projects[i];
+ if (existing_item.path == dir_path) {
+ _remove_project(i, false);
+ break;
+ }
+ }
+
+ int index = -1;
+ if (should_be_in_list) {
+ // Recreate it with updated info
+
+ Item item = load_project_data(dir_path, is_favourite);
+
+ _projects.push_back(item);
+ _create_project_item_control(_projects.size() - 1);
+
+ sort_projects();
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].path == dir_path) {
+ if (was_selected) {
+ select_project(i);
+ ensure_project_visible(i);
+ }
+ _load_project_icon(i);
+
+ index = i;
+ break;
+ }
+ }
+ }
+
+ return index;
+}
+
+void ProjectList::ensure_project_visible(int p_index) {
+ const Item &item = _projects[p_index];
+ ensure_control_visible(item.control);
+}
+
+void ProjectList::_create_project_item_control(int p_index) {
+ // Will be added last in the list, so make sure indexes match
+ ERR_FAIL_COND(p_index != project_list_vbox->get_child_count());
+
+ Item &item = _projects.write[p_index];
+ ERR_FAIL_COND(item.control != nullptr); // Already created
+
+ ProjectListItemControl *hb = memnew(ProjectListItemControl);
+ hb->add_theme_constant_override("separation", 10 * EDSCALE);
+
+ hb->set_project_title(!item.missing ? item.project_name : TTR("Missing Project"));
+ hb->set_project_path(item.path);
+ hb->set_tooltip_text(item.description);
+ hb->set_tags(item.tags, this);
+ hb->set_unsupported_features(item.unsupported_features.duplicate());
+
+ hb->set_is_favorite(item.favorite);
+ hb->set_is_missing(item.missing);
+ hb->set_is_grayed(item.grayed);
+
+ hb->connect("gui_input", callable_mp(this, &ProjectList::_list_item_input).bind(hb));
+ hb->connect("favorite_pressed", callable_mp(this, &ProjectList::_on_favorite_pressed).bind(hb));
+
+#if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
+ hb->connect("explore_pressed", callable_mp(this, &ProjectList::_on_explore_pressed).bind(item.path));
+#endif
+
+ project_list_vbox->add_child(hb);
+ item.control = hb;
+}
+
+void ProjectList::_toggle_project(int p_index) {
+ // This methods adds to the selection or removes from the
+ // selection.
+ Item &item = _projects.write[p_index];
+
+ if (_selected_project_paths.has(item.path)) {
+ _deselect_project_nocheck(p_index);
+ } else {
+ _select_project_nocheck(p_index);
+ }
+}
+
+void ProjectList::_remove_project(int p_index, bool p_update_config) {
+ const Item item = _projects[p_index]; // Take a copy
+
+ _selected_project_paths.erase(item.path);
+
+ if (_last_clicked == item.path) {
+ _last_clicked = "";
+ }
+
+ memdelete(item.control);
+ _projects.remove_at(p_index);
+
+ if (p_update_config) {
+ _config.erase_section(item.path);
+ // Not actually saving the file, in case you are doing more changes to settings
+ }
+
+ update_dock_menu();
+}
+
+void ProjectList::_list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
+ Ref<InputEventMouseButton> mb = p_ev;
+ int clicked_index = p_hb->get_index();
+ const Item &clicked_project = _projects[clicked_index];
+
+ if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
+ if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
+ int anchor_index = -1;
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &p = _projects[i];
+ if (p.path == _last_clicked) {
+ anchor_index = p.control->get_index();
+ break;
+ }
+ }
+ CRASH_COND(anchor_index == -1);
+ _select_project_range(anchor_index, clicked_index);
+
+ } else if (mb->is_command_or_control_pressed()) {
+ _toggle_project(clicked_index);
+
+ } else {
+ _last_clicked = clicked_project.path;
+ select_project(clicked_index);
+ }
+
+ emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
+
+ // Do not allow opening a project more than once using a single project manager instance.
+ // Opening the same project in several editor instances at once can lead to various issues.
+ if (!mb->is_command_or_control_pressed() && mb->is_double_click() && !project_opening_initiated) {
+ emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
+ }
+ }
+}
+
+void ProjectList::_on_favorite_pressed(Node *p_hb) {
+ ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
+
+ int index = control->get_index();
+ Item item = _projects.write[index]; // Take copy
+
+ item.favorite = !item.favorite;
+
+ _config.set_value(item.path, "favorite", item.favorite);
+ save_config();
+
+ _projects.write[index] = item;
+
+ control->set_is_favorite(item.favorite);
+
+ sort_projects();
+
+ if (item.favorite) {
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].path == item.path) {
+ ensure_project_visible(i);
+ break;
+ }
+ }
+ }
+
+ update_dock_menu();
+}
+
+void ProjectList::_on_explore_pressed(const String &p_path) {
+ OS::get_singleton()->shell_show_in_file_manager(p_path, true);
+}
+
+// Project list selection.
+
+void ProjectList::_clear_project_selection() {
+ Vector<Item> previous_selected_items = get_selected_projects();
+ _selected_project_paths.clear();
+
+ for (int i = 0; i < previous_selected_items.size(); ++i) {
+ previous_selected_items[i].control->set_selected(false);
+ }
+}
+
+void ProjectList::_select_project_nocheck(int p_index) {
+ Item &item = _projects.write[p_index];
+ _selected_project_paths.insert(item.path);
+ item.control->set_selected(true);
+}
+
+void ProjectList::_deselect_project_nocheck(int p_index) {
+ Item &item = _projects.write[p_index];
+ _selected_project_paths.erase(item.path);
+ item.control->set_selected(false);
+}
+
+inline void _sort_project_range(int &a, int &b) {
+ if (a > b) {
+ int temp = a;
+ a = b;
+ b = temp;
+ }
+}
+
+void ProjectList::_select_project_range(int p_begin, int p_end) {
+ _clear_project_selection();
+
+ _sort_project_range(p_begin, p_end);
+ for (int i = p_begin; i <= p_end; ++i) {
+ _select_project_nocheck(i);
+ }
+}
+
+void ProjectList::select_project(int p_index) {
+ // This method keeps only one project selected.
+ _clear_project_selection();
+ _select_project_nocheck(p_index);
+}
+
+void ProjectList::select_first_visible_project() {
+ _clear_project_selection();
+
+ for (int i = 0; i < _projects.size(); i++) {
+ if (_projects[i].control->is_visible()) {
+ _select_project_nocheck(i);
+ break;
+ }
+ }
+}
+
+Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
+ Vector<Item> items;
+ if (_selected_project_paths.size() == 0) {
+ return items;
+ }
+ items.resize(_selected_project_paths.size());
+ int j = 0;
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &item = _projects[i];
+ if (_selected_project_paths.has(item.path)) {
+ items.write[j++] = item;
+ }
+ }
+ ERR_FAIL_COND_V(j != items.size(), items);
+ return items;
+}
+
+const HashSet<String> &ProjectList::get_selected_project_keys() const {
+ // Faster if that's all you need
+ return _selected_project_paths;
+}
+
+int ProjectList::get_single_selected_index() const {
+ if (_selected_project_paths.size() == 0) {
+ // Default selection
+ return 0;
+ }
+ String key;
+ if (_selected_project_paths.size() == 1) {
+ // Only one selected
+ key = *_selected_project_paths.begin();
+ } else {
+ // Multiple selected, consider the last clicked one as "main"
+ key = _last_clicked;
+ }
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].path == key) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
+ if (_selected_project_paths.size() == 0) {
+ return;
+ }
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ Item &item = _projects.write[i];
+ if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
+ _config.erase_section(item.path);
+
+ // Comment out for now until we have a better warning system to
+ // ensure users delete their project only.
+ //if (p_delete_project_contents) {
+ // OS::get_singleton()->move_to_trash(item.path);
+ //}
+
+ memdelete(item.control);
+ _projects.remove_at(i);
+ --i;
+ }
+ }
+
+ save_config();
+ _selected_project_paths.clear();
+ _last_clicked = "";
+
+ update_dock_menu();
+}
+
+// Missing projects.
+
+bool ProjectList::is_any_project_missing() const {
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (_projects[i].missing) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void ProjectList::erase_missing_projects() {
+ if (_projects.is_empty()) {
+ return;
+ }
+
+ int deleted_count = 0;
+ int remaining_count = 0;
+
+ for (int i = 0; i < _projects.size(); ++i) {
+ const Item &item = _projects[i];
+
+ if (item.missing) {
+ _remove_project(i, true);
+ --i;
+ ++deleted_count;
+
+ } else {
+ ++remaining_count;
+ }
+ }
+
+ print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
+ save_config();
+}
+
+// Project list sorting and filtering.
+
+void ProjectList::set_search_term(String p_search_term) {
+ _search_term = p_search_term;
+}
+
+void ProjectList::add_search_tag(const String &p_tag) {
+ const String tag_string = "tag:" + p_tag;
+
+ int exists = _search_term.find(tag_string);
+ if (exists > -1) {
+ _search_term = _search_term.erase(exists, tag_string.length() + 1);
+ } else if (_search_term.is_empty() || _search_term.ends_with(" ")) {
+ _search_term += tag_string;
+ } else {
+ _search_term += " " + tag_string;
+ }
+ ProjectManager::get_singleton()->get_search_box()->set_text(_search_term);
+
+ sort_projects();
+}
+
+void ProjectList::set_order_option(int p_option) {
+ FilterOption selected = (FilterOption)p_option;
+ EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
+ EditorSettings::get_singleton()->save();
+ _order_option = selected;
+
+ sort_projects();
+}
+
+// Global menu integration.
+
+void ProjectList::update_dock_menu() {
+ if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) {
+ return;
+ }
+ DisplayServer::get_singleton()->global_menu_clear("_dock");
+
+ int favs_added = 0;
+ int total_added = 0;
+ for (int i = 0; i < _projects.size(); ++i) {
+ if (!_projects[i].grayed && !_projects[i].missing) {
+ if (_projects[i].favorite) {
+ favs_added++;
+ } else {
+ if (favs_added != 0) {
+ DisplayServer::get_singleton()->global_menu_add_separator("_dock");
+ }
+ favs_added = 0;
+ }
+ DisplayServer::get_singleton()->global_menu_add_item("_dock", _projects[i].project_name + " ( " + _projects[i].path + " )", callable_mp(this, &ProjectList::_global_menu_open_project), Callable(), i);
+ total_added++;
+ }
+ }
+ if (total_added != 0) {
+ DisplayServer::get_singleton()->global_menu_add_separator("_dock");
+ }
+ DisplayServer::get_singleton()->global_menu_add_item("_dock", TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
+}
+
+void ProjectList::_global_menu_new_window(const Variant &p_tag) {
+ List<String> args;
+ args.push_back("-p");
+ OS::get_singleton()->create_instance(args);
+}
+
+void ProjectList::_global_menu_open_project(const Variant &p_tag) {
+ int idx = (int)p_tag;
+
+ if (idx >= 0 && idx < _projects.size()) {
+ String conf = _projects[idx].path.path_join("project.godot");
+ List<String> args;
+ args.push_back(conf);
+ OS::get_singleton()->create_instance(args);
+ }
+}
+
+// Object methods.
+
+void ProjectList::_bind_methods() {
+ ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
+ ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
+ ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
+}
+
+ProjectList::ProjectList() {
+ project_list_vbox = memnew(VBoxContainer);
+ project_list_vbox->set_h_size_flags(Control::SIZE_EXPAND_FILL);
+ add_child(project_list_vbox);
+
+ _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
+ _migrate_config();
+}
diff --git a/editor/project_manager/project_list.h b/editor/project_manager/project_list.h
new file mode 100644
index 0000000000..86f1f13bd8
--- /dev/null
+++ b/editor/project_manager/project_list.h
@@ -0,0 +1,264 @@
+/**************************************************************************/
+/* project_list.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef PROJECT_LIST_H
+#define PROJECT_LIST_H
+
+#include "core/io/config_file.h"
+#include "scene/gui/box_container.h"
+#include "scene/gui/scroll_container.h"
+
+class Button;
+class Label;
+class ProjectList;
+class TextureButton;
+class TextureRect;
+
+class ProjectListItemControl : public HBoxContainer {
+ GDCLASS(ProjectListItemControl, HBoxContainer)
+
+ VBoxContainer *main_vbox = nullptr;
+ TextureButton *favorite_button = nullptr;
+ Button *explore_button = nullptr;
+
+ TextureRect *project_icon = nullptr;
+ Label *project_title = nullptr;
+ Label *project_path = nullptr;
+ TextureRect *project_unsupported_features = nullptr;
+ HBoxContainer *tag_container = nullptr;
+
+ bool project_is_missing = false;
+ bool icon_needs_reload = true;
+ bool is_selected = false;
+ bool is_hovering = false;
+
+ void _favorite_button_pressed();
+ void _explore_button_pressed();
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ void set_project_title(const String &p_title);
+ void set_project_path(const String &p_path);
+ void set_tags(const PackedStringArray &p_tags, ProjectList *p_parent_list);
+ void set_project_icon(const Ref<Texture2D> &p_icon);
+ void set_unsupported_features(PackedStringArray p_features);
+
+ bool should_load_project_icon() const;
+ void set_selected(bool p_selected);
+
+ void set_is_favorite(bool p_favorite);
+ void set_is_missing(bool p_missing);
+ void set_is_grayed(bool p_grayed);
+
+ ProjectListItemControl();
+};
+
+class ProjectList : public ScrollContainer {
+ GDCLASS(ProjectList, ScrollContainer)
+
+ friend class ProjectManager;
+
+public:
+ enum FilterOption {
+ EDIT_DATE,
+ NAME,
+ PATH,
+ TAGS,
+ };
+
+ // Can often be passed by copy.
+ struct Item {
+ String project_name;
+ String description;
+ PackedStringArray tags;
+ String tag_sort_string;
+ String path;
+ String icon;
+ String main_scene;
+ PackedStringArray unsupported_features;
+ uint64_t last_edited = 0;
+ bool favorite = false;
+ bool grayed = false;
+ bool missing = false;
+ int version = 0;
+
+ ProjectListItemControl *control = nullptr;
+
+ Item() {}
+
+ Item(const String &p_name,
+ const String &p_description,
+ const PackedStringArray &p_tags,
+ const String &p_path,
+ const String &p_icon,
+ const String &p_main_scene,
+ const PackedStringArray &p_unsupported_features,
+ uint64_t p_last_edited,
+ bool p_favorite,
+ bool p_grayed,
+ bool p_missing,
+ int p_version) {
+ project_name = p_name;
+ description = p_description;
+ tags = p_tags;
+ path = p_path;
+ icon = p_icon;
+ main_scene = p_main_scene;
+ unsupported_features = p_unsupported_features;
+ last_edited = p_last_edited;
+ favorite = p_favorite;
+ grayed = p_grayed;
+ missing = p_missing;
+ version = p_version;
+
+ control = nullptr;
+
+ PackedStringArray sorted_tags = tags;
+ sorted_tags.sort();
+ tag_sort_string = String().join(sorted_tags);
+ }
+
+ _FORCE_INLINE_ bool operator==(const Item &l) const {
+ return path == l.path;
+ }
+ };
+
+private:
+ String _config_path;
+ ConfigFile _config;
+
+ Vector<Item> _projects;
+
+ int _icon_load_index = 0;
+ bool project_opening_initiated = false;
+
+ String _search_term;
+ FilterOption _order_option = FilterOption::EDIT_DATE;
+ HashSet<String> _selected_project_paths;
+ String _last_clicked; // Project key
+
+ VBoxContainer *project_list_vbox = nullptr;
+
+ // Initialization & loading.
+
+ void _migrate_config();
+
+ static Item load_project_data(const String &p_property_key, bool p_favorite);
+ void _update_icons_async();
+ void _load_project_icon(int p_index);
+
+ // Project list updates.
+
+ void _scan_folder_recursive(const String &p_path, List<String> *r_projects);
+
+ // Project list items.
+
+ void _create_project_item_control(int p_index);
+ void _toggle_project(int p_index);
+ void _remove_project(int p_index, bool p_update_settings);
+
+ void _list_item_input(const Ref<InputEvent> &p_ev, Node *p_hb);
+ void _on_favorite_pressed(Node *p_hb);
+ void _on_explore_pressed(const String &p_path);
+
+ // Project list selection.
+
+ void _clear_project_selection();
+ void _select_project_nocheck(int p_index);
+ void _deselect_project_nocheck(int p_index);
+ void _select_project_range(int p_begin, int p_end);
+
+ // Global menu integration.
+
+ void _global_menu_new_window(const Variant &p_tag);
+ void _global_menu_open_project(const Variant &p_tag);
+
+protected:
+ void _notification(int p_what);
+ static void _bind_methods();
+
+public:
+ static const char *SIGNAL_LIST_CHANGED;
+ static const char *SIGNAL_SELECTION_CHANGED;
+ static const char *SIGNAL_PROJECT_ASK_OPEN;
+
+ static bool project_feature_looks_like_version(const String &p_feature);
+
+ // Initialization & loading.
+
+ void save_config();
+
+ // Project list updates.
+
+ void update_project_list();
+ void sort_projects();
+ int get_project_count() const;
+
+ void find_projects(const String &p_path);
+ void find_projects_multiple(const PackedStringArray &p_paths);
+
+ // Project list items.
+
+ void add_project(const String &dir_path, bool favorite);
+ void set_project_version(const String &p_project_path, int version);
+ int refresh_project(const String &dir_path);
+ void ensure_project_visible(int p_index);
+
+ // Project list selection.
+
+ void select_project(int p_index);
+ void select_first_visible_project();
+ Vector<Item> get_selected_projects() const;
+ const HashSet<String> &get_selected_project_keys() const;
+ int get_single_selected_index() const;
+ void erase_selected_projects(bool p_delete_project_contents);
+
+ // Missing projects.
+
+ bool is_any_project_missing() const;
+ void erase_missing_projects();
+
+ // Project list sorting and filtering.
+
+ void set_search_term(String p_search_term);
+ void add_search_tag(const String &p_tag);
+ void set_order_option(int p_option);
+
+ // Global menu integration.
+
+ void update_dock_menu();
+
+ ProjectList();
+};
+
+#endif // PROJECT_LIST_H
diff --git a/editor/project_manager/project_tag.cpp b/editor/project_manager/project_tag.cpp
new file mode 100644
index 0000000000..de9213177b
--- /dev/null
+++ b/editor/project_manager/project_tag.cpp
@@ -0,0 +1,74 @@
+/**************************************************************************/
+/* project_tag.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 "project_tag.h"
+
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/color_rect.h"
+
+void ProjectTag::_notification(int p_what) {
+ if (display_close && p_what == NOTIFICATION_THEME_CHANGED) {
+ button->set_icon(get_theme_icon(SNAME("close"), SNAME("TabBar")));
+ }
+}
+
+void ProjectTag::connect_button_to(const Callable &p_callable) {
+ button->connect(SNAME("pressed"), p_callable, CONNECT_DEFERRED);
+}
+
+const String ProjectTag::get_tag() const {
+ return tag_string;
+}
+
+ProjectTag::ProjectTag(const String &p_text, bool p_display_close) {
+ add_theme_constant_override(SNAME("separation"), 0);
+ set_v_size_flags(SIZE_SHRINK_CENTER);
+ tag_string = p_text;
+ display_close = p_display_close;
+
+ Color tag_color = Color(1, 0, 0);
+ tag_color.set_ok_hsl_s(0.8);
+ tag_color.set_ok_hsl_h(float(p_text.hash() * 10001 % UINT32_MAX) / float(UINT32_MAX));
+ set_self_modulate(tag_color);
+
+ ColorRect *cr = memnew(ColorRect);
+ add_child(cr);
+ cr->set_custom_minimum_size(Vector2(4, 0) * EDSCALE);
+ cr->set_color(tag_color);
+
+ button = memnew(Button);
+ add_child(button);
+ button->set_auto_translate(false);
+ button->set_text(p_text.capitalize());
+ button->set_focus_mode(FOCUS_NONE);
+ button->set_icon_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
+ button->set_theme_type_variation(SNAME("ProjectTag"));
+}
diff --git a/editor/project_manager/project_tag.h b/editor/project_manager/project_tag.h
new file mode 100644
index 0000000000..2cd15f64b9
--- /dev/null
+++ b/editor/project_manager/project_tag.h
@@ -0,0 +1,56 @@
+/**************************************************************************/
+/* project_tag.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef PROJECT_TAG_H
+#define PROJECT_TAG_H
+
+#include "scene/gui/box_container.h"
+
+class Button;
+
+class ProjectTag : public HBoxContainer {
+ GDCLASS(ProjectTag, HBoxContainer);
+
+ String tag_string;
+ bool display_close = false;
+
+ Button *button = nullptr;
+
+protected:
+ void _notification(int p_what);
+
+public:
+ void connect_button_to(const Callable &p_callable);
+ const String get_tag() const;
+
+ ProjectTag(const String &p_text, bool p_display_close = false);
+};
+
+#endif // PROJECT_TAG_H
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index 9d1c0d6c62..9c7ba827b7 100644
--- a/editor/scene_tree_dock.cpp
+++ b/editor/scene_tree_dock.cpp
@@ -3998,6 +3998,7 @@ void SceneTreeDock::_list_all_subresources(PopupMenu *p_menu) {
}
p_menu->add_item(display_text);
+ p_menu->set_item_tooltip(-1, pair.first->get_path());
p_menu->set_item_metadata(-1, pair.first->get_instance_id());
}
}
diff --git a/editor/themes/editor_fonts.cpp b/editor/themes/editor_fonts.cpp
index fc3631653c..ee61387702 100644
--- a/editor/themes/editor_fonts.cpp
+++ b/editor/themes/editor_fonts.cpp
@@ -107,7 +107,6 @@ Ref<FontVariation> make_bold_font(const Ref<Font> &p_font, double p_embolden, Ty
}
void editor_register_fonts(const Ref<Theme> &p_theme) {
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", "Register Fonts");
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
TextServer::FontAntialiasing font_antialiasing = (TextServer::FontAntialiasing)(int)EDITOR_GET("interface/editor/font_antialiasing");
@@ -444,6 +443,4 @@ void editor_register_fonts(const Ref<Theme> &p_theme) {
p_theme->set_font_size("status_source_size", EditorStringName(EditorFonts), default_font_size);
p_theme->set_font("status_source", EditorStringName(EditorFonts), mono_other_fc);
-
- OS::get_singleton()->benchmark_end_measure("EditorTheme", "Register Fonts");
}
diff --git a/editor/themes/editor_icons.cpp b/editor/themes/editor_icons.cpp
index bb767747f3..f318b640d0 100644
--- a/editor/themes/editor_icons.cpp
+++ b/editor/themes/editor_icons.cpp
@@ -78,9 +78,8 @@ Ref<ImageTexture> editor_generate_icon(int p_index, float p_scale, float p_satur
return ImageTexture::create_from_image(img);
}
-float get_gizmo_handle_scale(const String &gizmo_handle_name = "") {
- const float scale_gizmo_handles_for_touch = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
- if (scale_gizmo_handles_for_touch > 1.0f) {
+float get_gizmo_handle_scale(const String &p_gizmo_handle_name, float p_gizmo_handle_scale) {
+ if (p_gizmo_handle_scale > 1.0f) {
// The names of the icons that require additional scaling.
static HashSet<StringName> gizmo_to_scale;
if (gizmo_to_scale.is_empty()) {
@@ -92,18 +91,15 @@ float get_gizmo_handle_scale(const String &gizmo_handle_name = "") {
gizmo_to_scale.insert("EditorPathSmoothHandle");
}
- if (gizmo_to_scale.has(gizmo_handle_name)) {
- return EDSCALE * scale_gizmo_handles_for_touch;
+ if (gizmo_to_scale.has(p_gizmo_handle_name)) {
+ return EDSCALE * p_gizmo_handle_scale;
}
}
return EDSCALE;
}
-void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, bool p_only_thumbs) {
- const String benchmark_key = vformat("Generate Icons (%s)", (p_only_thumbs ? "Only Thumbs" : "All"));
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", benchmark_key);
-
+void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, float p_gizmo_handle_scale) {
// Before we register the icons, we adjust their colors and saturation.
// Most icons follow the standard rules for color conversion to follow the editor
// theme's polarity (dark/light). We also adjust the saturation for most icons,
@@ -158,13 +154,13 @@ void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p
accent_color_icons.insert("PlayOverlay");
// Generate icons.
- if (!p_only_thumbs) {
+ {
for (int i = 0; i < editor_icons_count; i++) {
Ref<ImageTexture> icon;
const String &editor_icon_name = editor_icons_names[i];
if (accent_color_icons.has(editor_icon_name)) {
- icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name), 1.0, accent_color_map);
+ icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name, p_gizmo_handle_scale), 1.0, accent_color_map);
} else {
float saturation = p_icon_saturation;
if (saturation_exceptions.has(editor_icon_name)) {
@@ -172,9 +168,9 @@ void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p
}
if (conversion_exceptions.has(editor_icon_name)) {
- icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name), saturation);
+ icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name, p_gizmo_handle_scale), saturation);
} else {
- icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name), saturation, color_conversion_map);
+ icon = editor_generate_icon(i, get_gizmo_handle_scale(editor_icon_name, p_gizmo_handle_scale), saturation, color_conversion_map);
}
}
@@ -231,7 +227,6 @@ void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p
p_theme->set_icon(editor_icons_names[index], EditorStringName(EditorIcons), icon);
}
}
- OS::get_singleton()->benchmark_end_measure("EditorTheme", benchmark_key);
}
void editor_copy_icons(const Ref<Theme> &p_theme, const Ref<Theme> &p_old_theme) {
diff --git a/editor/themes/editor_icons.h b/editor/themes/editor_icons.h
index 2094ebf27c..447057b5e4 100644
--- a/editor/themes/editor_icons.h
+++ b/editor/themes/editor_icons.h
@@ -34,7 +34,7 @@
#include "scene/resources/theme.h"
void editor_configure_icons(bool p_dark_theme);
-void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, bool p_only_thumbs = false);
+void editor_register_icons(const Ref<Theme> &p_theme, bool p_dark_theme, float p_icon_saturation, int p_thumb_size, float p_gizmo_handle_scale);
void editor_copy_icons(const Ref<Theme> &p_theme, const Ref<Theme> &p_old_theme);
String get_default_project_icon();
diff --git a/editor/themes/editor_theme.h b/editor/themes/editor_theme.h
index 41a60fdf96..2cc7ad287e 100644
--- a/editor/themes/editor_theme.h
+++ b/editor/themes/editor_theme.h
@@ -38,6 +38,10 @@ class EditorTheme : public Theme {
static Vector<StringName> editor_theme_types;
+ uint32_t generated_hash = 0;
+ uint32_t generated_fonts_hash = 0;
+ uint32_t generated_icons_hash = 0;
+
public:
virtual Color get_color(const StringName &p_name, const StringName &p_theme_type) const override;
virtual int get_constant(const StringName &p_name, const StringName &p_theme_type) const override;
@@ -46,6 +50,15 @@ public:
virtual Ref<Texture2D> get_icon(const StringName &p_name, const StringName &p_theme_type) const override;
virtual Ref<StyleBox> get_stylebox(const StringName &p_name, const StringName &p_theme_type) const override;
+ void set_generated_hash(uint32_t p_hash) { generated_hash = p_hash; }
+ uint32_t get_generated_hash() const { return generated_hash; }
+
+ void set_generated_fonts_hash(uint32_t p_hash) { generated_fonts_hash = p_hash; }
+ uint32_t get_generated_fonts_hash() const { return generated_fonts_hash; }
+
+ void set_generated_icons_hash(uint32_t p_hash) { generated_icons_hash = p_hash; }
+ uint32_t get_generated_icons_hash() const { return generated_icons_hash; }
+
static void initialize();
static void finalize();
};
diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp
index 4ce323c763..886f105efc 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -45,7 +45,81 @@
#include "scene/resources/style_box_texture.h"
#include "scene/resources/texture.h"
-// Helper methods.
+// Theme configuration.
+
+uint32_t EditorThemeManager::ThemeConfiguration::hash() {
+ uint32_t hash = hash_murmur3_one_float(EDSCALE);
+
+ // Basic properties.
+
+ hash = hash_murmur3_one_32(preset.hash(), hash);
+ hash = hash_murmur3_one_32(spacing_preset.hash(), hash);
+
+ hash = hash_murmur3_one_32(base_color.to_rgba32(), hash);
+ hash = hash_murmur3_one_32(accent_color.to_rgba32(), hash);
+ hash = hash_murmur3_one_float(contrast, hash);
+ hash = hash_murmur3_one_float(icon_saturation, hash);
+
+ // Extra properties.
+
+ hash = hash_murmur3_one_32(base_spacing, hash);
+ hash = hash_murmur3_one_32(extra_spacing, hash);
+ hash = hash_murmur3_one_32(border_width, hash);
+ hash = hash_murmur3_one_32(corner_radius, hash);
+
+ hash = hash_murmur3_one_32((int)draw_extra_borders, hash);
+ hash = hash_murmur3_one_float(relationship_line_opacity, hash);
+ hash = hash_murmur3_one_32(thumb_size, hash);
+ hash = hash_murmur3_one_32(class_icon_size, hash);
+ hash = hash_murmur3_one_32((int)increase_scrollbar_touch_area, hash);
+ hash = hash_murmur3_one_float(gizmo_handle_scale, hash);
+ hash = hash_murmur3_one_32(color_picker_button_height, hash);
+ hash = hash_murmur3_one_float(subresource_hue_tint, hash);
+
+ hash = hash_murmur3_one_float(default_contrast, hash);
+
+ // Generated properties.
+
+ hash = hash_murmur3_one_32((int)dark_theme, hash);
+
+ return hash;
+}
+
+uint32_t EditorThemeManager::ThemeConfiguration::hash_fonts() {
+ uint32_t hash = hash_murmur3_one_float(EDSCALE);
+
+ // TODO: Implement the hash based on what editor_register_fonts() uses.
+
+ return hash;
+}
+
+uint32_t EditorThemeManager::ThemeConfiguration::hash_icons() {
+ uint32_t hash = hash_murmur3_one_float(EDSCALE);
+
+ hash = hash_murmur3_one_32(accent_color.to_rgba32(), hash);
+ hash = hash_murmur3_one_float(icon_saturation, hash);
+
+ hash = hash_murmur3_one_32(thumb_size, hash);
+ hash = hash_murmur3_one_float(gizmo_handle_scale, hash);
+
+ hash = hash_murmur3_one_32((int)dark_theme, hash);
+
+ return hash;
+}
+
+// Benchmarks.
+
+int EditorThemeManager::benchmark_run = 0;
+
+String EditorThemeManager::get_benchmark_key() {
+ if (benchmark_run == 0) {
+ return "EditorTheme (Startup)";
+ }
+
+ return vformat("EditorTheme (Run %d)", benchmark_run);
+}
+
+// Generation helper methods.
Ref<StyleBoxTexture> make_stylebox(Ref<Texture2D> p_texture, float p_left, float p_top, float p_right, float p_bottom, float p_margin_left = -1, float p_margin_top = -1, float p_margin_right = -1, float p_margin_bottom = -1, bool p_draw_center = true) {
Ref<StyleBoxTexture> style(memnew(StyleBoxTexture));
@@ -86,73 +160,71 @@ Ref<StyleBoxLine> make_line_stylebox(Color p_color, int p_thickness = 1, float p
// Theme generation and population routines.
-Ref<Theme> EditorThemeManager::_create_base_theme(const Ref<Theme> &p_old_theme) {
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", "Create Base Theme");
+Ref<EditorTheme> EditorThemeManager::_create_base_theme(const Ref<EditorTheme> &p_old_theme) {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Create Base Theme");
Ref<EditorTheme> theme = memnew(EditorTheme);
ThemeConfiguration config = _create_theme_config(theme);
+ theme->set_generated_hash(config.hash());
+ theme->set_generated_fonts_hash(config.hash_fonts());
+ theme->set_generated_icons_hash(config.hash_icons());
+
+ print_verbose(vformat("EditorTheme: Generating new theme for the config '%d'.", theme->get_generated_hash()));
+
_create_shared_styles(theme, config);
- // FIXME: Make the comparison more robust and fix imprecision issues by hashing affecting values.
- // TODO: Refactor the icons check into their respective file, and add a similar check for fonts.
-
- // Register editor icons.
- // If settings are comparable to the old theme, then just copy existing icons over.
- // Otherwise, regenerate them. Also check if we need to regenerate "thumb" icons.
- bool keep_old_icons = false;
- bool regenerate_thumb_icons = true;
- if (p_old_theme != nullptr) {
- // We check editor scale, theme dark/light mode, icon saturation, and accent color.
-
- // That doesn't really work as expected, since theme constants are integers, and scales are floats.
- // So this check will never work when changing between 100-199% values.
- const float prev_scale = (float)p_old_theme->get_constant(SNAME("scale"), EditorStringName(Editor));
- const bool prev_dark_theme = (bool)p_old_theme->get_constant(SNAME("dark_theme"), EditorStringName(Editor));
- const Color prev_accent_color = p_old_theme->get_color(SNAME("accent_color"), EditorStringName(Editor));
- const float prev_icon_saturation = p_old_theme->get_color(SNAME("icon_saturation"), EditorStringName(Editor)).r;
- const float prev_gizmo_handle_scale = (float)p_old_theme->get_constant(SNAME("gizmo_handle_scale"), EditorStringName(Editor));
-
- keep_old_icons = (Math::is_equal_approx(prev_scale, EDSCALE) &&
- Math::is_equal_approx(prev_gizmo_handle_scale, config.gizmo_handle_scale) &&
- prev_dark_theme == config.dark_theme &&
- prev_accent_color == config.accent_color &&
- prev_icon_saturation == config.icon_saturation);
-
- const double prev_thumb_size = (double)p_old_theme->get_constant(SNAME("thumb_size"), EditorStringName(Editor));
-
- regenerate_thumb_icons = !Math::is_equal_approx(prev_thumb_size, config.thumb_size);
- }
+ // Register icons.
+ {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Register Icons");
- // External functions, see editor_icons.cpp.
- editor_configure_icons(config.dark_theme);
- if (keep_old_icons) {
- editor_copy_icons(theme, p_old_theme);
- } else {
- editor_register_icons(theme, config.dark_theme, config.icon_saturation, config.thumb_size, false);
+ // External functions, see editor_icons.cpp.
+ editor_configure_icons(config.dark_theme);
+
+ // If settings are comparable to the old theme, then just copy existing icons over.
+ // Otherwise, regenerate them.
+ bool keep_old_icons = (p_old_theme != nullptr && theme->get_generated_icons_hash() == p_old_theme->get_generated_icons_hash());
+ if (keep_old_icons) {
+ print_verbose("EditorTheme: Can keep old icons, copying.");
+ editor_copy_icons(theme, p_old_theme);
+ } else {
+ print_verbose("EditorTheme: Generating new icons.");
+ editor_register_icons(theme, config.dark_theme, config.icon_saturation, config.thumb_size, config.gizmo_handle_scale);
+ }
+
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Register Icons");
}
- if (regenerate_thumb_icons) {
- editor_register_icons(theme, config.dark_theme, config.icon_saturation, config.thumb_size, true);
+
+ // Register fonts.
+ {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Register Fonts");
+
+ // TODO: Check if existing font definitions from the old theme are usable and copy them.
+
+ // External function, see editor_fonts.cpp.
+ print_verbose("EditorTheme: Generating new fonts.");
+ editor_register_fonts(theme);
+
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Register Fonts");
}
- // External function, see editor_fonts.cpp.
- editor_register_fonts(theme);
+ // TODO: Check if existing style definitions from the old theme are usable and copy them.
+ print_verbose("EditorTheme: Generating new styles.");
_populate_standard_styles(theme, config);
_populate_editor_styles(theme, config);
_populate_text_editor_styles(theme, config);
- OS::get_singleton()->benchmark_end_measure("EditorTheme", "Create Base Theme");
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Create Base Theme");
return theme;
}
-EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(const Ref<Theme> &p_theme) {
+EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(const Ref<EditorTheme> &p_theme) {
ThemeConfiguration config;
// Basic properties.
config.preset = EDITOR_GET("interface/theme/preset");
config.spacing_preset = EDITOR_GET("interface/theme/spacing_preset");
- config.dark_theme = EditorSettings::get_singleton()->is_dark_theme();
config.base_color = EDITOR_GET("interface/theme/base_color");
config.accent_color = EDITOR_GET("interface/theme/accent_color");
@@ -174,6 +246,7 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(
config.increase_scrollbar_touch_area = EDITOR_GET("interface/touchscreen/increase_scrollbar_touch_area");
config.gizmo_handle_scale = EDITOR_GET("interface/touchscreen/scale_gizmo_handles");
config.color_picker_button_height = 28 * EDSCALE;
+ config.subresource_hue_tint = EDITOR_GET("docks/property_editor/subresource_hue_tint");
config.default_contrast = 0.3; // Make sure to keep this in sync with the editor settings definition.
@@ -275,6 +348,8 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(
// Generated properties.
+ config.dark_theme = is_dark_theme();
+
config.base_margin = config.base_spacing;
config.increased_margin = config.base_spacing + config.extra_spacing;
config.separation_margin = (config.base_spacing + config.extra_spacing / 2) * EDSCALE;
@@ -292,7 +367,7 @@ EditorThemeManager::ThemeConfiguration EditorThemeManager::_create_theme_config(
return config;
}
-void EditorThemeManager::_create_shared_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_create_shared_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
// Colors.
{
// Base colors.
@@ -556,7 +631,7 @@ void EditorThemeManager::_create_shared_styles(const Ref<Theme> &p_theme, ThemeC
}
}
-void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_populate_standard_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
// Panels.
{
// Panel.
@@ -1367,6 +1442,10 @@ void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &p_theme, Th
p_theme->set_color("selection_stroke", "GraphEdit", p_theme->get_color(SNAME("box_selection_stroke_color"), EditorStringName(Editor)));
p_theme->set_color("activity", "GraphEdit", p_config.accent_color);
+ p_theme->set_color("connection_hover_tint_color", "GraphEdit", p_config.dark_theme ? Color(0, 0, 0, 0.3) : Color(1, 1, 1, 0.3));
+ p_theme->set_color("connection_valid_target_tint_color", "GraphEdit", p_config.dark_theme ? Color(1, 1, 1, 0.4) : Color(0, 0, 0, 0.4));
+ p_theme->set_color("connection_rim_color", "GraphEdit", p_config.tree_panel_style->get_bg_color());
+
p_theme->set_icon("zoom_out", "GraphEdit", p_theme->get_icon(SNAME("ZoomLess"), EditorStringName(EditorIcons)));
p_theme->set_icon("zoom_in", "GraphEdit", p_theme->get_icon(SNAME("ZoomMore"), EditorStringName(EditorIcons)));
p_theme->set_icon("zoom_reset", "GraphEdit", p_theme->get_icon(SNAME("ZoomReset"), EditorStringName(EditorIcons)));
@@ -1508,7 +1587,7 @@ void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &p_theme, Th
}
}
-void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_populate_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
// Project manager.
{
p_theme->set_stylebox("search_panel", "ProjectManager", p_config.tree_panel_style);
@@ -1690,6 +1769,13 @@ void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, Them
p_theme->set_stylebox("pressed", "EditorLogFilterButton", editor_log_button_pressed);
}
+ // Top bar selectors.
+ {
+ p_theme->set_type_variation("TopBarOptionButton", "OptionButton");
+ p_theme->set_font("font", "TopBarOptionButton", p_theme->get_font(SNAME("bold"), EditorStringName(EditorFonts)));
+ p_theme->set_font_size("font_size", "TopBarOptionButton", p_theme->get_font_size(SNAME("bold_size"), EditorStringName(EditorFonts)));
+ }
+
// Complex editor windows.
{
Ref<StyleBoxFlat> style_complex_window = p_config.window_style->duplicate();
@@ -1789,7 +1875,7 @@ void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, Them
float hue_rotate = (i * 2 % 16) / 16.0;
si_base_color.set_hsv(Math::fmod(float(si_base_color.get_h() + hue_rotate), float(1.0)), si_base_color.get_s(), si_base_color.get_v());
- si_base_color = p_config.accent_color.lerp(si_base_color, float(EDITOR_GET("docks/property_editor/subresource_hue_tint")));
+ si_base_color = p_config.accent_color.lerp(si_base_color, p_config.subresource_hue_tint);
// Sub-inspector background.
Ref<StyleBoxFlat> sub_inspector_bg = p_config.base_style->duplicate();
@@ -1823,7 +1909,7 @@ void EditorThemeManager::_populate_editor_styles(const Ref<Theme> &p_theme, Them
style_property_child_bg->set_bg_color(p_config.dark_color_2);
style_property_child_bg->set_border_width_all(0);
- p_theme->set_stylebox("bg", "EditorProperty", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty)));
+ p_theme->set_stylebox("bg", "EditorProperty", memnew(StyleBoxEmpty));
p_theme->set_stylebox("bg_selected", "EditorProperty", style_property_bg);
p_theme->set_stylebox("child_bg", "EditorProperty", style_property_child_bg);
p_theme->set_constant("font_offset", "EditorProperty", 8 * EDSCALE);
@@ -2126,7 +2212,7 @@ void EditorThemeManager::_generate_text_editor_defaults(ThemeConfiguration &p_co
/* clang-format on */
}
-void EditorThemeManager::_populate_text_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config) {
+void EditorThemeManager::_populate_text_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config) {
String text_editor_color_theme = EditorSettings::get_singleton()->get("text_editor/theme/color_theme");
if (text_editor_color_theme == "Default") {
_generate_text_editor_defaults(p_config);
@@ -2155,7 +2241,7 @@ void EditorThemeManager::_populate_text_editor_styles(const Ref<Theme> &p_theme,
Ref<StyleBoxFlat> code_edit_stylebox = make_flat_stylebox(background_color, p_config.widget_margin.x, p_config.widget_margin.y, p_config.widget_margin.x, p_config.widget_margin.y, p_config.corner_radius);
p_theme->set_stylebox("normal", "CodeEdit", code_edit_stylebox);
p_theme->set_stylebox("read_only", "CodeEdit", code_edit_stylebox);
- p_theme->set_stylebox("focus", "CodeEdit", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty)));
+ p_theme->set_stylebox("focus", "CodeEdit", memnew(StyleBoxEmpty));
p_theme->set_color("background_color", "CodeEdit", Color(0, 0, 0, 0)); // Unset any color, we use a stylebox.
@@ -2186,10 +2272,12 @@ void EditorThemeManager::_populate_text_editor_styles(const Ref<Theme> &p_theme,
// Public interface for theme generation.
-Ref<Theme> EditorThemeManager::generate_theme(const Ref<Theme> &p_old_theme) {
- OS::get_singleton()->benchmark_begin_measure("EditorTheme", "Generate Theme");
+Ref<EditorTheme> EditorThemeManager::generate_theme(const Ref<EditorTheme> &p_old_theme) {
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Generate Theme");
+
+ Ref<EditorTheme> theme = _create_base_theme(p_old_theme);
- Ref<Theme> theme = _create_base_theme(p_old_theme);
+ OS::get_singleton()->benchmark_begin_measure(get_benchmark_key(), "Merge Custom Theme");
const String custom_theme_path = EDITOR_GET("interface/theme/custom_theme");
if (!custom_theme_path.is_empty()) {
@@ -2199,7 +2287,11 @@ Ref<Theme> EditorThemeManager::generate_theme(const Ref<Theme> &p_old_theme) {
}
}
- OS::get_singleton()->benchmark_end_measure("EditorTheme", "Generate Theme");
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Merge Custom Theme");
+
+ OS::get_singleton()->benchmark_end_measure(get_benchmark_key(), "Generate Theme");
+ benchmark_run++;
+
return theme;
}
@@ -2213,12 +2305,25 @@ bool EditorThemeManager::is_generated_theme_outdated() {
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/font") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/main_font") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("interface/editor/code_font") ||
+ EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/increase_scrollbar_touch_area") ||
+ EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/scale_gizmo_handles") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/theme") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("text_editor/help/help") ||
+ EditorSettings::get_singleton()->check_changed_settings_in_group("docks/property_editor/subresource_hue_tint") ||
EditorSettings::get_singleton()->check_changed_settings_in_group("filesystem/file_dialog/thumbnail_size") ||
- EditorSettings::get_singleton()->check_changed_settings_in_group("run/output/font_size") ||
- EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/increase_scrollbar_touch_area") ||
- EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen/scale_gizmo_handles");
+ EditorSettings::get_singleton()->check_changed_settings_in_group("run/output/font_size");
+}
+
+bool EditorThemeManager::is_dark_theme() {
+ // Light color mode for icons and fonts means it's a dark theme, and vice versa.
+ int icon_font_color_setting = EDITOR_GET("interface/theme/icon_and_font_color");
+
+ if (icon_font_color_setting == ColorMode::AUTO_COLOR) {
+ Color base_color = EDITOR_GET("interface/theme/base_color");
+ return base_color.get_luminance() < 0.5;
+ }
+
+ return icon_font_color_setting == ColorMode::LIGHT_COLOR;
}
void EditorThemeManager::initialize() {
diff --git a/editor/themes/editor_theme_manager.h b/editor/themes/editor_theme_manager.h
index 86188ec244..0b30a9c853 100644
--- a/editor/themes/editor_theme_manager.h
+++ b/editor/themes/editor_theme_manager.h
@@ -31,16 +31,25 @@
#ifndef EDITOR_THEME_MANAGER_H
#define EDITOR_THEME_MANAGER_H
+#include "editor/themes/editor_theme.h"
#include "scene/resources/style_box_flat.h"
-#include "scene/resources/theme.h"
class EditorThemeManager {
+ static int benchmark_run;
+
+ static String get_benchmark_key();
+
+ enum ColorMode {
+ AUTO_COLOR,
+ DARK_COLOR,
+ LIGHT_COLOR,
+ };
+
struct ThemeConfiguration {
// Basic properties.
String preset;
String spacing_preset;
- bool dark_theme = false;
Color base_color;
Color accent_color;
@@ -61,11 +70,14 @@ class EditorThemeManager {
bool increase_scrollbar_touch_area = false;
float gizmo_handle_scale = 1.0;
int color_picker_button_height = 28;
+ float subresource_hue_tint = 0.0;
float default_contrast = 1.0;
// Generated properties.
+ bool dark_theme = false;
+
int base_margin = 4;
int increased_margin = 4;
int separation_margin = 4;
@@ -127,22 +139,28 @@ class EditorThemeManager {
Ref<StyleBoxFlat> tree_panel_style;
Vector2 widget_margin;
+
+ uint32_t hash();
+ uint32_t hash_fonts();
+ uint32_t hash_icons();
};
- static Ref<Theme> _create_base_theme(const Ref<Theme> &p_old_theme = nullptr);
- static ThemeConfiguration _create_theme_config(const Ref<Theme> &p_theme);
+ static Ref<EditorTheme> _create_base_theme(const Ref<EditorTheme> &p_old_theme = nullptr);
+ static ThemeConfiguration _create_theme_config(const Ref<EditorTheme> &p_theme);
- static void _create_shared_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
- static void _populate_standard_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
- static void _populate_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
+ static void _create_shared_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
+ static void _populate_standard_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
+ static void _populate_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
static void _generate_text_editor_defaults(ThemeConfiguration &p_config);
- static void _populate_text_editor_styles(const Ref<Theme> &p_theme, ThemeConfiguration &p_config);
+ static void _populate_text_editor_styles(const Ref<EditorTheme> &p_theme, ThemeConfiguration &p_config);
public:
- static Ref<Theme> generate_theme(const Ref<Theme> &p_old_theme = nullptr);
+ static Ref<EditorTheme> generate_theme(const Ref<EditorTheme> &p_old_theme = nullptr);
static bool is_generated_theme_outdated();
+ static bool is_dark_theme();
+
static void initialize();
static void finalize();
};
diff --git a/main/main.cpp b/main/main.cpp
index b50def9cec..dbe186d63a 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -475,7 +475,7 @@ void Main::print_help(const char *p_binary) {
OS::get_singleton()->print(" --profiling Enable profiling in the script debugger.\n");
OS::get_singleton()->print(" --gpu-profile Show a GPU profile of the tasks that took the most time during frame rendering.\n");
OS::get_singleton()->print(" --gpu-validation Enable graphics API validation layers for debugging.\n");
-#if DEBUG_ENABLED
+#ifdef DEBUG_ENABLED
OS::get_singleton()->print(" --gpu-abort Abort on graphics API usage errors (usually validation layer errors). May help see the problem if your system freezes.\n");
#endif
OS::get_singleton()->print(" --generate-spirv-debug-info Generate SPIR-V debug information. This allows source-level shader debugging with RenderDoc.\n");
@@ -1615,12 +1615,18 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph
}
// Initialize WorkerThreadPool.
- if (editor || project_manager) {
- WorkerThreadPool::get_singleton()->init(-1, 0.75);
- } else {
- int worker_threads = GLOBAL_GET("threading/worker_pool/max_threads");
- float low_priority_ratio = GLOBAL_GET("threading/worker_pool/low_priority_thread_ratio");
- WorkerThreadPool::get_singleton()->init(worker_threads, low_priority_ratio);
+ {
+#ifdef THREADS_ENABLED
+ if (editor || project_manager) {
+ WorkerThreadPool::get_singleton()->init(-1, 0.75);
+ } else {
+ int worker_threads = GLOBAL_GET("threading/worker_pool/max_threads");
+ float low_priority_ratio = GLOBAL_GET("threading/worker_pool/low_priority_thread_ratio");
+ WorkerThreadPool::get_singleton()->init(worker_threads, low_priority_ratio);
+ }
+#else
+ WorkerThreadPool::get_singleton()->init(0, 0);
+#endif
}
#ifdef TOOLS_ENABLED
diff --git a/methods.py b/methods.py
index 7c511af930..a55c622ed0 100644
--- a/methods.py
+++ b/methods.py
@@ -1022,6 +1022,11 @@ def get_compiler_version(env):
"metadata1": None,
"metadata2": None,
"date": None,
+ "apple_major": -1,
+ "apple_minor": -1,
+ "apple_patch1": -1,
+ "apple_patch2": -1,
+ "apple_patch3": -1,
}
if not env.msvc:
@@ -1049,8 +1054,32 @@ def get_compiler_version(env):
for key, value in match.groupdict().items():
if value is not None:
ret[key] = value
+
+ match_apple = re.search(
+ r"(?:(?<=clang-)|(?<=\) )|(?<=^))"
+ r"(?P<apple_major>\d+)"
+ r"(?:\.(?P<apple_minor>\d*))?"
+ r"(?:\.(?P<apple_patch1>\d*))?"
+ r"(?:\.(?P<apple_patch2>\d*))?"
+ r"(?:\.(?P<apple_patch3>\d*))?",
+ version,
+ )
+ if match_apple is not None:
+ for key, value in match_apple.groupdict().items():
+ if value is not None:
+ ret[key] = value
+
# Transform semantic versioning to integers
- for key in ["major", "minor", "patch"]:
+ for key in [
+ "major",
+ "minor",
+ "patch",
+ "apple_major",
+ "apple_minor",
+ "apple_patch1",
+ "apple_patch2",
+ "apple_patch3",
+ ]:
ret[key] = int(ret[key] or -1)
return ret
diff --git a/misc/dist/html/editor.html b/misc/dist/html/editor.html
index 93afbf085d..e5c68c6338 100644
--- a/misc/dist/html/editor.html
+++ b/misc/dist/html/editor.html
@@ -23,7 +23,7 @@
<link id="-gd-engine-icon" rel="icon" type="image/png" href="favicon.png">
<link rel="apple-touch-icon" type="image/png" href="favicon.png">
<link rel="manifest" href="manifest.json">
- <title>Godot Engine Web Editor (@GODOT_VERSION@)</title>
+ <title>Godot Engine Web Editor (___GODOT_VERSION___)</title>
<style>
*:focus {
/* More visible outline for better keyboard navigation. */
@@ -294,7 +294,7 @@ a:active {
<br >
<img src="logo.svg" alt="Godot Engine logo" width="1024" height="414" style="width: auto; height: auto; max-width: min(85%, 50vh); max-height: 250px">
<br >
- @GODOT_VERSION@
+ ___GODOT_VERSION___
<br >
<a href="releases/">Need an old version?</a>
<br >
@@ -384,7 +384,9 @@ window.addEventListener('load', () => {
});
}
- const missing = Engine.getMissingFeatures();
+ const missing = Engine.getMissingFeatures({
+ threads: ___GODOT_THREADS_ENABLED___,
+ });
if (missing.length) {
// Display error dialog as threading support is required for the editor.
document.getElementById('startButton').disabled = 'disabled';
diff --git a/misc/dist/html/full-size.html b/misc/dist/html/full-size.html
index 6710cb1533..54571e27c7 100644
--- a/misc/dist/html/full-size.html
+++ b/misc/dist/html/full-size.html
@@ -136,6 +136,7 @@ body {
<script src="$GODOT_URL"></script>
<script>
const GODOT_CONFIG = $GODOT_CONFIG;
+const GODOT_THREADS_ENABLED = $GODOT_THREADS_ENABLED;
const engine = new Engine(GODOT_CONFIG);
(function () {
@@ -213,7 +214,9 @@ const engine = new Engine(GODOT_CONFIG);
initializing = false;
}
- const missing = Engine.getMissingFeatures();
+ const missing = Engine.getMissingFeatures({
+ threads: GODOT_THREADS_ENABLED,
+ });
if (missing.length !== 0) {
const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
displayFailureNotice(missingMsg + missing.join('\n'));
diff --git a/misc/dist/html/service-worker.js b/misc/dist/html/service-worker.js
index 310574f21d..70e7a399e1 100644
--- a/misc/dist/html/service-worker.js
+++ b/misc/dist/html/service-worker.js
@@ -3,14 +3,14 @@
// that they need an Internet connection to run the project if desired.
// Incrementing CACHE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
-const CACHE_VERSION = "@GODOT_VERSION@";
-const CACHE_PREFIX = "@GODOT_NAME@-sw-cache-";
+const CACHE_VERSION = "___GODOT_VERSION___";
+const CACHE_PREFIX = "___GODOT_NAME___-sw-cache-";
const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;
-const OFFLINE_URL = "@GODOT_OFFLINE_PAGE@";
+const OFFLINE_URL = "___GODOT_OFFLINE_PAGE___";
// Files that will be cached on load.
-const CACHED_FILES = @GODOT_CACHE@;
+const CACHED_FILES = ___GODOT_CACHE___;
// Files that we might not want the user to preload, and will only be cached on first load.
-const CACHABLE_FILES = @GODOT_OPT_CACHE@;
+const CACHABLE_FILES = ___GODOT_OPT_CACHE___;
const FULL_CACHE = CACHED_FILES.concat(CACHABLE_FILES);
self.addEventListener("install", (event) => {
@@ -22,7 +22,7 @@ self.addEventListener("activate", (event) => {
function (keys) {
// Remove old caches.
return Promise.all(keys.filter(key => key.startsWith(CACHE_PREFIX) && key != CACHE_NAME).map(key => caches.delete(key)));
- }).then(function() {
+ }).then(function () {
// Enable navigation preload if available.
return ("navigationPreload" in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve();
})
diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected
index 2c52144896..2c18b43948 100644
--- a/misc/extension_api_validation/4.2-stable.expected
+++ b/misc/extension_api_validation/4.2-stable.expected
@@ -51,3 +51,18 @@ Validate extension JSON: Error: Field 'classes/RenderingDevice/methods/texture_u
Barrier arguments have been removed from all relevant functions as they're no longer required.
Draw and compute list overlap no longer needs to be specified.
Initial and final actions have been simplified into fewer options.
+
+
+GH-87115
+--------
+Validate extension JSON: Error: Field 'classes/TileMap/methods/get_collision_visibility_mode': is_const changed value in new API, from false to true.
+Validate extension JSON: Error: Field 'classes/TileMap/methods/get_navigation_visibility_mode': is_const changed value in new API, from false to true.
+
+Two TileMap getters were made const. No adjustments should be necessary.
+
+
+GH-86158
+--------
+Validate extension JSON: Error: Field 'classes/GraphEdit/methods/get_connection_line': is_const changed value in new API, from false to true.
+
+get_connection_line was made const.
diff --git a/modules/etcpak/image_compress_etcpak.cpp b/modules/etcpak/image_compress_etcpak.cpp
index f528b92cf2..e9ce92dd80 100644
--- a/modules/etcpak/image_compress_etcpak.cpp
+++ b/modules/etcpak/image_compress_etcpak.cpp
@@ -40,7 +40,7 @@
EtcpakType _determine_etc_type(Image::UsedChannels p_channels) {
switch (p_channels) {
case Image::USED_CHANNELS_L:
- return EtcpakType::ETCPAK_TYPE_ETC1;
+ return EtcpakType::ETCPAK_TYPE_ETC2;
case Image::USED_CHANNELS_LA:
return EtcpakType::ETCPAK_TYPE_ETC2_ALPHA;
case Image::USED_CHANNELS_R:
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 933bfba5ba..b335bf8fae 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -627,7 +627,7 @@
[/codeblock]
[b]Note:[/b] Only the script can have a custom icon. Inner classes are not supported.
[b]Note:[/b] As annotations describe their subject, the [annotation @icon] annotation must be placed before the class definition and inheritance.
- [b]Note:[/b] Unlike other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
+ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @icon] annotation must be a string literal (constant expressions are not supported).
</description>
</annotation>
<annotation name="@onready">
@@ -681,6 +681,14 @@
[b]Note:[/b] As annotations describe their subject, the [annotation @tool] annotation must be placed before the class definition and inheritance.
</description>
</annotation>
+ <annotation name="@uid">
+ <return type="void" />
+ <param index="0" name="uid" type="String" />
+ <description>
+ Stores information about UID of this script. This annotation is auto-generated when saving the script and must not be modified manually. Only applies to scripts saved as separate files (i.e. not built-in).
+ [b]Note:[/b] Unlike most other annotations, the argument of the [annotation @uid] annotation must be a string literal (constant expressions are not supported).
+ </description>
+ </annotation>
<annotation name="@warning_ignore" qualifiers="vararg">
<return type="void" />
<param index="0" name="warning" type="String" />
diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp
index 3df07f9794..1f07def21c 100644
--- a/modules/gdscript/editor/gdscript_highlighter.cpp
+++ b/modules/gdscript/editor/gdscript_highlighter.cpp
@@ -35,6 +35,7 @@
#include "core/config/project_settings.h"
#include "editor/editor_settings.h"
+#include "editor/themes/editor_theme_manager.h"
Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_line) {
Dictionary color_map;
@@ -790,7 +791,7 @@ void GDScriptSyntaxHighlighter::_update_cache() {
const String text_edit_color_theme = EDITOR_GET("text_editor/theme/color_theme");
const bool godot_2_theme = text_edit_color_theme == "Godot 2";
- if (godot_2_theme || EditorSettings::get_singleton()->is_dark_theme()) {
+ if (godot_2_theme || EditorThemeManager::is_dark_theme()) {
function_definition_color = Color(0.4, 0.9, 1.0);
global_function_color = Color(0.64, 0.64, 0.96);
node_path_color = Color(0.72, 0.77, 0.49);
diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp
index 1f0830aa17..7b486f2a35 100644
--- a/modules/gdscript/gdscript.cpp
+++ b/modules/gdscript/gdscript.cpp
@@ -55,6 +55,7 @@
#ifdef TOOLS_ENABLED
#include "editor/editor_paths.h"
+#include "editor/editor_settings.h"
#endif
#include <stdint.h>
@@ -1076,6 +1077,36 @@ Ref<GDScript> GDScript::get_base() const {
return base;
}
+String GDScript::get_raw_source_code(const String &p_path, bool *r_error) {
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
+ if (f.is_null()) {
+ if (r_error) {
+ *r_error = true;
+ }
+ return String();
+ }
+ return f->get_as_utf8_string();
+}
+
+Vector2i GDScript::get_uid_lines(const String &p_source) {
+ GDScriptParser parser;
+ parser.parse(p_source, "", false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (!c) {
+ return Vector2i(-1, -1);
+ }
+ return c->uid_lines;
+}
+
+String GDScript::create_uid_line(const String &p_uid_str) {
+#ifdef TOOLS_ENABLED
+ if (EDITOR_GET("text_editor/completion/use_single_quotes")) {
+ return vformat(R"(@uid('%s') # %s)", p_uid_str, RTR("Generated automatically, do not modify."));
+ }
+#endif
+ return vformat(R"(@uid("%s") # %s)", p_uid_str, RTR("Generated automatically, do not modify."));
+}
+
bool GDScript::inherits_script(const Ref<Script> &p_script) const {
Ref<GDScript> gd = p_script;
if (gd.is_null()) {
@@ -2320,14 +2351,13 @@ struct GDScriptDepSort {
void GDScriptLanguage::reload_all_scripts() {
#ifdef DEBUG_ENABLED
print_verbose("GDScript: Reloading all scripts");
- List<Ref<GDScript>> scripts;
+ Array scripts;
{
MutexLock lock(this->mutex);
SelfList<GDScript> *elem = script_list.first();
while (elem) {
- // Scripts will reload all subclasses, so only reload root scripts.
- if (elem->self()->is_root_script() && elem->self()->get_path().is_resource_file()) {
+ if (elem->self()->get_path().is_resource_file()) {
print_verbose("GDScript: Found: " + elem->self()->get_path());
scripts.push_back(Ref<GDScript>(elem->self())); //cast to gdscript to avoid being erased by accident
}
@@ -2348,19 +2378,11 @@ void GDScriptLanguage::reload_all_scripts() {
#endif
}
- //as scripts are going to be reloaded, must proceed without locking here
-
- scripts.sort_custom<GDScriptDepSort>(); //update in inheritance dependency order
-
- for (Ref<GDScript> &scr : scripts) {
- print_verbose("GDScript: Reloading: " + scr->get_path());
- scr->load_source_code(scr->get_path());
- scr->reload(true);
- }
+ reload_scripts(scripts, true);
#endif
}
-void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) {
+void GDScriptLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload) {
#ifdef DEBUG_ENABLED
List<Ref<GDScript>> scripts;
@@ -2386,7 +2408,7 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
scripts.sort_custom<GDScriptDepSort>(); //update in inheritance dependency order
for (Ref<GDScript> &scr : scripts) {
- bool reload = scr == p_script || to_reload.has(scr->get_base());
+ bool reload = p_scripts.has(scr) || to_reload.has(scr->get_base());
if (!reload) {
continue;
@@ -2409,7 +2431,7 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
}
}
-//same thing for placeholders
+ //same thing for placeholders
#ifdef TOOLS_ENABLED
while (scr->placeholders.size()) {
@@ -2437,6 +2459,8 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
for (KeyValue<Ref<GDScript>, HashMap<ObjectID, List<Pair<StringName, Variant>>>> &E : to_reload) {
Ref<GDScript> scr = E.key;
+ print_verbose("GDScript: Reloading: " + scr->get_path());
+ scr->load_source_code(scr->get_path());
scr->reload(p_soft_reload);
//restore state if saved
@@ -2484,6 +2508,12 @@ void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_so
#endif
}
+void GDScriptLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) {
+ Array scripts;
+ scripts.push_back(p_script);
+ reload_scripts(scripts, p_soft_reload);
+}
+
void GDScriptLanguage::frame() {
calls = 0;
@@ -2594,17 +2624,8 @@ bool GDScriptLanguage::handles_global_class_type(const String &p_type) const {
}
String GDScriptLanguage::get_global_class_name(const String &p_path, String *r_base_type, String *r_icon_path) const {
- Error err;
- Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ, &err);
- if (err) {
- return String();
- }
-
- String source = f->get_as_utf8_string();
-
GDScriptParser parser;
- err = parser.parse(source, p_path, false);
-
+ parser.parse(GDScript::get_raw_source_code(p_path), p_path, false);
const GDScriptParser::ClassNode *c = parser.get_tree();
if (!c) {
return String(); // No class parsed.
@@ -2818,6 +2839,22 @@ String ResourceFormatLoaderGDScript::get_resource_type(const String &p_path) con
return "";
}
+ResourceUID::ID ResourceFormatLoaderGDScript::get_resource_uid(const String &p_path) const {
+ String ext = p_path.get_extension().to_lower();
+
+ if (ext != "gd") {
+ return ResourceUID::INVALID_ID;
+ }
+
+ GDScriptParser parser;
+ parser.parse(GDScript::get_raw_source_code(p_path), p_path, false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (!c) {
+ return ResourceUID::INVALID_ID;
+ }
+ return ResourceUID::get_singleton()->text_to_id(c->uid_string);
+}
+
void ResourceFormatLoaderGDScript::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) {
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::READ);
ERR_FAIL_COND_MSG(file.is_null(), "Cannot open file '" + p_path + "'.");
@@ -2842,17 +2879,49 @@ Error ResourceFormatSaverGDScript::save(const Ref<Resource> &p_resource, const S
ERR_FAIL_COND_V(sqscr.is_null(), ERR_INVALID_PARAMETER);
String source = sqscr->get_source_code();
+ ResourceUID::ID uid = ResourceSaver::get_resource_id_for_path(p_path, !p_resource->is_built_in());
{
+ bool source_changed = false;
Error err;
Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &err);
ERR_FAIL_COND_V_MSG(err, err, "Cannot save GDScript file '" + p_path + "'.");
- file->store_string(source);
+ if (uid != ResourceUID::INVALID_ID) {
+ GDScriptParser parser;
+ parser.parse(source, "", false);
+ const GDScriptParser::ClassNode *c = parser.get_tree();
+ if (c && ResourceUID::get_singleton()->text_to_id(c->uid_string) != uid) {
+ const Vector2i &uid_idx = c->uid_lines;
+ PackedStringArray lines = source.split("\n");
+
+ if (uid_idx.x > -1) {
+ for (int i = uid_idx.x + 1; i <= uid_idx.y; i++) {
+ // If UID is written across multiple lines, erase extra lines.
+ lines.remove_at(uid_idx.x + 1);
+ }
+ lines.write[uid_idx.x] = GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(uid));
+ } else {
+ lines.insert(0, GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(uid)));
+ }
+ source = String("\n").join(lines);
+ source_changed = true;
+ file->store_string(String("\n").join(lines));
+ } else {
+ file->store_string(source);
+ }
+ }
+
if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) {
return ERR_CANT_CREATE;
}
+
+ if (source_changed) {
+ sqscr->set_source_code(source);
+ sqscr->reload();
+ sqscr->emit_changed();
+ }
}
if (ScriptServer::is_reload_scripts_on_save_enabled()) {
@@ -2871,3 +2940,33 @@ void ResourceFormatSaverGDScript::get_recognized_extensions(const Ref<Resource>
bool ResourceFormatSaverGDScript::recognize(const Ref<Resource> &p_resource) const {
return Object::cast_to<GDScript>(*p_resource) != nullptr;
}
+
+Error ResourceFormatSaverGDScript::set_uid(const String &p_path, ResourceUID::ID p_uid) {
+ ERR_FAIL_COND_V(p_path.get_extension() != "gd", ERR_INVALID_PARAMETER);
+ ERR_FAIL_COND_V(p_uid == ResourceUID::INVALID_ID, ERR_INVALID_PARAMETER);
+
+ bool error = false;
+ const String &source_code = GDScript::get_raw_source_code(p_path, &error);
+ if (error) {
+ return ERR_CANT_OPEN;
+ }
+
+ Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
+ ERR_FAIL_COND_V(f.is_null(), ERR_CANT_OPEN);
+
+ const Vector2i &uid_idx = GDScript::get_uid_lines(source_code);
+ PackedStringArray lines = source_code.split("\n");
+
+ if (uid_idx.x > -1) {
+ for (int i = uid_idx.x + 1; i <= uid_idx.y; i++) {
+ // If UID is written across multiple lines, erase extra lines.
+ lines.remove_at(uid_idx.x + 1);
+ }
+ lines.write[uid_idx.x] = GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(p_uid));
+ } else {
+ f->store_line(GDScript::create_uid_line(ResourceUID::get_singleton()->id_to_text(p_uid)));
+ }
+ f->store_string(String("\n").join(lines));
+
+ return OK;
+}
diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h
index 7b0e2136ed..fdfd79f0fc 100644
--- a/modules/gdscript/gdscript.h
+++ b/modules/gdscript/gdscript.h
@@ -262,6 +262,10 @@ public:
bool is_tool() const override { return tool; }
Ref<GDScript> get_base() const;
+ static String get_raw_source_code(const String &p_path, bool *r_error = nullptr);
+ static Vector2i get_uid_lines(const String &p_source);
+ static String create_uid_line(const String &p_uid_str);
+
const HashMap<StringName, MemberInfo> &debug_get_member_indices() const { return member_indices; }
const HashMap<StringName, GDScriptFunction *> &debug_get_member_functions() const; //this is debug only
StringName debug_get_member_by_index(int p_idx) const;
@@ -575,6 +579,7 @@ public:
virtual String debug_parse_stack_level_expression(int p_level, const String &p_expression, int p_max_subitems = -1, int p_max_depth = -1) override;
virtual void reload_all_scripts() override;
+ virtual void reload_scripts(const Array &p_scripts, bool p_soft_reload) override;
virtual void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) override;
virtual void frame() override;
@@ -615,6 +620,7 @@ public:
virtual void get_recognized_extensions(List<String> *p_extensions) const;
virtual bool handles_type(const String &p_type) const;
virtual String get_resource_type(const String &p_path) const;
+ virtual ResourceUID::ID get_resource_uid(const String &p_path) const;
virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false);
};
@@ -623,6 +629,7 @@ public:
virtual Error save(const Ref<Resource> &p_resource, const String &p_path, uint32_t p_flags = 0);
virtual void get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const;
virtual bool recognize(const Ref<Resource> &p_resource) const;
+ virtual Error set_uid(const String &p_path, ResourceUID::ID p_uid);
};
#endif // GDSCRIPT_H
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index 3fd5b3f519..7026d131e3 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -4908,8 +4908,19 @@ GDScriptParser::DataType GDScriptAnalyzer::type_from_property(const PropertyInfo
}
result.builtin_type = p_property.type;
if (p_property.type == Variant::OBJECT) {
- result.kind = GDScriptParser::DataType::NATIVE;
- result.native_type = p_property.class_name == StringName() ? SNAME("Object") : p_property.class_name;
+ if (ScriptServer::is_global_class(p_property.class_name)) {
+ result.kind = GDScriptParser::DataType::SCRIPT;
+ result.script_path = ScriptServer::get_global_class_path(p_property.class_name);
+ result.native_type = ScriptServer::get_global_class_native_base(p_property.class_name);
+
+ Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_property.class_name));
+ if (scr.is_valid()) {
+ result.script_type = scr;
+ }
+ } else {
+ result.kind = GDScriptParser::DataType::NATIVE;
+ result.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name;
+ }
} else {
result.kind = GDScriptParser::DataType::BUILTIN;
result.builtin_type = p_property.type;
diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h
index 9bface6136..f902cb10cc 100644
--- a/modules/gdscript/gdscript_byte_codegen.h
+++ b/modules/gdscript/gdscript_byte_codegen.h
@@ -121,7 +121,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator {
RBMap<MethodBind *, int> method_bind_map;
RBMap<GDScriptFunction *, int> lambdas_map;
-#if DEBUG_ENABLED
+#ifdef DEBUG_ENABLED
// Keep method and property names for pointer and validated operations.
// Used when disassembling the bytecode.
Vector<String> operator_names;
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index f6633f8bf6..edba6340b9 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -37,6 +37,7 @@
#include "core/config/engine.h"
#include "core/config/project_settings.h"
+#include "core/core_string_names.h"
bool GDScriptCompiler::_is_class_member_property(CodeGen &codegen, const StringName &p_name) {
if (codegen.function_node && codegen.function_node->is_static) {
@@ -345,7 +346,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
scr = scr->_base;
}
- if (nc && (ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) {
+ if (nc && (identifier == CoreStringNames::get_singleton()->_free || ClassDB::has_signal(nc->get_name(), identifier) || ClassDB::has_method(nc->get_name(), identifier))) {
// Get like it was a property.
GDScriptCodeGenerator::Address temp = codegen.add_temporary(); // TODO: Get type here.
GDScriptCodeGenerator::Address self(GDScriptCodeGenerator::Address::SELF);
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index 9ad2ba1914..210e2c3898 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -1370,7 +1370,7 @@ static void _find_identifiers(const GDScriptParser::CompletionContext &p_context
}
}
-static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) {
+static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value, GDScriptParser::CompletionContext &p_context) {
GDScriptCompletionIdentifier ci;
ci.value = p_value;
ci.type.is_constant = true;
@@ -1392,8 +1392,22 @@ static GDScriptCompletionIdentifier _type_from_variant(const Variant &p_value) {
scr = obj->get_script();
}
if (scr.is_valid()) {
- ci.type.script_type = scr;
+ ci.type.script_path = scr->get_path();
+
+ if (scr->get_path().ends_with(".gd")) {
+ Error err;
+ Ref<GDScriptParserRef> parser = GDScriptCache::get_parser(scr->get_path(), GDScriptParserRef::INTERFACE_SOLVED, err);
+ if (err == OK) {
+ ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
+ ci.type.class_type = parser->get_parser()->get_tree();
+ ci.type.kind = GDScriptParser::DataType::CLASS;
+ p_context.dependent_parsers.push_back(parser);
+ return ci;
+ }
+ }
+
ci.type.kind = GDScriptParser::DataType::SCRIPT;
+ ci.type.script_type = scr;
ci.type.native_type = scr->get_instance_base_type();
} else {
ci.type.kind = GDScriptParser::DataType::NATIVE;
@@ -1418,8 +1432,19 @@ static GDScriptCompletionIdentifier _type_from_property(const PropertyInfo &p_pr
ci.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
ci.type.builtin_type = p_property.type;
if (p_property.type == Variant::OBJECT) {
- ci.type.kind = GDScriptParser::DataType::NATIVE;
- ci.type.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name;
+ if (ScriptServer::is_global_class(p_property.class_name)) {
+ ci.type.kind = GDScriptParser::DataType::SCRIPT;
+ ci.type.script_path = ScriptServer::get_global_class_path(p_property.class_name);
+ ci.type.native_type = ScriptServer::get_global_class_native_base(p_property.class_name);
+
+ Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_property.class_name));
+ if (scr.is_valid()) {
+ ci.type.script_type = scr;
+ }
+ } else {
+ ci.type.kind = GDScriptParser::DataType::NATIVE;
+ ci.type.native_type = p_property.class_name == StringName() ? "Object" : p_property.class_name;
+ }
} else {
ci.type.kind = GDScriptParser::DataType::BUILTIN;
}
@@ -1481,7 +1506,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (p_expression->is_constant) {
// Already has a value, so just use that.
- r_type = _type_from_variant(p_expression->reduced_value);
+ r_type = _type_from_variant(p_expression->reduced_value, p_context);
switch (p_expression->get_datatype().kind) {
case GDScriptParser::DataType::ENUM:
case GDScriptParser::DataType::CLASS:
@@ -1495,7 +1520,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
switch (p_expression->type) {
case GDScriptParser::Node::LITERAL: {
const GDScriptParser::LiteralNode *literal = static_cast<const GDScriptParser::LiteralNode *>(p_expression);
- r_type = _type_from_variant(literal->value);
+ r_type = _type_from_variant(literal->value, p_context);
found = true;
} break;
case GDScriptParser::Node::SELF: {
@@ -1676,7 +1701,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (!which.is_empty()) {
// Try singletons first
if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(which)) {
- r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which]);
+ r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[which], p_context);
found = true;
} else {
for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
@@ -1727,7 +1752,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (ce.error == Callable::CallError::CALL_OK && ret.get_type() != Variant::NIL) {
if (ret.get_type() != Variant::OBJECT || ret.operator Object *() != nullptr) {
- r_type = _type_from_variant(ret);
+ r_type = _type_from_variant(ret, p_context);
found = true;
}
}
@@ -1755,7 +1780,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (base.value.get_type() == Variant::DICTIONARY && base.value.operator Dictionary().has(String(subscript->attribute->name))) {
Variant value = base.value.operator Dictionary()[String(subscript->attribute->name)];
- r_type = _type_from_variant(value);
+ r_type = _type_from_variant(value, p_context);
found = true;
break;
}
@@ -1807,7 +1832,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
if (base.value.in(index.value)) {
Variant value = base.value.get(index.value);
- r_type = _type_from_variant(value);
+ r_type = _type_from_variant(value, p_context);
found = true;
break;
}
@@ -1862,7 +1887,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
bool valid = false;
Variant res = base_val.get(index.value, &valid);
if (valid) {
- r_type = _type_from_variant(res);
+ r_type = _type_from_variant(res, p_context);
r_type.value = Variant();
r_type.type.is_constant = false;
found = true;
@@ -1920,7 +1945,7 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context,
found = false;
break;
}
- r_type = _type_from_variant(res);
+ r_type = _type_from_variant(res, p_context);
if (!v1_use_value || !v2_use_value) {
r_type.value = Variant();
r_type.type.is_constant = false;
@@ -2155,7 +2180,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
} else {
Ref<Script> scr = ResourceLoader::load(ScriptServer::get_global_class_path(p_identifier->name));
if (scr.is_valid()) {
- r_type = _type_from_variant(scr);
+ r_type = _type_from_variant(scr, p_context);
r_type.type.is_meta_type = true;
return true;
}
@@ -2165,7 +2190,7 @@ static bool _guess_identifier_type(GDScriptParser::CompletionContext &p_context,
// Check global variables (including autoloads).
if (GDScriptLanguage::get_singleton()->get_named_globals_map().has(p_identifier->name)) {
- r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier->name]);
+ r_type = _type_from_variant(GDScriptLanguage::get_singleton()->get_named_globals_map()[p_identifier->name], p_context);
return true;
}
@@ -2218,7 +2243,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
const GDScriptParser::ExpressionNode *init = member.variable->initializer;
if (init->is_constant) {
r_type.value = init->reduced_value;
- r_type = _type_from_variant(init->reduced_value);
+ r_type = _type_from_variant(init->reduced_value, p_context);
return true;
} else if (init->start_line == p_context.current_line) {
return false;
@@ -2245,7 +2270,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
r_type.enumeration = member.m_enum->identifier->name;
return true;
case GDScriptParser::ClassNode::Member::ENUM_VALUE:
- r_type = _type_from_variant(member.enum_value.value);
+ r_type = _type_from_variant(member.enum_value.value, p_context);
return true;
case GDScriptParser::ClassNode::Member::SIGNAL:
r_type.type.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
@@ -2281,7 +2306,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
HashMap<StringName, Variant> constants;
scr->get_constants(&constants);
if (constants.has(p_identifier)) {
- r_type = _type_from_variant(constants[p_identifier]);
+ r_type = _type_from_variant(constants[p_identifier], p_context);
return true;
}
@@ -2345,7 +2370,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
bool valid = false;
Variant res = tmp.get(p_identifier, &valid);
if (valid) {
- r_type = _type_from_variant(res);
+ r_type = _type_from_variant(res, p_context);
r_type.value = Variant();
r_type.type.is_constant = false;
return true;
@@ -3197,6 +3222,11 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
List<String> opts;
p_owner->get_argument_options("get_node", 0, &opts);
+ bool for_unique_name = false;
+ if (completion_context.node != nullptr && completion_context.node->type == GDScriptParser::Node::GET_NODE && !static_cast<GDScriptParser::GetNodeNode *>(completion_context.node)->use_dollar) {
+ for_unique_name = true;
+ }
+
for (const String &E : opts) {
r_forced = true;
String opt = E.strip_edges();
@@ -3205,6 +3235,14 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
// or handle NodePaths which are valid identifiers and don't need quotes.
opt = opt.unquote();
}
+
+ if (for_unique_name) {
+ if (!opt.begins_with("%")) {
+ continue;
+ }
+ opt = opt.substr(1);
+ }
+
// The path needs quotes if it's not a valid identifier (with an exception
// for "/" as path separator, which also doesn't require quotes).
if (!opt.replace("/", "_").is_valid_identifier()) {
@@ -3216,11 +3254,13 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
options.insert(option.display, option);
}
- // Get autoloads.
- for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
- String path = "/root/" + E.key;
- ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH);
- options.insert(option.display, option);
+ if (!for_unique_name) {
+ // Get autoloads.
+ for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
+ String path = "/root/" + E.key;
+ ScriptLanguage::CodeCompletionOption option(path.quote(quote_style), ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH);
+ options.insert(option.display, option);
+ }
}
}
} break;
@@ -3564,7 +3604,8 @@ static Error _lookup_symbol_from_base(const GDScriptParser::DataType &p_base, co
case GDScriptParser::COMPLETION_ASSIGN:
case GDScriptParser::COMPLETION_CALL_ARGUMENTS:
case GDScriptParser::COMPLETION_IDENTIFIER:
- case GDScriptParser::COMPLETION_PROPERTY_METHOD: {
+ case GDScriptParser::COMPLETION_PROPERTY_METHOD:
+ case GDScriptParser::COMPLETION_SUBSCRIPT: {
GDScriptParser::DataType base_type;
if (context.current_class) {
if (context.type != GDScriptParser::COMPLETION_SUPER_METHOD) {
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index a4a12f8bc4..03cf334bed 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -93,6 +93,7 @@ bool GDScriptParser::annotation_exists(const String &p_annotation_name) const {
GDScriptParser::GDScriptParser() {
// Register valid annotations.
if (unlikely(valid_annotations.is_empty())) {
+ register_annotation(MethodInfo("@uid", PropertyInfo(Variant::STRING, "uid")), AnnotationInfo::SCRIPT, &GDScriptParser::uid_annotation);
register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation);
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
@@ -520,6 +521,8 @@ void GDScriptParser::parse_program() {
// `@icon` needs to be applied in the parser. See GH-72444.
if (annotation->name == SNAME("@icon")) {
annotation->apply(this, head, nullptr);
+ } else if (annotation->name == SNAME("@uid")) {
+ annotation->apply(this, head, nullptr);
} else {
head->annotations.push_back(annotation);
}
@@ -3834,18 +3837,18 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
}
// `@icon`'s argument needs to be resolved in the parser. See GH-72444.
- if (p_annotation->name == SNAME("@icon")) {
+ if (p_annotation->name == SNAME("@icon") || p_annotation->name == SNAME("@uid")) {
ExpressionNode *argument = p_annotation->arguments[0];
if (argument->type != Node::LITERAL) {
- push_error(R"(Argument 1 of annotation "@icon" must be a string literal.)", argument);
+ push_error(vformat(R"(Argument 1 of annotation "%s" must be a string literal.)", p_annotation->name), argument);
return false;
}
Variant value = static_cast<LiteralNode *>(argument)->value;
if (value.get_type() != Variant::STRING) {
- push_error(R"(Argument 1 of annotation "@icon" must be a string literal.)", argument);
+ push_error(vformat(R"(Argument 1 of annotation "%s" must be a string literal.)", p_annotation->name), argument);
return false;
}
@@ -3857,6 +3860,35 @@ bool GDScriptParser::validate_annotation_arguments(AnnotationNode *p_annotation)
return true;
}
+bool GDScriptParser::uid_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
+ ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, R"("@uid" annotation can only be applied to classes.)");
+ ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false);
+
+#ifdef DEBUG_ENABLED
+ if (this->_has_uid) {
+ push_error(R"("@uid" annotation can only be used once.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
+
+ // Assign line range early to allow replacing invalid UIDs.
+ ClassNode *class_node = static_cast<ClassNode *>(p_target);
+ class_node->uid_lines = Vector2i(p_annotation->start_line - 1, p_annotation->end_line - 1); // Lines start from 1, so need to subtract.
+
+ const String &uid_string = p_annotation->resolved_arguments[0];
+#ifdef DEBUG_ENABLED
+ if (ResourceUID::get_singleton()->text_to_id(uid_string) == ResourceUID::INVALID_ID) {
+ push_error(R"(The annotated UID is invalid.)", p_annotation);
+ return false;
+ }
+#endif // DEBUG_ENABLED
+
+ class_node->uid_string = uid_string;
+
+ this->_has_uid = true;
+ return true;
+}
+
bool GDScriptParser::tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
#ifdef DEBUG_ENABLED
if (this->_is_tool) {
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index 88b5bdc43f..e058737306 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -736,6 +736,8 @@ public:
IdentifierNode *identifier = nullptr;
String icon_path;
String simplified_icon_path;
+ String uid_string;
+ Vector2i uid_lines = Vector2i(-1, -1);
Vector<Member> members;
HashMap<StringName, int> members_indices;
ClassNode *outer = nullptr;
@@ -1318,6 +1320,7 @@ private:
friend class GDScriptAnalyzer;
bool _is_tool = false;
+ bool _has_uid = false;
String script_path;
bool for_completion = false;
bool panic_mode = false;
@@ -1473,6 +1476,7 @@ private:
static bool register_annotation(const MethodInfo &p_info, uint32_t p_target_kinds, AnnotationAction p_apply, const Vector<Variant> &p_default_arguments = Vector<Variant>(), bool p_is_vararg = false);
bool validate_annotation_arguments(AnnotationNode *p_annotation);
void clear_unused_annotations();
+ bool uid_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool tool_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool icon_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
bool onready_annotation(const AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp
index 95b3be2811..e00b92b752 100644
--- a/modules/gdscript/language_server/gdscript_text_document.cpp
+++ b/modules/gdscript/language_server/gdscript_text_document.cpp
@@ -114,7 +114,7 @@ void GDScriptTextDocument::didSave(const Variant &p_param) {
scr->update_exports();
ScriptEditor::get_singleton()->reload_scripts(true);
ScriptEditor::get_singleton()->update_docs_from_script(scr);
- ScriptEditor::get_singleton()->trigger_live_script_reload();
+ ScriptEditor::get_singleton()->trigger_live_script_reload(scr->get_path());
}
}
diff --git a/modules/gdscript/tests/README.md b/modules/gdscript/tests/README.md
index 361d586d32..cea251bab5 100644
--- a/modules/gdscript/tests/README.md
+++ b/modules/gdscript/tests/README.md
@@ -6,3 +6,44 @@ and output files.
See the
[Integration tests for GDScript documentation](https://docs.godotengine.org/en/latest/contributing/development/core_and_modules/unit_testing.html#integration-tests-for-gdscript)
for information about creating and running GDScript integration tests.
+
+# GDScript Autocompletion tests
+
+The `script/completion` folder contains test for the GDScript autocompletion.
+
+Each test case consists of at least one `.gd` file, which contains the code, and one `.cfg` file, which contains expected results and configuration. Inside of the GDScript file the character `➡` represents the cursor position, at which autocompletion is invoked.
+
+The config file contains two section:
+
+`[input]` contains keys that configure the test environment. The following keys are possible:
+
+- `cs: boolean = false`: If `true`, the test will be skipped when running a non C# build.
+- `use_single_quotes: boolean = false`: Configures the corresponding editor setting for the test.
+- `scene: String`: Allows to specify a scene which is opened while autocompletion is performed. If this is not set the test runner will search for a `.tscn` file with the same basename as the GDScript file. If that isn't found either, autocompletion will behave as if no scene was opened.
+
+`[output]` specifies the expected results for the test. The following key are supported:
+
+- `include: Array`: An unordered list of suggestions that should be in the result. Each entry is one dictionary with the following keys: `display`, `insert_text`, `kind`, `location`, which correspond to the suggestion struct which is used in the code. The runner only tests against specified keys, so in most cases `display` will suffice.
+- `exclude: Array`: An array of suggestions which should not be in the result. The entries take the same form as for `include`.
+- `call_hint: String`: The expected call hint returned by autocompletion.
+- `forced: boolean`: Whether autocompletion is expected to force opening a completion window.
+
+Tests will only test against entries in `[output]` that were specified.
+
+## Writing autocompletion tests
+
+To avoid failing edge cases a certain behaviour needs to be tested multiple times. Some things that tests should account for:
+
+- All possible types: Test with all possible types that apply to the tested behaviour. (For the last points testing against `SCRIPT` and `CLASS` should suffice. `CLASS` can be obtained through C#, `SCRIPT` through GDScript. Relying on autoloads to be of type `SCRIPT` is not good, since this might change in the future.)
+
+ - `BUILTIN`
+ - `NATIVE`
+ - GDScripts (with `class_name` as well as `preload`ed)
+ - C# (as standin for all other language bindings) (with `class_name` as well as `preload`ed)
+ - Autoloads
+
+- Possible contexts: the completion might be placed in different places of the program. e.g:
+ - initializers of class members
+ - directly inside a suite
+ - assignments inside a suite
+ - as parameter to a call
diff --git a/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg
index 4edee46039..27e695d245 100644
--- a/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg
+++ b/modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg
@@ -1,4 +1,4 @@
[output]
-expected=[
+include=[
{"display": "autoplay"},
]
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd
new file mode 100644
index 0000000000..4ded8e65db
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.gd
@@ -0,0 +1,5 @@
+@uid("uid://c4ckv3ryprcn4")
+@uid("uid://c4ckv3ryprcn4")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out
new file mode 100644
index 0000000000..be1061401a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_duplicate.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+"@uid" annotation can only be used once.
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd
new file mode 100644
index 0000000000..114d5b7e98
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.gd
@@ -0,0 +1,4 @@
+@uid("not a valid uid")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out
new file mode 100644
index 0000000000..83f9f63cbf
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_invalid.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+The annotated UID is invalid.
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd
new file mode 100644
index 0000000000..2b332447b7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.gd
@@ -0,0 +1,5 @@
+extends Object
+@uid("uid://c4ckv3ryprcn4")
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out
new file mode 100644
index 0000000000..328459923f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/uid_too_late.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Annotation "@uid" must be at the top of the script, before "extends" and "class_name".
diff --git a/modules/gdscript/tests/scripts/parser/features/uid.gd b/modules/gdscript/tests/scripts/parser/features/uid.gd
new file mode 100644
index 0000000000..4070500608
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/uid.gd
@@ -0,0 +1,5 @@
+@uid("uid://c4ckv3ryprcn4")
+extends Object
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/parser/features/uid.out b/modules/gdscript/tests/scripts/parser/features/uid.out
new file mode 100644
index 0000000000..d73c5eb7cd
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/uid.out
@@ -0,0 +1 @@
+GDTEST_OK
diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd
new file mode 100644
index 0000000000..b9746a8207
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.gd
@@ -0,0 +1,10 @@
+func test():
+ var node := Node.new()
+ var callable: Callable = node.free
+ callable.call()
+ print(node)
+
+ node = Node.new()
+ callable = node["free"]
+ callable.call()
+ print(node)
diff --git a/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out
new file mode 100644
index 0000000000..97bfc46d96
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/free_is_callable.out
@@ -0,0 +1,3 @@
+GDTEST_OK
+<Freed Object>
+<Freed Object>
diff --git a/modules/gdscript/tests/test_completion.h b/modules/gdscript/tests/test_completion.h
index abc34bd4bf..fd6b5321e6 100644
--- a/modules/gdscript/tests/test_completion.h
+++ b/modules/gdscript/tests/test_completion.h
@@ -128,19 +128,23 @@ static void test_directory(const String &p_dir) {
EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false));
List<Dictionary> include;
- to_dict_list(conf.get_value("result", "include", Array()), include);
+ to_dict_list(conf.get_value("output", "include", Array()), include);
List<Dictionary> exclude;
- to_dict_list(conf.get_value("result", "exclude", Array()), exclude);
+ to_dict_list(conf.get_value("output", "exclude", Array()), exclude);
List<ScriptLanguage::CodeCompletionOption> options;
String call_hint;
bool forced;
Node *owner = nullptr;
- if (dir->file_exists(next.get_basename() + ".tscn")) {
- String project_path = "res://completion";
- Ref<PackedScene> scene = ResourceLoader::load(project_path.path_join(next.get_basename() + ".tscn"), "PackedScene");
+ if (conf.has_section_key("input", "scene")) {
+ Ref<PackedScene> scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene");
+ if (scene.is_valid()) {
+ owner = scene->instantiate();
+ }
+ } else if (dir->file_exists(next.get_basename() + ".tscn")) {
+ Ref<PackedScene> scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene");
if (scene.is_valid()) {
owner = scene->instantiate();
}
@@ -169,8 +173,8 @@ static void test_directory(const String &p_dir) {
CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'.");
CHECK(include.is_empty());
- String expected_call_hint = conf.get_value("result", "call_hint", call_hint);
- bool expected_forced = conf.get_value("result", "forced", forced);
+ String expected_call_hint = conf.get_value("output", "call_hint", call_hint);
+ bool expected_forced = conf.get_value("output", "forced", forced);
CHECK(expected_call_hint == call_hint);
CHECK(expected_forced == forced);
diff --git a/modules/gdscript/tests/test_gdscript_uid.h b/modules/gdscript/tests/test_gdscript_uid.h
new file mode 100644
index 0000000000..918fe65890
--- /dev/null
+++ b/modules/gdscript/tests/test_gdscript_uid.h
@@ -0,0 +1,115 @@
+/**************************************************************************/
+/* test_gdscript_uid.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef TEST_GDSCRIPT_UID_H
+#define TEST_GDSCRIPT_UID_H
+
+#ifdef TOOLS_ENABLED
+
+#include "core/io/resource_saver.h"
+#include "core/os/os.h"
+#include "gdscript_test_runner.h"
+
+#include "../gdscript.h"
+#include "tests/test_macros.h"
+
+namespace GDScriptTests {
+
+static HashMap<String, ResourceUID::ID> id_cache;
+
+ResourceUID::ID _resource_saver_get_resource_id_for_path(const String &p_path, bool p_generate) {
+ return ResourceUID::get_singleton()->text_to_id("uid://baba");
+}
+
+static void test_script(const String &p_source, const String &p_target_source) {
+ const String script_path = OS::get_singleton()->get_cache_path().path_join("script.gd");
+
+ Ref<GDScript> script;
+ script.instantiate();
+ script->set_source_code(p_source);
+ ResourceSaver::save(script, script_path);
+
+ Ref<FileAccess> fa = FileAccess::open(script_path, FileAccess::READ);
+ CHECK_EQ(fa->get_as_text(), p_target_source);
+}
+
+TEST_SUITE("[Modules][GDScript][UID]") {
+ TEST_CASE("[ResourceSaver] Adding UID line to script") {
+ init_language("modules/gdscript/tests/scripts");
+ ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);
+
+ const String source = R"(extends Node
+class_name TestClass
+)";
+ const String final_source = R"(@uid("uid://baba") # Generated automatically, do not modify.
+extends Node
+class_name TestClass
+)";
+
+ // Script has no UID, add it.
+ test_script(source, final_source);
+ }
+
+ TEST_CASE("[ResourceSaver] Updating UID line in script") {
+ init_language("modules/gdscript/tests/scripts");
+ ResourceSaver::set_get_resource_id_for_path(_resource_saver_get_resource_id_for_path);
+
+ const String wrong_id_source = R"(
+
+@uid(
+ "uid://dead"
+ ) # G
+extends Node
+class_name TestClass
+)";
+ const String corrected_id_source = R"(
+
+@uid("uid://baba") # Generated automatically, do not modify.
+extends Node
+class_name TestClass
+)";
+ const String correct_id_source = R"(@uid("uid://baba") # G
+extends Node
+class_name TestClass
+)";
+
+ // Script has wrong UID saved. Remove it and add a correct one.
+ // Inserts in the same line, but multiline annotations are flattened.
+ test_script(wrong_id_source, corrected_id_source);
+ // The stored UID is correct, so do not modify it.
+ test_script(correct_id_source, correct_id_source);
+ }
+}
+
+} // namespace GDScriptTests
+
+#endif
+
+#endif // TEST_GDSCRIPT_UID_H
diff --git a/modules/gltf/structures/gltf_buffer_view.cpp b/modules/gltf/structures/gltf_buffer_view.cpp
index d40ed69915..8588de0752 100644
--- a/modules/gltf/structures/gltf_buffer_view.cpp
+++ b/modules/gltf/structures/gltf_buffer_view.cpp
@@ -94,6 +94,7 @@ void GLTFBufferView::set_indices(bool p_indices) {
}
Vector<uint8_t> GLTFBufferView::load_buffer_view_data(const Ref<GLTFState> p_state) const {
+ ERR_FAIL_COND_V(p_state.is_null(), Vector<uint8_t>());
ERR_FAIL_COND_V_MSG(byte_stride > 0, Vector<uint8_t>(), "Buffer views with byte stride are not yet supported by this method.");
const TypedArray<Vector<uint8_t>> &buffers = p_state->get_buffers();
ERR_FAIL_INDEX_V(buffer, buffers.size(), Vector<uint8_t>());
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp
index ac6977504a..ef24dc35ca 100644
--- a/modules/mono/csharp_script.cpp
+++ b/modules/mono/csharp_script.cpp
@@ -720,11 +720,22 @@ void CSharpLanguage::reload_all_scripts() {
#endif
}
-void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) {
- (void)p_script; // UNUSED
-
+void CSharpLanguage::reload_scripts(const Array &p_scripts, bool p_soft_reload) {
CRASH_COND(!Engine::get_singleton()->is_editor_hint());
+ bool has_csharp_script = false;
+ for (int i = 0; i < p_scripts.size(); ++i) {
+ Ref<CSharpScript> cs_script = p_scripts[i];
+ if (cs_script.is_valid()) {
+ has_csharp_script = true;
+ break;
+ }
+ }
+
+ if (!has_csharp_script) {
+ return;
+ }
+
#ifdef TOOLS_ENABLED
get_godotsharp_editor()->get_node(NodePath("HotReloadAssemblyWatcher"))->call("RestartTimer");
#endif
@@ -736,6 +747,12 @@ void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft
#endif
}
+void CSharpLanguage::reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) {
+ Array scripts;
+ scripts.push_back(p_script);
+ reload_scripts(scripts, p_soft_reload);
+}
+
#ifdef GD_MONO_HOT_RELOAD
bool CSharpLanguage::is_assembly_reloading_needed() {
ERR_FAIL_NULL_V(gdmono, false);
@@ -1074,7 +1091,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
}
// The script instance could not be instantiated or wasn't in the list of placeholders to replace.
obj->set_script(scr);
-#if DEBUG_ENABLED
+#ifdef DEBUG_ENABLED
// If we reached here, the instantiated script must be a placeholder.
CRASH_COND(!obj->get_script_instance()->is_placeholder());
#endif
@@ -2310,7 +2327,7 @@ void CSharpScript::reload_registered_script(Ref<CSharpScript> p_script) {
p_script->_update_exports();
-#if TOOLS_ENABLED
+#ifdef TOOLS_ENABLED
// If the EditorFileSystem singleton is available, update the file;
// otherwise, the file will be updated when the singleton becomes available.
EditorFileSystem *efs = EditorFileSystem::get_singleton();
@@ -2666,7 +2683,7 @@ Error CSharpScript::reload(bool p_keep_state) {
_update_exports();
-#if TOOLS_ENABLED
+#ifdef TOOLS_ENABLED
// If the EditorFileSystem singleton is available, update the file;
// otherwise, the file will be updated when the singleton becomes available.
EditorFileSystem *efs = EditorFileSystem::get_singleton();
diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h
index 41e8d63be1..310cb81929 100644
--- a/modules/mono/csharp_script.h
+++ b/modules/mono/csharp_script.h
@@ -478,6 +478,7 @@ public:
/* TODO? */ void get_public_annotations(List<MethodInfo> *p_annotations) const override {}
void reload_all_scripts() override;
+ void reload_scripts(const Array &p_scripts, bool p_soft_reload) override;
void reload_tool_script(const Ref<Script> &p_script, bool p_soft_reload) override;
/* LOADER FUNCTIONS */
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
index 03a7dc453c..9674626183 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.NET.Sdk.sln
@@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "G
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Sample", "Godot.SourceGenerators.Sample\Godot.SourceGenerators.Sample.csproj", "{7297A614-8DF5-43DE-9EAD-99671B26BD1F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Tests", "Godot.SourceGenerators.Tests\Godot.SourceGenerators.Tests.csproj", "{07E6D201-35C9-4463-9B29-D16621EA733D}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharp", "..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj", "{AEBF0036-DA76-4341-B651-A3F2856AB2FA}"
EndProject
Global
@@ -26,6 +28,10 @@ Global
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {07E6D201-35C9-4463-9B29-D16621EA733D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {07E6D201-35C9-4463-9B29-D16621EA733D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {07E6D201-35C9-4463-9B29-D16621EA733D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {07E6D201-35C9-4463-9B29-D16621EA733D}.Release|Any CPU.Build.0 = Release|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs
new file mode 100644
index 0000000000..b9f11908e1
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/GlobalClass.cs
@@ -0,0 +1,14 @@
+namespace Godot.SourceGenerators.Sample;
+
+[GlobalClass]
+public partial class CustomGlobalClass : GodotObject
+{
+}
+
+// This doesn't works because global classes can't have any generic type parameter.
+/*
+[GlobalClass]
+public partial class CustomGlobalClass<T> : Node
+{
+}
+*/
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj
index 3f569ebac3..d0907c1cd4 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/Godot.SourceGenerators.Sample.csproj
@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
+ <LangVersion>11</LangVersion>
</PropertyGroup>
<PropertyGroup>
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs
new file mode 100644
index 0000000000..1e06091e80
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Sample/MustBeVariantSamples.cs
@@ -0,0 +1,164 @@
+using System;
+using Godot.Collections;
+using Array = Godot.Collections.Array;
+
+namespace Godot.SourceGenerators.Sample;
+
+public class MustBeVariantMethods
+{
+ public void MustBeVariantMethodCalls()
+ {
+ Method<bool>();
+ Method<char>();
+ Method<sbyte>();
+ Method<byte>();
+ Method<short>();
+ Method<ushort>();
+ Method<int>();
+ Method<uint>();
+ Method<long>();
+ Method<ulong>();
+ Method<float>();
+ Method<double>();
+ Method<string>();
+ Method<Vector2>();
+ Method<Vector2I>();
+ Method<Rect2>();
+ Method<Rect2I>();
+ Method<Transform2D>();
+ Method<Vector3>();
+ Method<Vector3I>();
+ Method<Vector4>();
+ Method<Vector4I>();
+ Method<Basis>();
+ Method<Quaternion>();
+ Method<Transform3D>();
+ Method<Projection>();
+ Method<Aabb>();
+ Method<Color>();
+ Method<Plane>();
+ Method<Callable>();
+ Method<Signal>();
+ Method<GodotObject>();
+ Method<StringName>();
+ Method<NodePath>();
+ Method<Rid>();
+ Method<Dictionary>();
+ Method<Array>();
+ Method<byte[]>();
+ Method<int[]>();
+ Method<long[]>();
+ Method<float[]>();
+ Method<double[]>();
+ Method<string[]>();
+ Method<Vector2[]>();
+ Method<Vector3[]>();
+ Method<Color[]>();
+ Method<GodotObject[]>();
+ Method<StringName[]>();
+ Method<NodePath[]>();
+ Method<Rid[]>();
+
+ // This call fails because generic type is not Variant-compatible.
+ //Method<object>();
+ }
+
+ public void Method<[MustBeVariant] T>()
+ {
+ }
+
+ public void MustBeVariantClasses()
+ {
+ new ClassWithGenericVariant<bool>();
+ new ClassWithGenericVariant<char>();
+ new ClassWithGenericVariant<sbyte>();
+ new ClassWithGenericVariant<byte>();
+ new ClassWithGenericVariant<short>();
+ new ClassWithGenericVariant<ushort>();
+ new ClassWithGenericVariant<int>();
+ new ClassWithGenericVariant<uint>();
+ new ClassWithGenericVariant<long>();
+ new ClassWithGenericVariant<ulong>();
+ new ClassWithGenericVariant<float>();
+ new ClassWithGenericVariant<double>();
+ new ClassWithGenericVariant<string>();
+ new ClassWithGenericVariant<Vector2>();
+ new ClassWithGenericVariant<Vector2I>();
+ new ClassWithGenericVariant<Rect2>();
+ new ClassWithGenericVariant<Rect2I>();
+ new ClassWithGenericVariant<Transform2D>();
+ new ClassWithGenericVariant<Vector3>();
+ new ClassWithGenericVariant<Vector3I>();
+ new ClassWithGenericVariant<Vector4>();
+ new ClassWithGenericVariant<Vector4I>();
+ new ClassWithGenericVariant<Basis>();
+ new ClassWithGenericVariant<Quaternion>();
+ new ClassWithGenericVariant<Transform3D>();
+ new ClassWithGenericVariant<Projection>();
+ new ClassWithGenericVariant<Aabb>();
+ new ClassWithGenericVariant<Color>();
+ new ClassWithGenericVariant<Plane>();
+ new ClassWithGenericVariant<Callable>();
+ new ClassWithGenericVariant<Signal>();
+ new ClassWithGenericVariant<GodotObject>();
+ new ClassWithGenericVariant<StringName>();
+ new ClassWithGenericVariant<NodePath>();
+ new ClassWithGenericVariant<Rid>();
+ new ClassWithGenericVariant<Dictionary>();
+ new ClassWithGenericVariant<Array>();
+ new ClassWithGenericVariant<byte[]>();
+ new ClassWithGenericVariant<int[]>();
+ new ClassWithGenericVariant<long[]>();
+ new ClassWithGenericVariant<float[]>();
+ new ClassWithGenericVariant<double[]>();
+ new ClassWithGenericVariant<string[]>();
+ new ClassWithGenericVariant<Vector2[]>();
+ new ClassWithGenericVariant<Vector3[]>();
+ new ClassWithGenericVariant<Color[]>();
+ new ClassWithGenericVariant<GodotObject[]>();
+ new ClassWithGenericVariant<StringName[]>();
+ new ClassWithGenericVariant<NodePath[]>();
+ new ClassWithGenericVariant<Rid[]>();
+
+ // This class fails because generic type is not Variant-compatible.
+ //new ClassWithGenericVariant<object>();
+ }
+}
+
+public class ClassWithGenericVariant<[MustBeVariant] T>
+{
+}
+
+public class MustBeVariantAnnotatedMethods
+{
+ [GenericTypeAttribute<string>()]
+ public void MethodWithAttributeOk()
+ {
+ }
+
+ // This method definition fails because generic type is not Variant-compatible.
+ /*
+ [GenericTypeAttribute<object>()]
+ public void MethodWithWrongAttribute()
+ {
+ }
+ */
+}
+
+[GenericTypeAttribute<string>()]
+public class ClassVariantAnnotated
+{
+}
+
+// This class definition fails because generic type is not Variant-compatible.
+/*
+[GenericTypeAttribute<object>()]
+public class ClassNonVariantAnnotated
+{
+}
+*/
+
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
+public class GenericTypeAttribute<[MustBeVariant] T> : Attribute
+{
+}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs
new file mode 100644
index 0000000000..e3e7373b2e
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/CSharpAnalyzerVerifier.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis.Testing.Verifiers;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Godot.SourceGenerators.Tests;
+
+public static class CSharpAnalyzerVerifier<TAnalyzer>
+where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ public class Test : CSharpAnalyzerTest<TAnalyzer, XUnitVerifier>
+ {
+ public Test()
+ {
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net60;
+
+ SolutionTransforms.Add((Solution solution, ProjectId projectId) =>
+ {
+ Project project =
+ solution.GetProject(projectId)!.AddMetadataReference(Constants.GodotSharpAssembly
+ .CreateMetadataReference());
+
+ return project.Solution;
+ });
+ }
+ }
+
+ public static Task Verify(string sources, params DiagnosticResult[] expected)
+ {
+ return MakeVerifier(new string[] { sources }, expected).RunAsync();
+ }
+
+ public static Test MakeVerifier(ICollection<string> sources, params DiagnosticResult[] expected)
+ {
+ var verifier = new Test();
+
+ verifier.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", $"""
+ is_global = true
+ build_property.GodotProjectDir = {Constants.ExecutingAssemblyPath}
+ """));
+
+ verifier.TestState.Sources.AddRange(sources.Select(source =>
+ {
+ return (source, SourceText.From(File.ReadAllText(Path.Combine(Constants.SourceFolderPath, source))));
+ }));
+
+ verifier.ExpectedDiagnostics.AddRange(expected);
+ return verifier;
+ }
+}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs
new file mode 100644
index 0000000000..74d6afceb3
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/GlobalClassAnalyzerTests.cs
@@ -0,0 +1,20 @@
+using Xunit;
+
+namespace Godot.SourceGenerators.Tests;
+
+public class GlobalClassAnalyzerTests
+{
+ [Fact]
+ public async void GlobalClassMustDeriveFromGodotObjectTest()
+ {
+ const string GlobalClassGD0401 = "GlobalClass.GD0401.cs";
+ await CSharpAnalyzerVerifier<GlobalClassAnalyzer>.Verify(GlobalClassGD0401);
+ }
+
+ [Fact]
+ public async void GlobalClassMustNotBeGenericTest()
+ {
+ const string GlobalClassGD0402 = "GlobalClass.GD0402.cs";
+ await CSharpAnalyzerVerifier<GlobalClassAnalyzer>.Verify(GlobalClassGD0402);
+ }
+}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj
index e39c14f049..13e54a543f 100644
--- a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/Godot.SourceGenerators.Tests.csproj
@@ -17,6 +17,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
+ <PackageReference Include="Microsoft.CodeAnalysis.Testing.Verifiers.XUnit" Version="1.1.1" />
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs
new file mode 100644
index 0000000000..62c602efbb
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/MustBeVariantAnalyzerTests.cs
@@ -0,0 +1,20 @@
+using Xunit;
+
+namespace Godot.SourceGenerators.Tests;
+
+public class MustBeVariantAnalyzerTests
+{
+ [Fact]
+ public async void GenericTypeArgumentMustBeVariantTest()
+ {
+ const string MustBeVariantGD0301 = "MustBeVariant.GD0301.cs";
+ await CSharpAnalyzerVerifier<MustBeVariantAnalyzer>.Verify(MustBeVariantGD0301);
+ }
+
+ [Fact]
+ public async void GenericTypeParameterMustBeVariantAnnotatedTest()
+ {
+ const string MustBeVariantGD0302 = "MustBeVariant.GD0302.cs";
+ await CSharpAnalyzerVerifier<MustBeVariantAnalyzer>.Verify(MustBeVariantGD0302);
+ }
+}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs
new file mode 100644
index 0000000000..6e6d3a6f39
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0401.cs
@@ -0,0 +1,22 @@
+using Godot;
+
+// This works because it inherits from GodotObject.
+[GlobalClass]
+public partial class CustomGlobalClass1 : GodotObject
+{
+
+}
+
+// This works because it inherits from an object that inherits from GodotObject
+[GlobalClass]
+public partial class CustomGlobalClass2 : Node
+{
+
+}
+
+// This raises a GD0401 diagnostic error: global classes must inherit from GodotObject
+{|GD0401:[GlobalClass]
+public partial class CustomGlobalClass3
+{
+
+}|}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs
new file mode 100644
index 0000000000..1c0a169841
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/GlobalClass.GD0402.cs
@@ -0,0 +1,15 @@
+using Godot;
+
+// This works because it inherits from GodotObject and it doesn't have any generic type parameter.
+[GlobalClass]
+public partial class CustomGlobalClass : GodotObject
+{
+
+}
+
+// This raises a GD0402 diagnostic error: global classes can't have any generic type parameter
+{|GD0402:[GlobalClass]
+public partial class CustomGlobalClass<T> : GodotObject
+{
+
+}|}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs
new file mode 100644
index 0000000000..031039cba1
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0301.cs
@@ -0,0 +1,71 @@
+using System;
+using Godot;
+using Godot.Collections;
+using Array = Godot.Collections.Array;
+
+public class MustBeVariantGD0301
+{
+ public void MethodCallsError()
+ {
+ // This raises a GD0301 diagnostic error: object is not Variant (and Method<T> requires a variant generic type).
+ Method<{|GD0301:object|}>();
+ }
+ public void MethodCallsOk()
+ {
+ // All these calls are valid because they are Variant types.
+ Method<bool>();
+ Method<char>();
+ Method<sbyte>();
+ Method<byte>();
+ Method<short>();
+ Method<ushort>();
+ Method<int>();
+ Method<uint>();
+ Method<long>();
+ Method<ulong>();
+ Method<float>();
+ Method<double>();
+ Method<string>();
+ Method<Vector2>();
+ Method<Vector2I>();
+ Method<Rect2>();
+ Method<Rect2I>();
+ Method<Transform2D>();
+ Method<Vector3>();
+ Method<Vector3I>();
+ Method<Vector4>();
+ Method<Vector4I>();
+ Method<Basis>();
+ Method<Quaternion>();
+ Method<Transform3D>();
+ Method<Projection>();
+ Method<Aabb>();
+ Method<Color>();
+ Method<Plane>();
+ Method<Callable>();
+ Method<Signal>();
+ Method<GodotObject>();
+ Method<StringName>();
+ Method<NodePath>();
+ Method<Rid>();
+ Method<Dictionary>();
+ Method<Array>();
+ Method<byte[]>();
+ Method<int[]>();
+ Method<long[]>();
+ Method<float[]>();
+ Method<double[]>();
+ Method<string[]>();
+ Method<Vector2[]>();
+ Method<Vector3[]>();
+ Method<Color[]>();
+ Method<GodotObject[]>();
+ Method<StringName[]>();
+ Method<NodePath[]>();
+ Method<Rid[]>();
+ }
+
+ public void Method<[MustBeVariant] T>()
+ {
+ }
+}
diff --git a/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs
new file mode 100644
index 0000000000..ce182e8c62
--- /dev/null
+++ b/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators.Tests/TestData/Sources/MustBeVariant.GD0302.cs
@@ -0,0 +1,27 @@
+using Godot;
+
+public class MustBeVariantGD0302
+{
+ public void MethodOk<[MustBeVariant] T>()
+ {
+ // T is guaranteed to be a Variant-compatible type because it's annotated with the [MustBeVariant] attribute, so it can be used here.
+ new ExampleClass<T>();
+ Method<T>();
+ }
+
+ public void MethodFail<T>()
+ {
+ // These two calls raise a GD0302 diagnostic error: T is not valid here because it may not a Variant type and method call and class require it.
+ new ExampleClass<{|GD0302:T|}>();
+ Method<{|GD0302:T|}>();
+ }
+
+ public void Method<[MustBeVariant] T>()
+ {
+ }
+}
+
+public class ExampleClass<[MustBeVariant] T>
+{
+
+}
diff --git a/modules/mono/editor/editor_internal_calls.cpp b/modules/mono/editor/editor_internal_calls.cpp
index 322078423f..05dacd28fb 100644
--- a/modules/mono/editor/editor_internal_calls.cpp
+++ b/modules/mono/editor/editor_internal_calls.cpp
@@ -148,7 +148,7 @@ void godot_icall_Internal_ReloadAssemblies(bool p_soft_reload) {
}
void godot_icall_Internal_EditorDebuggerNodeReloadScripts() {
- EditorDebuggerNode::get_singleton()->reload_scripts();
+ EditorDebuggerNode::get_singleton()->reload_all_scripts();
}
bool godot_icall_Internal_ScriptEditorEdit(Resource *p_resource, int32_t p_line, int32_t p_col, bool p_grab_focus) {
@@ -175,7 +175,7 @@ void godot_icall_Internal_EditorPlugin_AddControlToEditorRunBar(Control *p_contr
void godot_icall_Internal_ScriptEditorDebugger_ReloadScripts() {
EditorDebuggerNode *ed = EditorDebuggerNode::get_singleton();
if (ed) {
- ed->reload_scripts();
+ ed->reload_all_scripts();
}
}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs
index 57b5b09ebb..63af6ee6e8 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Aabb.cs
@@ -98,11 +98,11 @@ namespace Godot
Vector3 dstMax = with._position + with._size;
return srcMin.X <= dstMin.X &&
- srcMax.X > dstMax.X &&
+ srcMax.X >= dstMax.X &&
srcMin.Y <= dstMin.Y &&
- srcMax.Y > dstMax.Y &&
+ srcMax.Y >= dstMax.Y &&
srcMin.Z <= dstMin.Z &&
- srcMax.Z > dstMax.Z;
+ srcMax.Z >= dstMax.Z;
}
/// <summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs
index 71a35ab809..cf4ac45a9f 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2.cs
@@ -123,8 +123,8 @@ namespace Godot
public readonly bool Encloses(Rect2 b)
{
return b._position.X >= _position.X && b._position.Y >= _position.Y &&
- b._position.X + b._size.X < _position.X + _size.X &&
- b._position.Y + b._size.Y < _position.Y + _size.Y;
+ b._position.X + b._size.X <= _position.X + _size.X &&
+ b._position.Y + b._size.Y <= _position.Y + _size.Y;
}
/// <summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs
index ef7e9eacd8..58560df0c5 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Rect2I.cs
@@ -113,8 +113,8 @@ namespace Godot
public readonly bool Encloses(Rect2I b)
{
return b._position.X >= _position.X && b._position.Y >= _position.Y &&
- b._position.X + b._size.X < _position.X + _size.X &&
- b._position.Y + b._size.Y < _position.Y + _size.Y;
+ b._position.X + b._size.X <= _position.X + _size.X &&
+ b._position.Y + b._size.Y <= _position.Y + _size.Y;
}
/// <summary>
diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp
index 3518507f8c..0089e9c2a2 100644
--- a/modules/mono/glue/runtime_interop.cpp
+++ b/modules/mono/glue/runtime_interop.cpp
@@ -316,7 +316,7 @@ void godotsharp_internal_new_csharp_script(Ref<CSharpScript> *r_dest) {
}
void godotsharp_internal_editor_file_system_update_file(const String *p_script_path) {
-#if TOOLS_ENABLED
+#ifdef TOOLS_ENABLED
// If the EditorFileSystem singleton is available, update the file;
// otherwise, the file will be updated when the singleton becomes available.
EditorFileSystem *efs = EditorFileSystem::get_singleton();
diff --git a/modules/mono/utils/path_utils.cpp b/modules/mono/utils/path_utils.cpp
index aa97534675..ee17a668d7 100644
--- a/modules/mono/utils/path_utils.cpp
+++ b/modules/mono/utils/path_utils.cpp
@@ -152,7 +152,7 @@ String realpath(const String &p_path) {
}
return result.simplify_path();
-#elif UNIX_ENABLED
+#elif defined(UNIX_ENABLED)
char *resolved_path = ::realpath(p_path.utf8().get_data(), nullptr);
if (!resolved_path) {
diff --git a/modules/openxr/doc_classes/OpenXRHand.xml b/modules/openxr/doc_classes/OpenXRHand.xml
index eb7decd30d..1c4da83138 100644
--- a/modules/openxr/doc_classes/OpenXRHand.xml
+++ b/modules/openxr/doc_classes/OpenXRHand.xml
@@ -7,10 +7,14 @@
This node enables OpenXR's hand tracking functionality. The node should be a child node of an [XROrigin3D] node, tracking will update its position to the player's tracked hand Palm joint location (the center of the middle finger's metacarpal bone). This node also updates the skeleton of a properly skinned hand or avatar model.
If the skeleton is a hand (one of the hand bones is the root node of the skeleton), then the skeleton will be placed relative to the hand palm location and the hand mesh and skeleton should be children of the OpenXRHand node.
If the hand bones are part of a full skeleton, then the root of the hand will keep its location with the assumption that IK is used to position the hand and arm.
+ By default the skeleton hand bones are repositioned to match the size of the tracked hand. To preserve the modeled bone sizes change [member bone_update] to apply rotation only.
</description>
<tutorials>
</tutorials>
<members>
+ <member name="bone_update" type="int" setter="set_bone_update" getter="get_bone_update" enum="OpenXRHand.BoneUpdate" default="0">
+ Specify the type of updates to perform on the bone.
+ </member>
<member name="hand" type="int" setter="set_hand" getter="get_hand" enum="OpenXRHand.Hands" default="0">
Specifies whether this node tracks the left or right hand of the player.
</member>
@@ -52,5 +56,14 @@
<constant name="SKELETON_RIG_MAX" value="2" enum="SkeletonRig">
Maximum supported hands.
</constant>
+ <constant name="BONE_UPDATE_FULL" value="0" enum="BoneUpdate">
+ The skeletons bones are fully updated (both position and rotation) to match the tracked bones.
+ </constant>
+ <constant name="BONE_UPDATE_ROTATION_ONLY" value="1" enum="BoneUpdate">
+ The skeletons bones are only rotated to align with the tracked bones, preserving bone length.
+ </constant>
+ <constant name="BONE_UPDATE_MAX" value="2" enum="BoneUpdate">
+ Maximum supported bone update mode.
+ </constant>
</constants>
</class>
diff --git a/modules/openxr/editor/openxr_editor_plugin.cpp b/modules/openxr/editor/openxr_editor_plugin.cpp
index 51ebbcf44a..559890ecb3 100644
--- a/modules/openxr/editor/openxr_editor_plugin.cpp
+++ b/modules/openxr/editor/openxr_editor_plugin.cpp
@@ -53,6 +53,11 @@ void OpenXREditorPlugin::make_visible(bool p_visible) {
OpenXREditorPlugin::OpenXREditorPlugin() {
action_map_editor = memnew(OpenXRActionMapEditor);
EditorNode::get_singleton()->add_bottom_panel_item(TTR("OpenXR Action Map"), action_map_editor);
+
+#ifndef ANDROID_ENABLED
+ select_runtime = memnew(OpenXRSelectRuntime);
+ add_control_to_container(CONTAINER_TOOLBAR, select_runtime);
+#endif
}
OpenXREditorPlugin::~OpenXREditorPlugin() {
diff --git a/modules/openxr/editor/openxr_editor_plugin.h b/modules/openxr/editor/openxr_editor_plugin.h
index 9764f8fe21..b80f20d049 100644
--- a/modules/openxr/editor/openxr_editor_plugin.h
+++ b/modules/openxr/editor/openxr_editor_plugin.h
@@ -32,6 +32,7 @@
#define OPENXR_EDITOR_PLUGIN_H
#include "openxr_action_map_editor.h"
+#include "openxr_select_runtime.h"
#include "editor/editor_plugin.h"
@@ -39,6 +40,9 @@ class OpenXREditorPlugin : public EditorPlugin {
GDCLASS(OpenXREditorPlugin, EditorPlugin);
OpenXRActionMapEditor *action_map_editor = nullptr;
+#ifndef ANDROID_ENABLED
+ OpenXRSelectRuntime *select_runtime = nullptr;
+#endif
public:
virtual String get_name() const override { return "OpenXRPlugin"; }
diff --git a/modules/openxr/editor/openxr_select_runtime.cpp b/modules/openxr/editor/openxr_select_runtime.cpp
new file mode 100644
index 0000000000..f6aa157907
--- /dev/null
+++ b/modules/openxr/editor/openxr_select_runtime.cpp
@@ -0,0 +1,132 @@
+/**************************************************************************/
+/* openxr_select_runtime.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 "openxr_select_runtime.h"
+
+#include "core/io/dir_access.h"
+#include "core/os/os.h"
+#include "editor/editor_settings.h"
+#include "editor/editor_string_names.h"
+
+void OpenXRSelectRuntime::_bind_methods() {
+}
+
+void OpenXRSelectRuntime::_update_items() {
+ Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+ OS *os = OS::get_singleton();
+ Dictionary runtimes = EDITOR_GET("xr/openxr/runtime_paths");
+
+ int current_runtime = 0;
+ int index = 0;
+ String current_path = os->get_environment("XR_RUNTIME_JSON");
+
+ // Parse the user's home folder.
+ String home_folder = os->get_environment("HOME");
+ if (home_folder.is_empty()) {
+ home_folder = os->get_environment("HOMEDRIVE") + os->get_environment("HOMEPATH");
+ }
+
+ clear();
+ add_item("Default", -1);
+ set_item_metadata(index, "");
+ index++;
+
+ Array keys = runtimes.keys();
+ for (int i = 0; i < keys.size(); i++) {
+ String key = keys[i];
+ String path = runtimes[key];
+ String adj_path = path.replace("~", home_folder);
+
+ if (da->file_exists(adj_path)) {
+ add_item(key, index);
+ set_item_metadata(index, adj_path);
+
+ if (current_path == adj_path) {
+ current_runtime = index;
+ }
+ index++;
+ }
+ }
+
+ select(current_runtime);
+}
+
+void OpenXRSelectRuntime::_item_selected(int p_which) {
+ OS *os = OS::get_singleton();
+
+ if (p_which == 0) {
+ // Return to default runtime
+ os->set_environment("XR_RUNTIME_JSON", "");
+ } else {
+ // Select the runtime we want
+ String runtime_path = get_item_metadata(p_which);
+ os->set_environment("XR_RUNTIME_JSON", runtime_path);
+ }
+}
+
+void OpenXRSelectRuntime::_notification(int p_notification) {
+ switch (p_notification) {
+ case NOTIFICATION_ENTER_TREE: {
+ // Update dropdown
+ _update_items();
+
+ // Connect signal
+ connect("item_selected", callable_mp(this, &OpenXRSelectRuntime::_item_selected));
+ } break;
+ case NOTIFICATION_EXIT_TREE: {
+ // Disconnect signal
+ disconnect("item_selected", callable_mp(this, &OpenXRSelectRuntime::_item_selected));
+ } break;
+ }
+}
+
+OpenXRSelectRuntime::OpenXRSelectRuntime() {
+ Dictionary default_runtimes;
+
+ // Add known common runtimes by default.
+#ifdef WINDOWS_ENABLED
+ default_runtimes["Meta"] = "C:\\Program Files\\Oculus\\Support\\oculus-runtime\\oculus_openxr_64.json";
+ default_runtimes["SteamVR"] = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\SteamVR\\steamxr_win64.json";
+ default_runtimes["Varjo"] = "C:\\Program Files\\Varjo\\varjo-openxr\\VarjoOpenXR.json";
+ default_runtimes["WMR"] = "C:\\WINDOWS\\system32\\MixedRealityRuntime.json";
+#endif
+#ifdef LINUXBSD_ENABLED
+ default_runtimes["Monado"] = "/usr/share/openxr/1/openxr_monado.json";
+ default_runtimes["SteamVR"] = "~/.steam/steam/steamapps/common/SteamVR/steamxr_linux64.json";
+#endif
+
+ EDITOR_DEF_RST("xr/openxr/runtime_paths", default_runtimes);
+
+ set_flat(true);
+ set_theme_type_variation("TopBarOptionButton");
+ set_fit_to_longest_item(false);
+ set_focus_mode(Control::FOCUS_NONE);
+ set_tooltip_text(TTR("Choose an XR runtime."));
+}
diff --git a/modules/openxr/editor/openxr_select_runtime.h b/modules/openxr/editor/openxr_select_runtime.h
new file mode 100644
index 0000000000..60b5137f67
--- /dev/null
+++ b/modules/openxr/editor/openxr_select_runtime.h
@@ -0,0 +1,51 @@
+/**************************************************************************/
+/* openxr_select_runtime.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef OPENXR_SELECT_RUNTIME_H
+#define OPENXR_SELECT_RUNTIME_H
+
+#include "scene/gui/option_button.h"
+
+class OpenXRSelectRuntime : public OptionButton {
+ GDCLASS(OpenXRSelectRuntime, OptionButton);
+
+public:
+ OpenXRSelectRuntime();
+
+protected:
+ static void _bind_methods();
+ void _notification(int p_notification);
+
+private:
+ void _update_items();
+ void _item_selected(int p_which);
+};
+
+#endif // OPENXR_SELECT_RUNTIME_H
diff --git a/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp b/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp
index 1289183ea4..c3f692185b 100644
--- a/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp
+++ b/modules/openxr/extensions/openxr_fb_update_swapchain_extension.cpp
@@ -30,7 +30,7 @@
#include "openxr_fb_update_swapchain_extension.h"
-// always include this as late as possible
+// Always include this as late as possible.
#include "../openxr_platform_inc.h"
OpenXRFBUpdateSwapchainExtension *OpenXRFBUpdateSwapchainExtension::singleton = nullptr;
diff --git a/modules/openxr/extensions/openxr_opengl_extension.h b/modules/openxr/extensions/openxr_opengl_extension.h
index 3b0aa0bce9..5f529829a7 100644
--- a/modules/openxr/extensions/openxr_opengl_extension.h
+++ b/modules/openxr/extensions/openxr_opengl_extension.h
@@ -39,7 +39,7 @@
#include "core/templates/vector.h"
-// always include this as late as possible
+// Always include this as late as possible.
#include "../openxr_platform_inc.h"
class OpenXROpenGLExtension : public OpenXRGraphicsExtensionWrapper {
@@ -65,9 +65,9 @@ private:
#ifdef WIN32
static XrGraphicsBindingOpenGLWin32KHR graphics_binding_gl;
-#elif ANDROID_ENABLED
+#elif defined(ANDROID_ENABLED)
static XrGraphicsBindingOpenGLESAndroidKHR graphics_binding_gl;
-#else
+#else // Linux/X11
static XrGraphicsBindingOpenGLXlibKHR graphics_binding_gl;
#endif
diff --git a/modules/openxr/extensions/openxr_vulkan_extension.h b/modules/openxr/extensions/openxr_vulkan_extension.h
index f31621fda0..86c0f327dd 100644
--- a/modules/openxr/extensions/openxr_vulkan_extension.h
+++ b/modules/openxr/extensions/openxr_vulkan_extension.h
@@ -37,7 +37,7 @@
#include "core/templates/vector.h"
-// always include this as late as possible
+// Always include this as late as possible.
#include "../openxr_platform_inc.h"
class OpenXRVulkanExtension : public OpenXRGraphicsExtensionWrapper, VulkanHooks {
diff --git a/modules/openxr/scene/openxr_hand.cpp b/modules/openxr/scene/openxr_hand.cpp
index 8ce33b55c3..2a4104f6ee 100644
--- a/modules/openxr/scene/openxr_hand.cpp
+++ b/modules/openxr/scene/openxr_hand.cpp
@@ -49,10 +49,14 @@ void OpenXRHand::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_skeleton_rig", "skeleton_rig"), &OpenXRHand::set_skeleton_rig);
ClassDB::bind_method(D_METHOD("get_skeleton_rig"), &OpenXRHand::get_skeleton_rig);
+ ClassDB::bind_method(D_METHOD("set_bone_update", "bone_update"), &OpenXRHand::set_bone_update);
+ ClassDB::bind_method(D_METHOD("get_bone_update"), &OpenXRHand::get_bone_update);
+
ADD_PROPERTY(PropertyInfo(Variant::INT, "hand", PROPERTY_HINT_ENUM, "Left,Right"), "set_hand", "get_hand");
ADD_PROPERTY(PropertyInfo(Variant::INT, "motion_range", PROPERTY_HINT_ENUM, "Unobstructed,Conform to controller"), "set_motion_range", "get_motion_range");
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "hand_skeleton", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Skeleton3D"), "set_hand_skeleton", "get_hand_skeleton");
ADD_PROPERTY(PropertyInfo(Variant::INT, "skeleton_rig", PROPERTY_HINT_ENUM, "OpenXR,Humanoid"), "set_skeleton_rig", "get_skeleton_rig");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "bone_update", PROPERTY_HINT_ENUM, "Full,Rotation Only"), "set_bone_update", "get_bone_update");
BIND_ENUM_CONSTANT(HAND_LEFT);
BIND_ENUM_CONSTANT(HAND_RIGHT);
@@ -65,6 +69,10 @@ void OpenXRHand::_bind_methods() {
BIND_ENUM_CONSTANT(SKELETON_RIG_OPENXR);
BIND_ENUM_CONSTANT(SKELETON_RIG_HUMANOID);
BIND_ENUM_CONSTANT(SKELETON_RIG_MAX);
+
+ BIND_ENUM_CONSTANT(BONE_UPDATE_FULL);
+ BIND_ENUM_CONSTANT(BONE_UPDATE_ROTATION_ONLY);
+ BIND_ENUM_CONSTANT(BONE_UPDATE_MAX);
}
OpenXRHand::OpenXRHand() {
@@ -134,6 +142,16 @@ OpenXRHand::SkeletonRig OpenXRHand::get_skeleton_rig() const {
return skeleton_rig;
}
+void OpenXRHand::set_bone_update(BoneUpdate p_bone_update) {
+ ERR_FAIL_INDEX(p_bone_update, BONE_UPDATE_MAX);
+
+ bone_update = p_bone_update;
+}
+
+OpenXRHand::BoneUpdate OpenXRHand::get_bone_update() const {
+ return bone_update;
+}
+
Skeleton3D *OpenXRHand::get_skeleton() {
if (!has_node(hand_skeleton)) {
return nullptr;
@@ -346,8 +364,12 @@ void OpenXRHand::_update_skeleton() {
const Quaternion q = inv_quaternions[parent_joint] * quaternions[joint];
const Vector3 p = inv_quaternions[parent_joint].xform(positions[joint] - positions[parent_joint]);
- // and set our pose
- skeleton->set_bone_pose_position(joints[joint].bone, p);
+ // Update the bone position if enabled by update mode.
+ if (bone_update == BONE_UPDATE_FULL) {
+ skeleton->set_bone_pose_position(joints[joint].bone, p);
+ }
+
+ // Always update the bone rotation.
skeleton->set_bone_pose_rotation(joints[joint].bone, q);
}
diff --git a/modules/openxr/scene/openxr_hand.h b/modules/openxr/scene/openxr_hand.h
index 14eb893bcc..4c77e7277c 100644
--- a/modules/openxr/scene/openxr_hand.h
+++ b/modules/openxr/scene/openxr_hand.h
@@ -61,6 +61,12 @@ public:
SKELETON_RIG_MAX
};
+ enum BoneUpdate {
+ BONE_UPDATE_FULL,
+ BONE_UPDATE_ROTATION_ONLY,
+ BONE_UPDATE_MAX
+ };
+
private:
struct JointData {
int bone = -1;
@@ -74,6 +80,7 @@ private:
MotionRange motion_range = MOTION_RANGE_UNOBSTRUCTED;
NodePath hand_skeleton;
SkeletonRig skeleton_rig = SKELETON_RIG_OPENXR;
+ BoneUpdate bone_update = BONE_UPDATE_FULL;
JointData joints[XR_HAND_JOINT_COUNT_EXT];
@@ -101,11 +108,15 @@ public:
void set_skeleton_rig(SkeletonRig p_skeleton_rig);
SkeletonRig get_skeleton_rig() const;
+ void set_bone_update(BoneUpdate p_bone_update);
+ BoneUpdate get_bone_update() const;
+
void _notification(int p_what);
};
VARIANT_ENUM_CAST(OpenXRHand::Hands)
VARIANT_ENUM_CAST(OpenXRHand::MotionRange)
VARIANT_ENUM_CAST(OpenXRHand::SkeletonRig)
+VARIANT_ENUM_CAST(OpenXRHand::BoneUpdate)
#endif // OPENXR_HAND_H
diff --git a/modules/svg/register_types.cpp b/modules/svg/register_types.cpp
index 7b61749f61..82d816d833 100644
--- a/modules/svg/register_types.cpp
+++ b/modules/svg/register_types.cpp
@@ -34,6 +34,12 @@
#include <thorvg.h>
+#ifdef THREADS_ENABLED
+#define TVG_THREADS 1
+#else
+#define TVG_THREADS 0
+#endif
+
static Ref<ImageLoaderSVG> image_loader_svg;
void initialize_svg_module(ModuleInitializationLevel p_level) {
@@ -42,7 +48,8 @@ void initialize_svg_module(ModuleInitializationLevel p_level) {
}
tvg::CanvasEngine tvgEngine = tvg::CanvasEngine::Sw;
- if (tvg::Initializer::init(tvgEngine, 1) != tvg::Result::Success) {
+
+ if (tvg::Initializer::init(tvgEngine, TVG_THREADS) != tvg::Result::Success) {
return;
}
diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp
index e276048d40..138714634f 100644
--- a/platform/android/export/export.cpp
+++ b/platform/android/export/export.cpp
@@ -42,6 +42,8 @@ void register_android_exporter_types() {
void register_android_exporter() {
#ifndef ANDROID_ENABLED
+ EDITOR_DEF("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME"));
+ EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME"));
EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR));
EDITOR_DEF("export/android/debug_keystore", "");
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index 653e7bfe6f..5e42d8a967 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -2115,8 +2115,17 @@ Ref<Texture2D> EditorExportPlatformAndroid::get_run_icon() const {
return run_icon;
}
+String EditorExportPlatformAndroid::get_java_path() {
+ String exe_ext;
+ if (OS::get_singleton()->get_name() == "Windows") {
+ exe_ext = ".exe";
+ }
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ return java_sdk_path.path_join("bin/java" + exe_ext);
+}
+
String EditorExportPlatformAndroid::get_adb_path() {
- String exe_ext = "";
+ String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
exe_ext = ".exe";
}
@@ -2128,13 +2137,13 @@ String EditorExportPlatformAndroid::get_apksigner_path(int p_target_sdk, bool p_
if (p_target_sdk == -1) {
p_target_sdk = DEFAULT_TARGET_SDK_VERSION;
}
- String exe_ext = "";
+ String exe_ext;
if (OS::get_singleton()->get_name() == "Windows") {
exe_ext = ".bat";
}
String apksigner_command_name = "apksigner" + exe_ext;
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
- String apksigner_path = "";
+ String apksigner_path;
Error errn;
String build_tools_dir = sdk_path.path_join("build-tools");
@@ -2381,6 +2390,32 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito
err += TTR("Release keystore incorrectly configured in the export preset.") + "\n";
}
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ if (java_sdk_path.is_empty()) {
+ err += TTR("A valid Java SDK path is required in Editor Settings.") + "\n";
+ valid = false;
+ } else {
+ // Validate the given path by checking that `java` is present under the `bin` directory.
+ Error errn;
+ // Check for the bin directory.
+ Ref<DirAccess> da = DirAccess::open(java_sdk_path.path_join("bin"), &errn);
+ if (errn != OK) {
+ err += TTR("Invalid Java SDK path in Editor Settings.");
+ err += TTR("Missing 'bin' directory!");
+ err += "\n";
+ valid = false;
+ } else {
+ // Check for the `java` command.
+ String java_path = get_java_path();
+ if (!FileAccess::exists(java_path)) {
+ err += TTR("Unable to find 'java' command using the Java SDK path.");
+ err += TTR("Please check the Java SDK directory specified in Editor Settings.");
+ err += "\n";
+ valid = false;
+ }
+ }
+ }
+
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
if (sdk_path.is_empty()) {
err += TTR("A valid Android SDK path is required in Editor Settings.") + "\n";
@@ -2918,6 +2953,13 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
}
}
const String assets_directory = get_assets_directory(p_preset, export_format);
+ String java_sdk_path = EDITOR_GET("export/android/java_sdk_path");
+ if (java_sdk_path.is_empty()) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Java SDK path must be configured in Editor Settings at 'export/android/java_sdk_path'."));
+ return ERR_UNCONFIGURED;
+ }
+ print_verbose("Java sdk path: " + java_sdk_path);
+
String sdk_path = EDITOR_GET("export/android/android_sdk_path");
if (sdk_path.is_empty()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Android SDK path must be configured in Editor Settings at 'export/android/android_sdk_path'."));
@@ -2968,8 +3010,11 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
print_verbose("Storing command line flags...");
store_file_at_path(assets_directory + "/_cl_", command_line_flags);
+ print_verbose("Updating JAVA_HOME environment to " + java_sdk_path);
+ OS::get_singleton()->set_environment("JAVA_HOME", java_sdk_path);
+
print_verbose("Updating ANDROID_HOME environment to " + sdk_path);
- OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required
+ OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path);
String build_command;
#ifdef WINDOWS_ENABLED
@@ -3032,6 +3077,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP
String combined_android_dependencies_maven_repos = String("|").join(android_dependencies_maven_repos);
List<String> cmdline;
+ cmdline.push_back("validateJavaVersion");
if (clean_build_required) {
cmdline.push_back("clean");
}
diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h
index 5b585581b0..c282055fba 100644
--- a/platform/android/export/export_plugin.h
+++ b/platform/android/export/export_plugin.h
@@ -226,6 +226,8 @@ public:
static String get_apksigner_path(int p_target_sdk = -1, bool p_check_executes = false);
+ static String get_java_path();
+
virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override;
virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override;
static bool has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error);
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle
index 01b148aeef..8a543a8550 100644
--- a/platform/android/java/app/build.gradle
+++ b/platform/android/java/app/build.gradle
@@ -241,3 +241,12 @@ task copyAndRenameReleaseAab(type: Copy) {
into getExportPath()
rename "build-release.aab", getExportFilename()
}
+
+/**
+ * Used to validate the version of the Java SDK used for the Godot gradle builds.
+ */
+task validateJavaVersion {
+ if (JavaVersion.current() != versions.javaVersion) {
+ throw new GradleException("Invalid Java version ${JavaVersion.current()}. Version ${versions.javaVersion} is the required Java version for Godot gradle builds.")
+ }
+}
diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle
index a91e7bc7ce..bf091098b4 100644
--- a/platform/android/java/app/config.gradle
+++ b/platform/android/java/app/config.gradle
@@ -9,7 +9,7 @@ ext.versions = [
kotlinVersion : '1.7.0',
fragmentVersion : '1.3.6',
nexusPublishVersion: '1.1.0',
- javaVersion : 17,
+ javaVersion : JavaVersion.VERSION_17,
// Also update 'platform/android/detect.py#get_ndk_version()' when this is updated.
ndkVersion : '23.2.8568313'
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index d8a81266d0..13a0a9f877 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -2010,8 +2010,8 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
if (!wd_window.no_focus && !wd_window.is_popup && wd_window.focused) {
- if ((xwa.map_state == IsViewable) && !wd_parent.no_focus && !wd_window.is_popup) {
- XSetInputFocus(x11_display, wd_parent.x11_window, RevertToPointerRoot, CurrentTime);
+ if ((xwa.map_state == IsViewable) && !wd_parent.no_focus && !wd_window.is_popup && _window_focus_check()) {
+ _set_input_focus(wd_parent.x11_window, RevertToPointerRoot);
}
}
} else {
@@ -2950,8 +2950,8 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win
XWindowAttributes xwa;
XSync(x11_display, False);
XGetWindowAttributes(x11_display, wd.x11_xim_window, &xwa);
- if (xwa.map_state == IsViewable) {
- XSetInputFocus(x11_display, wd.x11_xim_window, RevertToParent, CurrentTime);
+ if (xwa.map_state == IsViewable && _window_focus_check()) {
+ _set_input_focus(wd.x11_xim_window, RevertToParent);
}
XSetICFocus(wd.xic);
} else {
@@ -4024,6 +4024,18 @@ void DisplayServerX11::_send_window_event(const WindowData &wd, WindowEvent p_ev
}
}
+void DisplayServerX11::_set_input_focus(Window p_window, int p_revert_to) {
+ Window focused_window;
+ int focus_ret_state;
+ XGetInputFocus(x11_display, &focused_window, &focus_ret_state);
+
+ // Only attempt to change focus if the window isn't already focused, in order to
+ // prevent issues with Godot stealing input focus with alternative window managers.
+ if (p_window != focused_window) {
+ XSetInputFocus(x11_display, p_window, p_revert_to, CurrentTime);
+ }
+}
+
void DisplayServerX11::_poll_events_thread(void *ud) {
DisplayServerX11 *display_server = static_cast<DisplayServerX11 *>(ud);
display_server->_poll_events();
@@ -4233,6 +4245,22 @@ bool DisplayServerX11::mouse_process_popups() {
return closed;
}
+bool DisplayServerX11::_window_focus_check() {
+ Window focused_window;
+ int focus_ret_state;
+ XGetInputFocus(x11_display, &focused_window, &focus_ret_state);
+
+ bool has_focus = false;
+ for (const KeyValue<int, DisplayServerX11::WindowData> &wid : windows) {
+ if (wid.value.x11_window == focused_window) {
+ has_focus = true;
+ break;
+ }
+ }
+
+ return has_focus;
+}
+
void DisplayServerX11::process_events() {
_THREAD_SAFE_METHOD_
@@ -4504,8 +4532,8 @@ void DisplayServerX11::process_events() {
// Set focus when menu window is started.
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
- if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup) {
- XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime);
+ if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup && _window_focus_check()) {
+ _set_input_focus(wd.x11_window, RevertToPointerRoot);
}
// Have we failed to set fullscreen while the window was unmapped?
@@ -4680,8 +4708,8 @@ void DisplayServerX11::process_events() {
// Set focus when menu window is re-used.
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
- if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup) {
- XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime);
+ if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup && _window_focus_check()) {
+ _set_input_focus(wd.x11_window, RevertToPointerRoot);
}
_window_changed(&event);
@@ -4725,7 +4753,7 @@ void DisplayServerX11::process_events() {
// RevertToPointerRoot is used to make sure we don't lose all focus in case
// a subwindow and its parent are both destroyed.
if (!wd.no_focus && !wd.is_popup) {
- XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime);
+ _set_input_focus(wd.x11_window, RevertToPointerRoot);
}
uint64_t diff = OS::get_singleton()->get_ticks_usec() / 1000 - last_click_ms;
diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h
index 378d8bb407..27bf7951ff 100644
--- a/platform/linuxbsd/x11/display_server_x11.h
+++ b/platform/linuxbsd/x11/display_server_x11.h
@@ -354,10 +354,12 @@ class DisplayServerX11 : public DisplayServer {
Context context = CONTEXT_ENGINE;
WindowID _get_focused_window_or_popup() const;
+ bool _window_focus_check();
void _send_window_event(const WindowData &wd, WindowEvent p_event);
static void _dispatch_input_events(const Ref<InputEvent> &p_event);
void _dispatch_input_event(const Ref<InputEvent> &p_event);
+ void _set_input_focus(Window p_window, int p_revert_to);
mutable Mutex events_mutex;
Thread events_thread;
diff --git a/platform/macos/detect.py b/platform/macos/detect.py
index 12db2690de..4a8e9cd956 100644
--- a/platform/macos/detect.py
+++ b/platform/macos/detect.py
@@ -67,21 +67,30 @@ def get_mvk_sdk_path():
if not os.path.exists(dirname):
return ""
- ver_file = "0.0.0.0"
- ver_num = ver_parse(ver_file)
-
+ ver_num = ver_parse("0.0.0.0")
files = os.listdir(dirname)
+ lib_name_out = dirname
for file in files:
if os.path.isdir(os.path.join(dirname, file)):
ver_comp = ver_parse(file)
- lib_name = os.path.join(
- os.path.join(dirname, file), "MoltenVK/MoltenVK.xcframework/macos-arm64_x86_64/libMoltenVK.a"
- )
- if os.path.isfile(lib_name) and ver_comp > ver_num:
- ver_num = ver_comp
- ver_file = file
+ if ver_comp > ver_num:
+ # Try new SDK location.
+ lib_name = os.path.join(
+ os.path.join(dirname, file), "macOS/lib/MoltenVK.xcframework/macos-arm64_x86_64/"
+ )
+ if os.path.isfile(os.path.join(lib_name, "libMoltenVK.a")):
+ ver_num = ver_comp
+ lib_name_out = lib_name
+ else:
+ # Try old SDK location.
+ lib_name = os.path.join(
+ os.path.join(dirname, file), "MoltenVK/MoltenVK.xcframework/macos-arm64_x86_64/"
+ )
+ if os.path.isfile(os.path.join(lib_name, "libMoltenVK.a")):
+ ver_num = ver_comp
+ lib_name_out = lib_name
- return os.path.join(os.path.join(dirname, ver_file), "MoltenVK/MoltenVK.xcframework/macos-arm64_x86_64/")
+ return lib_name_out
def configure(env: "Environment"):
@@ -121,12 +130,12 @@ def configure(env: "Environment"):
env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"])
cc_version = get_compiler_version(env)
- cc_version_major = cc_version["major"]
- cc_version_minor = cc_version["minor"]
+ cc_version_major = cc_version["apple_major"]
+ cc_version_minor = cc_version["apple_minor"]
vanilla = is_vanilla_clang(env)
# Workaround for Xcode 15 linker bug.
- if not vanilla and cc_version_major == 15 and cc_version_minor == 0:
+ if not vanilla and cc_version_major == 1500 and cc_version_minor == 0:
env.Prepend(LINKFLAGS=["-ld_classic"])
env.Append(CCFLAGS=["-fobjc-arc"])
@@ -272,6 +281,12 @@ def configure(env: "Environment"):
mvk_list.insert(
0,
os.path.join(
+ os.path.expanduser(env["vulkan_sdk_path"]), "macOS/lib/MoltenVK.xcframework/macos-arm64_x86_64/"
+ ),
+ )
+ mvk_list.insert(
+ 0,
+ os.path.join(
os.path.expanduser(env["vulkan_sdk_path"]), "MoltenVK/MoltenVK.xcframework/macos-arm64_x86_64/"
),
)
diff --git a/platform/web/.eslintrc.html.js b/platform/web/.eslintrc.html.js
index 5cb8de360a..8c9a3d83da 100644
--- a/platform/web/.eslintrc.html.js
+++ b/platform/web/.eslintrc.html.js
@@ -15,5 +15,7 @@ module.exports = {
"Godot": true,
"Engine": true,
"$GODOT_CONFIG": true,
+ "$GODOT_THREADS_ENABLED": true,
+ "___GODOT_THREADS_ENABLED___": true,
},
};
diff --git a/platform/web/SCsub b/platform/web/SCsub
index 1af0642554..3e0cc9ac4a 100644
--- a/platform/web/SCsub
+++ b/platform/web/SCsub
@@ -13,7 +13,7 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS:
except Exception:
print("GODOT_WEB_TEST_PORT must be a valid integer")
sys.exit(255)
- serve(env.Dir("#bin/.web_zip").abspath, port, "run" in COMMAND_LINE_TARGETS)
+ serve(env.Dir(env.GetTemplateZipPath()).abspath, port, "run" in COMMAND_LINE_TARGETS)
sys.exit(0)
web_files = [
@@ -95,7 +95,7 @@ engine = [
"js/engine/engine.js",
]
externs = [env.File("#platform/web/js/engine/engine.externs.js")]
-js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs)
+js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs, env["threads"])
env.Depends(js_engine, externs)
wrap_list = [
diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp
index 1298d28ebf..ec3c22bf7c 100644
--- a/platform/web/audio_driver_web.cpp
+++ b/platform/web/audio_driver_web.cpp
@@ -30,6 +30,8 @@
#include "audio_driver_web.h"
+#include "godot_audio.h"
+
#include "core/config/project_settings.h"
#include <emscripten.h>
@@ -184,6 +186,8 @@ Error AudioDriverWeb::input_stop() {
return OK;
}
+#ifdef THREADS_ENABLED
+
/// AudioWorkletNode implementation (threads)
void AudioDriverWorklet::_audio_thread_func(void *p_data) {
AudioDriverWorklet *driver = static_cast<AudioDriverWorklet *>(p_data);
@@ -245,3 +249,51 @@ void AudioDriverWorklet::finish_driver() {
quit = true; // Ask thread to quit.
thread.wait_to_finish();
}
+
+#else // No threads.
+
+/// AudioWorkletNode implementation (no threads)
+AudioDriverWorklet *AudioDriverWorklet::singleton = nullptr;
+
+Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) {
+ if (!godot_audio_has_worklet()) {
+ return ERR_UNAVAILABLE;
+ }
+ return (Error)godot_audio_worklet_create(p_channels);
+}
+
+void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+ _audio_driver_process();
+ godot_audio_worklet_start_no_threads(p_out_buf, p_out_buf_size, &_process_callback, p_in_buf, p_in_buf_size, &_capture_callback);
+}
+
+void AudioDriverWorklet::_process_callback(int p_pos, int p_samples) {
+ AudioDriverWorklet *driver = AudioDriverWorklet::get_singleton();
+ driver->_audio_driver_process(p_pos, p_samples);
+}
+
+void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) {
+ AudioDriverWorklet *driver = AudioDriverWorklet::get_singleton();
+ driver->_audio_driver_capture(p_pos, p_samples);
+}
+
+/// ScriptProcessorNode implementation
+AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr;
+
+void AudioDriverScriptProcessor::_process_callback() {
+ AudioDriverScriptProcessor::get_singleton()->_audio_driver_capture();
+ AudioDriverScriptProcessor::get_singleton()->_audio_driver_process();
+}
+
+Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels) {
+ if (!godot_audio_has_script_processor()) {
+ return ERR_UNAVAILABLE;
+ }
+ return (Error)godot_audio_script_create(&p_buffer_samples, p_channels);
+}
+
+void AudioDriverScriptProcessor::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+ godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback);
+}
+
+#endif // THREADS_ENABLED
diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h
index 12a61746c3..df88d0a94c 100644
--- a/platform/web/audio_driver_web.h
+++ b/platform/web/audio_driver_web.h
@@ -90,6 +90,7 @@ public:
AudioDriverWeb() {}
};
+#ifdef THREADS_ENABLED
class AudioDriverWorklet : public AudioDriverWeb {
private:
enum {
@@ -120,4 +121,54 @@ public:
virtual void unlock() override;
};
+#else
+
+class AudioDriverWorklet : public AudioDriverWeb {
+private:
+ static void _process_callback(int p_pos, int p_samples);
+ static void _capture_callback(int p_pos, int p_samples);
+
+ static AudioDriverWorklet *singleton;
+
+protected:
+ virtual Error create(int &p_buffer_size, int p_output_channels) override;
+ virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
+
+public:
+ virtual const char *get_name() const override {
+ return "AudioWorklet";
+ }
+
+ virtual void lock() override {}
+ virtual void unlock() override {}
+
+ static AudioDriverWorklet *get_singleton() { return singleton; }
+
+ AudioDriverWorklet() { singleton = this; }
+};
+
+class AudioDriverScriptProcessor : public AudioDriverWeb {
+private:
+ static void _process_callback();
+
+ static AudioDriverScriptProcessor *singleton;
+
+protected:
+ virtual Error create(int &p_buffer_size, int p_output_channels) override;
+ virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
+ virtual void finish_driver() override;
+
+public:
+ virtual const char *get_name() const override { return "ScriptProcessor"; }
+
+ virtual void lock() override {}
+ virtual void unlock() override {}
+
+ static AudioDriverScriptProcessor *get_singleton() { return singleton; }
+
+ AudioDriverScriptProcessor() { singleton = this; }
+};
+
+#endif // THREADS_ENABLED
+
#endif // AUDIO_DRIVER_WEB_H
diff --git a/platform/web/detect.py b/platform/web/detect.py
index 53fae67f6a..bce03eb79e 100644
--- a/platform/web/detect.py
+++ b/platform/web/detect.py
@@ -8,6 +8,7 @@ from emscripten_helpers import (
add_js_pre,
add_js_externs,
create_template_zip,
+ get_template_zip_path,
)
from methods import get_compiler_version
from SCons.Util import WhereIs
@@ -161,6 +162,9 @@ def configure(env: "Environment"):
# Add method that joins/compiles our Engine files.
env.AddMethod(create_engine_file, "CreateEngineFile")
+ # Add method for getting the final zip path
+ env.AddMethod(get_template_zip_path, "GetTemplateZipPath")
+
# Add method for creating the final zip file
env.AddMethod(create_template_zip, "CreateTemplateZip")
@@ -209,13 +213,17 @@ def configure(env: "Environment"):
stack_size_opt = "STACK_SIZE" if cc_semver >= (3, 1, 25) else "TOTAL_STACK"
env.Append(LINKFLAGS=["-s", "%s=%sKB" % (stack_size_opt, env["stack_size"])])
- # Thread support (via SharedArrayBuffer).
- env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
- env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"])
- env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"])
- env.Append(LINKFLAGS=["-s", "DEFAULT_PTHREAD_STACK_SIZE=%sKB" % env["default_pthread_stack_size"]])
- env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"])
- env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"])
+ if env["threads"]:
+ # Thread support (via SharedArrayBuffer).
+ env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
+ env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"])
+ env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"])
+ env.Append(LINKFLAGS=["-s", "DEFAULT_PTHREAD_STACK_SIZE=%sKB" % env["default_pthread_stack_size"]])
+ env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"])
+ env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"])
+ elif env["proxy_to_pthread"]:
+ print('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.')
+ env["proxy_to_pthread"] = False
if env["lto"] != "none":
# Workaround https://github.com/emscripten-core/emscripten/issues/19781.
@@ -224,7 +232,7 @@ def configure(env: "Environment"):
if env["dlink_enabled"]:
if env["proxy_to_pthread"]:
- print("GDExtension support requires proxy_to_pthread=no, disabling")
+ print("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.")
env["proxy_to_pthread"] = False
if cc_semver < (3, 1, 14):
diff --git a/platform/web/doc_classes/EditorExportPlatformWeb.xml b/platform/web/doc_classes/EditorExportPlatformWeb.xml
index c4c4fd870b..f07f265b0d 100644
--- a/platform/web/doc_classes/EditorExportPlatformWeb.xml
+++ b/platform/web/doc_classes/EditorExportPlatformWeb.xml
@@ -47,6 +47,10 @@
</member>
<member name="variant/extensions_support" type="bool" setter="" getter="">
</member>
+ <member name="variant/thread_support" type="bool" setter="" getter="">
+ If enabled, the exported game will support threads. It requires [url=https://web.dev/articles/coop-coep]a "cross-origin isolated" website[/url], which can be difficult to setup and brings some limitations (e.g. not being able to communicate with third-party websites).
+ If disabled, the exported game will not support threads. As a result, it is more prone to performance and audio issues, but will only require to be run on a HTTPS website.
+ </member>
<member name="vram_texture_compression/for_desktop" type="bool" setter="" getter="">
</member>
<member name="vram_texture_compression/for_mobile" type="bool" setter="" getter="">
diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py
index ec33397842..3ba133c9a1 100644
--- a/platform/web/emscripten_helpers.py
+++ b/platform/web/emscripten_helpers.py
@@ -4,7 +4,12 @@ from SCons.Util import WhereIs
def run_closure_compiler(target, source, env, for_signature):
- closure_bin = os.path.join(os.path.dirname(WhereIs("emcc")), "node_modules", ".bin", "google-closure-compiler")
+ closure_bin = os.path.join(
+ os.path.dirname(WhereIs("emcc")),
+ "node_modules",
+ ".bin",
+ "google-closure-compiler",
+ )
cmd = [WhereIs("node"), closure_bin]
cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"])
for f in env["JSEXTERNS"]:
@@ -31,27 +36,29 @@ def get_build_version():
return v
-def create_engine_file(env, target, source, externs):
+def create_engine_file(env, target, source, externs, threads_enabled):
if env["use_closure_compiler"]:
return env.BuildJS(target, source, JSEXTERNS=externs)
- return env.Textfile(target, [env.File(s) for s in source])
+ subst_dict = {"___GODOT_THREADS_ENABLED": "true" if threads_enabled else "false"}
+ return env.Substfile(target=target, source=[env.File(s) for s in source], SUBST_DICT=subst_dict)
def create_template_zip(env, js, wasm, worker, side):
binary_name = "godot.editor" if env.editor_build else "godot"
- zip_dir = env.Dir("#bin/.web_zip")
+ zip_dir = env.Dir(env.GetTemplateZipPath())
in_files = [
js,
wasm,
- worker,
"#platform/web/js/libs/audio.worklet.js",
]
out_files = [
zip_dir.File(binary_name + ".js"),
zip_dir.File(binary_name + ".wasm"),
- zip_dir.File(binary_name + ".worker.js"),
zip_dir.File(binary_name + ".audio.worklet.js"),
]
+ if env["threads"]:
+ in_files.append(worker)
+ out_files.append(zip_dir.File(binary_name + ".worker.js"))
# Dynamic linking (extensions) specific.
if env["dlink_enabled"]:
in_files.append(side) # Side wasm (contains the actual Godot code).
@@ -65,18 +72,20 @@ def create_template_zip(env, js, wasm, worker, side):
"godot.editor.html",
"offline.html",
"godot.editor.js",
- "godot.editor.worker.js",
"godot.editor.audio.worklet.js",
"logo.svg",
"favicon.png",
]
+ if env["threads"]:
+ cache.append("godot.editor.worker.js")
opt_cache = ["godot.editor.wasm"]
subst_dict = {
- "@GODOT_VERSION@": get_build_version(),
- "@GODOT_NAME@": "GodotEngine",
- "@GODOT_CACHE@": json.dumps(cache),
- "@GODOT_OPT_CACHE@": json.dumps(opt_cache),
- "@GODOT_OFFLINE_PAGE@": "offline.html",
+ "___GODOT_VERSION___": get_build_version(),
+ "___GODOT_NAME___": "GodotEngine",
+ "___GODOT_CACHE___": json.dumps(cache),
+ "___GODOT_OPT_CACHE___": json.dumps(opt_cache),
+ "___GODOT_OFFLINE_PAGE___": "offline.html",
+ "___GODOT_THREADS_ENABLED___": "true" if env["threads"] else "false",
}
html = env.Substfile(target="#bin/godot${PROGSUFFIX}.html", source=html, SUBST_DICT=subst_dict)
in_files.append(html)
@@ -88,7 +97,9 @@ def create_template_zip(env, js, wasm, worker, side):
out_files.append(zip_dir.File("favicon.png"))
# PWA
service_worker = env.Substfile(
- target="#bin/godot${PROGSUFFIX}.service.worker.js", source=service_worker, SUBST_DICT=subst_dict
+ target="#bin/godot${PROGSUFFIX}.service.worker.js",
+ source=service_worker,
+ SUBST_DICT=subst_dict,
)
in_files.append(service_worker)
out_files.append(zip_dir.File("service.worker.js"))
@@ -115,6 +126,10 @@ def create_template_zip(env, js, wasm, worker, side):
)
+def get_template_zip_path(env):
+ return "#bin/.web_zip"
+
+
def add_js_libraries(env, libraries):
env.Append(JS_LIBS=env.File(libraries))
diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp
index 9ffb776c29..d7e72612b4 100644
--- a/platform/web/export/export_plugin.cpp
+++ b/platform/web/export/export_plugin.cpp
@@ -169,6 +169,13 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name");
replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
replaces["$GODOT_CONFIG"] = str_config;
+
+ if (p_preset->get("variant/thread_support")) {
+ replaces["$GODOT_THREADS_ENABLED"] = "true";
+ } else {
+ replaces["$GODOT_THREADS_ENABLED"] = "false";
+ }
+
_replace_strings(replaces, p_html);
}
@@ -216,9 +223,9 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
const String name = p_path.get_file().get_basename();
bool extensions = (bool)p_preset->get("variant/extensions_support");
HashMap<String, String> replaces;
- replaces["@GODOT_VERSION@"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
- replaces["@GODOT_NAME@"] = proj_name.substr(0, 16);
- replaces["@GODOT_OFFLINE_PAGE@"] = name + ".offline.html";
+ replaces["___GODOT_VERSION___"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
+ replaces["___GODOT_NAME___"] = proj_name.substr(0, 16);
+ replaces["___GODOT_OFFLINE_PAGE___"] = name + ".offline.html";
// Files cached during worker install.
Array cache_files;
@@ -231,7 +238,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
}
cache_files.push_back(name + ".worker.js");
cache_files.push_back(name + ".audio.worklet.js");
- replaces["@GODOT_CACHE@"] = Variant(cache_files).to_json_string();
+ replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();
// Heavy files that are cached on demand.
Array opt_cache_files;
@@ -243,7 +250,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
opt_cache_files.push_back(p_shared_objects[i].path.get_file());
}
}
- replaces["@GODOT_OPT_CACHE@"] = Variant(opt_cache_files).to_json_string();
+ replaces["___GODOT_OPT_CACHE___"] = Variant(opt_cache_files).to_json_string();
const String sw_path = dir.path_join(name + ".service.worker.js");
Vector<uint8_t> sw;
@@ -335,6 +342,7 @@ void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // Export type.
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), true)); // Thread support (i.e. run with or without COEP/COOP headers).
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
@@ -377,10 +385,11 @@ bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExp
String err;
bool valid = false;
bool extensions = (bool)p_preset->get("variant/extensions_support");
+ bool thread_support = (bool)p_preset->get("variant/thread_support");
// Look for export templates (first official, and if defined custom templates).
- bool dvalid = exists_export_template(_get_template_name(extensions, true), &err);
- bool rvalid = exists_export_template(_get_template_name(extensions, false), &err);
+ bool dvalid = exists_export_template(_get_template_name(extensions, thread_support, true), &err);
+ bool rvalid = exists_export_template(_get_template_name(extensions, thread_support, false), &err);
if (p_preset->get("custom_template/debug") != "") {
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
@@ -454,7 +463,8 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p
template_path = template_path.strip_edges();
if (template_path.is_empty()) {
bool extensions = (bool)p_preset->get("variant/extensions_support");
- template_path = find_export_template(_get_template_name(extensions, p_debug));
+ bool thread_support = (bool)p_preset->get("variant/thread_support");
+ template_path = find_export_template(_get_template_name(extensions, thread_support, p_debug));
}
if (!template_path.is_empty() && !FileAccess::exists(template_path)) {
diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h
index 9bb82d472e..98e3fe729e 100644
--- a/platform/web/export/export_plugin.h
+++ b/platform/web/export/export_plugin.h
@@ -56,11 +56,14 @@ class EditorExportPlatformWeb : public EditorExportPlatform {
Mutex server_lock;
Thread server_thread;
- String _get_template_name(bool p_extension, bool p_debug) const {
+ String _get_template_name(bool p_extension, bool p_thread_support, bool p_debug) const {
String name = "web";
if (p_extension) {
name += "_dlink";
}
+ if (!p_thread_support) {
+ name += "_nothreads";
+ }
if (p_debug) {
name += "_debug.zip";
} else {
diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js
index b7c6c9d445..81bc82f3c6 100644
--- a/platform/web/js/engine/features.js
+++ b/platform/web/js/engine/features.js
@@ -72,8 +72,14 @@ const Features = { // eslint-disable-line no-unused-vars
*
* @returns {Array<string>} A list of human-readable missing features.
* @function Engine.getMissingFeatures
+ * @typedef {{ threads: boolean }} SupportedFeatures
+ * @param {SupportedFeatures} supportedFeatures
*/
- getMissingFeatures: function () {
+ getMissingFeatures: function (supportedFeatures = {}) {
+ const {
+ threads: supportsThreads = true,
+ } = supportedFeatures;
+
const missing = [];
if (!Features.isWebGLAvailable(2)) {
missing.push('WebGL2 - Check web browser configuration and hardware support');
@@ -84,12 +90,16 @@ const Features = { // eslint-disable-line no-unused-vars
if (!Features.isSecureContext()) {
missing.push('Secure Context - Check web server configuration (use HTTPS)');
}
- if (!Features.isCrossOriginIsolated()) {
- missing.push('Cross Origin Isolation - Check web server configuration (send correct headers)');
- }
- if (!Features.isSharedArrayBufferAvailable()) {
- missing.push('SharedArrayBuffer - Check web server configuration (send correct headers)');
+
+ if (supportsThreads) {
+ if (!Features.isCrossOriginIsolated()) {
+ missing.push('Cross-Origin Isolation - Check that the web server configuration sends the correct headers.');
+ }
+ if (!Features.isSharedArrayBufferAvailable()) {
+ missing.push('SharedArrayBuffer - Check that the web server configuration sends the correct headers.');
+ }
}
+
// Audio is normally optional since we have a dummy fallback.
return missing;
},
diff --git a/platform/web/js/libs/audio.worklet.js b/platform/web/js/libs/audio.worklet.js
index 89b581b3d6..3b94cab85c 100644
--- a/platform/web/js/libs/audio.worklet.js
+++ b/platform/web/js/libs/audio.worklet.js
@@ -167,7 +167,7 @@ class GodotProcessor extends AudioWorkletProcessor {
GodotProcessor.write_input(this.input_buffer, input);
this.input.write(this.input_buffer);
} else {
- this.port.postMessage('Input buffer is full! Skipping input frame.');
+ // this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
}
}
const process_output = GodotProcessor.array_has_data(outputs);
@@ -184,7 +184,7 @@ class GodotProcessor extends AudioWorkletProcessor {
this.port.postMessage({ 'cmd': 'read', 'data': chunk });
}
} else {
- this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
+ // this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
}
}
this.process_notify();
diff --git a/scene/2d/audio_stream_player_2d.cpp b/scene/2d/audio_stream_player_2d.cpp
index a99964c0e0..f6e6eb8b17 100644
--- a/scene/2d/audio_stream_player_2d.cpp
+++ b/scene/2d/audio_stream_player_2d.cpp
@@ -33,86 +33,48 @@
#include "core/config/project_settings.h"
#include "scene/2d/area_2d.h"
#include "scene/2d/audio_listener_2d.h"
-#include "scene/main/window.h"
+#include "scene/audio/audio_stream_player_internal.h"
+#include "scene/main/viewport.h"
#include "scene/resources/world_2d.h"
-
-#define PARAM_PREFIX "parameters/"
+#include "scene/scene_string_names.h"
+#include "servers/audio/audio_stream.h"
+#include "servers/audio_server.h"
void AudioStreamPlayer2D::_notification(int p_what) {
+ internal->notification(p_what);
+
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
AudioServer::get_singleton()->add_listener_changed_callback(_listener_changed_cb, this);
- if (autoplay && !Engine::get_singleton()->is_editor_hint()) {
- play();
- }
- set_stream_paused(!can_process());
} break;
case NOTIFICATION_EXIT_TREE: {
- set_stream_paused(true);
AudioServer::get_singleton()->remove_listener_changed_callback(_listener_changed_cb, this);
} break;
- case NOTIFICATION_PREDELETE: {
- stop();
- } break;
-
- case NOTIFICATION_PAUSED: {
- if (!can_process()) {
- // Node can't process so we start fading out to silence.
- set_stream_paused(true);
- }
- } break;
-
- case NOTIFICATION_UNPAUSED: {
- set_stream_paused(false);
- } break;
-
case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
// Update anything related to position first, if possible of course.
- if (setplay.get() > 0 || (active.is_set() && last_mix_count != AudioServer::get_singleton()->get_mix_count()) || force_update_panning) {
+ if (setplay.get() > 0 || (internal->active.is_set() && last_mix_count != AudioServer::get_singleton()->get_mix_count()) || force_update_panning) {
force_update_panning = false;
_update_panning();
}
if (setplayback.is_valid() && setplay.get() >= 0) {
- active.set();
- AudioServer::get_singleton()->start_playback_stream(setplayback, _get_actual_bus(), volume_vector, setplay.get(), pitch_scale);
+ internal->active.set();
+ AudioServer::get_singleton()->start_playback_stream(setplayback, _get_actual_bus(), volume_vector, setplay.get(), internal->pitch_scale);
setplayback.unref();
setplay.set(-1);
}
- if (!stream_playbacks.is_empty() && active.is_set()) {
- // Stop playing if no longer active.
- Vector<Ref<AudioStreamPlayback>> playbacks_to_remove;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (playback.is_valid() && !AudioServer::get_singleton()->is_playback_active(playback) && !AudioServer::get_singleton()->is_playback_paused(playback)) {
- playbacks_to_remove.push_back(playback);
- }
- }
- // Now go through and remove playbacks that have finished. Removing elements from a Vector in a range based for is asking for trouble.
- for (Ref<AudioStreamPlayback> &playback : playbacks_to_remove) {
- stream_playbacks.erase(playback);
- }
- if (!playbacks_to_remove.is_empty() && stream_playbacks.is_empty()) {
- // This node is no longer actively playing audio.
- active.clear();
- set_physics_process_internal(false);
- }
- if (!playbacks_to_remove.is_empty()) {
- emit_signal(SNAME("finished"));
- }
- }
-
- while (stream_playbacks.size() > max_polyphony) {
- AudioServer::get_singleton()->stop_playback_stream(stream_playbacks[0]);
- stream_playbacks.remove_at(0);
+ if (!internal->stream_playbacks.is_empty() && internal->active.is_set()) {
+ internal->process();
}
+ internal->ensure_playback_limit();
} break;
}
}
-// Interacts with PhysicsServer2D, so can only be called during _physics_process
+// Interacts with PhysicsServer2D, so can only be called during _physics_process.
StringName AudioStreamPlayer2D::_get_actual_bus() {
Vector2 global_pos = get_global_position();
@@ -144,12 +106,12 @@ StringName AudioStreamPlayer2D::_get_actual_bus() {
return area2d->get_audio_bus_name();
}
- return default_bus;
+ return internal->bus;
}
// Interacts with PhysicsServer2D, so can only be called during _physics_process
void AudioStreamPlayer2D::_update_panning() {
- if (!active.is_set() || stream.is_null()) {
+ if (!internal->active.is_set() || internal->stream.is_null()) {
return;
}
@@ -194,7 +156,7 @@ void AudioStreamPlayer2D::_update_panning() {
}
float multiplier = Math::pow(1.0f - dist / max_distance, attenuation);
- multiplier *= Math::db_to_linear(volume_db); // Also apply player volume!
+ multiplier *= Math::db_to_linear(internal->volume_db); // Also apply player volume!
float pan = relative_to_listener.x / screen_size.x;
// Don't let the panning effect extend (too far) beyond the screen.
@@ -213,178 +175,96 @@ void AudioStreamPlayer2D::_update_panning() {
volume_vector.write[0] = AudioFrame(MAX(prev_sample[0], new_sample[0]), MAX(prev_sample[1], new_sample[1]));
}
- for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ for (const Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
AudioServer::get_singleton()->set_playback_bus_exclusive(playback, _get_actual_bus(), volume_vector);
}
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->set_playback_pitch_scale(playback, pitch_scale);
+ for (Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
+ AudioServer::get_singleton()->set_playback_pitch_scale(playback, internal->pitch_scale);
}
last_mix_count = AudioServer::get_singleton()->get_mix_count();
}
-void AudioStreamPlayer2D::_update_stream_parameters() {
- if (stream.is_null()) {
- return;
- }
-
- List<AudioStream::Parameter> parameters;
- stream->get_parameter_list(&parameters);
- for (const AudioStream::Parameter &K : parameters) {
- const PropertyInfo &pi = K.property;
- StringName key = PARAM_PREFIX + pi.name;
- if (!playback_parameters.has(key)) {
- ParameterData pd;
- pd.path = pi.name;
- pd.value = K.default_value;
- playback_parameters.insert(key, pd);
- }
- }
-}
-
void AudioStreamPlayer2D::set_stream(Ref<AudioStream> p_stream) {
- if (stream.is_valid()) {
- stream->disconnect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayer2D::_update_stream_parameters));
- }
- stop();
- stream = p_stream;
- _update_stream_parameters();
- if (stream.is_valid()) {
- stream->connect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayer2D::_update_stream_parameters));
- }
- notify_property_list_changed();
+ internal->set_stream(p_stream);
}
Ref<AudioStream> AudioStreamPlayer2D::get_stream() const {
- return stream;
+ return internal->stream;
}
void AudioStreamPlayer2D::set_volume_db(float p_volume) {
- volume_db = p_volume;
+ internal->volume_db = p_volume;
}
float AudioStreamPlayer2D::get_volume_db() const {
- return volume_db;
+ return internal->volume_db;
}
void AudioStreamPlayer2D::set_pitch_scale(float p_pitch_scale) {
- ERR_FAIL_COND(!(p_pitch_scale > 0.0));
- pitch_scale = p_pitch_scale;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->set_playback_pitch_scale(playback, p_pitch_scale);
- }
+ internal->set_pitch_scale(p_pitch_scale);
}
float AudioStreamPlayer2D::get_pitch_scale() const {
- return pitch_scale;
+ return internal->pitch_scale;
}
void AudioStreamPlayer2D::play(float p_from_pos) {
- if (stream.is_null()) {
+ Ref<AudioStreamPlayback> stream_playback = internal->play_basic();
+ if (stream_playback.is_null()) {
return;
}
- ERR_FAIL_COND_MSG(!is_inside_tree(), "Playback can only happen when a node is inside the scene tree");
- if (stream->is_monophonic() && is_playing()) {
- stop();
- }
- Ref<AudioStreamPlayback> stream_playback = stream->instantiate_playback();
- ERR_FAIL_COND_MSG(stream_playback.is_null(), "Failed to instantiate playback.");
-
- for (const KeyValue<StringName, ParameterData> &K : playback_parameters) {
- stream_playback->set_parameter(K.value.path, K.value.value);
- }
-
- stream_playbacks.push_back(stream_playback);
setplayback = stream_playback;
setplay.set(p_from_pos);
- active.set();
- set_physics_process_internal(true);
}
void AudioStreamPlayer2D::seek(float p_seconds) {
- if (is_playing()) {
- stop();
- play(p_seconds);
- }
+ internal->seek(p_seconds);
}
void AudioStreamPlayer2D::stop() {
setplay.set(-1);
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->stop_playback_stream(playback);
- }
- stream_playbacks.clear();
- active.clear();
- set_physics_process_internal(false);
+ internal->stop();
}
bool AudioStreamPlayer2D::is_playing() const {
- for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (AudioServer::get_singleton()->is_playback_active(playback)) {
- return true;
- }
- }
if (setplay.get() >= 0) {
return true; // play() has been called this frame, but no playback exists just yet.
}
- return false;
+ return internal->is_playing();
}
float AudioStreamPlayer2D::get_playback_position() {
- // Return the playback position of the most recently started playback stream.
- if (!stream_playbacks.is_empty()) {
- return AudioServer::get_singleton()->get_playback_position(stream_playbacks[stream_playbacks.size() - 1]);
- }
- return 0;
+ return internal->get_playback_position();
}
void AudioStreamPlayer2D::set_bus(const StringName &p_bus) {
- default_bus = p_bus; // This will be pushed to the audio server during the next physics timestep, which is fast enough.
+ internal->bus = p_bus; // This will be pushed to the audio server during the next physics timestep, which is fast enough.
}
StringName AudioStreamPlayer2D::get_bus() const {
- for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
- if (AudioServer::get_singleton()->get_bus_name(i) == default_bus) {
- return default_bus;
- }
- }
- return SceneStringNames::get_singleton()->Master;
+ return internal->get_bus();
}
void AudioStreamPlayer2D::set_autoplay(bool p_enable) {
- autoplay = p_enable;
+ internal->autoplay = p_enable;
}
bool AudioStreamPlayer2D::is_autoplay_enabled() {
- return autoplay;
+ return internal->autoplay;
}
void AudioStreamPlayer2D::_set_playing(bool p_enable) {
- if (p_enable) {
- play();
- } else {
- stop();
- }
+ internal->set_playing(p_enable);
}
bool AudioStreamPlayer2D::_is_active() const {
- return active.is_set();
+ return internal->is_active();
}
void AudioStreamPlayer2D::_validate_property(PropertyInfo &p_property) const {
- if (p_property.name == "bus") {
- String options;
- for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
- if (i > 0) {
- options += ",";
- }
- String name = AudioServer::get_singleton()->get_bus_name(i);
- options += name;
- }
-
- p_property.hint_string = options;
- }
+ internal->validate_property(p_property);
}
void AudioStreamPlayer2D::set_max_distance(float p_pixels) {
@@ -413,37 +293,27 @@ uint32_t AudioStreamPlayer2D::get_area_mask() const {
}
void AudioStreamPlayer2D::set_stream_paused(bool p_pause) {
- // TODO this does not have perfect recall, fix that maybe? If there are zero playbacks registered with the AudioServer, this bool isn't persisted.
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->set_playback_paused(playback, p_pause);
- }
+ internal->set_stream_paused(p_pause);
}
bool AudioStreamPlayer2D::get_stream_paused() const {
- // There's currently no way to pause some playback streams but not others. Check the first and don't bother looking at the rest.
- if (!stream_playbacks.is_empty()) {
- return AudioServer::get_singleton()->is_playback_paused(stream_playbacks[0]);
- }
- return false;
+ return internal->get_stream_paused();
}
bool AudioStreamPlayer2D::has_stream_playback() {
- return !stream_playbacks.is_empty();
+ return internal->has_stream_playback();
}
Ref<AudioStreamPlayback> AudioStreamPlayer2D::get_stream_playback() {
- ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback().");
- return stream_playbacks[stream_playbacks.size() - 1];
+ return internal->get_stream_playback();
}
void AudioStreamPlayer2D::set_max_polyphony(int p_max_polyphony) {
- if (p_max_polyphony > 0) {
- max_polyphony = p_max_polyphony;
- }
+ internal->set_max_polyphony(p_max_polyphony);
}
int AudioStreamPlayer2D::get_max_polyphony() const {
- return max_polyphony;
+ return internal->max_polyphony;
}
void AudioStreamPlayer2D::set_panning_strength(float p_panning_strength) {
@@ -455,48 +325,16 @@ float AudioStreamPlayer2D::get_panning_strength() const {
return panning_strength;
}
-void AudioStreamPlayer2D::_on_bus_layout_changed() {
- notify_property_list_changed();
-}
-
-void AudioStreamPlayer2D::_on_bus_renamed(int p_bus_index, const StringName &p_old_name, const StringName &p_new_name) {
- notify_property_list_changed();
-}
-
bool AudioStreamPlayer2D::_set(const StringName &p_name, const Variant &p_value) {
- HashMap<StringName, ParameterData>::Iterator I = playback_parameters.find(p_name);
- if (!I) {
- return false;
- }
- ParameterData &pd = I->value;
- pd.value = p_value;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- playback->set_parameter(pd.path, pd.value);
- }
- return true;
+ return internal->set(p_name, p_value);
}
bool AudioStreamPlayer2D::_get(const StringName &p_name, Variant &r_ret) const {
- HashMap<StringName, ParameterData>::ConstIterator I = playback_parameters.find(p_name);
- if (!I) {
- return false;
- }
-
- r_ret = I->value.value;
- return true;
+ return internal->get(p_name, r_ret);
}
void AudioStreamPlayer2D::_get_property_list(List<PropertyInfo> *p_list) const {
- if (stream.is_null()) {
- return;
- }
- List<AudioStream::Parameter> parameters;
- stream->get_parameter_list(&parameters);
- for (const AudioStream::Parameter &K : parameters) {
- PropertyInfo pi = K.property;
- pi.name = PARAM_PREFIX + pi.name;
- p_list->push_back(pi);
- }
+ internal->get_property_list(p_list);
}
void AudioStreamPlayer2D::_bind_methods() {
@@ -563,11 +401,11 @@ void AudioStreamPlayer2D::_bind_methods() {
}
AudioStreamPlayer2D::AudioStreamPlayer2D() {
- AudioServer::get_singleton()->connect("bus_layout_changed", callable_mp(this, &AudioStreamPlayer2D::_on_bus_layout_changed));
- AudioServer::get_singleton()->connect("bus_renamed", callable_mp(this, &AudioStreamPlayer2D::_on_bus_renamed));
+ internal = memnew(AudioStreamPlayerInternal(this, callable_mp(this, &AudioStreamPlayer2D::play), true));
cached_global_panning_strength = GLOBAL_GET("audio/general/2d_panning_strength");
set_hide_clip_children(true);
}
AudioStreamPlayer2D::~AudioStreamPlayer2D() {
+ memdelete(internal);
}
diff --git a/scene/2d/audio_stream_player_2d.h b/scene/2d/audio_stream_player_2d.h
index 267d6a625b..3552735cd7 100644
--- a/scene/2d/audio_stream_player_2d.h
+++ b/scene/2d/audio_stream_player_2d.h
@@ -32,9 +32,11 @@
#define AUDIO_STREAM_PLAYER_2D_H
#include "scene/2d/node_2d.h"
-#include "scene/scene_string_names.h"
-#include "servers/audio/audio_stream.h"
-#include "servers/audio_server.h"
+
+struct AudioFrame;
+class AudioStream;
+class AudioStreamPlayback;
+class AudioStreamPlayerInternal;
class AudioStreamPlayer2D : public Node2D {
GDCLASS(AudioStreamPlayer2D, Node2D);
@@ -52,10 +54,8 @@ private:
Viewport *viewport = nullptr; //pointer only used for reference to previous mix
};
- Vector<Ref<AudioStreamPlayback>> stream_playbacks;
- Ref<AudioStream> stream;
+ AudioStreamPlayerInternal *internal = nullptr;
- SafeFlag active{ false };
SafeNumeric<float> setplay{ -1.0 };
Ref<AudioStreamPlayback> setplayback;
@@ -64,21 +64,12 @@ private:
uint64_t last_mix_count = -1;
bool force_update_panning = false;
- float volume_db = 0.0;
- float pitch_scale = 1.0;
- bool autoplay = false;
- StringName default_bus = SceneStringNames::get_singleton()->Master;
- int max_polyphony = 1;
-
void _set_playing(bool p_enable);
bool _is_active() const;
StringName _get_actual_bus();
void _update_panning();
- void _on_bus_layout_changed();
- void _on_bus_renamed(int p_bus_index, const StringName &p_old_name, const StringName &p_new_name);
-
static void _listener_changed_cb(void *self) { reinterpret_cast<AudioStreamPlayer2D *>(self)->force_update_panning = true; }
uint32_t area_mask = 1;
@@ -89,14 +80,6 @@ private:
float panning_strength = 1.0f;
float cached_global_panning_strength = 0.5f;
- struct ParameterData {
- StringName path;
- Variant value;
- };
-
- HashMap<StringName, ParameterData> playback_parameters;
- void _update_stream_parameters();
-
protected:
void _validate_property(PropertyInfo &p_property) const;
void _notification(int p_what);
diff --git a/scene/2d/tile_map.compat.inc b/scene/2d/tile_map.compat.inc
index ed216173c7..04937bdf7e 100644
--- a/scene/2d/tile_map.compat.inc
+++ b/scene/2d/tile_map.compat.inc
@@ -42,10 +42,20 @@ int TileMap::_get_quadrant_size_compat_81070() const {
return get_rendering_quadrant_size();
}
+TileMap::VisibilityMode TileMap::_get_collision_visibility_mode_bind_compat_87115() {
+ return get_collision_visibility_mode();
+}
+
+TileMap::VisibilityMode TileMap::_get_navigation_visibility_mode_bind_compat_87115() {
+ return get_navigation_visibility_mode();
+}
+
void TileMap::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("get_used_rect"), &TileMap::_get_used_rect_bind_compat_78328);
ClassDB::bind_compatibility_method(D_METHOD("set_quadrant_size", "quadrant_size"), &TileMap::_set_quadrant_size_compat_81070);
ClassDB::bind_compatibility_method(D_METHOD("get_quadrant_size"), &TileMap::_get_quadrant_size_compat_81070);
+ ClassDB::bind_compatibility_method(D_METHOD("get_collision_visibility_mode"), &TileMap::_get_collision_visibility_mode_bind_compat_87115);
+ ClassDB::bind_compatibility_method(D_METHOD("get_navigation_visibility_mode"), &TileMap::_get_navigation_visibility_mode_bind_compat_87115);
}
#endif
diff --git a/scene/2d/tile_map.cpp b/scene/2d/tile_map.cpp
index decbbab476..24eefce99d 100644
--- a/scene/2d/tile_map.cpp
+++ b/scene/2d/tile_map.cpp
@@ -167,7 +167,7 @@ void TileMap::set_selected_layer(int p_layer_id) {
emit_signal(CoreStringNames::get_singleton()->changed);
// Update the layers modulation.
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_SELECTED_LAYER);
}
}
@@ -178,45 +178,9 @@ int TileMap::get_selected_layer() const {
void TileMap::_notification(int p_what) {
switch (p_what) {
- case NOTIFICATION_ENTER_TREE: {
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_IN_TREE);
- }
- } break;
-
- case NOTIFICATION_EXIT_TREE: {
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_IN_TREE);
- }
- } break;
-
- case TileMap::NOTIFICATION_ENTER_CANVAS: {
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_IN_CANVAS);
- }
- } break;
-
- case TileMap::NOTIFICATION_EXIT_CANVAS: {
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_IN_CANVAS);
- }
- } break;
-
- case NOTIFICATION_DRAW: {
- // Rendering.
- if (tile_set.is_valid()) {
- RenderingServer::get_singleton()->canvas_item_set_sort_children_by_y(get_canvas_item(), is_y_sort_enabled());
- }
- } break;
-
- case TileMap::NOTIFICATION_VISIBILITY_CHANGED: {
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_VISIBILITY);
- }
- } break;
-
case TileMap::NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
- // Physics.
+ // This is only executed when collision_animatable is enabled.
+
bool in_editor = false;
#ifdef TOOLS_ENABLED
in_editor = Engine::get_singleton()->is_editor_hint();
@@ -224,39 +188,30 @@ void TileMap::_notification(int p_what) {
if (is_inside_tree() && collision_animatable && !in_editor) {
// Update transform on the physics tick when in animatable mode.
last_valid_transform = new_transform;
+ print_line("Physics: ", new_transform);
set_notify_local_transform(false);
set_global_transform(new_transform);
- _update_notify_local_transform();
- }
- } break;
-
- case NOTIFICATION_TRANSFORM_CHANGED: {
- // Physics.
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_XFORM);
+ set_notify_local_transform(true);
}
} break;
case TileMap::NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_LOCAL_XFORM);
- }
+ // This is only executed when collision_animatable is enabled.
- // Physics.
bool in_editor = false;
#ifdef TOOLS_ENABLED
in_editor = Engine::get_singleton()->is_editor_hint();
#endif
- // Only active when animatable. Send the new transform to the physics...
if (is_inside_tree() && collision_animatable && !in_editor) {
// Store last valid transform.
new_transform = get_global_transform();
+ print_line("local XFORM: ", last_valid_transform);
// ... but then revert changes.
set_notify_local_transform(false);
set_global_transform(last_valid_transform);
- _update_notify_local_transform();
+ set_notify_local_transform(true);
}
} break;
}
@@ -285,7 +240,7 @@ void TileMap::_internal_update() {
}
// Update dirty quadrants on layers.
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->internal_update();
}
@@ -308,7 +263,7 @@ void TileMap::set_tileset(const Ref<TileSet> &p_tileset) {
tile_set->connect_changed(callable_mp(this, &TileMap::_tile_set_changed));
}
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_TILE_SET);
}
@@ -323,7 +278,7 @@ void TileMap::set_rendering_quadrant_size(int p_size) {
ERR_FAIL_COND_MSG(p_size < 1, "TileMapQuadrant size cannot be smaller than 1.");
rendering_quadrant_size = p_size;
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_QUADRANT_SIZE);
}
emit_signal(CoreStringNames::get_singleton()->changed);
@@ -429,10 +384,11 @@ void TileMap::add_layer(int p_to_pos) {
ERR_FAIL_INDEX(p_to_pos, (int)layers.size() + 1);
// Must clear before adding the layer.
- Ref<TileMapLayer> new_layer;
- new_layer.instantiate();
- new_layer->set_tile_map(this);
+ TileMapLayer *new_layer = memnew(TileMapLayer);
layers.insert(p_to_pos, new_layer);
+ add_child(new_layer);
+ new_layer->set_name(vformat("Layer%d", p_to_pos));
+ move_child(new_layer, p_to_pos);
for (uint32_t i = 0; i < layers.size(); i++) {
layers[i]->set_layer_index_in_tile_map_node(i);
}
@@ -449,10 +405,11 @@ void TileMap::move_layer(int p_layer, int p_to_pos) {
ERR_FAIL_INDEX(p_to_pos, (int)layers.size() + 1);
// Clear before shuffling layers.
- Ref<TileMapLayer> layer = layers[p_layer];
+ TileMapLayer *layer = layers[p_layer];
layers.insert(p_to_pos, layer);
layers.remove_at(p_to_pos < p_layer ? p_layer + 1 : p_layer);
for (uint32_t i = 0; i < layers.size(); i++) {
+ move_child(layer, i);
layers[i]->set_layer_index_in_tile_map_node(i);
}
queue_internal_update();
@@ -471,6 +428,7 @@ void TileMap::remove_layer(int p_layer) {
ERR_FAIL_INDEX(p_layer, (int)layers.size());
// Clear before removing the layer.
+ layers[p_layer]->queue_free();
layers.remove_at(p_layer);
for (uint32_t i = 0; i < layers.size(); i++) {
layers[i]->set_layer_index_in_tile_map_node(i);
@@ -513,7 +471,6 @@ Color TileMap::get_layer_modulate(int p_layer) const {
void TileMap::set_layer_y_sort_enabled(int p_layer, bool p_y_sort_enabled) {
TILEMAP_CALL_FOR_LAYER(p_layer, set_y_sort_enabled, p_y_sort_enabled);
- _update_notify_local_transform();
}
bool TileMap::is_layer_y_sort_enabled(int p_layer) const {
@@ -552,17 +509,16 @@ RID TileMap::get_layer_navigation_map(int p_layer) const {
TILEMAP_CALL_FOR_LAYER_V(p_layer, RID(), get_navigation_map);
}
-void TileMap::set_collision_animatable(bool p_enabled) {
- if (collision_animatable == p_enabled) {
+void TileMap::set_collision_animatable(bool p_collision_animatable) {
+ if (collision_animatable == p_collision_animatable) {
return;
}
- collision_animatable = p_enabled;
- _update_notify_local_transform();
- set_physics_process_internal(p_enabled);
- for (Ref<TileMapLayer> &layer : layers) {
- layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_COLLISION_ANIMATABLE);
+ collision_animatable = p_collision_animatable;
+ set_notify_local_transform(p_collision_animatable);
+ set_physics_process_internal(p_collision_animatable);
+ for (TileMapLayer *layer : layers) {
+ layer->set_use_kinematic_bodies(layer);
}
- emit_signal(CoreStringNames::get_singleton()->changed);
}
bool TileMap::is_collision_animatable() const {
@@ -574,13 +530,13 @@ void TileMap::set_collision_visibility_mode(TileMap::VisibilityMode p_show_colli
return;
}
collision_visibility_mode = p_show_collision;
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_COLLISION_VISIBILITY_MODE);
}
emit_signal(CoreStringNames::get_singleton()->changed);
}
-TileMap::VisibilityMode TileMap::get_collision_visibility_mode() {
+TileMap::VisibilityMode TileMap::get_collision_visibility_mode() const {
return collision_visibility_mode;
}
@@ -589,13 +545,13 @@ void TileMap::set_navigation_visibility_mode(TileMap::VisibilityMode p_show_navi
return;
}
navigation_visibility_mode = p_show_navigation;
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_NAVIGATION_VISIBILITY_MODE);
}
emit_signal(CoreStringNames::get_singleton()->changed);
}
-TileMap::VisibilityMode TileMap::get_navigation_visibility_mode() {
+TileMap::VisibilityMode TileMap::get_navigation_visibility_mode() const {
return navigation_visibility_mode;
}
@@ -604,7 +560,7 @@ void TileMap::set_y_sort_enabled(bool p_enable) {
return;
}
Node2D::set_y_sort_enabled(p_enable);
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_Y_SORT_ENABLED);
}
emit_signal(CoreStringNames::get_singleton()->changed);
@@ -640,27 +596,8 @@ Ref<TileMapPattern> TileMap::get_pattern(int p_layer, TypedArray<Vector2i> p_coo
}
Vector2i TileMap::map_pattern(const Vector2i &p_position_in_tilemap, const Vector2i &p_coords_in_pattern, Ref<TileMapPattern> p_pattern) {
- ERR_FAIL_COND_V(p_pattern.is_null(), Vector2i());
- ERR_FAIL_COND_V(!p_pattern->has_cell(p_coords_in_pattern), Vector2i());
-
- Vector2i output = p_position_in_tilemap + p_coords_in_pattern;
- if (tile_set->get_tile_shape() != TileSet::TILE_SHAPE_SQUARE) {
- if (tile_set->get_tile_layout() == TileSet::TILE_LAYOUT_STACKED) {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL && bool(p_position_in_tilemap.y % 2) && bool(p_coords_in_pattern.y % 2)) {
- output.x += 1;
- } else if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_VERTICAL && bool(p_position_in_tilemap.x % 2) && bool(p_coords_in_pattern.x % 2)) {
- output.y += 1;
- }
- } else if (tile_set->get_tile_layout() == TileSet::TILE_LAYOUT_STACKED_OFFSET) {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL && bool(p_position_in_tilemap.y % 2) && bool(p_coords_in_pattern.y % 2)) {
- output.x -= 1;
- } else if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_VERTICAL && bool(p_position_in_tilemap.x % 2) && bool(p_coords_in_pattern.x % 2)) {
- output.y -= 1;
- }
- }
- }
-
- return output;
+ ERR_FAIL_COND_V(!tile_set.is_valid(), Vector2i());
+ return tile_set->map_pattern(p_position_in_tilemap, p_coords_in_pattern, p_pattern);
}
void TileMap::set_pattern(int p_layer, const Vector2i &p_position, const Ref<TileMapPattern> p_pattern) {
@@ -700,7 +637,7 @@ TileMapCell TileMap::get_cell(int p_layer, const Vector2i &p_coords, bool p_use_
}
Vector2i TileMap::get_coords_for_body_rid(RID p_physics_body) {
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
if (layer->has_body_rid(p_physics_body)) {
return layer->get_coords_for_body_rid(p_physics_body);
}
@@ -718,7 +655,7 @@ int TileMap::get_layer_for_body_rid(RID p_physics_body) {
}
void TileMap::fix_invalid_tiles() {
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->fix_invalid_tiles();
}
}
@@ -728,7 +665,7 @@ void TileMap::clear_layer(int p_layer) {
}
void TileMap::clear() {
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->clear();
}
}
@@ -742,7 +679,7 @@ void TileMap::notify_runtime_tile_data_update(int p_layer) {
if (p_layer >= 0) {
TILEMAP_CALL_FOR_LAYER(p_layer, notify_tile_map_change, TileMapLayer::DIRTY_FLAGS_TILE_MAP_RUNTIME_UPDATE);
} else {
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_RUNTIME_UPDATE);
}
}
@@ -780,9 +717,9 @@ bool TileMap::_set(const StringName &p_name, const Variant &p_value) {
else if (p_name == "tile_data") { // Kept for compatibility reasons.
if (p_value.is_array()) {
if (layers.size() == 0) {
- Ref<TileMapLayer> new_layer;
- new_layer.instantiate();
- new_layer->set_tile_map(this);
+ TileMapLayer *new_layer = memnew(TileMapLayer);
+ add_child(new_layer);
+ new_layer->set_name("Layer0");
new_layer->set_layer_index_in_tile_map_node(0);
layers.push_back(new_layer);
}
@@ -804,9 +741,9 @@ bool TileMap::_set(const StringName &p_name, const Variant &p_value) {
if (index >= (int)layers.size()) {
while (index >= (int)layers.size()) {
- Ref<TileMapLayer> new_layer;
- new_layer.instantiate();
- new_layer->set_tile_map(this);
+ TileMapLayer *new_layer = memnew(TileMapLayer);
+ add_child(new_layer);
+ new_layer->set_name(vformat("Layer%d", index));
new_layer->set_layer_index_in_tile_map_node(index);
layers.push_back(new_layer);
}
@@ -985,623 +922,23 @@ bool TileMap::_property_get_revert(const StringName &p_name, Variant &r_property
}
Vector2 TileMap::map_to_local(const Vector2i &p_pos) const {
- // SHOULD RETURN THE CENTER OF THE CELL.
ERR_FAIL_COND_V(!tile_set.is_valid(), Vector2());
-
- Vector2 ret = p_pos;
- TileSet::TileShape tile_shape = tile_set->get_tile_shape();
- TileSet::TileOffsetAxis tile_offset_axis = tile_set->get_tile_offset_axis();
-
- if (tile_shape == TileSet::TILE_SHAPE_HALF_OFFSET_SQUARE || tile_shape == TileSet::TILE_SHAPE_HEXAGON || tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- // Technically, those 3 shapes are equivalent, as they are basically half-offset, but with different levels or overlap.
- // square = no overlap, hexagon = 0.25 overlap, isometric = 0.5 overlap.
- if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- switch (tile_set->get_tile_layout()) {
- case TileSet::TILE_LAYOUT_STACKED:
- ret = Vector2(ret.x + (Math::posmod(ret.y, 2) == 0 ? 0.0 : 0.5), ret.y);
- break;
- case TileSet::TILE_LAYOUT_STACKED_OFFSET:
- ret = Vector2(ret.x + (Math::posmod(ret.y, 2) == 1 ? 0.0 : 0.5), ret.y);
- break;
- case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
- ret = Vector2(ret.x + ret.y / 2, ret.y);
- break;
- case TileSet::TILE_LAYOUT_STAIRS_DOWN:
- ret = Vector2(ret.x / 2, ret.y * 2 + ret.x);
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
- ret = Vector2((ret.x + ret.y) / 2, ret.y - ret.x);
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
- ret = Vector2((ret.x - ret.y) / 2, ret.y + ret.x);
- break;
- }
- } else { // TILE_OFFSET_AXIS_VERTICAL.
- switch (tile_set->get_tile_layout()) {
- case TileSet::TILE_LAYOUT_STACKED:
- ret = Vector2(ret.x, ret.y + (Math::posmod(ret.x, 2) == 0 ? 0.0 : 0.5));
- break;
- case TileSet::TILE_LAYOUT_STACKED_OFFSET:
- ret = Vector2(ret.x, ret.y + (Math::posmod(ret.x, 2) == 1 ? 0.0 : 0.5));
- break;
- case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
- ret = Vector2(ret.x * 2 + ret.y, ret.y / 2);
- break;
- case TileSet::TILE_LAYOUT_STAIRS_DOWN:
- ret = Vector2(ret.x, ret.y + ret.x / 2);
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
- ret = Vector2(ret.x + ret.y, (ret.y - ret.x) / 2);
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
- ret = Vector2(ret.x - ret.y, (ret.y + ret.x) / 2);
- break;
- }
- }
- }
-
- // Multiply by the overlapping ratio.
- double overlapping_ratio = 1.0;
- if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- overlapping_ratio = 0.5;
- } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
- overlapping_ratio = 0.75;
- }
- ret.y *= overlapping_ratio;
- } else { // TILE_OFFSET_AXIS_VERTICAL.
- if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- overlapping_ratio = 0.5;
- } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
- overlapping_ratio = 0.75;
- }
- ret.x *= overlapping_ratio;
- }
-
- return (ret + Vector2(0.5, 0.5)) * tile_set->get_tile_size();
+ return tile_set->map_to_local(p_pos);
}
-Vector2i TileMap::local_to_map(const Vector2 &p_local_position) const {
+Vector2i TileMap::local_to_map(const Vector2 &p_pos) const {
ERR_FAIL_COND_V(!tile_set.is_valid(), Vector2i());
-
- Vector2 ret = p_local_position;
- ret /= tile_set->get_tile_size();
-
- TileSet::TileShape tile_shape = tile_set->get_tile_shape();
- TileSet::TileOffsetAxis tile_offset_axis = tile_set->get_tile_offset_axis();
- TileSet::TileLayout tile_layout = tile_set->get_tile_layout();
-
- // Divide by the overlapping ratio.
- double overlapping_ratio = 1.0;
- if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- overlapping_ratio = 0.5;
- } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
- overlapping_ratio = 0.75;
- }
- ret.y /= overlapping_ratio;
- } else { // TILE_OFFSET_AXIS_VERTICAL.
- if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- overlapping_ratio = 0.5;
- } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
- overlapping_ratio = 0.75;
- }
- ret.x /= overlapping_ratio;
- }
-
- // For each half-offset shape, we check if we are in the corner of the tile, and thus should correct the local position accordingly.
- if (tile_shape == TileSet::TILE_SHAPE_HALF_OFFSET_SQUARE || tile_shape == TileSet::TILE_SHAPE_HEXAGON || tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- // Technically, those 3 shapes are equivalent, as they are basically half-offset, but with different levels or overlap.
- // square = no overlap, hexagon = 0.25 overlap, isometric = 0.5 overlap.
- if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- // Smart floor of the position
- Vector2 raw_pos = ret;
- if (Math::posmod(Math::floor(ret.y), 2) ^ (tile_layout == TileSet::TILE_LAYOUT_STACKED_OFFSET)) {
- ret = Vector2(Math::floor(ret.x + 0.5) - 0.5, Math::floor(ret.y));
- } else {
- ret = ret.floor();
- }
-
- // Compute the tile offset, and if we might the output for a neighbor top tile.
- Vector2 in_tile_pos = raw_pos - ret;
- bool in_top_left_triangle = (in_tile_pos - Vector2(0.5, 0.0)).cross(Vector2(-0.5, 1.0 / overlapping_ratio - 1)) <= 0;
- bool in_top_right_triangle = (in_tile_pos - Vector2(0.5, 0.0)).cross(Vector2(0.5, 1.0 / overlapping_ratio - 1)) > 0;
-
- switch (tile_layout) {
- case TileSet::TILE_LAYOUT_STACKED:
- ret = ret.floor();
- if (in_top_left_triangle) {
- ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? 0 : -1, -1);
- } else if (in_top_right_triangle) {
- ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? 1 : 0, -1);
- }
- break;
- case TileSet::TILE_LAYOUT_STACKED_OFFSET:
- ret = ret.floor();
- if (in_top_left_triangle) {
- ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? -1 : 0, -1);
- } else if (in_top_right_triangle) {
- ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? 0 : 1, -1);
- }
- break;
- case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
- ret = Vector2(ret.x - ret.y / 2, ret.y).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(0, -1);
- } else if (in_top_right_triangle) {
- ret += Vector2i(1, -1);
- }
- break;
- case TileSet::TILE_LAYOUT_STAIRS_DOWN:
- ret = Vector2(ret.x * 2, ret.y / 2 - ret.x).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(-1, 0);
- } else if (in_top_right_triangle) {
- ret += Vector2i(1, -1);
- }
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
- ret = Vector2(ret.x - ret.y / 2, ret.y / 2 + ret.x).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(0, -1);
- } else if (in_top_right_triangle) {
- ret += Vector2i(1, 0);
- }
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
- ret = Vector2(ret.x + ret.y / 2, ret.y / 2 - ret.x).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(-1, 0);
- } else if (in_top_right_triangle) {
- ret += Vector2i(0, -1);
- }
- break;
- }
- } else { // TILE_OFFSET_AXIS_VERTICAL.
- // Smart floor of the position.
- Vector2 raw_pos = ret;
- if (Math::posmod(Math::floor(ret.x), 2) ^ (tile_layout == TileSet::TILE_LAYOUT_STACKED_OFFSET)) {
- ret = Vector2(Math::floor(ret.x), Math::floor(ret.y + 0.5) - 0.5);
- } else {
- ret = ret.floor();
- }
-
- // Compute the tile offset, and if we might the output for a neighbor top tile.
- Vector2 in_tile_pos = raw_pos - ret;
- bool in_top_left_triangle = (in_tile_pos - Vector2(0.0, 0.5)).cross(Vector2(1.0 / overlapping_ratio - 1, -0.5)) > 0;
- bool in_bottom_left_triangle = (in_tile_pos - Vector2(0.0, 0.5)).cross(Vector2(1.0 / overlapping_ratio - 1, 0.5)) <= 0;
-
- switch (tile_layout) {
- case TileSet::TILE_LAYOUT_STACKED:
- ret = ret.floor();
- if (in_top_left_triangle) {
- ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? 0 : -1);
- } else if (in_bottom_left_triangle) {
- ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? 1 : 0);
- }
- break;
- case TileSet::TILE_LAYOUT_STACKED_OFFSET:
- ret = ret.floor();
- if (in_top_left_triangle) {
- ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? -1 : 0);
- } else if (in_bottom_left_triangle) {
- ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? 0 : 1);
- }
- break;
- case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
- ret = Vector2(ret.x / 2 - ret.y, ret.y * 2).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(0, -1);
- } else if (in_bottom_left_triangle) {
- ret += Vector2i(-1, 1);
- }
- break;
- case TileSet::TILE_LAYOUT_STAIRS_DOWN:
- ret = Vector2(ret.x, ret.y - ret.x / 2).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(-1, 0);
- } else if (in_bottom_left_triangle) {
- ret += Vector2i(-1, 1);
- }
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
- ret = Vector2(ret.x / 2 - ret.y, ret.y + ret.x / 2).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(0, -1);
- } else if (in_bottom_left_triangle) {
- ret += Vector2i(-1, 0);
- }
- break;
- case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
- ret = Vector2(ret.x / 2 + ret.y, ret.y - ret.x / 2).floor();
- if (in_top_left_triangle) {
- ret += Vector2i(-1, 0);
- } else if (in_bottom_left_triangle) {
- ret += Vector2i(0, 1);
- }
- break;
- }
- }
- } else {
- ret = (ret + Vector2(0.00005, 0.00005)).floor();
- }
- return Vector2i(ret);
+ return tile_set->local_to_map(p_pos);
}
bool TileMap::is_existing_neighbor(TileSet::CellNeighbor p_cell_neighbor) const {
ERR_FAIL_COND_V(!tile_set.is_valid(), false);
-
- TileSet::TileShape shape = tile_set->get_tile_shape();
- if (shape == TileSet::TILE_SHAPE_SQUARE) {
- return p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER;
-
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC) {
- return p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
- } else {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- return p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
- } else {
- return p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE ||
- p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
- }
- }
+ return tile_set->is_existing_neighbor(p_cell_neighbor);
}
Vector2i TileMap::get_neighbor_cell(const Vector2i &p_coords, TileSet::CellNeighbor p_cell_neighbor) const {
- ERR_FAIL_COND_V(!tile_set.is_valid(), p_coords);
-
- TileSet::TileShape shape = tile_set->get_tile_shape();
- if (shape == TileSet::TILE_SHAPE_SQUARE) {
- switch (p_cell_neighbor) {
- case TileSet::CELL_NEIGHBOR_RIGHT_SIDE:
- return p_coords + Vector2i(1, 0);
- case TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER:
- return p_coords + Vector2i(1, 1);
- case TileSet::CELL_NEIGHBOR_BOTTOM_SIDE:
- return p_coords + Vector2i(0, 1);
- case TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER:
- return p_coords + Vector2i(-1, 1);
- case TileSet::CELL_NEIGHBOR_LEFT_SIDE:
- return p_coords + Vector2i(-1, 0);
- case TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER:
- return p_coords + Vector2i(-1, -1);
- case TileSet::CELL_NEIGHBOR_TOP_SIDE:
- return p_coords + Vector2i(0, -1);
- case TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER:
- return p_coords + Vector2i(1, -1);
- default:
- ERR_FAIL_V(p_coords);
- }
- } else { // Half-offset shapes (square and hexagon).
- switch (tile_set->get_tile_layout()) {
- case TileSet::TILE_LAYOUT_STACKED: {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- bool is_offset = p_coords.y % 2;
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
- return p_coords + Vector2i(1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(is_offset ? 1 : 0, 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
- return p_coords + Vector2i(0, 2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(is_offset ? 0 : -1, 1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
- return p_coords + Vector2i(-1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(is_offset ? 0 : -1, -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
- return p_coords + Vector2i(0, -2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(is_offset ? 1 : 0, -1);
- } else {
- ERR_FAIL_V(p_coords);
- }
- } else {
- bool is_offset = p_coords.x % 2;
-
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
- return p_coords + Vector2i(0, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(1, is_offset ? 1 : 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
- return p_coords + Vector2i(2, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, is_offset ? 0 : -1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
- return p_coords + Vector2i(0, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(-1, is_offset ? 0 : -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
- return p_coords + Vector2i(-2, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, is_offset ? 1 : 0);
- } else {
- ERR_FAIL_V(p_coords);
- }
- }
- } break;
- case TileSet::TILE_LAYOUT_STACKED_OFFSET: {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- bool is_offset = p_coords.y % 2;
-
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
- return p_coords + Vector2i(1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(is_offset ? 0 : 1, 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
- return p_coords + Vector2i(0, 2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(is_offset ? -1 : 0, 1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
- return p_coords + Vector2i(-1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(is_offset ? -1 : 0, -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
- return p_coords + Vector2i(0, -2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(is_offset ? 0 : 1, -1);
- } else {
- ERR_FAIL_V(p_coords);
- }
- } else {
- bool is_offset = p_coords.x % 2;
-
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
- return p_coords + Vector2i(0, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(1, is_offset ? 0 : 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
- return p_coords + Vector2i(2, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, is_offset ? -1 : 0);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
- return p_coords + Vector2i(0, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(-1, is_offset ? -1 : 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
- return p_coords + Vector2i(-2, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, is_offset ? 0 : 1);
- } else {
- ERR_FAIL_V(p_coords);
- }
- }
- } break;
- case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
- case TileSet::TILE_LAYOUT_STAIRS_DOWN: {
- if ((tile_set->get_tile_layout() == TileSet::TILE_LAYOUT_STAIRS_RIGHT) ^ (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_VERTICAL)) {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
- return p_coords + Vector2i(1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(0, 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
- return p_coords + Vector2i(-1, 2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
- return p_coords + Vector2i(-1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(0, -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
- return p_coords + Vector2i(1, -2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, -1);
- } else {
- ERR_FAIL_V(p_coords);
- }
-
- } else {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
- return p_coords + Vector2i(0, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
- return p_coords + Vector2i(2, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, -1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
- return p_coords + Vector2i(0, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
- return p_coords + Vector2i(-2, 1);
-
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 1);
- } else {
- ERR_FAIL_V(p_coords);
- }
- }
- } else {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
- return p_coords + Vector2i(2, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
- return p_coords + Vector2i(0, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
- return p_coords + Vector2i(-2, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
- return p_coords + Vector2i(0, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, -1);
- } else {
- ERR_FAIL_V(p_coords);
- }
-
- } else {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
- return p_coords + Vector2i(-1, 2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(0, 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
- return p_coords + Vector2i(1, 0);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, -1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
- return p_coords + Vector2i(1, -2);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(0, -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
- return p_coords + Vector2i(-1, 0);
-
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 1);
- } else {
- ERR_FAIL_V(p_coords);
- }
- }
- }
- } break;
- case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
- case TileSet::TILE_LAYOUT_DIAMOND_DOWN: {
- if ((tile_set->get_tile_layout() == TileSet::TILE_LAYOUT_DIAMOND_RIGHT) ^ (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_VERTICAL)) {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
- return p_coords + Vector2i(1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(0, 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
- return p_coords + Vector2i(-1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 0);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
- return p_coords + Vector2i(-1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(0, -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
- return p_coords + Vector2i(1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, 0);
- } else {
- ERR_FAIL_V(p_coords);
- }
-
- } else {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
- return p_coords + Vector2i(1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
- return p_coords + Vector2i(1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(0, -1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
- return p_coords + Vector2i(-1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
- return p_coords + Vector2i(-1, 1);
-
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(0, 1);
- } else {
- ERR_FAIL_V(p_coords);
- }
- }
- } else {
- if (tile_set->get_tile_offset_axis() == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
- return p_coords + Vector2i(1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
- return p_coords + Vector2i(1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(0, 1);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
- return p_coords + Vector2i(-1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 0);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
- return p_coords + Vector2i(-1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(0, -1);
- } else {
- ERR_FAIL_V(p_coords);
- }
-
- } else {
- if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
- return p_coords + Vector2i(-1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
- return p_coords + Vector2i(0, 1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
- return p_coords + Vector2i(1, 1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
- return p_coords + Vector2i(1, 0);
- } else if ((shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
- (shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
- return p_coords + Vector2i(1, -1);
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
- return p_coords + Vector2i(0, -1);
- } else if (shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
- return p_coords + Vector2i(-1, -1);
-
- } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
- return p_coords + Vector2i(-1, 0);
- } else {
- ERR_FAIL_V(p_coords);
- }
- }
- }
- } break;
- default:
- ERR_FAIL_V(p_coords);
- }
- }
+ ERR_FAIL_COND_V(!tile_set.is_valid(), Vector2i());
+ return tile_set->get_neighbor_cell(p_coords, p_cell_neighbor);
}
TypedArray<Vector2i> TileMap::get_used_cells(int p_layer) const {
@@ -1616,7 +953,7 @@ Rect2i TileMap::get_used_rect() const {
// Return the visible rect of the tilemap.
bool first = true;
Rect2i rect = Rect2i();
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
Rect2i layer_rect = layer->get_used_rect();
if (layer_rect == Rect2i()) {
continue;
@@ -1636,7 +973,7 @@ Rect2i TileMap::get_used_rect() const {
void TileMap::set_light_mask(int p_light_mask) {
// Occlusion: set light mask.
CanvasItem::set_light_mask(p_light_mask);
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_LIGHT_MASK);
}
}
@@ -1646,7 +983,7 @@ void TileMap::set_material(const Ref<Material> &p_material) {
CanvasItem::set_material(p_material);
// Update material for the whole tilemap.
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_MATERIAL);
}
}
@@ -1656,7 +993,7 @@ void TileMap::set_use_parent_material(bool p_use_parent_material) {
CanvasItem::set_use_parent_material(p_use_parent_material);
// Update use_parent_material for the whole tilemap.
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_USE_PARENT_MATERIAL);
}
}
@@ -1664,7 +1001,7 @@ void TileMap::set_use_parent_material(bool p_use_parent_material) {
void TileMap::set_texture_filter(TextureFilter p_texture_filter) {
// Set a default texture filter for the whole tilemap.
CanvasItem::set_texture_filter(p_texture_filter);
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_TEXTURE_FILTER);
}
}
@@ -1672,7 +1009,7 @@ void TileMap::set_texture_filter(TextureFilter p_texture_filter) {
void TileMap::set_texture_repeat(CanvasItem::TextureRepeat p_texture_repeat) {
// Set a default texture repeat for the whole tilemap.
CanvasItem::set_texture_repeat(p_texture_repeat);
- for (Ref<TileMapLayer> &layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_TEXTURE_REPEAT);
}
}
@@ -1771,14 +1108,14 @@ PackedStringArray TileMap::get_configuration_warnings() const {
// Retrieve the set of Z index values with a Y-sorted layer.
RBSet<int> y_sorted_z_index;
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
if (layer->is_y_sort_enabled()) {
y_sorted_z_index.insert(layer->get_z_index());
}
}
// Check if we have a non-sorted layer in a Z-index with a Y-sorted layer.
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
if (!layer->is_y_sort_enabled() && y_sorted_z_index.has(layer->get_z_index())) {
warnings.push_back(RTR("A Y-sorted layer has the same Z-index value as a not Y-sorted layer.\nThis may lead to unwanted behaviors, as a layer that is not Y-sorted will be Y-sorted as a whole with tiles from Y-sorted layers."));
break;
@@ -1787,7 +1124,7 @@ PackedStringArray TileMap::get_configuration_warnings() const {
if (!is_y_sort_enabled()) {
// Check if Y-sort is enabled on a layer but not on the node.
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
if (layer->is_y_sort_enabled()) {
warnings.push_back(RTR("A TileMap layer is set as Y-sorted, but Y-sort is not enabled on the TileMap node itself."));
break;
@@ -1796,7 +1133,7 @@ PackedStringArray TileMap::get_configuration_warnings() const {
} else {
// Check if Y-sort is enabled on the node, but not on any of the layers.
bool need_warning = true;
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
if (layer->is_y_sort_enabled()) {
need_warning = false;
break;
@@ -1811,7 +1148,7 @@ PackedStringArray TileMap::get_configuration_warnings() const {
if (tile_set.is_valid() && tile_set->get_tile_shape() == TileSet::TILE_SHAPE_ISOMETRIC) {
bool warn = !is_y_sort_enabled();
if (!warn) {
- for (const Ref<TileMapLayer> &layer : layers) {
+ for (const TileMapLayer *layer : layers) {
if (!layer->is_y_sort_enabled()) {
warn = true;
break;
@@ -1926,42 +1263,27 @@ void TileMap::_bind_methods() {
void TileMap::_tile_set_changed() {
emit_signal(CoreStringNames::get_singleton()->changed);
- for (Ref<TileMapLayer> layer : layers) {
+ for (TileMapLayer *layer : layers) {
layer->notify_tile_map_change(TileMapLayer::DIRTY_FLAGS_TILE_MAP_TILE_SET);
}
update_configuration_warnings();
}
-void TileMap::_update_notify_local_transform() {
- bool notify = collision_animatable || is_y_sort_enabled();
- if (!notify) {
- for (const Ref<TileMapLayer> &layer : layers) {
- if (layer->is_y_sort_enabled()) {
- notify = true;
- break;
- }
- }
- }
- set_notify_local_transform(notify);
-}
-
TileMap::TileMap() {
- set_notify_transform(true);
- _update_notify_local_transform();
-
- Ref<TileMapLayer> new_layer;
- new_layer.instantiate();
- new_layer->set_tile_map(this);
+ TileMapLayer *new_layer = memnew(TileMapLayer);
+ add_child(new_layer);
+ new_layer->set_name("Layer0");
new_layer->set_layer_index_in_tile_map_node(0);
layers.push_back(new_layer);
- default_layer.instantiate();
+ default_layer = memnew(TileMapLayer);
}
TileMap::~TileMap() {
if (tile_set.is_valid()) {
tile_set->disconnect_changed(callable_mp(this, &TileMap::_tile_set_changed));
}
+ memdelete(default_layer);
}
#undef TILEMAP_CALL_FOR_LAYER
diff --git a/scene/2d/tile_map.h b/scene/2d/tile_map.h
index 29af0ad2b1..f2b481cdd1 100644
--- a/scene/2d/tile_map.h
+++ b/scene/2d/tile_map.h
@@ -71,18 +71,17 @@ private:
VisibilityMode navigation_visibility_mode = VISIBILITY_MODE_DEFAULT;
// Layers.
- LocalVector<Ref<TileMapLayer>> layers;
- Ref<TileMapLayer> default_layer; // Dummy layer to fetch default values.
+ LocalVector<TileMapLayer *> layers;
+ TileMapLayer *default_layer; // Dummy layer to fetch default values.
int selected_layer = -1;
bool pending_update = false;
+ // Transforms for collision_animatable.
Transform2D last_valid_transform;
Transform2D new_transform;
void _tile_set_changed();
- void _update_notify_local_transform();
-
protected:
bool _set(const StringName &p_name, const Variant &p_value);
bool _get(const StringName &p_name, Variant &r_ret) const;
@@ -97,6 +96,8 @@ protected:
Rect2i _get_used_rect_bind_compat_78328();
void _set_quadrant_size_compat_81070(int p_quadrant_size);
int _get_quadrant_size_compat_81070() const;
+ VisibilityMode _get_collision_visibility_mode_bind_compat_87115();
+ VisibilityMode _get_navigation_visibility_mode_bind_compat_87115();
static void _bind_compatibility_methods();
#endif
@@ -150,15 +151,15 @@ public:
void set_selected_layer(int p_layer_id); // For editor use.
int get_selected_layer() const;
- void set_collision_animatable(bool p_enabled);
+ void set_collision_animatable(bool p_collision_animatable);
bool is_collision_animatable() const;
// Debug visibility modes.
void set_collision_visibility_mode(VisibilityMode p_show_collision);
- VisibilityMode get_collision_visibility_mode();
+ VisibilityMode get_collision_visibility_mode() const;
void set_navigation_visibility_mode(VisibilityMode p_show_navigation);
- VisibilityMode get_navigation_visibility_mode();
+ VisibilityMode get_navigation_visibility_mode() const;
// Cells accessors.
void set_cell(int p_layer, const Vector2i &p_coords, int p_source_id = TileSet::INVALID_SOURCE, const Vector2i p_atlas_coords = TileSetSource::INVALID_ATLAS_COORDS, int p_alternative_tile = 0);
@@ -192,7 +193,6 @@ public:
Vector2 map_to_local(const Vector2i &p_pos) const;
Vector2i local_to_map(const Vector2 &p_pos) const;
-
bool is_existing_neighbor(TileSet::CellNeighbor p_cell_neighbor) const;
Vector2i get_neighbor_cell(const Vector2i &p_coords, TileSet::CellNeighbor p_cell_neighbor) const;
diff --git a/scene/2d/tile_map_layer.cpp b/scene/2d/tile_map_layer.cpp
index ed93b8d051..df79b3fee6 100644
--- a/scene/2d/tile_map_layer.cpp
+++ b/scene/2d/tile_map_layer.cpp
@@ -40,6 +40,18 @@
#include "servers/navigation_server_3d.h"
#endif // DEBUG_ENABLED
+TileMap *TileMapLayer::_fetch_tilemap() const {
+ return TileMap::cast_to<TileMap>(get_parent());
+}
+
+Ref<TileSet> TileMapLayer::_fetch_tileset() const {
+ TileMap *tile_map_node = _fetch_tilemap();
+ if (!tile_map_node) {
+ return Ref<TileSet>();
+ }
+ return tile_map_node->get_tileset();
+}
+
#ifdef DEBUG_ENABLED
/////////////////////////////// Debug //////////////////////////////////////////
constexpr int TILE_MAP_DEBUG_QUADRANT_SIZE = 16;
@@ -51,11 +63,11 @@ Vector2i TileMapLayer::_coords_to_debug_quadrant_coords(const Vector2i &p_coords
}
void TileMapLayer::_debug_update() {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
RenderingServer *rs = RenderingServer::get_singleton();
// Check if we should cleanup everything.
- bool forced_cleanup = in_destructor || !enabled || !tile_map_node->is_inside_tree() || !tile_set.is_valid() || !tile_map_node->is_visible_in_tree();
+ bool forced_cleanup = in_destructor || !enabled || !tile_set.is_valid() || !is_visible_in_tree();
if (forced_cleanup) {
for (KeyValue<Vector2i, Ref<DebugQuadrant>> &kv : debug_quadrant_map) {
@@ -120,10 +132,10 @@ void TileMapLayer::_debug_update() {
} else {
ci = rs->canvas_item_create();
rs->canvas_item_set_z_index(ci, RS::CANVAS_ITEM_Z_MAX - 1);
- rs->canvas_item_set_parent(ci, tile_map_node->get_canvas_item());
+ rs->canvas_item_set_parent(ci, get_canvas_item());
}
- const Vector2 quadrant_pos = tile_map_node->map_to_local(debug_quadrant.quadrant_coords * TILE_MAP_DEBUG_QUADRANT_SIZE);
+ const Vector2 quadrant_pos = tile_set->map_to_local(debug_quadrant.quadrant_coords * TILE_MAP_DEBUG_QUADRANT_SIZE);
Transform2D xform(0, quadrant_pos);
rs->canvas_item_set_transform(ci, xform);
@@ -179,48 +191,33 @@ void TileMapLayer::_debug_quadrants_update_cell(CellData &r_cell_data, SelfList<
/////////////////////////////// Rendering //////////////////////////////////////
void TileMapLayer::_rendering_update() {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
RenderingServer *rs = RenderingServer::get_singleton();
// Check if we should cleanup everything.
- bool forced_cleanup = in_destructor || !enabled || !tile_map_node->is_inside_tree() || !tile_set.is_valid() || !tile_map_node->is_visible_in_tree();
+ bool forced_cleanup = in_destructor || !enabled || !tile_set.is_valid() || !is_visible_in_tree();
// ----------- Layer level processing -----------
- if (forced_cleanup) {
- // Cleanup.
- if (canvas_item.is_valid()) {
- rs->free(canvas_item);
- canvas_item = RID();
- }
- } else {
- // Create/Update the layer's CanvasItem.
- if (!canvas_item.is_valid()) {
- RID ci = rs->canvas_item_create();
- rs->canvas_item_set_parent(ci, tile_map_node->get_canvas_item());
- canvas_item = ci;
- }
- RID &ci = canvas_item;
- rs->canvas_item_set_draw_index(ci, layer_index_in_tile_map_node - (int64_t)0x80000000);
- rs->canvas_item_set_sort_children_by_y(ci, y_sort_enabled);
- rs->canvas_item_set_use_parent_material(ci, tile_map_node->get_use_parent_material() || tile_map_node->get_material().is_valid());
- rs->canvas_item_set_z_index(ci, z_index);
- rs->canvas_item_set_default_texture_filter(ci, RS::CanvasItemTextureFilter(tile_map_node->get_texture_filter_in_tree()));
- rs->canvas_item_set_default_texture_repeat(ci, RS::CanvasItemTextureRepeat(tile_map_node->get_texture_repeat_in_tree()));
- rs->canvas_item_set_light_mask(ci, tile_map_node->get_light_mask());
+ if (!forced_cleanup) {
+ // Update the layer's CanvasItem.
+ set_use_parent_material(true);
+ set_light_mask(tile_map_node->get_light_mask());
// Modulate the layer.
- Color layer_modulate = modulate;
+ Color layer_modulate = get_modulate();
int selected_layer = tile_map_node->get_selected_layer();
if (selected_layer >= 0 && layer_index_in_tile_map_node != selected_layer) {
int z_selected = tile_map_node->get_layer_z_index(selected_layer);
- if (z_index < z_selected || (z_index == z_selected && layer_index_in_tile_map_node < selected_layer)) {
+ int layer_z_index = get_z_index();
+ if (layer_z_index < z_selected || (layer_z_index == z_selected && layer_index_in_tile_map_node < selected_layer)) {
layer_modulate = layer_modulate.darkened(0.5);
- } else if (z_index > z_selected || (z_index == z_selected && layer_index_in_tile_map_node > selected_layer)) {
+ } else if (layer_z_index > z_selected || (layer_z_index == z_selected && layer_index_in_tile_map_node > selected_layer)) {
layer_modulate = layer_modulate.darkened(0.5);
layer_modulate.a *= 0.3;
}
}
- rs->canvas_item_set_modulate(ci, layer_modulate);
+ rs->canvas_item_set_modulate(get_canvas_item(), layer_modulate);
}
// ----------- Quadrants processing -----------
@@ -231,7 +228,7 @@ void TileMapLayer::_rendering_update() {
// Check if anything changed that might change the quadrant shape.
// If so, recreate everything.
bool quandrant_shape_changed = dirty.flags[DIRTY_FLAGS_TILE_MAP_QUADRANT_SIZE] ||
- (tile_map_node->is_y_sort_enabled() && y_sort_enabled && (dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ORIGIN] || dirty.flags[DIRTY_FLAGS_TILE_MAP_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_TILE_MAP_LOCAL_XFORM] || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET]));
+ (is_y_sort_enabled() && (dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ORIGIN] || dirty.flags[DIRTY_FLAGS_TILE_MAP_Y_SORT_ENABLED] || dirty.flags[DIRTY_FLAGS_LAYER_LOCAL_TRANSFORM] || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET]));
// Free all quadrants.
if (forced_cleanup || quandrant_shape_changed) {
@@ -250,7 +247,7 @@ void TileMapLayer::_rendering_update() {
if (!forced_cleanup) {
// List all quadrants to update, recreating them if needed.
- if (dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET] || _rendering_was_cleaned_up) {
+ if (dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET] || dirty.flags[DIRTY_FLAGS_LAYER_IN_TREE] || _rendering_was_cleaned_up) {
// Update all cells.
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
CellData &cell_data = kv.value;
@@ -290,7 +287,7 @@ void TileMapLayer::_rendering_update() {
rendering_quadrant->canvas_items.clear();
// Sort the quadrant cells.
- if (tile_map_node->is_y_sort_enabled() && is_y_sort_enabled()) {
+ if (is_y_sort_enabled()) {
// For compatibility reasons, we use another comparator for Y-sorted layers.
rendering_quadrant->cells.sort_custom<CellDataYSortedComparator>();
} else {
@@ -330,8 +327,8 @@ void TileMapLayer::_rendering_update() {
if (mat.is_valid()) {
rs->canvas_item_set_material(ci, mat->get_rid());
}
- rs->canvas_item_set_parent(ci, canvas_item);
- rs->canvas_item_set_use_parent_material(ci, tile_map_node->get_use_parent_material() || tile_map_node->get_material().is_valid());
+ rs->canvas_item_set_parent(ci, get_canvas_item());
+ rs->canvas_item_set_use_parent_material(ci, true);
Transform2D xform(0, rendering_quadrant->canvas_items_position);
rs->canvas_item_set_transform(ci, xform);
@@ -340,8 +337,8 @@ void TileMapLayer::_rendering_update() {
rs->canvas_item_set_z_as_relative_to_parent(ci, true);
rs->canvas_item_set_z_index(ci, tile_z_index);
- rs->canvas_item_set_default_texture_filter(ci, RS::CanvasItemTextureFilter(tile_map_node->get_texture_filter_in_tree()));
- rs->canvas_item_set_default_texture_repeat(ci, RS::CanvasItemTextureRepeat(tile_map_node->get_texture_repeat_in_tree()));
+ rs->canvas_item_set_default_texture_filter(ci, RS::CanvasItemTextureFilter(get_texture_filter_in_tree()));
+ rs->canvas_item_set_default_texture_repeat(ci, RS::CanvasItemTextureRepeat(get_texture_repeat_in_tree()));
rendering_quadrant->canvas_items.push_back(ci);
@@ -354,7 +351,7 @@ void TileMapLayer::_rendering_update() {
ci = prev_ci;
}
- const Vector2 local_tile_pos = tile_map_node->map_to_local(cell_data.coords);
+ const Vector2 local_tile_pos = tile_set->map_to_local(cell_data.coords);
// Random animation offset.
real_t random_animation_offset = 0.0;
@@ -366,7 +363,7 @@ void TileMapLayer::_rendering_update() {
}
// Drawing the tile in the canvas item.
- tile_map_node->draw_tile(ci, local_tile_pos - rendering_quadrant->canvas_items_position, tile_set, cell_data.cell.source_id, cell_data.cell.get_atlas_coords(), cell_data.cell.alternative_tile, -1, tile_map_node->get_self_modulate(), tile_data, random_animation_offset);
+ TileMap::draw_tile(ci, local_tile_pos - rendering_quadrant->canvas_items_position, tile_set, cell_data.cell.source_id, cell_data.cell.get_atlas_coords(), cell_data.cell.alternative_tile, -1, get_self_modulate(), tile_data, random_animation_offset);
}
} else {
// Free the quadrant.
@@ -393,7 +390,7 @@ void TileMapLayer::_rendering_update() {
RBMap<Vector2, Ref<RenderingQuadrant>, RenderingQuadrant::CoordsWorldComparator> local_to_map;
for (KeyValue<Vector2i, Ref<RenderingQuadrant>> &kv : rendering_quadrant_map) {
Ref<RenderingQuadrant> &rendering_quadrant = kv.value;
- local_to_map[tile_map_node->map_to_local(rendering_quadrant->quadrant_coords)] = rendering_quadrant;
+ local_to_map[tile_set->map_to_local(rendering_quadrant->quadrant_coords)] = rendering_quadrant;
}
// Sort the quadrants.
@@ -409,14 +406,15 @@ void TileMapLayer::_rendering_update() {
dirty.flags[DIRTY_FLAGS_TILE_MAP_USE_PARENT_MATERIAL] ||
dirty.flags[DIRTY_FLAGS_TILE_MAP_MATERIAL] ||
dirty.flags[DIRTY_FLAGS_TILE_MAP_TEXTURE_FILTER] ||
- dirty.flags[DIRTY_FLAGS_TILE_MAP_TEXTURE_REPEAT]) {
+ dirty.flags[DIRTY_FLAGS_TILE_MAP_TEXTURE_REPEAT] ||
+ dirty.flags[DIRTY_FLAGS_LAYER_SELF_MODULATE]) {
for (KeyValue<Vector2i, Ref<RenderingQuadrant>> &kv : rendering_quadrant_map) {
Ref<RenderingQuadrant> &rendering_quadrant = kv.value;
for (const RID &ci : rendering_quadrant->canvas_items) {
rs->canvas_item_set_light_mask(ci, tile_map_node->get_light_mask());
- rs->canvas_item_set_use_parent_material(ci, tile_map_node->get_use_parent_material() || tile_map_node->get_material().is_valid());
- rs->canvas_item_set_default_texture_filter(ci, RS::CanvasItemTextureFilter(tile_map_node->get_texture_filter_in_tree()));
- rs->canvas_item_set_default_texture_repeat(ci, RS::CanvasItemTextureRepeat(tile_map_node->get_texture_repeat_in_tree()));
+ rs->canvas_item_set_default_texture_filter(ci, RS::CanvasItemTextureFilter(get_texture_filter_in_tree()));
+ rs->canvas_item_set_default_texture_repeat(ci, RS::CanvasItemTextureRepeat(get_texture_repeat_in_tree()));
+ rs->canvas_item_set_self_modulate(ci, get_self_modulate());
}
}
}
@@ -441,30 +439,37 @@ void TileMapLayer::_rendering_update() {
_rendering_occluders_update_cell(cell_data);
}
}
+ }
- // Updates on TileMap changes.
- if (dirty.flags[DIRTY_FLAGS_TILE_MAP_IN_CANVAS] || dirty.flags[DIRTY_FLAGS_TILE_MAP_VISIBILITY]) {
+ // -----------
+ // Mark the rendering state as up to date.
+ _rendering_was_cleaned_up = forced_cleanup;
+}
+
+void TileMapLayer::_rendering_notification(int p_what) {
+ RenderingServer *rs = RenderingServer::get_singleton();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
+ if (p_what == NOTIFICATION_TRANSFORM_CHANGED || p_what == NOTIFICATION_ENTER_CANVAS || p_what == NOTIFICATION_VISIBILITY_CHANGED) {
+ if (tile_set.is_valid()) {
+ Transform2D tilemap_xform = get_global_transform();
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
- CellData &cell_data = kv.value;
+ const CellData &cell_data = kv.value;
for (const RID &occluder : cell_data.occluders) {
if (occluder.is_null()) {
continue;
}
- Transform2D xform(0, tile_map_node->map_to_local(kv.key));
- rs->canvas_light_occluder_attach_to_canvas(occluder, tile_map_node->get_canvas());
- rs->canvas_light_occluder_set_transform(occluder, tile_map_node->get_global_transform() * xform);
+ Transform2D xform(0, tile_set->map_to_local(kv.key));
+ rs->canvas_light_occluder_attach_to_canvas(occluder, get_canvas());
+ rs->canvas_light_occluder_set_transform(occluder, tilemap_xform * xform);
}
}
}
}
-
- // -----------
- // Mark the rendering state as up to date.
- _rendering_was_cleaned_up = forced_cleanup;
}
void TileMapLayer::_rendering_quadrants_update_cell(CellData &r_cell_data, SelfList<RenderingQuadrant>::List &r_dirty_rendering_quadrant_list) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
// Check if the cell is valid and retrieve its y_sort_origin.
bool is_valid = false;
@@ -489,8 +494,8 @@ void TileMapLayer::_rendering_quadrants_update_cell(CellData &r_cell_data, SelfL
// Get the quadrant coords.
Vector2 canvas_items_position;
Vector2i quadrant_coords;
- if (tile_map_node->is_y_sort_enabled() && is_y_sort_enabled()) {
- canvas_items_position = Vector2(0, tile_map_node->map_to_local(r_cell_data.coords).y + tile_y_sort_origin + y_sort_origin);
+ if (is_y_sort_enabled()) {
+ canvas_items_position = Vector2(0, tile_set->map_to_local(r_cell_data.coords).y + tile_y_sort_origin + y_sort_origin);
quadrant_coords = canvas_items_position * 100;
} else {
int quad_size = tile_map_node->get_rendering_quadrant_size();
@@ -500,7 +505,7 @@ void TileMapLayer::_rendering_quadrants_update_cell(CellData &r_cell_data, SelfL
quadrant_coords = Vector2i(
coords.x > 0 ? coords.x / quad_size : (coords.x - (quad_size - 1)) / quad_size,
coords.y > 0 ? coords.y / quad_size : (coords.y - (quad_size - 1)) / quad_size);
- canvas_items_position = tile_map_node->map_to_local(quad_size * quadrant_coords);
+ canvas_items_position = tile_set->map_to_local(quad_size * quadrant_coords);
}
Ref<RenderingQuadrant> rendering_quadrant;
@@ -564,8 +569,7 @@ void TileMapLayer::_rendering_occluders_clear_cell(CellData &r_cell_data) {
}
void TileMapLayer::_rendering_occluders_update_cell(CellData &r_cell_data) {
- bool node_visible = tile_map_node->is_visible_in_tree();
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
RenderingServer *rs = RenderingServer::get_singleton();
// Free unused occluders then resize the occluders array.
@@ -606,14 +610,13 @@ void TileMapLayer::_rendering_occluders_update_cell(CellData &r_cell_data) {
if (occluder_polygon.is_valid()) {
// Create or update occluder.
Transform2D xform;
- xform.set_origin(tile_map_node->map_to_local(r_cell_data.coords));
+ xform.set_origin(tile_set->map_to_local(r_cell_data.coords));
if (!occluder.is_valid()) {
occluder = rs->canvas_light_occluder_create();
}
- rs->canvas_light_occluder_set_enabled(occluder, node_visible);
- rs->canvas_light_occluder_set_transform(occluder, tile_map_node->get_global_transform() * xform);
+ rs->canvas_light_occluder_set_transform(occluder, get_global_transform() * xform);
rs->canvas_light_occluder_set_polygon(occluder, tile_data->get_occluder(occlusion_layer_index, flip_h, flip_v, transpose)->get_rid());
- rs->canvas_light_occluder_attach_to_canvas(occluder, tile_map_node->get_canvas());
+ rs->canvas_light_occluder_attach_to_canvas(occluder, get_canvas());
rs->canvas_light_occluder_set_light_mask(occluder, tile_set->get_occlusion_layer_light_mask(occlusion_layer_index));
} else {
// Clear occluder.
@@ -635,7 +638,7 @@ void TileMapLayer::_rendering_occluders_update_cell(CellData &r_cell_data) {
#ifdef DEBUG_ENABLED
void TileMapLayer::_rendering_draw_cell_debug(const RID &p_canvas_item, const Vector2 &p_quadrant_pos, const CellData &r_cell_data) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND(!tile_set.is_valid());
if (!Engine::get_singleton()->is_editor_hint()) {
@@ -671,7 +674,7 @@ void TileMapLayer::_rendering_draw_cell_debug(const RID &p_canvas_item, const Ve
// Draw a placeholder tile.
Transform2D cell_to_quadrant;
- cell_to_quadrant.set_origin(tile_map_node->map_to_local(r_cell_data.coords) - p_quadrant_pos);
+ cell_to_quadrant.set_origin(tile_set->map_to_local(r_cell_data.coords) - p_quadrant_pos);
rs->canvas_item_add_set_transform(p_canvas_item, cell_to_quadrant);
rs->canvas_item_add_circle(p_canvas_item, Vector2(), MIN(tile_set->get_tile_size().x, tile_set->get_tile_size().y) / 4.0, color);
}
@@ -684,17 +687,17 @@ void TileMapLayer::_rendering_draw_cell_debug(const RID &p_canvas_item, const Ve
/////////////////////////////// Physics //////////////////////////////////////
void TileMapLayer::_physics_update() {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
// Check if we should cleanup everything.
- bool forced_cleanup = in_destructor || !enabled || !tile_map_node->is_inside_tree() || !tile_set.is_valid();
+ bool forced_cleanup = in_destructor || !enabled || !is_inside_tree() || !tile_set.is_valid();
if (forced_cleanup) {
// Clean everything.
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
_physics_clear_cell(kv.value);
}
} else {
- if (_physics_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET] || dirty.flags[DIRTY_FLAGS_TILE_MAP_COLLISION_ANIMATABLE]) {
+ if (_physics_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET] || dirty.flags[DIRTY_FLAGS_LAYER_USE_KINEMATIC_BODIES] || dirty.flags[DIRTY_FLAGS_LAYER_IN_TREE]) {
// Update all cells.
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
_physics_update_cell(kv.value);
@@ -713,60 +716,43 @@ void TileMapLayer::_physics_update() {
_physics_was_cleaned_up = forced_cleanup;
}
-void TileMapLayer::_physics_notify_tilemap_change(TileMapLayer::DirtyFlags p_what) {
- Transform2D gl_transform = tile_map_node->get_global_transform();
+void TileMapLayer::_physics_notification(int p_what) {
+ const Ref<TileSet> &tile_set = _fetch_tileset();
+ Transform2D gl_transform = get_global_transform();
PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
- bool in_editor = false;
-#ifdef TOOLS_ENABLED
- in_editor = Engine::get_singleton()->is_editor_hint();
-#endif
-
- if (p_what == DIRTY_FLAGS_TILE_MAP_XFORM) {
- if (tile_map_node->is_inside_tree() && (!tile_map_node->is_collision_animatable() || in_editor)) {
+ switch (p_what) {
+ case NOTIFICATION_TRANSFORM_CHANGED:
// Move the collisison shapes along with the TileMap.
- for (KeyValue<Vector2i, CellData> &kv : tile_map) {
- const CellData &cell_data = kv.value;
+ if (is_inside_tree() && tile_set.is_valid()) {
+ for (KeyValue<Vector2i, CellData> &kv : tile_map) {
+ const CellData &cell_data = kv.value;
- for (RID body : cell_data.bodies) {
- if (body.is_valid()) {
- Transform2D xform(0, tile_map_node->map_to_local(bodies_coords[body]));
- xform = gl_transform * xform;
- ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, xform);
- }
- }
- }
- }
- } else if (p_what == DIRTY_FLAGS_TILE_MAP_LOCAL_XFORM) {
- // With collisions animatable, move the collisison shapes along with the TileMap only on local xform change (they are synchornized on physics tick instead).
- if (tile_map_node->is_inside_tree() && tile_map_node->is_collision_animatable() && !in_editor) {
- for (KeyValue<Vector2i, CellData> &kv : tile_map) {
- const CellData &cell_data = kv.value;
-
- for (RID body : cell_data.bodies) {
- if (body.is_valid()) {
- Transform2D xform(0, tile_map_node->map_to_local(bodies_coords[body]));
- xform = gl_transform * xform;
- ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, xform);
+ for (RID body : cell_data.bodies) {
+ if (body.is_valid()) {
+ Transform2D xform(0, tile_set->map_to_local(kv.key));
+ xform = gl_transform * xform;
+ ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, xform);
+ }
}
}
}
- }
- } else if (p_what == DIRTY_FLAGS_TILE_MAP_IN_TREE) {
- // Changes in the tree may cause the space to change (e.g. when reparenting to a SubViewport).
- if (tile_map_node->is_inside_tree()) {
- RID space = tile_map_node->get_world_2d()->get_space();
+ break;
+ case NOTIFICATION_ENTER_TREE:
+ // Changes in the tree may cause the space to change (e.g. when reparenting to a SubViewport).
+ if (is_inside_tree()) {
+ RID space = get_world_2d()->get_space();
- for (KeyValue<Vector2i, CellData> &kv : tile_map) {
- const CellData &cell_data = kv.value;
+ for (KeyValue<Vector2i, CellData> &kv : tile_map) {
+ const CellData &cell_data = kv.value;
- for (RID body : cell_data.bodies) {
- if (body.is_valid()) {
- ps->body_set_space(body, space);
+ for (RID body : cell_data.bodies) {
+ if (body.is_valid()) {
+ ps->body_set_space(body, space);
+ }
}
}
}
- }
}
}
@@ -784,9 +770,10 @@ void TileMapLayer::_physics_clear_cell(CellData &r_cell_data) {
}
void TileMapLayer::_physics_update_cell(CellData &r_cell_data) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
- Transform2D gl_transform = tile_map_node->get_global_transform();
- RID space = tile_map_node->get_world_2d()->get_space();
+ const TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
+ Transform2D gl_transform = get_global_transform();
+ RID space = get_world_2d()->get_space();
PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
// Recreate bodies and shapes.
@@ -813,10 +800,11 @@ void TileMapLayer::_physics_update_cell(CellData &r_cell_data) {
// Free unused bodies then resize the bodies array.
for (uint32_t i = tile_set->get_physics_layers_count(); i < r_cell_data.bodies.size(); i++) {
- RID body = r_cell_data.bodies[i];
+ RID &body = r_cell_data.bodies[i];
if (body.is_valid()) {
bodies_coords.erase(body);
ps->free(body);
+ body = RID();
}
}
r_cell_data.bodies.resize(tile_set->get_physics_layers_count());
@@ -844,7 +832,7 @@ void TileMapLayer::_physics_update_cell(CellData &r_cell_data) {
ps->body_set_space(body, space);
Transform2D xform;
- xform.set_origin(tile_map_node->map_to_local(r_cell_data.coords));
+ xform.set_origin(tile_set->map_to_local(r_cell_data.coords));
xform = gl_transform * xform;
ps->body_set_state(body, PhysicsServer2D::BODY_STATE_TRANSFORM, xform);
@@ -900,17 +888,18 @@ void TileMapLayer::_physics_update_cell(CellData &r_cell_data) {
#ifdef DEBUG_ENABLED
void TileMapLayer::_physics_draw_cell_debug(const RID &p_canvas_item, const Vector2 &p_quadrant_pos, const CellData &r_cell_data) {
// Draw the debug collision shapes.
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND(!tile_set.is_valid());
- if (!tile_map_node->get_tree()) {
+ if (!get_tree()) {
return;
}
bool show_collision = false;
switch (tile_map_node->get_collision_visibility_mode()) {
case TileMap::VISIBILITY_MODE_DEFAULT:
- show_collision = !Engine::get_singleton()->is_editor_hint() && tile_map_node->get_tree()->is_debugging_collisions_hint();
+ show_collision = !Engine::get_singleton()->is_editor_hint() && get_tree()->is_debugging_collisions_hint();
break;
case TileMap::VISIBILITY_MODE_FORCE_HIDE:
show_collision = false;
@@ -926,12 +915,12 @@ void TileMapLayer::_physics_draw_cell_debug(const RID &p_canvas_item, const Vect
RenderingServer *rs = RenderingServer::get_singleton();
PhysicsServer2D *ps = PhysicsServer2D::get_singleton();
- Color debug_collision_color = tile_map_node->get_tree()->get_debug_collisions_color();
+ Color debug_collision_color = get_tree()->get_debug_collisions_color();
Vector<Color> color;
color.push_back(debug_collision_color);
Transform2D quadrant_to_local(0, p_quadrant_pos);
- Transform2D global_to_quadrant = (tile_map_node->get_global_transform() * quadrant_to_local).affine_inverse();
+ Transform2D global_to_quadrant = (get_global_transform() * quadrant_to_local).affine_inverse();
for (RID body : r_cell_data.bodies) {
if (body.is_valid()) {
@@ -956,11 +945,11 @@ void TileMapLayer::_physics_draw_cell_debug(const RID &p_canvas_item, const Vect
void TileMapLayer::_navigation_update() {
ERR_FAIL_NULL(NavigationServer2D::get_singleton());
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
NavigationServer2D *ns = NavigationServer2D::get_singleton();
// Check if we should cleanup everything.
- bool forced_cleanup = in_destructor || !enabled || !navigation_enabled || !tile_map_node->is_inside_tree() || !tile_set.is_valid();
+ bool forced_cleanup = in_destructor || !enabled || !navigation_enabled || !is_inside_tree() || !tile_set.is_valid();
// ----------- Layer level processing -----------
if (forced_cleanup) {
@@ -973,7 +962,7 @@ void TileMapLayer::_navigation_update() {
if (!navigation_map.is_valid()) {
if (layer_index_in_tile_map_node == 0) {
// Use the default World2D navigation map for the first layer when empty.
- navigation_map = tile_map_node->get_world_2d()->get_navigation_map();
+ navigation_map = get_world_2d()->get_navigation_map();
uses_world_navigation_map = true;
} else {
RID new_layer_map = ns->map_create();
@@ -993,7 +982,7 @@ void TileMapLayer::_navigation_update() {
_navigation_clear_cell(kv.value);
}
} else {
- if (_navigation_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET]) {
+ if (_navigation_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET] || dirty.flags[DIRTY_FLAGS_LAYER_IN_TREE]) {
// Update all cells.
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
_navigation_update_cell(kv.value);
@@ -1005,9 +994,18 @@ void TileMapLayer::_navigation_update() {
_navigation_update_cell(cell_data);
}
}
+ }
+
+ // -----------
+ // Mark the navigation state as up to date.
+ _navigation_was_cleaned_up = forced_cleanup;
+}
- if (dirty.flags[DIRTY_FLAGS_TILE_MAP_XFORM]) {
- Transform2D tilemap_xform = tile_map_node->get_global_transform();
+void TileMapLayer::_navigation_notification(int p_what) {
+ const Ref<TileSet> &tile_set = _fetch_tileset();
+ if (p_what == NOTIFICATION_TRANSFORM_CHANGED) {
+ if (tile_set.is_valid()) {
+ Transform2D tilemap_xform = get_global_transform();
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
const CellData &cell_data = kv.value;
// Update navigation regions transform.
@@ -1016,16 +1014,12 @@ void TileMapLayer::_navigation_update() {
continue;
}
Transform2D tile_transform;
- tile_transform.set_origin(tile_map_node->map_to_local(kv.key));
+ tile_transform.set_origin(tile_set->map_to_local(kv.key));
NavigationServer2D::get_singleton()->region_set_transform(region, tilemap_xform * tile_transform);
}
}
}
}
-
- // -----------
- // Mark the navigation state as up to date.
- _navigation_was_cleaned_up = forced_cleanup;
}
void TileMapLayer::_navigation_clear_cell(CellData &r_cell_data) {
@@ -1042,9 +1036,10 @@ void TileMapLayer::_navigation_clear_cell(CellData &r_cell_data) {
}
void TileMapLayer::_navigation_update_cell(CellData &r_cell_data) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
NavigationServer2D *ns = NavigationServer2D::get_singleton();
- Transform2D tilemap_xform = tile_map_node->get_global_transform();
+ Transform2D gl_xform = get_global_transform();
// Get the navigation polygons and create regions.
TileMapCell &c = r_cell_data.cell;
@@ -1088,13 +1083,13 @@ void TileMapLayer::_navigation_update_cell(CellData &r_cell_data) {
if (navigation_polygon.is_valid() && (navigation_polygon->get_polygon_count() > 0 || navigation_polygon->get_outline_count() > 0)) {
// Create or update regions.
Transform2D tile_transform;
- tile_transform.set_origin(tile_map_node->map_to_local(r_cell_data.coords));
+ tile_transform.set_origin(tile_set->map_to_local(r_cell_data.coords));
if (!region.is_valid()) {
region = ns->region_create();
}
ns->region_set_owner_id(region, tile_map_node->get_instance_id());
ns->region_set_map(region, navigation_map);
- ns->region_set_transform(region, tilemap_xform * tile_transform);
+ ns->region_set_transform(region, gl_xform * tile_transform);
ns->region_set_navigation_layers(region, tile_set->get_navigation_layer_layers(navigation_layer_index));
ns->region_set_navigation_polygon(region, navigation_polygon);
} else {
@@ -1119,10 +1114,11 @@ void TileMapLayer::_navigation_update_cell(CellData &r_cell_data) {
#ifdef DEBUG_ENABLED
void TileMapLayer::_navigation_draw_cell_debug(const RID &p_canvas_item, const Vector2 &p_quadrant_pos, const CellData &r_cell_data) {
// Draw the debug collision shapes.
+ const TileMap *tile_map_node = _fetch_tilemap();
bool show_navigation = false;
switch (tile_map_node->get_navigation_visibility_mode()) {
case TileMap::VISIBILITY_MODE_DEFAULT:
- show_navigation = !Engine::get_singleton()->is_editor_hint() && tile_map_node->get_tree()->is_debugging_navigation_hint();
+ show_navigation = !Engine::get_singleton()->is_editor_hint() && get_tree()->is_debugging_navigation_hint();
break;
case TileMap::VISIBILITY_MODE_FORCE_HIDE:
show_navigation = false;
@@ -1140,7 +1136,7 @@ void TileMapLayer::_navigation_draw_cell_debug(const RID &p_canvas_item, const V
return;
}
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
RenderingServer *rs = RenderingServer::get_singleton();
const NavigationServer2D *ns2d = NavigationServer2D::get_singleton();
@@ -1170,7 +1166,7 @@ void TileMapLayer::_navigation_draw_cell_debug(const RID &p_canvas_item, const V
}
Transform2D cell_to_quadrant;
- cell_to_quadrant.set_origin(tile_map_node->map_to_local(r_cell_data.coords) - p_quadrant_pos);
+ cell_to_quadrant.set_origin(tile_set->map_to_local(r_cell_data.coords) - p_quadrant_pos);
rs->canvas_item_add_set_transform(p_canvas_item, cell_to_quadrant);
for (int layer_index = 0; layer_index < tile_set->get_navigation_layers_count(); layer_index++) {
@@ -1226,10 +1222,10 @@ void TileMapLayer::_navigation_draw_cell_debug(const RID &p_canvas_item, const V
/////////////////////////////// Scenes //////////////////////////////////////
void TileMapLayer::_scenes_update() {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
// Check if we should cleanup everything.
- bool forced_cleanup = in_destructor || !enabled || !tile_map_node->is_inside_tree() || !tile_set.is_valid();
+ bool forced_cleanup = in_destructor || !enabled || !is_inside_tree() || !tile_set.is_valid();
if (forced_cleanup) {
// Clean everything.
@@ -1237,7 +1233,7 @@ void TileMapLayer::_scenes_update() {
_scenes_clear_cell(kv.value);
}
} else {
- if (_scenes_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET]) {
+ if (_scenes_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET] || dirty.flags[DIRTY_FLAGS_LAYER_IN_TREE]) {
// Update all cells.
for (KeyValue<Vector2i, CellData> &kv : tile_map) {
_scenes_update_cell(kv.value);
@@ -1257,6 +1253,11 @@ void TileMapLayer::_scenes_update() {
}
void TileMapLayer::_scenes_clear_cell(CellData &r_cell_data) {
+ const TileMap *tile_map_node = _fetch_tilemap();
+ if (!tile_map_node) {
+ return;
+ }
+
// Cleanup existing scene.
Node *node = tile_map_node->get_node_or_null(r_cell_data.scene);
if (node) {
@@ -1266,7 +1267,8 @@ void TileMapLayer::_scenes_clear_cell(CellData &r_cell_data) {
}
void TileMapLayer::_scenes_update_cell(CellData &r_cell_data) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
// Clear the scene in any case.
_scenes_clear_cell(r_cell_data);
@@ -1287,10 +1289,10 @@ void TileMapLayer::_scenes_update_cell(CellData &r_cell_data) {
Control *scene_as_control = Object::cast_to<Control>(scene);
Node2D *scene_as_node2d = Object::cast_to<Node2D>(scene);
if (scene_as_control) {
- scene_as_control->set_position(tile_map_node->map_to_local(r_cell_data.coords) + scene_as_control->get_position());
+ scene_as_control->set_position(tile_set->map_to_local(r_cell_data.coords) + scene_as_control->get_position());
} else if (scene_as_node2d) {
Transform2D xform;
- xform.set_origin(tile_map_node->map_to_local(r_cell_data.coords));
+ xform.set_origin(tile_set->map_to_local(r_cell_data.coords));
scene_as_node2d->set_transform(xform * scene_as_node2d->get_transform());
}
tile_map_node->add_child(scene);
@@ -1303,7 +1305,7 @@ void TileMapLayer::_scenes_update_cell(CellData &r_cell_data) {
#ifdef DEBUG_ENABLED
void TileMapLayer::_scenes_draw_cell_debug(const RID &p_canvas_item, const Vector2 &p_quadrant_pos, const CellData &r_cell_data) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND(!tile_set.is_valid());
if (!Engine::get_singleton()->is_editor_hint()) {
@@ -1341,7 +1343,7 @@ void TileMapLayer::_scenes_draw_cell_debug(const RID &p_canvas_item, const Vecto
// Draw a placeholder tile.
Transform2D cell_to_quadrant;
- cell_to_quadrant.set_origin(tile_map_node->map_to_local(r_cell_data.coords) - p_quadrant_pos);
+ cell_to_quadrant.set_origin(tile_set->map_to_local(r_cell_data.coords) - p_quadrant_pos);
rs->canvas_item_add_set_transform(p_canvas_item, cell_to_quadrant);
rs->canvas_item_add_circle(p_canvas_item, Vector2(), MIN(tile_set->get_tile_size().x, tile_set->get_tile_size().y) / 4.0, color);
}
@@ -1353,10 +1355,11 @@ void TileMapLayer::_scenes_draw_cell_debug(const RID &p_canvas_item, const Vecto
/////////////////////////////////////////////////////////////////////
void TileMapLayer::_build_runtime_update_tile_data() {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
// Check if we should cleanup everything.
- bool forced_cleanup = in_destructor || !enabled || !tile_map_node->is_inside_tree() || !tile_set.is_valid() || !tile_map_node->is_visible_in_tree();
+ bool forced_cleanup = in_destructor || !enabled || !tile_set.is_valid() || !is_visible_in_tree();
if (!forced_cleanup) {
if (tile_map_node->GDVIRTUAL_IS_OVERRIDDEN(_use_tile_data_runtime_update) && tile_map_node->GDVIRTUAL_IS_OVERRIDDEN(_tile_data_runtime_update)) {
if (_runtime_update_tile_data_was_cleaned_up || dirty.flags[DIRTY_FLAGS_TILE_MAP_TILE_SET]) {
@@ -1382,7 +1385,8 @@ void TileMapLayer::_build_runtime_update_tile_data() {
}
void TileMapLayer::_build_runtime_update_tile_data_for_cell(CellData &r_cell_data, bool p_auto_add_to_dirty_list) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ TileMap *tile_map_node = _fetch_tilemap();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
TileMapCell &c = r_cell_data.cell;
TileSetSource *source;
@@ -1425,7 +1429,7 @@ void TileMapLayer::_clear_runtime_update_tile_data() {
}
TileSet::TerrainsPattern TileMapLayer::_get_best_terrain_pattern_for_constraints(int p_terrain_set, const Vector2i &p_position, const RBSet<TerrainConstraint> &p_constraints, TileSet::TerrainsPattern p_current_pattern) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (!tile_set.is_valid()) {
return TileSet::TerrainsPattern();
}
@@ -1437,7 +1441,7 @@ TileSet::TerrainsPattern TileMapLayer::_get_best_terrain_pattern_for_constraints
int score = 0;
// Check the center bit constraint.
- TerrainConstraint terrain_constraint = TerrainConstraint(tile_map_node, p_position, terrain_pattern.get_terrain());
+ TerrainConstraint terrain_constraint = TerrainConstraint(tile_set, p_position, terrain_pattern.get_terrain());
const RBSet<TerrainConstraint>::Element *in_set_constraint_element = p_constraints.find(terrain_constraint);
if (in_set_constraint_element) {
if (in_set_constraint_element->get().get_terrain() != terrain_constraint.get_terrain()) {
@@ -1453,7 +1457,7 @@ TileSet::TerrainsPattern TileMapLayer::_get_best_terrain_pattern_for_constraints
TileSet::CellNeighbor bit = TileSet::CellNeighbor(i);
if (tile_set->is_valid_terrain_peering_bit(p_terrain_set, bit)) {
// Check if the bit is compatible with the constraints.
- TerrainConstraint terrain_bit_constraint = TerrainConstraint(tile_map_node, p_position, bit, terrain_pattern.get_terrain_peering_bit(bit));
+ TerrainConstraint terrain_bit_constraint = TerrainConstraint(tile_set, p_position, bit, terrain_pattern.get_terrain_peering_bit(bit));
in_set_constraint_element = p_constraints.find(terrain_bit_constraint);
if (in_set_constraint_element) {
if (in_set_constraint_element->get().get_terrain() != terrain_bit_constraint.get_terrain()) {
@@ -1486,19 +1490,19 @@ TileSet::TerrainsPattern TileMapLayer::_get_best_terrain_pattern_for_constraints
}
RBSet<TerrainConstraint> TileMapLayer::_get_terrain_constraints_from_added_pattern(const Vector2i &p_position, int p_terrain_set, TileSet::TerrainsPattern p_terrains_pattern) const {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (!tile_set.is_valid()) {
return RBSet<TerrainConstraint>();
}
// Compute the constraints needed from the surrounding tiles.
RBSet<TerrainConstraint> output;
- output.insert(TerrainConstraint(tile_map_node, p_position, p_terrains_pattern.get_terrain()));
+ output.insert(TerrainConstraint(tile_set, p_position, p_terrains_pattern.get_terrain()));
for (uint32_t i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) {
TileSet::CellNeighbor side = TileSet::CellNeighbor(i);
if (tile_set->is_valid_terrain_peering_bit(p_terrain_set, side)) {
- TerrainConstraint c = TerrainConstraint(tile_map_node, p_position, side, p_terrains_pattern.get_terrain_peering_bit(side));
+ TerrainConstraint c = TerrainConstraint(tile_set, p_position, side, p_terrains_pattern.get_terrain_peering_bit(side));
output.insert(c);
}
}
@@ -1507,7 +1511,7 @@ RBSet<TerrainConstraint> TileMapLayer::_get_terrain_constraints_from_added_patte
}
RBSet<TerrainConstraint> TileMapLayer::_get_terrain_constraints_from_painted_cells_list(const RBSet<Vector2i> &p_painted, int p_terrain_set, bool p_ignore_empty_terrains) const {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (!tile_set.is_valid()) {
return RBSet<TerrainConstraint>();
}
@@ -1520,7 +1524,7 @@ RBSet<TerrainConstraint> TileMapLayer::_get_terrain_constraints_from_painted_cel
for (int i = 0; i < TileSet::CELL_NEIGHBOR_MAX; i++) { // Iterates over neighbor bits.
TileSet::CellNeighbor bit = TileSet::CellNeighbor(i);
if (tile_set->is_valid_terrain_peering_bit(p_terrain_set, bit)) {
- dummy_constraints.insert(TerrainConstraint(tile_map_node, E, bit, -1));
+ dummy_constraints.insert(TerrainConstraint(tile_set, E, bit, -1));
}
}
}
@@ -1587,27 +1591,89 @@ RBSet<TerrainConstraint> TileMapLayer::_get_terrain_constraints_from_painted_cel
int terrain = (tile_data && tile_data->get_terrain_set() == p_terrain_set) ? tile_data->get_terrain() : -1;
if (!p_ignore_empty_terrains || terrain >= 0) {
- constraints.insert(TerrainConstraint(tile_map_node, E_coords, terrain));
+ constraints.insert(TerrainConstraint(tile_set, E_coords, terrain));
}
}
return constraints;
}
-void TileMapLayer::set_tile_map(TileMap *p_tile_map) {
- tile_map_node = p_tile_map;
+void TileMapLayer::_renamed() {
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
+}
+
+void TileMapLayer::_update_notify_local_transform() {
+ TileMap *tile_map_node = _fetch_tilemap();
+ bool notify = tile_map_node->is_collision_animatable() || is_y_sort_enabled();
+ if (!notify) {
+ if (is_y_sort_enabled()) {
+ notify = true;
+ }
+ }
+ set_notify_local_transform(notify);
+}
+
+void TileMapLayer::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_POSTINITIALIZE: {
+ connect(SNAME("renamed"), callable_mp(this, &TileMapLayer::_renamed));
+ break;
+ }
+ case NOTIFICATION_ENTER_TREE: {
+ _update_notify_local_transform();
+ dirty.flags[DIRTY_FLAGS_LAYER_IN_TREE] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->queue_internal_update();
+ } break;
+
+ case NOTIFICATION_EXIT_TREE: {
+ dirty.flags[DIRTY_FLAGS_LAYER_IN_TREE] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->queue_internal_update();
+ } break;
+
+ case TileMap::NOTIFICATION_ENTER_CANVAS: {
+ dirty.flags[DIRTY_FLAGS_LAYER_IN_CANVAS] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->queue_internal_update();
+ } break;
+
+ case TileMap::NOTIFICATION_EXIT_CANVAS: {
+ dirty.flags[DIRTY_FLAGS_LAYER_IN_CANVAS] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->queue_internal_update();
+ } break;
+
+ case TileMap::NOTIFICATION_VISIBILITY_CHANGED: {
+ dirty.flags[DIRTY_FLAGS_LAYER_VISIBILITY] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->queue_internal_update();
+ } break;
+ }
+
+ _rendering_notification(p_what);
+ _physics_notification(p_what);
+ _navigation_notification(p_what);
}
void TileMapLayer::set_layer_index_in_tile_map_node(int p_index) {
if (p_index == layer_index_in_tile_map_node) {
return;
}
+ TileMap *tile_map_node = _fetch_tilemap();
layer_index_in_tile_map_node = p_index;
dirty.flags[DIRTY_FLAGS_LAYER_INDEX_IN_TILE_MAP_NODE] = true;
tile_map_node->queue_internal_update();
}
Rect2 TileMapLayer::get_rect(bool &r_changed) const {
+ const Ref<TileSet> &tile_set = _fetch_tileset();
+ if (tile_set.is_null()) {
+ r_changed = rect_cache != Rect2();
+ return Rect2();
+ }
+
// Compute the displayed area of the tilemap.
r_changed = false;
#ifdef DEBUG_ENABLED
@@ -1617,7 +1683,7 @@ Rect2 TileMapLayer::get_rect(bool &r_changed) const {
bool first = true;
for (const KeyValue<Vector2i, CellData> &E : tile_map) {
Rect2 r;
- r.position = tile_map_node->map_to_local(E.key);
+ r.position = tile_set->map_to_local(E.key);
r.size = Size2();
if (first) {
r_total = r;
@@ -1637,7 +1703,7 @@ Rect2 TileMapLayer::get_rect(bool &r_changed) const {
}
HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_constraints(const Vector<Vector2i> &p_to_replace, int p_terrain_set, const RBSet<TerrainConstraint> &p_constraints) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (!tile_set.is_valid()) {
return HashMap<Vector2i, TileSet::TerrainsPattern>();
}
@@ -1686,7 +1752,7 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_constrain
HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_connect(const Vector<Vector2i> &p_coords_array, int p_terrain_set, int p_terrain, bool p_ignore_empty_terrains) {
HashMap<Vector2i, TileSet::TerrainsPattern> output;
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND_V(!tile_set.is_valid(), output);
ERR_FAIL_INDEX_V(p_terrain_set, tile_set->get_terrain_sets_count(), output);
@@ -1704,8 +1770,8 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_connect(c
// Find the adequate neighbor.
for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) {
TileSet::CellNeighbor bit = TileSet::CellNeighbor(j);
- if (tile_map_node->is_existing_neighbor(bit)) {
- Vector2i neighbor = tile_map_node->get_neighbor_cell(coords, bit);
+ if (tile_set->is_existing_neighbor(bit)) {
+ Vector2i neighbor = tile_set->get_neighbor_cell(coords, bit);
if (!can_modify_set.has(neighbor)) {
can_modify_list.push_back(neighbor);
can_modify_set.insert(neighbor);
@@ -1746,7 +1812,7 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_connect(c
// Add new constraints from the path drawn.
for (Vector2i coords : p_coords_array) {
// Constraints on the center bit.
- TerrainConstraint c = TerrainConstraint(tile_map_node, coords, p_terrain);
+ TerrainConstraint c = TerrainConstraint(tile_set, coords, p_terrain);
c.set_priority(10);
constraints.insert(c);
@@ -1754,11 +1820,11 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_connect(c
for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) {
TileSet::CellNeighbor bit = TileSet::CellNeighbor(j);
if (tile_set->is_valid_terrain_peering_bit(p_terrain_set, bit)) {
- c = TerrainConstraint(tile_map_node, coords, bit, p_terrain);
+ c = TerrainConstraint(tile_set, coords, bit, p_terrain);
c.set_priority(10);
if ((int(bit) % 2) == 0) {
// Side peering bits: add the constraint if the center is of the same terrain.
- Vector2i neighbor = tile_map_node->get_neighbor_cell(coords, bit);
+ Vector2i neighbor = tile_set->get_neighbor_cell(coords, bit);
if (cells_with_terrain_center_bit.has(neighbor)) {
constraints.insert(c);
}
@@ -1792,7 +1858,7 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_connect(c
HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_path(const Vector<Vector2i> &p_coords_array, int p_terrain_set, int p_terrain, bool p_ignore_empty_terrains) {
HashMap<Vector2i, TileSet::TerrainsPattern> output;
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND_V(!tile_set.is_valid(), output);
ERR_FAIL_INDEX_V(p_terrain_set, tile_set->get_terrain_sets_count(), output);
@@ -1803,8 +1869,8 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_path(cons
TileSet::CellNeighbor found_bit = TileSet::CELL_NEIGHBOR_MAX;
for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) {
TileSet::CellNeighbor bit = TileSet::CellNeighbor(j);
- if (tile_map_node->is_existing_neighbor(bit)) {
- if (tile_map_node->get_neighbor_cell(p_coords_array[i], bit) == p_coords_array[i + 1]) {
+ if (tile_set->is_existing_neighbor(bit)) {
+ if (tile_set->get_neighbor_cell(p_coords_array[i], bit) == p_coords_array[i + 1]) {
found_bit = bit;
break;
}
@@ -1829,7 +1895,7 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_path(cons
for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) {
TileSet::CellNeighbor bit = TileSet::CellNeighbor(j);
if (tile_set->is_valid_terrain_peering_bit(p_terrain_set, bit)) {
- Vector2i neighbor = tile_map_node->get_neighbor_cell(coords, bit);
+ Vector2i neighbor = tile_set->get_neighbor_cell(coords, bit);
if (!can_modify_set.has(neighbor)) {
can_modify_list.push_back(neighbor);
can_modify_set.insert(neighbor);
@@ -1843,13 +1909,13 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_path(cons
// Add new constraints from the path drawn.
for (Vector2i coords : p_coords_array) {
// Constraints on the center bit.
- TerrainConstraint c = TerrainConstraint(tile_map_node, coords, p_terrain);
+ TerrainConstraint c = TerrainConstraint(tile_set, coords, p_terrain);
c.set_priority(10);
constraints.insert(c);
}
for (int i = 0; i < p_coords_array.size() - 1; i++) {
// Constraints on the peering bits.
- TerrainConstraint c = TerrainConstraint(tile_map_node, p_coords_array[i], neighbor_list[i], p_terrain);
+ TerrainConstraint c = TerrainConstraint(tile_set, p_coords_array[i], neighbor_list[i], p_terrain);
c.set_priority(10);
constraints.insert(c);
}
@@ -1866,7 +1932,7 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_path(cons
HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_pattern(const Vector<Vector2i> &p_coords_array, int p_terrain_set, TileSet::TerrainsPattern p_terrains_pattern, bool p_ignore_empty_terrains) {
HashMap<Vector2i, TileSet::TerrainsPattern> output;
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND_V(!tile_set.is_valid(), output);
ERR_FAIL_INDEX_V(p_terrain_set, tile_set->get_terrain_sets_count(), output);
@@ -1885,7 +1951,7 @@ HashMap<Vector2i, TileSet::TerrainsPattern> TileMapLayer::terrain_fill_pattern(c
for (int j = 0; j < TileSet::CELL_NEIGHBOR_MAX; j++) {
TileSet::CellNeighbor bit = TileSet::CellNeighbor(j);
if (tile_set->is_valid_terrain_peering_bit(p_terrain_set, bit)) {
- Vector2i neighbor = tile_map_node->get_neighbor_cell(coords, bit);
+ Vector2i neighbor = tile_set->get_neighbor_cell(coords, bit);
if (!can_modify_set.has(neighbor)) {
can_modify_list.push_back(neighbor);
can_modify_set.insert(neighbor);
@@ -1922,7 +1988,7 @@ TileMapCell TileMapLayer::get_cell(const Vector2i &p_coords, bool p_use_proxies)
return TileMapCell();
} else {
TileMapCell c = tile_map.find(p_coords)->value.cell;
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (p_use_proxies && tile_set.is_valid()) {
Array proxyed = tile_set->map_tile_proxy(c.source_id, c.get_atlas_coords(), c.alternative_tile);
c.source_id = proxyed[0];
@@ -1998,7 +2064,7 @@ void TileMapLayer::set_tile_data(TileMapDataFormat p_format, const Vector<int> &
coord_y = decode_uint16(&local[10]);
}
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (tile_set.is_valid()) {
Array a = tile_set->compatibility_tilemap_map(v, Vector2i(coord_x, coord_y), flip_h, flip_v, transpose);
if (a.size() == 3) {
@@ -2039,9 +2105,9 @@ Vector<int> TileMapLayer::get_tile_data() const {
}
void TileMapLayer::notify_tile_map_change(DirtyFlags p_what) {
+ TileMap *tile_map_node = _fetch_tilemap();
dirty.flags[p_what] = true;
tile_map_node->queue_internal_update();
- _physics_notify_tilemap_change(p_what);
}
void TileMapLayer::internal_update() {
@@ -2124,7 +2190,10 @@ void TileMapLayer::set_cell(const Vector2i &p_coords, int p_source_id, const Vec
if (!E->value.dirty_list_element.in_list()) {
dirty.cell_list.add(&(E->value.dirty_list_element));
}
- tile_map_node->queue_internal_update();
+ TileMap *tile_map_node = _fetch_tilemap();
+ if (tile_map_node) { // Needed to avoid crashes in destructor.
+ tile_map_node->queue_internal_update();
+ }
used_rect_cache_dirty = true;
}
@@ -2141,7 +2210,7 @@ int TileMapLayer::get_cell_source_id(const Vector2i &p_coords, bool p_use_proxie
return TileSet::INVALID_SOURCE;
}
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (p_use_proxies && tile_set.is_valid()) {
Array proxyed = tile_set->map_tile_proxy(E->value.cell.source_id, E->value.cell.get_atlas_coords(), E->value.cell.alternative_tile);
return proxyed[0];
@@ -2158,7 +2227,7 @@ Vector2i TileMapLayer::get_cell_atlas_coords(const Vector2i &p_coords, bool p_us
return TileSetSource::INVALID_ATLAS_COORDS;
}
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (p_use_proxies && tile_set.is_valid()) {
Array proxyed = tile_set->map_tile_proxy(E->value.cell.source_id, E->value.cell.get_atlas_coords(), E->value.cell.alternative_tile);
return proxyed[1];
@@ -2175,7 +2244,7 @@ int TileMapLayer::get_cell_alternative_tile(const Vector2i &p_coords, bool p_use
return TileSetSource::INVALID_TILE_ALTERNATIVE;
}
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
if (p_use_proxies && tile_set.is_valid()) {
Array proxyed = tile_set->map_tile_proxy(E->value.cell.source_id, E->value.cell.get_atlas_coords(), E->value.cell.alternative_tile);
return proxyed[2];
@@ -2190,7 +2259,7 @@ TileData *TileMapLayer::get_cell_tile_data(const Vector2i &p_coords, bool p_use_
return nullptr;
}
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
Ref<TileSetAtlasSource> source = tile_set->get_source(source_id);
if (source.is_valid()) {
return source->get_tile_data(get_cell_atlas_coords(p_coords, p_use_proxies), get_cell_alternative_tile(p_coords, p_use_proxies));
@@ -2208,7 +2277,7 @@ void TileMapLayer::clear() {
}
Ref<TileMapPattern> TileMapLayer::get_pattern(TypedArray<Vector2i> p_coords_array) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND_V(!tile_set.is_valid(), nullptr);
Ref<TileMapPattern> output;
@@ -2262,19 +2331,19 @@ Ref<TileMapPattern> TileMapLayer::get_pattern(TypedArray<Vector2i> p_coords_arra
}
void TileMapLayer::set_pattern(const Vector2i &p_position, const Ref<TileMapPattern> p_pattern) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND(tile_set.is_null());
ERR_FAIL_COND(p_pattern.is_null());
TypedArray<Vector2i> used_cells = p_pattern->get_used_cells();
for (int i = 0; i < used_cells.size(); i++) {
- Vector2i coords = tile_map_node->map_pattern(p_position, used_cells[i], p_pattern);
+ Vector2i coords = tile_set->map_pattern(p_position, used_cells[i], p_pattern);
set_cell(coords, p_pattern->get_cell_source_id(used_cells[i]), p_pattern->get_cell_atlas_coords(used_cells[i]), p_pattern->get_cell_alternative_tile(used_cells[i]));
}
}
void TileMapLayer::set_cells_terrain_connect(TypedArray<Vector2i> p_cells, int p_terrain_set, int p_terrain, bool p_ignore_empty_terrains) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND(!tile_set.is_valid());
ERR_FAIL_INDEX(p_terrain_set, tile_set->get_terrain_sets_count());
@@ -2314,7 +2383,7 @@ void TileMapLayer::set_cells_terrain_connect(TypedArray<Vector2i> p_cells, int p
}
void TileMapLayer::set_cells_terrain_path(TypedArray<Vector2i> p_path, int p_terrain_set, int p_terrain, bool p_ignore_empty_terrains) {
- const Ref<TileSet> &tile_set = tile_map_node->get_tileset();
+ const Ref<TileSet> &tile_set = _fetch_tileset();
ERR_FAIL_COND(!tile_set.is_valid());
ERR_FAIL_INDEX(p_terrain_set, tile_set->get_terrain_sets_count());
@@ -2415,24 +2484,13 @@ Rect2i TileMapLayer::get_used_rect() const {
return used_rect_cache;
}
-void TileMapLayer::set_name(String p_name) {
- if (name == p_name) {
- return;
- }
- name = p_name;
- tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
-}
-
-String TileMapLayer::get_name() const {
- return name;
-}
-
void TileMapLayer::set_enabled(bool p_enabled) {
if (enabled == p_enabled) {
return;
}
enabled = p_enabled;
dirty.flags[DIRTY_FLAGS_LAYER_ENABLED] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
tile_map_node->queue_internal_update();
tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
@@ -2443,34 +2501,29 @@ bool TileMapLayer::is_enabled() const {
return enabled;
}
-void TileMapLayer::set_modulate(Color p_modulate) {
- if (modulate == p_modulate) {
+void TileMapLayer::set_self_modulate(const Color &p_self_modulate) {
+ if (get_self_modulate() == p_self_modulate) {
return;
}
- modulate = p_modulate;
- dirty.flags[DIRTY_FLAGS_LAYER_MODULATE] = true;
+ CanvasItem::set_self_modulate(p_self_modulate);
+ dirty.flags[DIRTY_FLAGS_LAYER_SELF_MODULATE] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
tile_map_node->queue_internal_update();
tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
}
-Color TileMapLayer::get_modulate() const {
- return modulate;
-}
-
void TileMapLayer::set_y_sort_enabled(bool p_y_sort_enabled) {
- if (y_sort_enabled == p_y_sort_enabled) {
+ if (is_y_sort_enabled() == p_y_sort_enabled) {
return;
}
- y_sort_enabled = p_y_sort_enabled;
+ CanvasItem::set_y_sort_enabled(p_y_sort_enabled);
dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ENABLED] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
tile_map_node->queue_internal_update();
tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
tile_map_node->update_configuration_warnings();
-}
-
-bool TileMapLayer::is_y_sort_enabled() const {
- return y_sort_enabled;
+ _update_notify_local_transform();
}
void TileMapLayer::set_y_sort_origin(int p_y_sort_origin) {
@@ -2479,6 +2532,7 @@ void TileMapLayer::set_y_sort_origin(int p_y_sort_origin) {
}
y_sort_origin = p_y_sort_origin;
dirty.flags[DIRTY_FLAGS_LAYER_Y_SORT_ORIGIN] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
tile_map_node->queue_internal_update();
tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
}
@@ -2488,19 +2542,28 @@ int TileMapLayer::get_y_sort_origin() const {
}
void TileMapLayer::set_z_index(int p_z_index) {
- if (z_index == p_z_index) {
+ if (get_z_index() == p_z_index) {
return;
}
- z_index = p_z_index;
+ CanvasItem::set_z_index(p_z_index);
dirty.flags[DIRTY_FLAGS_LAYER_Z_INDEX] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
tile_map_node->queue_internal_update();
tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
tile_map_node->update_configuration_warnings();
}
-int TileMapLayer::get_z_index() const {
- return z_index;
+void TileMapLayer::set_use_kinematic_bodies(bool p_use_kinematic_bodies) {
+ use_kinematic_bodies = p_use_kinematic_bodies;
+ dirty.flags[DIRTY_FLAGS_LAYER_USE_KINEMATIC_BODIES] = p_use_kinematic_bodies;
+ TileMap *tile_map_node = _fetch_tilemap();
+ tile_map_node->queue_internal_update();
+ tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
+}
+
+bool TileMapLayer::is_using_kinematic_bodies() const {
+ return use_kinematic_bodies;
}
void TileMapLayer::set_navigation_enabled(bool p_enabled) {
@@ -2509,6 +2572,7 @@ void TileMapLayer::set_navigation_enabled(bool p_enabled) {
}
navigation_enabled = p_enabled;
dirty.flags[DIRTY_FLAGS_LAYER_NAVIGATION_ENABLED] = true;
+ TileMap *tile_map_node = _fetch_tilemap();
tile_map_node->queue_internal_update();
tile_map_node->emit_signal(CoreStringNames::get_singleton()->changed);
}
@@ -2518,9 +2582,9 @@ bool TileMapLayer::is_navigation_enabled() const {
}
void TileMapLayer::set_navigation_map(RID p_map) {
- ERR_FAIL_COND_MSG(!tile_map_node->is_inside_tree(), "A TileMap navigation map can only be changed while inside the SceneTree.");
+ ERR_FAIL_COND_MSG(!is_inside_tree(), "A TileMap navigation map can only be changed while inside the SceneTree.");
navigation_map = p_map;
- uses_world_navigation_map = p_map == tile_map_node->get_world_2d()->get_navigation_map();
+ uses_world_navigation_map = p_map == get_world_2d()->get_navigation_map();
}
RID TileMapLayer::get_navigation_map() const {
@@ -2531,7 +2595,7 @@ RID TileMapLayer::get_navigation_map() const {
}
void TileMapLayer::fix_invalid_tiles() {
- Ref<TileSet> tileset = tile_map_node->get_tileset();
+ Ref<TileSet> tileset = _fetch_tileset();
ERR_FAIL_COND_MSG(tileset.is_null(), "Cannot call fix_invalid_tiles() on a TileMap without a valid TileSet.");
RBSet<Vector2i> coords;
@@ -2554,12 +2618,11 @@ Vector2i TileMapLayer::get_coords_for_body_rid(RID p_physics_body) const {
return bodies_coords[p_physics_body];
}
-TileMapLayer::~TileMapLayer() {
- if (!tile_map_node) {
- // Temporary layer.
- return;
- }
+TileMapLayer::TileMapLayer() {
+ set_notify_transform(true);
+}
+TileMapLayer::~TileMapLayer() {
in_destructor = true;
clear();
internal_update();
@@ -2569,26 +2632,24 @@ HashMap<Vector2i, TileSet::CellNeighbor> TerrainConstraint::get_overlapping_coor
HashMap<Vector2i, TileSet::CellNeighbor> output;
ERR_FAIL_COND_V(is_center_bit(), output);
+ ERR_FAIL_COND_V(!tile_set.is_valid(), output);
- Ref<TileSet> ts = tile_map->get_tileset();
- ERR_FAIL_COND_V(!ts.is_valid(), output);
-
- TileSet::TileShape shape = ts->get_tile_shape();
+ TileSet::TileShape shape = tile_set->get_tile_shape();
if (shape == TileSet::TILE_SHAPE_SQUARE) {
switch (bit) {
case 1:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_RIGHT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_SIDE;
break;
case 2:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER;
break;
case 3:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_SIDE;
break;
default:
ERR_FAIL_V(output);
@@ -2597,47 +2658,47 @@ HashMap<Vector2i, TileSet::CellNeighbor> TerrainConstraint::get_overlapping_coor
switch (bit) {
case 1:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE;
break;
case 2:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_CORNER)] = TileSet::CELL_NEIGHBOR_TOP_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_RIGHT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_CORNER)] = TileSet::CELL_NEIGHBOR_TOP_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_RIGHT_CORNER;
break;
case 3:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
break;
default:
ERR_FAIL_V(output);
}
} else {
// Half offset shapes.
- TileSet::TileOffsetAxis offset_axis = ts->get_tile_offset_axis();
+ TileSet::TileOffsetAxis offset_axis = tile_set->get_tile_offset_axis();
if (offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
switch (bit) {
case 1:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_RIGHT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_SIDE;
break;
case 2:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_CORNER;
break;
case 3:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE;
break;
case 4:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER;
break;
case 5:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
break;
default:
ERR_FAIL_V(output);
@@ -2646,25 +2707,25 @@ HashMap<Vector2i, TileSet::CellNeighbor> TerrainConstraint::get_overlapping_coor
switch (bit) {
case 1:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_RIGHT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
break;
case 2:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE;
break;
case 3:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_CORNER;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE)] = TileSet::CELL_NEIGHBOR_LEFT_CORNER;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER;
break;
case 4:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_SIDE;
break;
case 5:
output[base_cell_coords] = TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE;
- output[tile_map->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
+ output[tile_set->get_neighbor_cell(base_cell_coords, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE)] = TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
break;
default:
ERR_FAIL_V(output);
@@ -2674,25 +2735,20 @@ HashMap<Vector2i, TileSet::CellNeighbor> TerrainConstraint::get_overlapping_coor
return output;
}
-TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &p_position, int p_terrain) {
- tile_map = p_tile_map;
-
- Ref<TileSet> ts = tile_map->get_tileset();
- ERR_FAIL_COND(!ts.is_valid());
-
+TerrainConstraint::TerrainConstraint(Ref<TileSet> p_tile_set, const Vector2i &p_position, int p_terrain) {
+ ERR_FAIL_COND(!p_tile_set.is_valid());
+ tile_set = p_tile_set;
bit = 0;
base_cell_coords = p_position;
terrain = p_terrain;
}
-TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &p_position, const TileSet::CellNeighbor &p_bit, int p_terrain) {
+TerrainConstraint::TerrainConstraint(Ref<TileSet> p_tile_set, const Vector2i &p_position, const TileSet::CellNeighbor &p_bit, int p_terrain) {
// The way we build the constraint make it easy to detect conflicting constraints.
- tile_map = p_tile_map;
-
- Ref<TileSet> ts = tile_map->get_tileset();
- ERR_FAIL_COND(!ts.is_valid());
+ ERR_FAIL_COND(!p_tile_set.is_valid());
+ tile_set = p_tile_set;
- TileSet::TileShape shape = ts->get_tile_shape();
+ TileSet::TileShape shape = tile_set->get_tile_shape();
if (shape == TileSet::TILE_SHAPE_SQUARE) {
switch (p_bit) {
case TileSet::CELL_NEIGHBOR_RIGHT_SIDE:
@@ -2709,23 +2765,23 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
break;
case TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_LEFT_SIDE:
bit = 1;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER);
break;
case TileSet::CELL_NEIGHBOR_TOP_SIDE:
bit = 3;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
break;
default:
ERR_FAIL();
@@ -2735,7 +2791,7 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
switch (p_bit) {
case TileSet::CELL_NEIGHBOR_RIGHT_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE:
bit = 1;
@@ -2751,19 +2807,19 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
break;
case TileSet::CELL_NEIGHBOR_LEFT_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE:
bit = 1;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_CORNER);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_CORNER);
break;
case TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE:
bit = 3;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
break;
default:
ERR_FAIL();
@@ -2771,7 +2827,7 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
}
} else {
// Half-offset shapes.
- TileSet::TileOffsetAxis offset_axis = ts->get_tile_offset_axis();
+ TileSet::TileOffsetAxis offset_axis = tile_set->get_tile_offset_axis();
if (offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
switch (p_bit) {
case TileSet::CELL_NEIGHBOR_RIGHT_SIDE:
@@ -2796,31 +2852,31 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
break;
case TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_LEFT_SIDE:
bit = 1;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER:
bit = 4;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE:
bit = 3;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_CORNER:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE:
bit = 5;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER:
bit = 4;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
break;
default:
ERR_FAIL();
@@ -2846,7 +2902,7 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
break;
case TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER:
bit = 1;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE:
bit = 5;
@@ -2854,27 +2910,27 @@ TerrainConstraint::TerrainConstraint(const TileMap *p_tile_map, const Vector2i &
break;
case TileSet::CELL_NEIGHBOR_LEFT_CORNER:
bit = 3;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE:
bit = 2;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER:
bit = 1;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_SIDE:
bit = 4;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER:
bit = 3;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_SIDE);
break;
case TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE:
bit = 5;
- base_cell_coords = p_tile_map->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
+ base_cell_coords = tile_set->get_neighbor_cell(p_position, TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE);
break;
default:
ERR_FAIL();
diff --git a/scene/2d/tile_map_layer.h b/scene/2d/tile_map_layer.h
index f31c5406a4..2cc57f50d1 100644
--- a/scene/2d/tile_map_layer.h
+++ b/scene/2d/tile_map_layer.h
@@ -38,7 +38,7 @@ class TileSetAtlasSource;
class TerrainConstraint {
private:
- const TileMap *tile_map = nullptr;
+ Ref<TileSet> tile_set;
Vector2i base_cell_coords;
int bit = -1;
int terrain = -1;
@@ -83,8 +83,8 @@ public:
return priority;
}
- TerrainConstraint(const TileMap *p_tile_map, const Vector2i &p_position, int p_terrain); // For the center terrain bit
- TerrainConstraint(const TileMap *p_tile_map, const Vector2i &p_position, const TileSet::CellNeighbor &p_bit, int p_terrain); // For peering bits
+ TerrainConstraint(Ref<TileSet> p_tile_set, const Vector2i &p_position, int p_terrain); // For the center terrain bit
+ TerrainConstraint(Ref<TileSet> p_tile_set, const Vector2i &p_position, const TileSet::CellNeighbor &p_bit, int p_terrain); // For peering bits
TerrainConstraint(){};
};
@@ -214,23 +214,24 @@ public:
}
};
-class TileMapLayer : public RefCounted {
- GDCLASS(TileMapLayer, RefCounted);
+class TileMapLayer : public Node2D {
+ GDCLASS(TileMapLayer, Node2D);
public:
enum DirtyFlags {
DIRTY_FLAGS_LAYER_ENABLED = 0,
- DIRTY_FLAGS_LAYER_MODULATE,
+ DIRTY_FLAGS_LAYER_IN_TREE,
+ DIRTY_FLAGS_LAYER_IN_CANVAS,
+ DIRTY_FLAGS_LAYER_LOCAL_TRANSFORM,
+ DIRTY_FLAGS_LAYER_VISIBILITY,
+ DIRTY_FLAGS_LAYER_SELF_MODULATE,
DIRTY_FLAGS_LAYER_Y_SORT_ENABLED,
DIRTY_FLAGS_LAYER_Y_SORT_ORIGIN,
DIRTY_FLAGS_LAYER_Z_INDEX,
+ DIRTY_FLAGS_LAYER_USE_KINEMATIC_BODIES,
DIRTY_FLAGS_LAYER_NAVIGATION_ENABLED,
DIRTY_FLAGS_LAYER_INDEX_IN_TILE_MAP_NODE,
- DIRTY_FLAGS_TILE_MAP_IN_TREE,
- DIRTY_FLAGS_TILE_MAP_IN_CANVAS,
- DIRTY_FLAGS_TILE_MAP_VISIBILITY,
- DIRTY_FLAGS_TILE_MAP_XFORM,
- DIRTY_FLAGS_TILE_MAP_LOCAL_XFORM,
+
DIRTY_FLAGS_TILE_MAP_SELECTED_LAYER,
DIRTY_FLAGS_TILE_MAP_LIGHT_MASK,
DIRTY_FLAGS_TILE_MAP_MATERIAL,
@@ -239,7 +240,6 @@ public:
DIRTY_FLAGS_TILE_MAP_TEXTURE_REPEAT,
DIRTY_FLAGS_TILE_MAP_TILE_SET,
DIRTY_FLAGS_TILE_MAP_QUADRANT_SIZE,
- DIRTY_FLAGS_TILE_MAP_COLLISION_ANIMATABLE,
DIRTY_FLAGS_TILE_MAP_COLLISION_VISIBILITY_MODE,
DIRTY_FLAGS_TILE_MAP_NAVIGATION_VISIBILITY_MODE,
DIRTY_FLAGS_TILE_MAP_Y_SORT_ENABLED,
@@ -249,20 +249,15 @@ public:
private:
// Exposed properties.
- String name;
bool enabled = true;
- Color modulate = Color(1, 1, 1, 1);
- bool y_sort_enabled = false;
int y_sort_origin = 0;
- int z_index = 0;
+ bool use_kinematic_bodies = false;
bool navigation_enabled = true;
RID navigation_map;
bool uses_world_navigation_map = false;
// Internal.
- TileMap *tile_map_node = nullptr;
int layer_index_in_tile_map_node = -1;
- RID canvas_item;
HashMap<Vector2i, CellData> tile_map;
// Dirty flag. Allows knowing what was modified since the last update.
@@ -278,6 +273,10 @@ private:
mutable Rect2i used_rect_cache;
mutable bool used_rect_cache_dirty = true;
+ // Method to fetch the TileSet to use
+ TileMap *_fetch_tilemap() const;
+ Ref<TileSet> _fetch_tileset() const;
+
// Runtime tile data.
bool _runtime_update_tile_data_was_cleaned_up = false;
void _build_runtime_update_tile_data();
@@ -296,6 +295,7 @@ private:
HashMap<Vector2i, Ref<RenderingQuadrant>> rendering_quadrant_map;
bool _rendering_was_cleaned_up = false;
void _rendering_update();
+ void _rendering_notification(int p_what);
void _rendering_quadrants_update_cell(CellData &r_cell_data, SelfList<RenderingQuadrant>::List &r_dirty_rendering_quadrant_list);
void _rendering_occluders_clear_cell(CellData &r_cell_data);
void _rendering_occluders_update_cell(CellData &r_cell_data);
@@ -306,7 +306,7 @@ private:
HashMap<RID, Vector2i> bodies_coords; // Mapping for RID to coords.
bool _physics_was_cleaned_up = false;
void _physics_update();
- void _physics_notify_tilemap_change(DirtyFlags p_what);
+ void _physics_notification(int p_what);
void _physics_clear_cell(CellData &r_cell_data);
void _physics_update_cell(CellData &r_cell_data);
#ifdef DEBUG_ENABLED
@@ -315,6 +315,7 @@ private:
bool _navigation_was_cleaned_up = false;
void _navigation_update();
+ void _navigation_notification(int p_what);
void _navigation_clear_cell(CellData &r_cell_data);
void _navigation_update_cell(CellData &r_cell_data);
#ifdef DEBUG_ENABLED
@@ -334,9 +335,14 @@ private:
RBSet<TerrainConstraint> _get_terrain_constraints_from_added_pattern(const Vector2i &p_position, int p_terrain_set, TileSet::TerrainsPattern p_terrains_pattern) const;
RBSet<TerrainConstraint> _get_terrain_constraints_from_painted_cells_list(const RBSet<Vector2i> &p_painted, int p_terrain_set, bool p_ignore_empty_terrains) const;
+ void _renamed();
+ void _update_notify_local_transform();
+
+protected:
+ void _notification(int p_what);
+
public:
// TileMap node.
- void set_tile_map(TileMap *p_tile_map);
void set_layer_index_in_tile_map_node(int p_index);
// Rect caching.
@@ -383,18 +389,15 @@ public:
Rect2i get_used_rect() const;
// Layer properties.
- void set_name(String p_name);
- String get_name() const;
void set_enabled(bool p_enabled);
bool is_enabled() const;
- void set_modulate(Color p_modulate);
- Color get_modulate() const;
- void set_y_sort_enabled(bool p_y_sort_enabled);
- bool is_y_sort_enabled() const;
+ virtual void set_self_modulate(const Color &p_self_modulate) override;
+ virtual void set_y_sort_enabled(bool p_y_sort_enabled) override;
void set_y_sort_origin(int p_y_sort_origin);
int get_y_sort_origin() const;
- void set_z_index(int p_z_index);
- int get_z_index() const;
+ virtual void set_z_index(int p_z_index) override;
+ void set_use_kinematic_bodies(bool p_use_kinematic_bodies);
+ bool is_using_kinematic_bodies() const;
void set_navigation_enabled(bool p_enabled);
bool is_navigation_enabled() const;
void set_navigation_map(RID p_map);
@@ -407,6 +410,7 @@ public:
bool has_body_rid(RID p_physics_body) const;
Vector2i get_coords_for_body_rid(RID p_physics_body) const; // For finding tiles from collision.
+ TileMapLayer();
~TileMapLayer();
};
diff --git a/scene/3d/audio_stream_player_3d.cpp b/scene/3d/audio_stream_player_3d.cpp
index bfdbd14cc9..b01be4dffb 100644
--- a/scene/3d/audio_stream_player_3d.cpp
+++ b/scene/3d/audio_stream_player_3d.cpp
@@ -34,10 +34,10 @@
#include "scene/3d/area_3d.h"
#include "scene/3d/audio_listener_3d.h"
#include "scene/3d/camera_3d.h"
+#include "scene/3d/velocity_tracker_3d.h"
+#include "scene/audio/audio_stream_player_internal.h"
#include "scene/main/viewport.h"
-#include "scene/scene_string_names.h"
-
-#define PARAM_PREFIX "parameters/"
+#include "servers/audio/audio_stream.h"
// Based on "A Novel Multichannel Panning Method for Standard and Arbitrary Loudspeaker Configurations" by Ramy Sadek and Chris Kyriakakis (2004)
// Speaker-Placement Correction Amplitude Panning (SPCAP)
@@ -231,7 +231,7 @@ float AudioStreamPlayer3D::_get_attenuation_db(float p_distance) const {
}
}
- att += volume_db;
+ att += internal->volume_db;
if (att > max_db) {
att = max_db;
}
@@ -244,32 +244,12 @@ void AudioStreamPlayer3D::_notification(int p_what) {
case NOTIFICATION_ENTER_TREE: {
velocity_tracker->reset(get_global_transform().origin);
AudioServer::get_singleton()->add_listener_changed_callback(_listener_changed_cb, this);
- if (autoplay && !Engine::get_singleton()->is_editor_hint()) {
- play();
- }
- set_stream_paused(!can_process());
} break;
case NOTIFICATION_EXIT_TREE: {
- set_stream_paused(true);
AudioServer::get_singleton()->remove_listener_changed_callback(_listener_changed_cb, this);
} break;
- case NOTIFICATION_PREDELETE: {
- stop();
- } break;
-
- case NOTIFICATION_PAUSED: {
- if (!can_process()) {
- // Node can't process so we start fading out to silence.
- set_stream_paused(true);
- }
- } break;
-
- case NOTIFICATION_UNPAUSED: {
- set_stream_paused(false);
- } break;
-
case NOTIFICATION_TRANSFORM_CHANGED: {
if (doppler_tracking != DOPPLER_TRACKING_DISABLED) {
velocity_tracker->update_position(get_global_transform().origin);
@@ -279,13 +259,13 @@ void AudioStreamPlayer3D::_notification(int p_what) {
case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
// Update anything related to position first, if possible of course.
Vector<AudioFrame> volume_vector;
- if (setplay.get() > 0 || (active.is_set() && last_mix_count != AudioServer::get_singleton()->get_mix_count()) || force_update_panning) {
+ if (setplay.get() > 0 || (internal->active.is_set() && last_mix_count != AudioServer::get_singleton()->get_mix_count()) || force_update_panning) {
force_update_panning = false;
volume_vector = _update_panning();
}
if (setplayback.is_valid() && setplay.get() >= 0) {
- active.set();
+ internal->active.set();
HashMap<StringName, Vector<AudioFrame>> bus_map;
bus_map[_get_actual_bus()] = volume_vector;
AudioServer::get_singleton()->start_playback_stream(setplayback, bus_map, setplay.get(), actual_pitch_scale, linear_attenuation, attenuation_filter_cutoff_hz);
@@ -293,32 +273,10 @@ void AudioStreamPlayer3D::_notification(int p_what) {
setplay.set(-1);
}
- if (!stream_playbacks.is_empty() && active.is_set()) {
- // Stop playing if no longer active.
- Vector<Ref<AudioStreamPlayback>> playbacks_to_remove;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (playback.is_valid() && !AudioServer::get_singleton()->is_playback_active(playback) && !AudioServer::get_singleton()->is_playback_paused(playback)) {
- playbacks_to_remove.push_back(playback);
- }
- }
- // Now go through and remove playbacks that have finished. Removing elements from a Vector in a range based for is asking for trouble.
- for (Ref<AudioStreamPlayback> &playback : playbacks_to_remove) {
- stream_playbacks.erase(playback);
- }
- if (!playbacks_to_remove.is_empty() && stream_playbacks.is_empty()) {
- // This node is no longer actively playing audio.
- active.clear();
- set_physics_process_internal(false);
- }
- if (!playbacks_to_remove.is_empty()) {
- emit_signal(SNAME("finished"));
- }
- }
-
- while (stream_playbacks.size() > max_polyphony) {
- AudioServer::get_singleton()->stop_playback_stream(stream_playbacks[0]);
- stream_playbacks.remove_at(0);
+ if (!internal->stream_playbacks.is_empty() && internal->active.is_set()) {
+ internal->process();
}
+ internal->ensure_playback_limit();
} break;
}
}
@@ -362,16 +320,16 @@ Area3D *AudioStreamPlayer3D::_get_overriding_area() {
return nullptr;
}
-// Interacts with PhysicsServer3D, so can only be called during _physics_process
+// Interacts with PhysicsServer3D, so can only be called during _physics_process.
StringName AudioStreamPlayer3D::_get_actual_bus() {
Area3D *overriding_area = _get_overriding_area();
if (overriding_area && overriding_area->is_overriding_audio_bus() && !overriding_area->is_using_reverb_bus()) {
return overriding_area->get_audio_bus_name();
}
- return bus;
+ return internal->bus;
}
-// Interacts with PhysicsServer3D, so can only be called during _physics_process
+// Interacts with PhysicsServer3D, so can only be called during _physics_process.
Vector<AudioFrame> AudioStreamPlayer3D::_update_panning() {
Vector<AudioFrame> output_volume_vector;
output_volume_vector.resize(4);
@@ -379,7 +337,7 @@ Vector<AudioFrame> AudioStreamPlayer3D::_update_panning() {
frame = AudioFrame(0, 0);
}
- if (!active.is_set() || stream.is_null()) {
+ if (!internal->active.is_set() || internal->stream.is_null()) {
return output_volume_vector;
}
@@ -463,7 +421,7 @@ Vector<AudioFrame> AudioStreamPlayer3D::_update_panning() {
}
linear_attenuation = Math::db_to_linear(db_att);
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ for (Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
AudioServer::get_singleton()->set_playback_highshelf_params(playback, linear_attenuation, attenuation_filter_cutoff_hz);
}
// Bake in a constant factor here to allow the project setting defaults for 2d and 3d to be normalized to 1.0.
@@ -489,10 +447,10 @@ Vector<AudioFrame> AudioStreamPlayer3D::_update_panning() {
bus_volumes[reverb_bus_name] = reverb_vol;
}
} else {
- bus_volumes[bus] = output_volume_vector;
+ bus_volumes[internal->bus] = output_volume_vector;
}
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ for (Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
AudioServer::get_singleton()->set_playback_bus_volumes_linear(playback, bus_volumes);
}
@@ -510,64 +468,37 @@ Vector<AudioFrame> AudioStreamPlayer3D::_update_panning() {
float velocity = local_velocity.length();
float speed_of_sound = 343.0;
- float doppler_pitch_scale = pitch_scale * speed_of_sound / (speed_of_sound + velocity * approaching);
+ float doppler_pitch_scale = internal->pitch_scale * speed_of_sound / (speed_of_sound + velocity * approaching);
doppler_pitch_scale = CLAMP(doppler_pitch_scale, (1 / 8.0), 8.0); //avoid crazy stuff
actual_pitch_scale = doppler_pitch_scale;
} else {
- actual_pitch_scale = pitch_scale;
+ actual_pitch_scale = internal->pitch_scale;
}
} else {
- actual_pitch_scale = pitch_scale;
+ actual_pitch_scale = internal->pitch_scale;
}
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ for (Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
AudioServer::get_singleton()->set_playback_pitch_scale(playback, actual_pitch_scale);
}
}
return output_volume_vector;
}
-void AudioStreamPlayer3D::_update_stream_parameters() {
- if (stream.is_null()) {
- return;
- }
- List<AudioStream::Parameter> parameters;
- stream->get_parameter_list(&parameters);
- for (const AudioStream::Parameter &K : parameters) {
- const PropertyInfo &pi = K.property;
- StringName key = PARAM_PREFIX + pi.name;
- if (!playback_parameters.has(key)) {
- ParameterData pd;
- pd.path = pi.name;
- pd.value = K.default_value;
- playback_parameters.insert(key, pd);
- }
- }
-}
-
void AudioStreamPlayer3D::set_stream(Ref<AudioStream> p_stream) {
- if (stream.is_valid()) {
- stream->disconnect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayer3D::_update_stream_parameters));
- }
- stop();
- stream = p_stream;
- _update_stream_parameters();
- if (stream.is_valid()) {
- stream->connect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayer3D::_update_stream_parameters));
- }
- notify_property_list_changed();
+ internal->set_stream(p_stream);
}
Ref<AudioStream> AudioStreamPlayer3D::get_stream() const {
- return stream;
+ return internal->stream;
}
void AudioStreamPlayer3D::set_volume_db(float p_volume) {
- volume_db = p_volume;
+ internal->volume_db = p_volume;
}
float AudioStreamPlayer3D::get_volume_db() const {
- return volume_db;
+ return internal->volume_db;
}
void AudioStreamPlayer3D::set_unit_size(float p_volume) {
@@ -588,34 +519,20 @@ float AudioStreamPlayer3D::get_max_db() const {
}
void AudioStreamPlayer3D::set_pitch_scale(float p_pitch_scale) {
- ERR_FAIL_COND(!(p_pitch_scale > 0.0));
- pitch_scale = p_pitch_scale;
+ internal->set_pitch_scale(p_pitch_scale);
}
float AudioStreamPlayer3D::get_pitch_scale() const {
- return pitch_scale;
+ return internal->pitch_scale;
}
void AudioStreamPlayer3D::play(float p_from_pos) {
- if (stream.is_null()) {
+ Ref<AudioStreamPlayback> stream_playback = internal->play_basic();
+ if (stream_playback.is_null()) {
return;
}
- ERR_FAIL_COND_MSG(!is_inside_tree(), "Playback can only happen when a node is inside the scene tree");
- if (stream->is_monophonic() && is_playing()) {
- stop();
- }
- Ref<AudioStreamPlayback> stream_playback = stream->instantiate_playback();
- ERR_FAIL_COND_MSG(stream_playback.is_null(), "Failed to instantiate playback.");
-
- for (const KeyValue<StringName, ParameterData> &K : playback_parameters) {
- stream_playback->set_parameter(K.value.path, K.value.value);
- }
-
- stream_playbacks.push_back(stream_playback);
setplayback = stream_playback;
setplay.set(p_from_pos);
- active.set();
- set_physics_process_internal(true);
}
void AudioStreamPlayer3D::seek(float p_seconds) {
@@ -627,83 +544,46 @@ void AudioStreamPlayer3D::seek(float p_seconds) {
void AudioStreamPlayer3D::stop() {
setplay.set(-1);
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->stop_playback_stream(playback);
- }
- stream_playbacks.clear();
- active.clear();
- set_physics_process_internal(false);
+ internal->stop();
}
bool AudioStreamPlayer3D::is_playing() const {
- for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (AudioServer::get_singleton()->is_playback_active(playback)) {
- return true;
- }
- }
if (setplay.get() >= 0) {
return true; // play() has been called this frame, but no playback exists just yet.
}
- return false;
+ return internal->is_playing();
}
float AudioStreamPlayer3D::get_playback_position() {
- // Return the playback position of the most recently started playback stream.
- if (!stream_playbacks.is_empty()) {
- return AudioServer::get_singleton()->get_playback_position(stream_playbacks[stream_playbacks.size() - 1]);
- }
- return 0;
+ return internal->get_playback_position();
}
void AudioStreamPlayer3D::set_bus(const StringName &p_bus) {
- //if audio is active, must lock this
- AudioServer::get_singleton()->lock();
- bus = p_bus;
- AudioServer::get_singleton()->unlock();
+ internal->bus = p_bus; // This will be pushed to the audio server during the next physics timestep, which is fast enough.
}
StringName AudioStreamPlayer3D::get_bus() const {
- for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
- if (AudioServer::get_singleton()->get_bus_name(i) == bus) {
- return bus;
- }
- }
- return SceneStringNames::get_singleton()->Master;
+ return internal->get_bus();
}
void AudioStreamPlayer3D::set_autoplay(bool p_enable) {
- autoplay = p_enable;
+ internal->autoplay = p_enable;
}
bool AudioStreamPlayer3D::is_autoplay_enabled() {
- return autoplay;
+ return internal->autoplay;
}
void AudioStreamPlayer3D::_set_playing(bool p_enable) {
- if (p_enable) {
- play();
- } else {
- stop();
- }
+ internal->set_playing(p_enable);
}
bool AudioStreamPlayer3D::_is_active() const {
- return active.is_set();
+ return internal->is_active();
}
void AudioStreamPlayer3D::_validate_property(PropertyInfo &p_property) const {
- if (p_property.name == "bus") {
- String options;
- for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
- if (i > 0) {
- options += ",";
- }
- String name = AudioServer::get_singleton()->get_bus_name(i);
- options += name;
- }
-
- p_property.hint_string = options;
- }
+ internal->validate_property(p_property);
}
void AudioStreamPlayer3D::set_max_distance(float p_metres) {
@@ -800,37 +680,27 @@ AudioStreamPlayer3D::DopplerTracking AudioStreamPlayer3D::get_doppler_tracking()
}
void AudioStreamPlayer3D::set_stream_paused(bool p_pause) {
- // TODO this does not have perfect recall, fix that maybe? If there are zero playbacks registered with the AudioServer, this bool isn't persisted.
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->set_playback_paused(playback, p_pause);
- }
+ internal->set_stream_paused(p_pause);
}
bool AudioStreamPlayer3D::get_stream_paused() const {
- // There's currently no way to pause some playback streams but not others. Check the first and don't bother looking at the rest.
- if (!stream_playbacks.is_empty()) {
- return AudioServer::get_singleton()->is_playback_paused(stream_playbacks[0]);
- }
- return false;
+ return internal->get_stream_paused();
}
bool AudioStreamPlayer3D::has_stream_playback() {
- return !stream_playbacks.is_empty();
+ return internal->has_stream_playback();
}
Ref<AudioStreamPlayback> AudioStreamPlayer3D::get_stream_playback() {
- ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback().");
- return stream_playbacks[stream_playbacks.size() - 1];
+ return internal->get_stream_playback();
}
void AudioStreamPlayer3D::set_max_polyphony(int p_max_polyphony) {
- if (p_max_polyphony > 0) {
- max_polyphony = p_max_polyphony;
- }
+ internal->set_max_polyphony(p_max_polyphony);
}
int AudioStreamPlayer3D::get_max_polyphony() const {
- return max_polyphony;
+ return internal->max_polyphony;
}
void AudioStreamPlayer3D::set_panning_strength(float p_panning_strength) {
@@ -842,48 +712,16 @@ float AudioStreamPlayer3D::get_panning_strength() const {
return panning_strength;
}
-void AudioStreamPlayer3D::_on_bus_layout_changed() {
- notify_property_list_changed();
-}
-
-void AudioStreamPlayer3D::_on_bus_renamed(int p_bus_index, const StringName &p_old_name, const StringName &p_new_name) {
- notify_property_list_changed();
-}
-
bool AudioStreamPlayer3D::_set(const StringName &p_name, const Variant &p_value) {
- HashMap<StringName, ParameterData>::Iterator I = playback_parameters.find(p_name);
- if (!I) {
- return false;
- }
- ParameterData &pd = I->value;
- pd.value = p_value;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- playback->set_parameter(pd.path, pd.value);
- }
- return true;
+ return internal->set(p_name, p_value);
}
bool AudioStreamPlayer3D::_get(const StringName &p_name, Variant &r_ret) const {
- HashMap<StringName, ParameterData>::ConstIterator I = playback_parameters.find(p_name);
- if (!I) {
- return false;
- }
-
- r_ret = I->value.value;
- return true;
+ return internal->get(p_name, r_ret);
}
void AudioStreamPlayer3D::_get_property_list(List<PropertyInfo> *p_list) const {
- if (stream.is_null()) {
- return;
- }
- List<AudioStream::Parameter> parameters;
- stream->get_parameter_list(&parameters);
- for (const AudioStream::Parameter &K : parameters) {
- PropertyInfo pi = K.property;
- pi.name = PARAM_PREFIX + pi.name;
- p_list->push_back(pi);
- }
+ internal->get_property_list(p_list);
}
void AudioStreamPlayer3D::_bind_methods() {
@@ -994,12 +832,12 @@ void AudioStreamPlayer3D::_bind_methods() {
}
AudioStreamPlayer3D::AudioStreamPlayer3D() {
+ internal = memnew(AudioStreamPlayerInternal(this, callable_mp(this, &AudioStreamPlayer3D::play), true));
velocity_tracker.instantiate();
- AudioServer::get_singleton()->connect("bus_layout_changed", callable_mp(this, &AudioStreamPlayer3D::_on_bus_layout_changed));
- AudioServer::get_singleton()->connect("bus_renamed", callable_mp(this, &AudioStreamPlayer3D::_on_bus_renamed));
set_disable_scale(true);
cached_global_panning_strength = GLOBAL_GET("audio/general/3d_panning_strength");
}
AudioStreamPlayer3D::~AudioStreamPlayer3D() {
+ memdelete(internal);
}
diff --git a/scene/3d/audio_stream_player_3d.h b/scene/3d/audio_stream_player_3d.h
index facded1b9c..3cc1efaf67 100644
--- a/scene/3d/audio_stream_player_3d.h
+++ b/scene/3d/audio_stream_player_3d.h
@@ -31,15 +31,16 @@
#ifndef AUDIO_STREAM_PLAYER_3D_H
#define AUDIO_STREAM_PLAYER_3D_H
-#include "core/os/mutex.h"
-#include "scene/3d/area_3d.h"
#include "scene/3d/node_3d.h"
-#include "scene/3d/velocity_tracker_3d.h"
-#include "servers/audio/audio_filter_sw.h"
-#include "servers/audio/audio_stream.h"
-#include "servers/audio_server.h"
+class Area3D;
+struct AudioFrame;
+class AudioStream;
+class AudioStreamPlayback;
+class AudioStreamPlayerInternal;
class Camera3D;
+class VelocityTracker3D;
+
class AudioStreamPlayer3D : public Node3D {
GDCLASS(AudioStreamPlayer3D, Node3D);
@@ -64,23 +65,16 @@ private:
};
- Vector<Ref<AudioStreamPlayback>> stream_playbacks;
- Ref<AudioStream> stream;
+ AudioStreamPlayerInternal *internal = nullptr;
- SafeFlag active{ false };
SafeNumeric<float> setplay{ -1.0 };
Ref<AudioStreamPlayback> setplayback;
AttenuationModel attenuation_model = ATTENUATION_INVERSE_DISTANCE;
- float volume_db = 0.0;
float unit_size = 10.0;
float max_db = 3.0;
- float pitch_scale = 1.0;
// Internally used to take doppler tracking into account.
float actual_pitch_scale = 1.0;
- bool autoplay = false;
- StringName bus = SNAME("Master");
- int max_polyphony = 1;
uint64_t last_mix_count = -1;
bool force_update_panning = false;
@@ -97,9 +91,6 @@ private:
Area3D *_get_overriding_area();
Vector<AudioFrame> _update_panning();
- void _on_bus_layout_changed();
- void _on_bus_renamed(int p_bus_index, const StringName &p_old_name, const StringName &p_new_name);
-
uint32_t area_mask = 1;
bool emission_angle_enabled = false;
@@ -121,14 +112,6 @@ private:
float panning_strength = 1.0f;
float cached_global_panning_strength = 0.5f;
- struct ParameterData {
- StringName path;
- Variant value;
- };
-
- HashMap<StringName, ParameterData> playback_parameters;
- void _update_stream_parameters();
-
protected:
void _validate_property(PropertyInfo &p_property) const;
void _notification(int p_what);
diff --git a/scene/3d/collision_object_3d.cpp b/scene/3d/collision_object_3d.cpp
index 97b1e282ad..0cfe0f8cb7 100644
--- a/scene/3d/collision_object_3d.cpp
+++ b/scene/3d/collision_object_3d.cpp
@@ -624,6 +624,7 @@ void CollisionObject3D::shape_owner_add_shape(uint32_t p_owner, const Ref<Shape3
total_subshapes++;
_update_shape_data(p_owner);
+ update_gizmos();
}
int CollisionObject3D::shape_owner_get_shape_count(uint32_t p_owner) const {
@@ -687,6 +688,8 @@ void CollisionObject3D::shape_owner_clear_shapes(uint32_t p_owner) {
while (shape_owner_get_shape_count(p_owner) > 0) {
shape_owner_remove_shape(p_owner, 0);
}
+
+ update_gizmos();
}
uint32_t CollisionObject3D::shape_find_owner(int p_shape_index) const {
diff --git a/scene/audio/audio_stream_player.cpp b/scene/audio/audio_stream_player.cpp
index bd4731d8dd..d7582526a3 100644
--- a/scene/audio/audio_stream_player.cpp
+++ b/scene/audio/audio_stream_player.cpp
@@ -30,253 +30,104 @@
#include "audio_stream_player.h"
-#include "core/config/engine.h"
-#include "core/math/audio_frame.h"
-#include "servers/audio_server.h"
-
-#define PARAM_PREFIX "parameters/"
+#include "scene/audio/audio_stream_player_internal.h"
+#include "servers/audio/audio_stream.h"
void AudioStreamPlayer::_notification(int p_what) {
- switch (p_what) {
- case NOTIFICATION_ENTER_TREE: {
- if (autoplay && !Engine::get_singleton()->is_editor_hint()) {
- play();
- }
- set_stream_paused(!can_process());
- } break;
-
- case NOTIFICATION_INTERNAL_PROCESS: {
- Vector<Ref<AudioStreamPlayback>> playbacks_to_remove;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (playback.is_valid() && !AudioServer::get_singleton()->is_playback_active(playback) && !AudioServer::get_singleton()->is_playback_paused(playback)) {
- playbacks_to_remove.push_back(playback);
- }
- }
- // Now go through and remove playbacks that have finished. Removing elements from a Vector in a range based for is asking for trouble.
- for (Ref<AudioStreamPlayback> &playback : playbacks_to_remove) {
- stream_playbacks.erase(playback);
- }
- if (!playbacks_to_remove.is_empty() && stream_playbacks.is_empty()) {
- // This node is no longer actively playing audio.
- active.clear();
- set_process_internal(false);
- }
- if (!playbacks_to_remove.is_empty()) {
- emit_signal(SNAME("finished"));
- }
- } break;
-
- case NOTIFICATION_EXIT_TREE: {
- set_stream_paused(true);
- } break;
-
- case NOTIFICATION_PREDELETE: {
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->stop_playback_stream(playback);
- }
- stream_playbacks.clear();
- } break;
-
- case NOTIFICATION_PAUSED: {
- if (!can_process()) {
- // Node can't process so we start fading out to silence
- set_stream_paused(true);
- }
- } break;
-
- case NOTIFICATION_UNPAUSED: {
- set_stream_paused(false);
- } break;
- }
-}
-
-void AudioStreamPlayer::_update_stream_parameters() {
- if (stream.is_null()) {
- return;
- }
- List<AudioStream::Parameter> parameters;
- stream->get_parameter_list(&parameters);
- for (const AudioStream::Parameter &K : parameters) {
- const PropertyInfo &pi = K.property;
- StringName key = PARAM_PREFIX + pi.name;
- if (!playback_parameters.has(key)) {
- ParameterData pd;
- pd.path = pi.name;
- pd.value = K.default_value;
- playback_parameters.insert(key, pd);
- }
- }
+ internal->notification(p_what);
}
void AudioStreamPlayer::set_stream(Ref<AudioStream> p_stream) {
- if (stream.is_valid()) {
- stream->disconnect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayer::_update_stream_parameters));
- }
- stop();
- stream = p_stream;
- _update_stream_parameters();
- if (stream.is_valid()) {
- stream->connect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayer::_update_stream_parameters));
- }
- notify_property_list_changed();
+ internal->set_stream(p_stream);
}
bool AudioStreamPlayer::_set(const StringName &p_name, const Variant &p_value) {
- HashMap<StringName, ParameterData>::Iterator I = playback_parameters.find(p_name);
- if (!I) {
- return false;
- }
- ParameterData &pd = I->value;
- pd.value = p_value;
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- playback->set_parameter(pd.path, pd.value);
- }
- return true;
+ return internal->set(p_name, p_value);
}
bool AudioStreamPlayer::_get(const StringName &p_name, Variant &r_ret) const {
- HashMap<StringName, ParameterData>::ConstIterator I = playback_parameters.find(p_name);
- if (!I) {
- return false;
- }
-
- r_ret = I->value.value;
- return true;
+ return internal->get(p_name, r_ret);
}
void AudioStreamPlayer::_get_property_list(List<PropertyInfo> *p_list) const {
- if (stream.is_null()) {
- return;
- }
- List<AudioStream::Parameter> parameters;
- stream->get_parameter_list(&parameters);
- for (const AudioStream::Parameter &K : parameters) {
- PropertyInfo pi = K.property;
- pi.name = PARAM_PREFIX + pi.name;
- p_list->push_back(pi);
- }
+ internal->get_property_list(p_list);
}
Ref<AudioStream> AudioStreamPlayer::get_stream() const {
- return stream;
+ return internal->stream;
}
void AudioStreamPlayer::set_volume_db(float p_volume) {
- volume_db = p_volume;
+ internal->volume_db = p_volume;
Vector<AudioFrame> volume_vector = _get_volume_vector();
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ for (Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
AudioServer::get_singleton()->set_playback_all_bus_volumes_linear(playback, volume_vector);
}
}
float AudioStreamPlayer::get_volume_db() const {
- return volume_db;
+ return internal->volume_db;
}
void AudioStreamPlayer::set_pitch_scale(float p_pitch_scale) {
- ERR_FAIL_COND(!(p_pitch_scale > 0.0));
- pitch_scale = p_pitch_scale;
-
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->set_playback_pitch_scale(playback, pitch_scale);
- }
+ internal->set_pitch_scale(p_pitch_scale);
}
float AudioStreamPlayer::get_pitch_scale() const {
- return pitch_scale;
+ return internal->pitch_scale;
}
void AudioStreamPlayer::set_max_polyphony(int p_max_polyphony) {
- if (p_max_polyphony > 0) {
- max_polyphony = p_max_polyphony;
- }
+ internal->set_max_polyphony(p_max_polyphony);
}
int AudioStreamPlayer::get_max_polyphony() const {
- return max_polyphony;
+ return internal->max_polyphony;
}
void AudioStreamPlayer::play(float p_from_pos) {
- if (stream.is_null()) {
+ Ref<AudioStreamPlayback> stream_playback = internal->play_basic();
+ if (stream_playback.is_null()) {
return;
}
- ERR_FAIL_COND_MSG(!is_inside_tree(), "Playback can only happen when a node is inside the scene tree");
- if (stream->is_monophonic() && is_playing()) {
- stop();
- }
- Ref<AudioStreamPlayback> stream_playback = stream->instantiate_playback();
- ERR_FAIL_COND_MSG(stream_playback.is_null(), "Failed to instantiate playback.");
-
- for (const KeyValue<StringName, ParameterData> &K : playback_parameters) {
- stream_playback->set_parameter(K.value.path, K.value.value);
- }
-
- AudioServer::get_singleton()->start_playback_stream(stream_playback, bus, _get_volume_vector(), p_from_pos, pitch_scale);
- stream_playbacks.push_back(stream_playback);
- active.set();
- set_process_internal(true);
- while (stream_playbacks.size() > max_polyphony) {
- AudioServer::get_singleton()->stop_playback_stream(stream_playbacks[0]);
- stream_playbacks.remove_at(0);
- }
+ AudioServer::get_singleton()->start_playback_stream(stream_playback, internal->bus, _get_volume_vector(), p_from_pos, internal->pitch_scale);
+ internal->ensure_playback_limit();
}
void AudioStreamPlayer::seek(float p_seconds) {
- if (is_playing()) {
- stop();
- play(p_seconds);
- }
+ internal->seek(p_seconds);
}
void AudioStreamPlayer::stop() {
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->stop_playback_stream(playback);
- }
- stream_playbacks.clear();
- active.clear();
- set_process_internal(false);
+ internal->stop();
}
bool AudioStreamPlayer::is_playing() const {
- for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (AudioServer::get_singleton()->is_playback_active(playback)) {
- return true;
- }
- }
- return false;
+ return internal->is_playing();
}
float AudioStreamPlayer::get_playback_position() {
- // Return the playback position of the most recently started playback stream.
- if (!stream_playbacks.is_empty()) {
- return AudioServer::get_singleton()->get_playback_position(stream_playbacks[stream_playbacks.size() - 1]);
- }
- return 0;
+ return internal->get_playback_position();
}
void AudioStreamPlayer::set_bus(const StringName &p_bus) {
- bus = p_bus;
- for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ internal->bus = p_bus;
+ for (const Ref<AudioStreamPlayback> &playback : internal->stream_playbacks) {
AudioServer::get_singleton()->set_playback_bus_exclusive(playback, p_bus, _get_volume_vector());
}
}
StringName AudioStreamPlayer::get_bus() const {
- for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
- if (AudioServer::get_singleton()->get_bus_name(i) == String(bus)) {
- return bus;
- }
- }
- return SceneStringNames::get_singleton()->Master;
+ return internal->get_bus();
}
void AudioStreamPlayer::set_autoplay(bool p_enable) {
- autoplay = p_enable;
+ internal->autoplay = p_enable;
}
bool AudioStreamPlayer::is_autoplay_enabled() {
- return autoplay;
+ return internal->autoplay;
}
void AudioStreamPlayer::set_mix_target(MixTarget p_target) {
@@ -288,43 +139,19 @@ AudioStreamPlayer::MixTarget AudioStreamPlayer::get_mix_target() const {
}
void AudioStreamPlayer::_set_playing(bool p_enable) {
- if (p_enable) {
- play();
- } else {
- stop();
- }
+ internal->set_playing(p_enable);
}
bool AudioStreamPlayer::_is_active() const {
- for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- if (AudioServer::get_singleton()->is_playback_active(playback)) {
- return true;
- }
- }
- return false;
-}
-
-void AudioStreamPlayer::_on_bus_layout_changed() {
- notify_property_list_changed();
-}
-
-void AudioStreamPlayer::_on_bus_renamed(int p_bus_index, const StringName &p_old_name, const StringName &p_new_name) {
- notify_property_list_changed();
+ return internal->is_active();
}
void AudioStreamPlayer::set_stream_paused(bool p_pause) {
- // TODO this does not have perfect recall, fix that maybe? If there are zero playbacks registered with the AudioServer, this bool isn't persisted.
- for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
- AudioServer::get_singleton()->set_playback_paused(playback, p_pause);
- }
+ internal->set_stream_paused(p_pause);
}
bool AudioStreamPlayer::get_stream_paused() const {
- // There's currently no way to pause some playback streams but not others. Check the first and don't bother looking at the rest.
- if (!stream_playbacks.is_empty()) {
- return AudioServer::get_singleton()->is_playback_paused(stream_playbacks[0]);
- }
- return false;
+ return internal->get_stream_paused();
}
Vector<AudioFrame> AudioStreamPlayer::_get_volume_vector() {
@@ -337,7 +164,7 @@ Vector<AudioFrame> AudioStreamPlayer::_get_volume_vector() {
channel_volume_db = AudioFrame(0, 0);
}
- float volume_linear = Math::db_to_linear(volume_db);
+ float volume_linear = Math::db_to_linear(internal->volume_db);
// Set the volume vector up according to the speaker mode and mix target.
// TODO do we need to scale the volume down when we output to more channels?
@@ -365,27 +192,15 @@ Vector<AudioFrame> AudioStreamPlayer::_get_volume_vector() {
}
void AudioStreamPlayer::_validate_property(PropertyInfo &p_property) const {
- if (p_property.name == "bus") {
- String options;
- for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
- if (i > 0) {
- options += ",";
- }
- String name = AudioServer::get_singleton()->get_bus_name(i);
- options += name;
- }
-
- p_property.hint_string = options;
- }
+ internal->validate_property(p_property);
}
bool AudioStreamPlayer::has_stream_playback() {
- return !stream_playbacks.is_empty();
+ return internal->has_stream_playback();
}
Ref<AudioStreamPlayback> AudioStreamPlayer::get_stream_playback() {
- ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback().");
- return stream_playbacks[stream_playbacks.size() - 1];
+ return internal->get_stream_playback();
}
void AudioStreamPlayer::_bind_methods() {
@@ -444,9 +259,9 @@ void AudioStreamPlayer::_bind_methods() {
}
AudioStreamPlayer::AudioStreamPlayer() {
- AudioServer::get_singleton()->connect("bus_layout_changed", callable_mp(this, &AudioStreamPlayer::_on_bus_layout_changed));
- AudioServer::get_singleton()->connect("bus_renamed", callable_mp(this, &AudioStreamPlayer::_on_bus_renamed));
+ internal = memnew(AudioStreamPlayerInternal(this, callable_mp(this, &AudioStreamPlayer::play), false));
}
AudioStreamPlayer::~AudioStreamPlayer() {
+ memdelete(internal);
}
diff --git a/scene/audio/audio_stream_player.h b/scene/audio/audio_stream_player.h
index 404d6fbebf..754e670553 100644
--- a/scene/audio/audio_stream_player.h
+++ b/scene/audio/audio_stream_player.h
@@ -31,10 +31,12 @@
#ifndef AUDIO_STREAM_PLAYER_H
#define AUDIO_STREAM_PLAYER_H
-#include "core/templates/safe_refcount.h"
#include "scene/main/node.h"
-#include "scene/scene_string_names.h"
-#include "servers/audio/audio_stream.h"
+
+struct AudioFrame;
+class AudioStream;
+class AudioStreamPlayback;
+class AudioStreamPlayerInternal;
class AudioStreamPlayer : public Node {
GDCLASS(AudioStreamPlayer, Node);
@@ -47,35 +49,15 @@ public:
};
private:
- Vector<Ref<AudioStreamPlayback>> stream_playbacks;
- Ref<AudioStream> stream;
-
- SafeFlag active;
-
- float pitch_scale = 1.0;
- float volume_db = 0.0;
- bool autoplay = false;
- StringName bus = SceneStringNames::get_singleton()->Master;
- int max_polyphony = 1;
+ AudioStreamPlayerInternal *internal = nullptr;
MixTarget mix_target = MIX_TARGET_STEREO;
void _set_playing(bool p_enable);
bool _is_active() const;
- void _on_bus_layout_changed();
- void _on_bus_renamed(int p_bus_index, const StringName &p_old_name, const StringName &p_new_name);
-
Vector<AudioFrame> _get_volume_vector();
- struct ParameterData {
- StringName path;
- Variant value;
- };
-
- HashMap<StringName, ParameterData> playback_parameters;
- void _update_stream_parameters();
-
protected:
void _validate_property(PropertyInfo &p_property) const;
void _notification(int p_what);
diff --git a/scene/audio/audio_stream_player_internal.cpp b/scene/audio/audio_stream_player_internal.cpp
new file mode 100644
index 0000000000..d5f6f91c88
--- /dev/null
+++ b/scene/audio/audio_stream_player_internal.cpp
@@ -0,0 +1,321 @@
+/**************************************************************************/
+/* audio_stream_player_internal.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 "audio_stream_player_internal.h"
+
+#include "scene/main/node.h"
+#include "scene/scene_string_names.h"
+#include "servers/audio/audio_stream.h"
+
+void AudioStreamPlayerInternal::_set_process(bool p_enabled) {
+ if (physical) {
+ node->set_physics_process_internal(p_enabled);
+ } else {
+ node->set_process(p_enabled);
+ }
+}
+
+void AudioStreamPlayerInternal::_update_stream_parameters() {
+ if (stream.is_null()) {
+ return;
+ }
+
+ List<AudioStream::Parameter> parameters;
+ stream->get_parameter_list(&parameters);
+ for (const AudioStream::Parameter &K : parameters) {
+ const PropertyInfo &pi = K.property;
+ StringName key = PARAM_PREFIX + pi.name;
+ if (!playback_parameters.has(key)) {
+ ParameterData pd;
+ pd.path = pi.name;
+ pd.value = K.default_value;
+ playback_parameters.insert(key, pd);
+ }
+ }
+}
+
+void AudioStreamPlayerInternal::process() {
+ Vector<Ref<AudioStreamPlayback>> playbacks_to_remove;
+ for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ if (playback.is_valid() && !AudioServer::get_singleton()->is_playback_active(playback) && !AudioServer::get_singleton()->is_playback_paused(playback)) {
+ playbacks_to_remove.push_back(playback);
+ }
+ }
+ // Now go through and remove playbacks that have finished. Removing elements from a Vector in a range based for is asking for trouble.
+ for (Ref<AudioStreamPlayback> &playback : playbacks_to_remove) {
+ stream_playbacks.erase(playback);
+ }
+ if (!playbacks_to_remove.is_empty() && stream_playbacks.is_empty()) {
+ // This node is no longer actively playing audio.
+ active.clear();
+ _set_process(false);
+ }
+ if (!playbacks_to_remove.is_empty()) {
+ node->emit_signal(SNAME("finished"));
+ }
+}
+
+void AudioStreamPlayerInternal::ensure_playback_limit() {
+ while (stream_playbacks.size() > max_polyphony) {
+ AudioServer::get_singleton()->stop_playback_stream(stream_playbacks[0]);
+ stream_playbacks.remove_at(0);
+ }
+}
+
+void AudioStreamPlayerInternal::notification(int p_what) {
+ switch (p_what) {
+ case Node::NOTIFICATION_ENTER_TREE: {
+ if (autoplay && !Engine::get_singleton()->is_editor_hint()) {
+ play_callable.call(0.0);
+ }
+ set_stream_paused(!node->can_process());
+ } break;
+
+ case Node::NOTIFICATION_EXIT_TREE: {
+ set_stream_paused(true);
+ } break;
+
+ case Node::NOTIFICATION_INTERNAL_PROCESS: {
+ process();
+ } break;
+
+ case Node::NOTIFICATION_PREDELETE: {
+ for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ AudioServer::get_singleton()->stop_playback_stream(playback);
+ }
+ stream_playbacks.clear();
+ } break;
+
+ case Node::NOTIFICATION_PAUSED: {
+ if (!node->can_process()) {
+ // Node can't process so we start fading out to silence
+ set_stream_paused(true);
+ }
+ } break;
+
+ case Node::NOTIFICATION_UNPAUSED: {
+ set_stream_paused(false);
+ } break;
+ }
+}
+
+Ref<AudioStreamPlayback> AudioStreamPlayerInternal::play_basic() {
+ Ref<AudioStreamPlayback> stream_playback;
+ if (stream.is_null()) {
+ return stream_playback;
+ }
+ ERR_FAIL_COND_V_MSG(!node->is_inside_tree(), stream_playback, "Playback can only happen when a node is inside the scene tree");
+ if (stream->is_monophonic() && is_playing()) {
+ stop();
+ }
+ stream_playback = stream->instantiate_playback();
+ ERR_FAIL_COND_V_MSG(stream_playback.is_null(), stream_playback, "Failed to instantiate playback.");
+
+ for (const KeyValue<StringName, ParameterData> &K : playback_parameters) {
+ stream_playback->set_parameter(K.value.path, K.value.value);
+ }
+
+ stream_playbacks.push_back(stream_playback);
+ active.set();
+ _set_process(true);
+ return stream_playback;
+}
+
+void AudioStreamPlayerInternal::set_stream_paused(bool p_pause) {
+ // TODO this does not have perfect recall, fix that maybe? If there are zero playbacks registered with the AudioServer, this bool isn't persisted.
+ for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ AudioServer::get_singleton()->set_playback_paused(playback, p_pause);
+ }
+}
+
+bool AudioStreamPlayerInternal::get_stream_paused() const {
+ // There's currently no way to pause some playback streams but not others. Check the first and don't bother looking at the rest.
+ if (!stream_playbacks.is_empty()) {
+ return AudioServer::get_singleton()->is_playback_paused(stream_playbacks[0]);
+ }
+ return false;
+}
+
+void AudioStreamPlayerInternal::validate_property(PropertyInfo &p_property) const {
+ if (p_property.name == "bus") {
+ String options;
+ for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
+ if (i > 0) {
+ options += ",";
+ }
+ String name = AudioServer::get_singleton()->get_bus_name(i);
+ options += name;
+ }
+
+ p_property.hint_string = options;
+ }
+}
+
+bool AudioStreamPlayerInternal::set(const StringName &p_name, const Variant &p_value) {
+ ParameterData *pd = playback_parameters.getptr(p_name);
+ if (!pd) {
+ return false;
+ }
+ pd->value = p_value;
+ for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ playback->set_parameter(pd->path, pd->value);
+ }
+ return true;
+}
+
+bool AudioStreamPlayerInternal::get(const StringName &p_name, Variant &r_ret) const {
+ const ParameterData *pd = playback_parameters.getptr(p_name);
+ if (!pd) {
+ return false;
+ }
+ r_ret = pd->value;
+ return true;
+}
+
+void AudioStreamPlayerInternal::get_property_list(List<PropertyInfo> *p_list) const {
+ if (stream.is_null()) {
+ return;
+ }
+ List<AudioStream::Parameter> parameters;
+ stream->get_parameter_list(&parameters);
+ for (const AudioStream::Parameter &K : parameters) {
+ PropertyInfo pi = K.property;
+ pi.name = PARAM_PREFIX + pi.name;
+
+ const ParameterData *pd = playback_parameters.getptr(pi.name);
+ if (pd && pd->value == K.default_value) {
+ pi.usage &= ~PROPERTY_USAGE_STORAGE;
+ }
+
+ p_list->push_back(pi);
+ }
+}
+
+void AudioStreamPlayerInternal::set_stream(Ref<AudioStream> p_stream) {
+ if (stream.is_valid()) {
+ stream->disconnect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayerInternal::_update_stream_parameters));
+ }
+ stop();
+ stream = p_stream;
+ _update_stream_parameters();
+ if (stream.is_valid()) {
+ stream->connect(SNAME("parameter_list_changed"), callable_mp(this, &AudioStreamPlayerInternal::_update_stream_parameters));
+ }
+ node->notify_property_list_changed();
+}
+
+void AudioStreamPlayerInternal::seek(float p_seconds) {
+ if (is_playing()) {
+ stop();
+ play_callable.call(p_seconds);
+ }
+}
+
+void AudioStreamPlayerInternal::stop() {
+ for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ AudioServer::get_singleton()->stop_playback_stream(playback);
+ }
+ stream_playbacks.clear();
+ active.clear();
+ _set_process(false);
+}
+
+bool AudioStreamPlayerInternal::is_playing() const {
+ for (const Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ if (AudioServer::get_singleton()->is_playback_active(playback)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+float AudioStreamPlayerInternal::get_playback_position() {
+ // Return the playback position of the most recently started playback stream.
+ if (!stream_playbacks.is_empty()) {
+ return AudioServer::get_singleton()->get_playback_position(stream_playbacks[stream_playbacks.size() - 1]);
+ }
+ return 0;
+}
+
+void AudioStreamPlayerInternal::set_playing(bool p_enable) {
+ if (p_enable) {
+ play_callable.call(0.0);
+ } else {
+ stop();
+ }
+}
+
+bool AudioStreamPlayerInternal::is_active() const {
+ return active.is_set();
+}
+
+void AudioStreamPlayerInternal::set_pitch_scale(float p_pitch_scale) {
+ ERR_FAIL_COND(p_pitch_scale <= 0.0);
+ pitch_scale = p_pitch_scale;
+
+ for (Ref<AudioStreamPlayback> &playback : stream_playbacks) {
+ AudioServer::get_singleton()->set_playback_pitch_scale(playback, pitch_scale);
+ }
+}
+
+void AudioStreamPlayerInternal::set_max_polyphony(int p_max_polyphony) {
+ if (p_max_polyphony > 0) {
+ max_polyphony = p_max_polyphony;
+ }
+}
+
+bool AudioStreamPlayerInternal::has_stream_playback() {
+ return !stream_playbacks.is_empty();
+}
+
+Ref<AudioStreamPlayback> AudioStreamPlayerInternal::get_stream_playback() {
+ ERR_FAIL_COND_V_MSG(stream_playbacks.is_empty(), Ref<AudioStreamPlayback>(), "Player is inactive. Call play() before requesting get_stream_playback().");
+ return stream_playbacks[stream_playbacks.size() - 1];
+}
+
+StringName AudioStreamPlayerInternal::get_bus() const {
+ const String bus_name = bus;
+ for (int i = 0; i < AudioServer::get_singleton()->get_bus_count(); i++) {
+ if (AudioServer::get_singleton()->get_bus_name(i) == bus_name) {
+ return bus;
+ }
+ }
+ return SceneStringNames::get_singleton()->Master;
+}
+
+AudioStreamPlayerInternal::AudioStreamPlayerInternal(Node *p_node, const Callable &p_play_callable, bool p_physical) {
+ node = p_node;
+ play_callable = p_play_callable;
+ physical = p_physical;
+ bus = SceneStringNames::get_singleton()->Master;
+
+ AudioServer::get_singleton()->connect("bus_layout_changed", callable_mp((Object *)node, &Object::notify_property_list_changed));
+ AudioServer::get_singleton()->connect("bus_renamed", callable_mp((Object *)node, &Object::notify_property_list_changed).unbind(3));
+}
diff --git a/scene/audio/audio_stream_player_internal.h b/scene/audio/audio_stream_player_internal.h
new file mode 100644
index 0000000000..3662752441
--- /dev/null
+++ b/scene/audio/audio_stream_player_internal.h
@@ -0,0 +1,105 @@
+/**************************************************************************/
+/* audio_stream_player_internal.h */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#ifndef AUDIO_STREAM_PLAYER_INTERNAL_H
+#define AUDIO_STREAM_PLAYER_INTERNAL_H
+
+#include "core/object/ref_counted.h"
+#include "core/templates/safe_refcount.h"
+
+class AudioStream;
+class AudioStreamPlayback;
+class Node;
+
+class AudioStreamPlayerInternal : public Object {
+ GDCLASS(AudioStreamPlayerInternal, Object);
+
+ struct ParameterData {
+ StringName path;
+ Variant value;
+ };
+
+ static inline const String PARAM_PREFIX = "parameters/";
+
+ Node *node = nullptr;
+ Callable play_callable;
+ bool physical = false;
+
+ HashMap<StringName, ParameterData> playback_parameters;
+
+ void _set_process(bool p_enabled);
+ void _update_stream_parameters();
+
+public:
+ Vector<Ref<AudioStreamPlayback>> stream_playbacks;
+ Ref<AudioStream> stream;
+
+ SafeFlag active;
+
+ float pitch_scale = 1.0;
+ float volume_db = 0.0;
+ bool autoplay = false;
+ StringName bus;
+ int max_polyphony = 1;
+
+ void process();
+ void ensure_playback_limit();
+
+ void notification(int p_what);
+ void validate_property(PropertyInfo &p_property) const;
+ bool set(const StringName &p_name, const Variant &p_value);
+ bool get(const StringName &p_name, Variant &r_ret) const;
+ void get_property_list(List<PropertyInfo> *p_list) const;
+
+ void set_stream(Ref<AudioStream> p_stream);
+ void set_pitch_scale(float p_pitch_scale);
+ void set_max_polyphony(int p_max_polyphony);
+
+ StringName get_bus() const;
+
+ Ref<AudioStreamPlayback> play_basic();
+ void seek(float p_seconds);
+ void stop();
+ bool is_playing() const;
+ float get_playback_position();
+
+ void set_playing(bool p_enable);
+ bool is_active() const;
+
+ void set_stream_paused(bool p_pause);
+ bool get_stream_paused() const;
+
+ bool has_stream_playback();
+ Ref<AudioStreamPlayback> get_stream_playback();
+
+ AudioStreamPlayerInternal(Node *p_node, const Callable &p_play_callable, bool p_physical);
+};
+
+#endif // AUDIO_STREAM_PLAYER_INTERNAL_H
diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp
index 23a581c5f6..222cdd15e4 100644
--- a/scene/gui/button.cpp
+++ b/scene/gui/button.cpp
@@ -50,6 +50,63 @@ void Button::_set_internal_margin(Side p_side, float p_value) {
void Button::_queue_update_size_cache() {
}
+void Button::_set_h_separation_is_valid_when_no_text(bool p_h_separation_is_valid_when_no_text) {
+ h_separation_is_valid_when_no_text = p_h_separation_is_valid_when_no_text;
+}
+
+Ref<StyleBox> Button::_get_current_stylebox() const {
+ Ref<StyleBox> stylebox = theme_cache.normal;
+ const bool rtl = is_layout_rtl();
+
+ switch (get_draw_mode()) {
+ case DRAW_NORMAL: {
+ if (rtl && has_theme_stylebox(SNAME("normal_mirrored"))) {
+ stylebox = theme_cache.normal_mirrored;
+ } else {
+ stylebox = theme_cache.normal;
+ }
+ } break;
+
+ case DRAW_HOVER_PRESSED: {
+ // Edge case for CheckButton and CheckBox.
+ if (has_theme_stylebox("hover_pressed")) {
+ if (rtl && has_theme_stylebox(SNAME("hover_pressed_mirrored"))) {
+ stylebox = theme_cache.hover_pressed_mirrored;
+ } else {
+ stylebox = theme_cache.hover_pressed;
+ }
+ break;
+ }
+ }
+ [[fallthrough]];
+ case DRAW_PRESSED: {
+ if (rtl && has_theme_stylebox(SNAME("pressed_mirrored"))) {
+ stylebox = theme_cache.pressed_mirrored;
+ } else {
+ stylebox = theme_cache.pressed;
+ }
+ } break;
+
+ case DRAW_HOVER: {
+ if (rtl && has_theme_stylebox(SNAME("hover_mirrored"))) {
+ stylebox = theme_cache.hover_mirrored;
+ } else {
+ stylebox = theme_cache.hover;
+ }
+ } break;
+
+ case DRAW_DISABLED: {
+ if (rtl && has_theme_stylebox(SNAME("disabled_mirrored"))) {
+ stylebox = theme_cache.disabled_mirrored;
+ } else {
+ stylebox = theme_cache.disabled;
+ }
+ } break;
+ }
+
+ return stylebox;
+}
+
void Button::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
@@ -72,287 +129,265 @@ void Button::_notification(int p_what) {
} break;
case NOTIFICATION_DRAW: {
- RID ci = get_canvas_item();
- Size2 size = get_size();
- Color color;
- Color color_icon(1, 1, 1, 1);
+ const RID ci = get_canvas_item();
+ const Size2 size = get_size();
- Ref<StyleBox> style = theme_cache.normal;
- bool rtl = is_layout_rtl();
- const bool is_clipped = clip_text || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING;
+ const Ref<StyleBox> style = _get_current_stylebox();
+ { // Draws the stylebox in the current state.
+ if (!flat) {
+ style->draw(ci, Rect2(Point2(), size));
+ }
- switch (get_draw_mode()) {
- case DRAW_NORMAL: {
- if (rtl && has_theme_stylebox(SNAME("normal_mirrored"))) {
- style = theme_cache.normal_mirrored;
- } else {
- style = theme_cache.normal;
+ if (has_focus()) {
+ Ref<StyleBox> style2 = theme_cache.focus;
+ style2->draw(ci, Rect2(Point2(), size));
+ }
+ }
+
+ Ref<Texture2D> _icon = icon;
+ if (_icon.is_null() && has_theme_icon(SNAME("icon"))) {
+ _icon = theme_cache.icon;
+ }
+
+ if (xl_text.is_empty() && _icon.is_null()) {
+ break;
+ }
+
+ const float style_margin_left = style->get_margin(SIDE_LEFT);
+ const float style_margin_right = style->get_margin(SIDE_RIGHT);
+ const float style_margin_top = style->get_margin(SIDE_TOP);
+ const float style_margin_bottom = style->get_margin(SIDE_BOTTOM);
+
+ Size2 drawable_size_remained = size;
+
+ { // The size after the stelybox is stripped.
+ drawable_size_remained.width -= style_margin_left + style_margin_right;
+ drawable_size_remained.height -= style_margin_top + style_margin_bottom;
+ }
+
+ const int h_separation = MAX(0, theme_cache.h_separation);
+
+ { // The width reserved for internal element in derived classes (and h_separation if need).
+ float internal_margin = _internal_margin[SIDE_LEFT] + _internal_margin[SIDE_RIGHT];
+
+ if (!xl_text.is_empty() || h_separation_is_valid_when_no_text) {
+ if (_internal_margin[SIDE_LEFT] > 0.0f) {
+ internal_margin += h_separation;
}
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
+ if (_internal_margin[SIDE_RIGHT] > 0.0f) {
+ internal_margin += h_separation;
}
+ }
+
+ drawable_size_remained.width -= internal_margin; // The size after the internal element is stripped.
+ }
+
+ HorizontalAlignment icon_align_rtl_checked = horizontal_icon_alignment;
+ HorizontalAlignment align_rtl_checked = alignment;
+ // Swap icon and text alignment sides if right-to-left layout is set.
+ if (is_layout_rtl()) {
+ if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
+ icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
+ } else if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_LEFT) {
+ icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
+ }
+ if (alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
+ align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
+ } else if (alignment == HORIZONTAL_ALIGNMENT_LEFT) {
+ align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
+ }
+ }
+ Color font_color;
+ Color icon_modulate_color(1, 1, 1, 1);
+ // Get the font color and icon modulate color in the current state.
+ switch (get_draw_mode()) {
+ case DRAW_NORMAL: {
// Focus colors only take precedence over normal state.
if (has_focus()) {
- color = theme_cache.font_focus_color;
+ font_color = theme_cache.font_focus_color;
if (has_theme_color(SNAME("icon_focus_color"))) {
- color_icon = theme_cache.icon_focus_color;
+ icon_modulate_color = theme_cache.icon_focus_color;
}
} else {
- color = theme_cache.font_color;
+ font_color = theme_cache.font_color;
if (has_theme_color(SNAME("icon_normal_color"))) {
- color_icon = theme_cache.icon_normal_color;
+ icon_modulate_color = theme_cache.icon_normal_color;
}
}
} break;
case DRAW_HOVER_PRESSED: {
// Edge case for CheckButton and CheckBox.
if (has_theme_stylebox("hover_pressed")) {
- if (rtl && has_theme_stylebox(SNAME("hover_pressed_mirrored"))) {
- style = theme_cache.hover_pressed_mirrored;
- } else {
- style = theme_cache.hover_pressed;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
if (has_theme_color(SNAME("font_hover_pressed_color"))) {
- color = theme_cache.font_hover_pressed_color;
+ font_color = theme_cache.font_hover_pressed_color;
}
if (has_theme_color(SNAME("icon_hover_pressed_color"))) {
- color_icon = theme_cache.icon_hover_pressed_color;
+ icon_modulate_color = theme_cache.icon_hover_pressed_color;
}
break;
}
- [[fallthrough]];
}
+ [[fallthrough]];
case DRAW_PRESSED: {
- if (rtl && has_theme_stylebox(SNAME("pressed_mirrored"))) {
- style = theme_cache.pressed_mirrored;
- } else {
- style = theme_cache.pressed;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
if (has_theme_color(SNAME("font_pressed_color"))) {
- color = theme_cache.font_pressed_color;
+ font_color = theme_cache.font_pressed_color;
} else {
- color = theme_cache.font_color;
+ font_color = theme_cache.font_color;
}
if (has_theme_color(SNAME("icon_pressed_color"))) {
- color_icon = theme_cache.icon_pressed_color;
+ icon_modulate_color = theme_cache.icon_pressed_color;
}
} break;
case DRAW_HOVER: {
- if (rtl && has_theme_stylebox(SNAME("hover_mirrored"))) {
- style = theme_cache.hover_mirrored;
- } else {
- style = theme_cache.hover;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
- color = theme_cache.font_hover_color;
+ font_color = theme_cache.font_hover_color;
if (has_theme_color(SNAME("icon_hover_color"))) {
- color_icon = theme_cache.icon_hover_color;
+ icon_modulate_color = theme_cache.icon_hover_color;
}
} break;
case DRAW_DISABLED: {
- if (rtl && has_theme_stylebox(SNAME("disabled_mirrored"))) {
- style = theme_cache.disabled_mirrored;
- } else {
- style = theme_cache.disabled;
- }
-
- if (!flat) {
- style->draw(ci, Rect2(Point2(0, 0), size));
- }
- color = theme_cache.font_disabled_color;
+ font_color = theme_cache.font_disabled_color;
if (has_theme_color(SNAME("icon_disabled_color"))) {
- color_icon = theme_cache.icon_disabled_color;
+ icon_modulate_color = theme_cache.icon_disabled_color;
} else {
- color_icon.a = 0.4;
+ icon_modulate_color.a = 0.4;
}
} break;
}
- if (has_focus()) {
- Ref<StyleBox> style2 = theme_cache.focus;
- style2->draw(ci, Rect2(Point2(), size));
- }
-
- Ref<Texture2D> _icon;
- if (icon.is_null() && has_theme_icon(SNAME("icon"))) {
- _icon = theme_cache.icon;
- } else {
- _icon = icon;
- }
-
- Rect2 icon_region;
- HorizontalAlignment icon_align_rtl_checked = horizontal_icon_alignment;
- HorizontalAlignment align_rtl_checked = alignment;
- // Swap icon and text alignment sides if right-to-left layout is set.
- if (rtl) {
- if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
- icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
- } else if (horizontal_icon_alignment == HORIZONTAL_ALIGNMENT_LEFT) {
- icon_align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
- }
- if (alignment == HORIZONTAL_ALIGNMENT_RIGHT) {
- align_rtl_checked = HORIZONTAL_ALIGNMENT_LEFT;
- } else if (alignment == HORIZONTAL_ALIGNMENT_LEFT) {
- align_rtl_checked = HORIZONTAL_ALIGNMENT_RIGHT;
- }
- }
- if (!_icon.is_null()) {
- int valign = size.height - style->get_minimum_size().y;
+ const bool is_clipped = clip_text || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING;
+ const Size2 custom_element_size = drawable_size_remained;
+
+ // Draw the icon.
+ if (_icon.is_valid()) {
+ Size2 icon_size;
+
+ { // Calculate the drawing size of the icon.
+ icon_size = _icon->get_size();
+
+ if (expand_icon) {
+ const Size2 text_buf_size = text_buf->get_size();
+ Size2 _size = custom_element_size;
+ if (!is_clipped && icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER && text_buf_size.width > 0.0f) {
+ // If there is not enough space for icon and h_separation, h_separation will occupy the space first,
+ // so the icon's width may be negative. Keep it negative to make it easier to calculate the space
+ // reserved for text later.
+ _size.width -= text_buf_size.width + h_separation;
+ }
+ if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
+ _size.height -= text_buf_size.height;
+ }
- int voffset = 0;
- Size2 icon_size = _icon->get_size();
+ float icon_width = icon_size.width * _size.height / icon_size.height;
+ float icon_height = _size.height;
- // Fix vertical size.
- if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
- valign -= text_buf->get_size().height;
- }
+ if (icon_width > _size.width) {
+ icon_width = _size.width;
+ icon_height = icon_size.height * icon_width / icon_size.width;
+ }
- float icon_ofs_region = 0.0;
- Point2 style_offset;
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
- style_offset.x = style->get_margin(SIDE_LEFT);
- if (_internal_margin[SIDE_LEFT] > 0) {
- icon_ofs_region = _internal_margin[SIDE_LEFT] + theme_cache.h_separation;
- }
- } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) {
- style_offset.x = 0.0;
- } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_RIGHT) {
- style_offset.x = -style->get_margin(SIDE_RIGHT);
- if (_internal_margin[SIDE_RIGHT] > 0) {
- icon_ofs_region = -_internal_margin[SIDE_RIGHT] - theme_cache.h_separation;
+ icon_size = Size2(icon_width, icon_height);
}
+ icon_size = _fit_icon_size(icon_size);
}
- style_offset.y = style->get_margin(SIDE_TOP);
-
- if (expand_icon) {
- Size2 _size = get_size() - style->get_offset() * 2;
- int icon_text_separation = text.is_empty() ? 0 : theme_cache.h_separation;
- _size.width -= icon_text_separation + icon_ofs_region;
- if (!is_clipped && icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER) {
- _size.width -= text_buf->get_size().width;
- }
- if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
- _size.height -= text_buf->get_size().height;
- }
- float icon_width = _icon->get_width() * _size.height / _icon->get_height();
- float icon_height = _size.height;
- if (icon_width > _size.width) {
- icon_width = _size.width;
- icon_height = _icon->get_height() * icon_width / _icon->get_width();
+ if (icon_size.width > 0.0f) {
+ // Calculate the drawing position of the icon.
+ Point2 icon_ofs;
+
+ switch (icon_align_rtl_checked) {
+ case HORIZONTAL_ALIGNMENT_CENTER: {
+ icon_ofs.x = (custom_element_size.width - icon_size.width) / 2.0f;
+ }
+ [[fallthrough]];
+ case HORIZONTAL_ALIGNMENT_FILL:
+ case HORIZONTAL_ALIGNMENT_LEFT: {
+ icon_ofs.x += style_margin_left;
+ icon_ofs.x += _internal_margin[SIDE_LEFT];
+ } break;
+
+ case HORIZONTAL_ALIGNMENT_RIGHT: {
+ icon_ofs.x = size.x - style_margin_right;
+ icon_ofs.x -= _internal_margin[SIDE_RIGHT];
+ icon_ofs.x -= icon_size.width;
+ } break;
}
- icon_size = Size2(icon_width, icon_height);
- }
- icon_size = _fit_icon_size(icon_size);
+ switch (vertical_icon_alignment) {
+ case VERTICAL_ALIGNMENT_CENTER: {
+ icon_ofs.y = (custom_element_size.height - icon_size.height) / 2.0f;
+ }
+ [[fallthrough]];
+ case VERTICAL_ALIGNMENT_FILL:
+ case VERTICAL_ALIGNMENT_TOP: {
+ icon_ofs.y += style_margin_top;
+ } break;
+
+ case VERTICAL_ALIGNMENT_BOTTOM: {
+ icon_ofs.y = size.y - style_margin_bottom - icon_size.height;
+ } break;
+ }
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_TOP) {
- voffset = -(valign - icon_size.y) / 2;
- }
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_BOTTOM) {
- voffset = (valign - icon_size.y) / 2 + text_buf->get_size().y;
+ Rect2 icon_region = Rect2(icon_ofs, icon_size);
+ draw_texture_rect(_icon, icon_region, false, icon_modulate_color);
}
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
- icon_region = Rect2(style_offset + Point2(icon_ofs_region, voffset + Math::floor((valign - icon_size.y) * 0.5)), icon_size);
- } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) {
- icon_region = Rect2(style_offset + Point2(icon_ofs_region + Math::floor((size.x - icon_size.x) * 0.5), voffset + Math::floor((valign - icon_size.y) * 0.5)), icon_size);
- } else {
- icon_region = Rect2(style_offset + Point2(icon_ofs_region + size.x - icon_size.x, voffset + Math::floor((valign - icon_size.y) * 0.5)), icon_size);
- }
+ if (!xl_text.is_empty()) {
+ // Update the size after the icon is stripped. Stripping only when the icon alignments are not center.
+ if (icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER) {
+ // Subtract the space's width occupied by icon and h_separation together.
+ drawable_size_remained.width -= icon_size.width + h_separation;
+ }
- if (icon_region.size.width > 0) {
- Rect2 icon_region_rounded = Rect2(icon_region.position.round(), icon_region.size.round());
- draw_texture_rect(_icon, icon_region_rounded, false, color_icon);
+ if (vertical_icon_alignment != VERTICAL_ALIGNMENT_CENTER) {
+ drawable_size_remained.height -= icon_size.height;
+ }
}
}
- Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + theme_cache.h_separation, 0) : Point2();
- if (align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER && icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) {
- icon_ofs.x = 0.0;
- }
-
- int text_clip = size.width - style->get_minimum_size().width - icon_ofs.width;
- if (_internal_margin[SIDE_LEFT] > 0) {
- text_clip -= _internal_margin[SIDE_LEFT] + theme_cache.h_separation;
- }
- if (_internal_margin[SIDE_RIGHT] > 0) {
- text_clip -= _internal_margin[SIDE_RIGHT] + theme_cache.h_separation;
- }
-
- text_buf->set_width(is_clipped ? text_clip : -1);
+ // Draw the text.
+ if (!xl_text.is_empty()) {
+ text_buf->set_alignment(align_rtl_checked);
- int text_width = MAX(1, is_clipped ? MIN(text_clip, text_buf->get_size().x) : text_buf->get_size().x);
+ float text_buf_width = MAX(1.0f, drawable_size_remained.width); // The space's width filled by the text_buf.
+ text_buf->set_width(text_buf_width);
- Point2 text_ofs = (size - style->get_minimum_size() - icon_ofs - text_buf->get_size() - Point2(_internal_margin[SIDE_RIGHT] - _internal_margin[SIDE_LEFT], 0)) / 2.0;
-
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_TOP) {
- text_ofs.y += icon_region.size.height / 2;
- }
- if (vertical_icon_alignment == VERTICAL_ALIGNMENT_BOTTOM) {
- text_ofs.y -= icon_region.size.height / 2;
- }
+ Point2 text_ofs;
- text_buf->set_alignment(align_rtl_checked);
- text_buf->set_width(text_width);
- switch (align_rtl_checked) {
- case HORIZONTAL_ALIGNMENT_FILL:
- case HORIZONTAL_ALIGNMENT_LEFT: {
- if (icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_LEFT) {
- icon_ofs.x = 0.0;
- }
- if (_internal_margin[SIDE_LEFT] > 0) {
- text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + theme_cache.h_separation;
- } else {
- text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x;
- }
- text_ofs.y += style->get_offset().y;
- } break;
- case HORIZONTAL_ALIGNMENT_CENTER: {
- if (text_ofs.x < 0) {
- text_ofs.x = 0;
- }
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
- text_ofs += icon_ofs;
- }
- text_ofs += style->get_offset();
- } break;
- case HORIZONTAL_ALIGNMENT_RIGHT: {
- if (_internal_margin[SIDE_RIGHT] > 0) {
- text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - theme_cache.h_separation;
- } else {
- text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width;
- }
- text_ofs.y += style->get_offset().y;
- if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_RIGHT) {
- text_ofs.x -= icon_ofs.x;
+ switch (align_rtl_checked) {
+ case HORIZONTAL_ALIGNMENT_CENTER: {
+ text_ofs.x = (drawable_size_remained.width - text_buf_width) / 2.0f;
}
- } break;
- }
+ [[fallthrough]];
+ case HORIZONTAL_ALIGNMENT_FILL:
+ case HORIZONTAL_ALIGNMENT_LEFT:
+ case HORIZONTAL_ALIGNMENT_RIGHT: {
+ text_ofs.x += style_margin_left;
+ text_ofs.x += _internal_margin[SIDE_LEFT];
+ if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_LEFT) {
+ // Offset by the space's width that occupied by icon and h_separation together.
+ text_ofs.x += custom_element_size.width - drawable_size_remained.width;
+ }
+ } break;
+ }
+
+ text_ofs.y = (drawable_size_remained.height - text_buf->get_size().height) / 2.0f + style_margin_top;
+ if (vertical_icon_alignment == VERTICAL_ALIGNMENT_TOP) {
+ text_ofs.y += custom_element_size.height - drawable_size_remained.height; // Offset by the icon's height.
+ }
- Color font_outline_color = theme_cache.font_outline_color;
- int outline_size = theme_cache.outline_size;
- if (outline_size > 0 && font_outline_color.a > 0) {
- text_buf->draw_outline(ci, text_ofs, outline_size, font_outline_color);
+ Color font_outline_color = theme_cache.font_outline_color;
+ int outline_size = theme_cache.outline_size;
+ if (outline_size > 0 && font_outline_color.a > 0.0f) {
+ text_buf->draw_outline(ci, text_ofs, outline_size, font_outline_color);
+ }
+ text_buf->draw(ci, text_ofs, font_color);
}
- text_buf->draw(ci, text_ofs, color);
} break;
}
}
@@ -411,7 +446,7 @@ Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Textu
}
}
- return theme_cache.normal->get_minimum_size() + minsize;
+ return _get_current_stylebox()->get_minimum_size() + minsize;
}
void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) {
@@ -443,9 +478,13 @@ void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) {
void Button::set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior) {
if (overrun_behavior != p_behavior) {
+ bool need_update_cache = overrun_behavior == TextServer::OVERRUN_NO_TRIMMING || p_behavior == TextServer::OVERRUN_NO_TRIMMING;
overrun_behavior = p_behavior;
_shape();
+ if (need_update_cache) {
+ _queue_update_size_cache();
+ }
queue_redraw();
update_minimum_size();
}
@@ -550,6 +589,8 @@ bool Button::is_flat() const {
void Button::set_clip_text(bool p_enabled) {
if (clip_text != p_enabled) {
clip_text = p_enabled;
+
+ _queue_update_size_cache();
queue_redraw();
update_minimum_size();
}
@@ -571,13 +612,25 @@ HorizontalAlignment Button::get_text_alignment() const {
}
void Button::set_icon_alignment(HorizontalAlignment p_alignment) {
+ if (horizontal_icon_alignment == p_alignment) {
+ return;
+ }
+
horizontal_icon_alignment = p_alignment;
update_minimum_size();
queue_redraw();
}
void Button::set_vertical_icon_alignment(VerticalAlignment p_alignment) {
+ if (vertical_icon_alignment == p_alignment) {
+ return;
+ }
+ bool need_update_cache = vertical_icon_alignment == VERTICAL_ALIGNMENT_CENTER || p_alignment == VERTICAL_ALIGNMENT_CENTER;
vertical_icon_alignment = p_alignment;
+
+ if (need_update_cache) {
+ _queue_update_size_cache();
+ }
update_minimum_size();
queue_redraw();
}
diff --git a/scene/gui/button.h b/scene/gui/button.h
index ad7412b54e..d0243a9eeb 100644
--- a/scene/gui/button.h
+++ b/scene/gui/button.h
@@ -55,6 +55,8 @@ private:
VerticalAlignment vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER;
float _internal_margin[4] = {};
+ bool h_separation_is_valid_when_no_text = false;
+
struct ThemeCache {
Ref<StyleBox> normal;
Ref<StyleBox> normal_mirrored;
@@ -102,6 +104,8 @@ protected:
void _set_internal_margin(Side p_side, float p_value);
virtual void _queue_update_size_cache();
+ void _set_h_separation_is_valid_when_no_text(bool p_h_separation_is_valid_when_no_text);
+ Ref<StyleBox> _get_current_stylebox() const;
void _notification(int p_what);
static void _bind_methods();
diff --git a/scene/gui/graph_edit.compat.inc b/scene/gui/graph_edit.compat.inc
index 9059637a2a..7c2af20066 100644
--- a/scene/gui/graph_edit.compat.inc
+++ b/scene/gui/graph_edit.compat.inc
@@ -38,9 +38,14 @@ void GraphEdit::_set_arrange_nodes_button_hidden_bind_compat_81582(bool p_enable
set_show_arrange_button(!p_enable);
}
+PackedVector2Array GraphEdit::_get_connection_line_bind_compat_86158(const Vector2 &p_from, const Vector2 &p_to) {
+ return get_connection_line(p_from, p_to);
+}
+
void GraphEdit::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("is_arrange_nodes_button_hidden"), &GraphEdit::_is_arrange_nodes_button_hidden_bind_compat_81582);
ClassDB::bind_compatibility_method(D_METHOD("set_arrange_nodes_button_hidden", "enable"), &GraphEdit::_set_arrange_nodes_button_hidden_bind_compat_81582);
+ ClassDB::bind_compatibility_method(D_METHOD("get_connection_line", "from_node", "to_node"), &GraphEdit::_get_connection_line_bind_compat_86158);
}
#endif
diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp
index f5cf7eb59d..c23d21775f 100644
--- a/scene/gui/graph_edit.cpp
+++ b/scene/gui/graph_edit.cpp
@@ -32,8 +32,10 @@
#include "graph_edit.compat.inc"
#include "core/input/input.h"
+#include "core/math/geometry_2d.h"
#include "core/math/math_funcs.h"
#include "core/os/keyboard.h"
+#include "scene/2d/line_2d.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/graph_edit_arranger.h"
@@ -52,7 +54,6 @@ constexpr int MAX_CONNECTION_LINE_CURVE_TESSELATION_STAGES = 5;
constexpr int GRID_MINOR_STEPS_PER_MAJOR_LINE = 10;
constexpr int GRID_MIN_SNAPPING_DISTANCE = 2;
constexpr int GRID_MAX_SNAPPING_DISTANCE = 100;
-constexpr float CONNECTING_TARGET_LINE_COLOR_BRIGHTENING = 0.4;
bool GraphEditFilter::has_point(const Point2 &p_point) const {
return ge->_filter_input(p_point);
@@ -212,6 +213,36 @@ GraphEditMinimap::GraphEditMinimap(GraphEdit *p_edit) {
minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding);
}
+Ref<Shader> GraphEdit::default_connections_shader;
+
+void GraphEdit::init_shaders() {
+ default_connections_shader.instantiate();
+ default_connections_shader->set_code(R"(
+// Connection lines shader.
+shader_type canvas_item;
+render_mode blend_mix;
+
+uniform vec4 rim_color : source_color;
+uniform int from_type;
+uniform int to_type;
+uniform float line_width;
+
+void fragment(){
+ float fake_aa_width = 1.5/line_width;
+ float rim_width = 1.5/line_width;
+
+ float dist = abs(UV.y - 0.5);
+ float alpha = smoothstep(0.5, 0.5-fake_aa_width, dist);
+ vec4 final_color = mix(rim_color, COLOR, smoothstep(0.5-rim_width, 0.5-fake_aa_width-rim_width, dist));
+ COLOR = vec4(final_color.rgb, final_color.a*alpha);
+}
+)");
+}
+
+void GraphEdit::finish_shaders() {
+ default_connections_shader.unref();
+}
+
Control::CursorShape GraphEdit::get_cursor_shape(const Point2 &p_pos) const {
if (moving_selection) {
return CURSOR_MOVE;
@@ -232,24 +263,48 @@ Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const S
if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) {
return OK;
}
- Connection c;
- c.from_node = p_from;
- c.from_port = p_from_port;
- c.to_node = p_to;
- c.to_port = p_to_port;
- c.activity = 0;
+ Ref<Connection> c;
+ c.instantiate();
+ c->from_node = p_from;
+ c->from_port = p_from_port;
+ c->to_node = p_to;
+ c->to_port = p_to_port;
+ c->activity = 0;
connections.push_back(c);
- top_layer->queue_redraw();
+ connection_map[p_from].push_back(c);
+ connection_map[p_to].push_back(c);
+
+ Line2D *line = memnew(Line2D);
+ line->set_texture_mode(Line2D::LineTextureMode::LINE_TEXTURE_STRETCH);
+
+ Ref<ShaderMaterial> line_material;
+ line_material.instantiate();
+ line_material->set_shader(connections_shader);
+
+ float line_width = _get_shader_line_width();
+ line_material->set_shader_parameter("line_width", line_width);
+ line_material->set_shader_parameter("from_type", c->from_port);
+ line_material->set_shader_parameter("to_type", c->to_port);
+
+ Ref<StyleBoxFlat> bg_panel = theme_cache.panel;
+ Color connection_line_rim_color = bg_panel.is_valid() ? bg_panel->get_bg_color() : Color(0.0, 0.0, 0.0, 0.0);
+ line_material->set_shader_parameter("rim_color", connection_line_rim_color);
+ line->set_material(line_material);
+
+ connections_layer->add_child(line);
+ c->_cache.line = line;
+
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
return OK;
}
bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) {
- for (const Connection &E : connections) {
- if (E.from_node == p_from && E.from_port == p_from_port && E.to_node == p_to && E.to_port == p_to_port) {
+ for (const Ref<Connection> &conn : connection_map[p_from]) {
+ if (conn->from_node == p_from && conn->from_port == p_from_port && conn->to_node == p_to && conn->to_port == p_to_port) {
return true;
}
}
@@ -258,20 +313,24 @@ bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, con
}
void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) {
- for (const List<Connection>::Element *E = connections.front(); E; E = E->next()) {
- if (E->get().from_node == p_from && E->get().from_port == p_from_port && E->get().to_node == p_to && E->get().to_port == p_to_port) {
+ for (const List<Ref<Connection>>::Element *E = connections.front(); E; E = E->next()) {
+ if (E->get()->from_node == p_from && E->get()->from_port == p_from_port && E->get()->to_node == p_to && E->get()->to_port == p_to_port) {
+ connection_map[p_from].erase(E->get());
+ connection_map[p_to].erase(E->get());
+ E->get()->_cache.line->queue_free();
connections.erase(E);
- top_layer->queue_redraw();
+
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
return;
}
}
}
-void GraphEdit::get_connection_list(List<Connection> *r_connections) const {
- *r_connections = connections;
+const List<Ref<GraphEdit::Connection>> &GraphEdit::get_connection_list() const {
+ return connections;
}
void GraphEdit::set_scroll_offset(const Vector2 &p_offset) {
@@ -291,9 +350,9 @@ void GraphEdit::_scroll_moved(double) {
callable_mp(this, &GraphEdit::_update_scroll_offset).call_deferred();
awaiting_scroll_offset_update = true;
}
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
void GraphEdit::_update_scroll_offset() {
@@ -415,20 +474,34 @@ void GraphEdit::_graph_element_moved(Node *p_node) {
GraphElement *graph_element = Object::cast_to<GraphElement>(p_node);
ERR_FAIL_NULL(graph_element);
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_node) {
GraphNode *graph_node = Object::cast_to<GraphNode>(p_node);
ERR_FAIL_NULL(graph_node);
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
+}
+
+void GraphEdit::_graph_node_rect_changed(GraphNode *p_node) {
+ // Only invalidate the cache when zooming or the node is moved/resized in graph space.
+ if (panner->is_panning()) {
+ return;
+ }
+
+ for (Ref<Connection> &c : connection_map[p_node->get_name()]) {
+ c->_cache.dirty = true;
+ }
+
+ connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
void GraphEdit::add_child_notify(Node *p_child) {
@@ -445,12 +518,12 @@ void GraphEdit::add_child_notify(Node *p_child) {
GraphNode *graph_node = Object::cast_to<GraphNode>(graph_element);
if (graph_node) {
- graph_element->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated).bind(graph_element));
+ graph_node->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated).bind(graph_element));
+ graph_node->connect("item_rect_changed", callable_mp(this, &GraphEdit::_graph_node_rect_changed).bind(graph_node));
}
graph_element->connect("raise_request", callable_mp(this, &GraphEdit::_graph_element_moved_to_front).bind(graph_element));
graph_element->connect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resized).bind(graph_element));
- graph_element->connect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw));
graph_element->connect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw));
graph_element->set_scale(Vector2(zoom, zoom));
@@ -482,16 +555,20 @@ void GraphEdit::remove_child_notify(Node *p_child) {
GraphNode *graph_node = Object::cast_to<GraphNode>(graph_element);
if (graph_node) {
- graph_element->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated));
+ graph_node->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated));
+ graph_node->disconnect("item_rect_changed", callable_mp(this, &GraphEdit::_graph_node_rect_changed));
+
+ // Invalidate all adjacent connections, so that they are removed before the next redraw.
+ for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) {
+ conn->_cache.dirty = true;
+ }
+ connections_layer->queue_redraw();
}
graph_element->disconnect("raise_request", callable_mp(this, &GraphEdit::_graph_element_moved_to_front));
graph_element->disconnect("resize_request", callable_mp(this, &GraphEdit::_graph_element_resized));
// In case of the whole GraphEdit being destroyed these references can already be freed.
- if (connections_layer != nullptr && connections_layer->is_inside_tree()) {
- graph_element->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::queue_redraw));
- }
if (minimap != nullptr && minimap->is_inside_tree()) {
graph_element->disconnect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::queue_redraw));
}
@@ -520,7 +597,6 @@ void GraphEdit::_notification(int p_what) {
menu_panel->add_theme_style_override("panel", theme_cache.menu_panel);
} break;
-
case NOTIFICATION_READY: {
Size2 hmin = h_scrollbar->get_combined_minimum_size();
Size2 vmin = v_scrollbar->get_combined_minimum_size();
@@ -535,7 +611,6 @@ void GraphEdit::_notification(int p_what) {
v_scrollbar->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0);
v_scrollbar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0);
} break;
-
case NOTIFICATION_DRAW: {
// Draw background fill.
draw_style_box(theme_cache.panel, Rect2(Point2(), get_size()));
@@ -547,8 +622,8 @@ void GraphEdit::_notification(int p_what) {
} break;
case NOTIFICATION_RESIZED: {
_update_scroll();
- top_layer->queue_redraw();
minimap->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
} break;
}
}
@@ -593,7 +668,7 @@ bool GraphEdit::_filter_input(const Point2 &p_point) {
return false;
}
-void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
+void GraphEdit::_top_connection_layer_input(const Ref<InputEvent> &p_ev) {
Ref<InputEventMouseButton> mb = p_ev;
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) {
connecting_valid = false;
@@ -618,26 +693,26 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if (is_in_output_hotzone(graph_node, j, click_pos, port_size)) {
if (valid_left_disconnect_types.has(graph_node->get_output_port_type(j))) {
// Check disconnect.
- for (const Connection &E : connections) {
- if (E.from_node == graph_node->get_name() && E.from_port == j) {
- Node *to = get_node(NodePath(E.to_node));
+ for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) {
+ if (conn->from_node == graph_node->get_name() && conn->from_port == j) {
+ Node *to = get_node(NodePath(conn->to_node));
if (Object::cast_to<GraphNode>(to)) {
- connecting_from = E.to_node;
- connecting_index = E.to_port;
- connecting_out = false;
- connecting_type = Object::cast_to<GraphNode>(to)->get_input_port_type(E.to_port);
- connecting_color = Object::cast_to<GraphNode>(to)->get_input_port_color(E.to_port);
- connecting_target = false;
- connecting_to = pos;
+ connecting_from_node = conn->to_node;
+ connecting_from_port_index = conn->to_port;
+ connecting_from_output = false;
+ connecting_type = Object::cast_to<GraphNode>(to)->get_input_port_type(conn->to_port);
+ connecting_color = Object::cast_to<GraphNode>(to)->get_input_port_color(conn->to_port);
+ connecting_target_valid = false;
+ connecting_to_point = pos;
if (connecting_type >= 0) {
just_disconnected = true;
- emit_signal(SNAME("disconnection_request"), E.from_node, E.from_port, E.to_node, E.to_port);
- to = get_node(NodePath(connecting_from)); // Maybe it was erased.
+ emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port);
+ to = get_node(NodePath(connecting_from_node)); // Maybe it was erased.
if (Object::cast_to<GraphNode>(to)) {
connecting = true;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false);
}
}
return;
@@ -646,17 +721,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
}
}
- connecting_from = graph_node->get_name();
- connecting_index = j;
- connecting_out = true;
+ connecting_from_node = graph_node->get_name();
+ connecting_from_port_index = j;
+ connecting_from_output = true;
connecting_type = graph_node->get_output_port_type(j);
connecting_color = graph_node->get_output_port_color(j);
- connecting_target = false;
- connecting_to = pos;
+ connecting_target_valid = false;
+ connecting_to_point = pos;
if (connecting_type >= 0) {
connecting = true;
just_disconnected = false;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true);
}
return;
}
@@ -675,25 +750,25 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if (is_in_input_hotzone(graph_node, j, click_pos, port_size)) {
if (right_disconnects || valid_right_disconnect_types.has(graph_node->get_input_port_type(j))) {
// Check disconnect.
- for (const Connection &E : connections) {
- if (E.to_node == graph_node->get_name() && E.to_port == j) {
- Node *fr = get_node(NodePath(E.from_node));
+ for (const Ref<Connection> &conn : connection_map[graph_node->get_name()]) {
+ if (conn->to_node == graph_node->get_name() && conn->to_port == j) {
+ Node *fr = get_node(NodePath(conn->from_node));
if (Object::cast_to<GraphNode>(fr)) {
- connecting_from = E.from_node;
- connecting_index = E.from_port;
- connecting_out = true;
- connecting_type = Object::cast_to<GraphNode>(fr)->get_output_port_type(E.from_port);
- connecting_color = Object::cast_to<GraphNode>(fr)->get_output_port_color(E.from_port);
- connecting_target = false;
- connecting_to = pos;
+ connecting_from_node = conn->from_node;
+ connecting_from_port_index = conn->from_port;
+ connecting_from_output = true;
+ connecting_type = Object::cast_to<GraphNode>(fr)->get_output_port_type(conn->from_port);
+ connecting_color = Object::cast_to<GraphNode>(fr)->get_output_port_color(conn->from_port);
+ connecting_target_valid = false;
+ connecting_to_point = pos;
just_disconnected = true;
if (connecting_type >= 0) {
- emit_signal(SNAME("disconnection_request"), E.from_node, E.from_port, E.to_node, E.to_port);
- fr = get_node(NodePath(connecting_from));
+ emit_signal(SNAME("disconnection_request"), conn->from_node, conn->from_port, conn->to_node, conn->to_port);
+ fr = get_node(NodePath(connecting_from_node));
if (Object::cast_to<GraphNode>(fr)) {
connecting = true;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, true);
}
}
return;
@@ -702,17 +777,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
}
}
- connecting_from = graph_node->get_name();
- connecting_index = j;
- connecting_out = false;
+ connecting_from_node = graph_node->get_name();
+ connecting_from_port_index = j;
+ connecting_from_output = false;
connecting_type = graph_node->get_input_port_type(j);
connecting_color = graph_node->get_input_port_color(j);
- connecting_target = false;
- connecting_to = pos;
+ connecting_target_valid = false;
+ connecting_to_point = pos;
if (connecting_type >= 0) {
connecting = true;
just_disconnected = false;
- emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false);
+ emit_signal(SNAME("connection_drag_started"), connecting_from_node, connecting_from_port_index, false);
}
return;
}
@@ -722,12 +797,11 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
Ref<InputEventMouseMotion> mm = p_ev;
if (mm.is_valid() && connecting) {
- connecting_to = mm->get_position();
- connecting_target = false;
- top_layer->queue_redraw();
+ connecting_to_point = mm->get_position();
minimap->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
- connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > MIN_DRAG_DISTANCE_FOR_VALID_CONNECTION;
+ connecting_valid = just_disconnected || click_pos.distance_to(connecting_to_point / zoom) > MIN_DRAG_DISTANCE_FOR_VALID_CONNECTION;
if (connecting_valid) {
Vector2 mpos = mm->get_position() / zoom;
@@ -739,7 +813,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
Ref<Texture2D> port_icon = graph_node->theme_cache.port;
- if (!connecting_out) {
+ if (!connecting_from_output) {
for (int j = 0; j < graph_node->get_output_port_count(); j++) {
Vector2 pos = graph_node->get_output_port_position(j) * zoom + graph_node->get_position();
Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height());
@@ -753,16 +827,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if ((type == connecting_type ||
valid_connection_types.has(ConnectionType(type, connecting_type))) &&
is_in_output_hotzone(graph_node, j, mpos, port_size)) {
- if (!is_node_hover_valid(graph_node->get_name(), j, connecting_from, connecting_index)) {
+ if (!is_node_hover_valid(graph_node->get_name(), j, connecting_from_node, connecting_from_port_index)) {
continue;
}
- connecting_target = true;
- connecting_to = pos;
- connecting_target_to = graph_node->get_name();
- connecting_target_index = j;
+ connecting_target_valid = true;
+ connecting_to_point = pos;
+ connecting_target_node = graph_node->get_name();
+ connecting_target_port_index = j;
return;
}
}
+ connecting_target_valid = false;
} else {
for (int j = 0; j < graph_node->get_input_port_count(); j++) {
Vector2 pos = graph_node->get_input_port_position(j) * zoom + graph_node->get_position();
@@ -776,16 +851,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
int type = graph_node->get_input_port_type(j);
if ((type == connecting_type || valid_connection_types.has(ConnectionType(connecting_type, type))) &&
is_in_input_hotzone(graph_node, j, mpos, port_size)) {
- if (!is_node_hover_valid(connecting_from, connecting_index, graph_node->get_name(), j)) {
+ if (!is_node_hover_valid(connecting_from_node, connecting_from_port_index, graph_node->get_name(), j)) {
continue;
}
- connecting_target = true;
- connecting_to = pos;
- connecting_target_to = graph_node->get_name();
- connecting_target_index = j;
+ connecting_target_valid = true;
+ connecting_to_point = pos;
+ connecting_target_node = graph_node->get_name();
+ connecting_target_port_index = j;
return;
}
}
+ connecting_target_valid = false;
}
}
}
@@ -793,17 +869,17 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) {
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed()) {
if (connecting_valid) {
- if (connecting && connecting_target) {
- if (connecting_out) {
- emit_signal(SNAME("connection_request"), connecting_from, connecting_index, connecting_target_to, connecting_target_index);
+ if (connecting && connecting_target_valid) {
+ if (connecting_from_output) {
+ emit_signal(SNAME("connection_request"), connecting_from_node, connecting_from_port_index, connecting_target_node, connecting_target_port_index);
} else {
- emit_signal(SNAME("connection_request"), connecting_target_to, connecting_target_index, connecting_from, connecting_index);
+ emit_signal(SNAME("connection_request"), connecting_target_node, connecting_target_port_index, connecting_from_node, connecting_from_port_index);
}
} else if (!just_disconnected) {
- if (connecting_out) {
- emit_signal(SNAME("connection_to_empty"), connecting_from, connecting_index, mb->get_position());
+ if (connecting_from_output) {
+ emit_signal(SNAME("connection_to_empty"), connecting_from_node, connecting_from_port_index, mb->get_position());
} else {
- emit_signal(SNAME("connection_from_empty"), connecting_from, connecting_index, mb->get_position());
+ emit_signal(SNAME("connection_from_empty"), connecting_from_node, connecting_from_port_index, mb->get_position());
}
}
}
@@ -905,7 +981,7 @@ bool GraphEdit::is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_
return true;
}
-PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) {
+PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const {
Vector<Vector2> ret;
if (GDVIRTUAL_CALL(_get_connection_line, p_from, p_to, ret)) {
return ret;
@@ -930,96 +1006,249 @@ PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const V
}
}
-void GraphEdit::_draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom) {
- Vector<Vector2> points = get_connection_line(p_from / p_zoom, p_to / p_zoom);
- Vector<Vector2> scaled_points;
- Vector<Color> colors;
- float length = (p_from / p_zoom).distance_to(p_to / p_zoom);
- for (int i = 0; i < points.size(); i++) {
- float d = (p_from / p_zoom).distance_to(points[i]) / length;
- colors.push_back(p_color.lerp(p_to_color, d));
- scaled_points.push_back(points[i] * p_zoom);
+Ref<GraphEdit::Connection> GraphEdit::get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance) const {
+ Vector2 transformed_point = p_point + get_scroll_offset();
+
+ Ref<GraphEdit::Connection> closest_connection;
+ float closest_distance = p_max_distance;
+ for (const Ref<Connection> &c : connections) {
+ if (c->_cache.aabb.distance_to(transformed_point) > p_max_distance) {
+ continue;
+ }
+
+ Vector<Vector2> points = get_connection_line(c->_cache.from_pos * zoom, c->_cache.to_pos * zoom);
+ for (int i = 0; i < points.size() - 1; i++) {
+ float distance = Geometry2D::get_distance_to_segment(transformed_point, &points[i]);
+ if (distance <= lines_thickness * 0.5 + p_max_distance && distance < closest_distance) {
+ closest_connection = c;
+ closest_distance = distance;
+ }
+ }
}
- // Thickness below 0.5 doesn't look good on the graph or its minimap.
- p_where->draw_polyline_colors(scaled_points, colors, MAX(0.5, Math::floor(p_width * theme_cache.base_scale)), lines_antialiased);
+ return closest_connection;
}
-void GraphEdit::_connections_layer_draw() {
- // Draw connections.
- List<List<Connection>::Element *> to_erase;
- for (List<Connection>::Element *E = connections.front(); E; E = E->next()) {
- const Connection &c = E->get();
-
- Node *from = get_node(NodePath(c.from_node));
- GraphNode *gnode_from = Object::cast_to<GraphNode>(from);
+List<Ref<GraphEdit::Connection>> GraphEdit::get_connections_intersecting_with_rect(const Rect2 &p_rect) const {
+ Rect2 transformed_rect = p_rect;
+ transformed_rect.position += get_scroll_offset();
- if (!gnode_from) {
- to_erase.push_back(E);
+ List<Ref<Connection>> intersecting_connections;
+ for (const Ref<Connection> &c : connections) {
+ if (!c->_cache.aabb.intersects(transformed_rect)) {
continue;
}
- Node *to = get_node(NodePath(c.to_node));
- GraphNode *gnode_to = Object::cast_to<GraphNode>(to);
+ Vector<Vector2> points = get_connection_line(c->_cache.from_pos * zoom, c->_cache.to_pos * zoom);
+ for (int i = 0; i < points.size() - 1; i++) {
+ if (Geometry2D::segment_intersects_rect(points[i], points[i + 1], transformed_rect)) {
+ intersecting_connections.push_back(c);
+ break;
+ }
+ }
+ }
+ return intersecting_connections;
+}
+
+void GraphEdit::_draw_minimap_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_from_color, const Color &p_to_color) {
+ const Vector<Vector2> &points = get_connection_line(p_from, p_to);
+ LocalVector<Color> colors;
+ colors.reserve(points.size());
+
+ float length_inv = 1.0 / (p_from).distance_to(p_to);
+ for (const Vector2 &point : points) {
+ float normalized_curve_position = (p_from).distance_to(point) * length_inv;
+ colors.push_back(p_from_color.lerp(p_to_color, normalized_curve_position));
+ }
+
+ p_where->draw_polyline_colors(points, colors, 0.5, lines_antialiased);
+}
+
+void GraphEdit::_update_connections() {
+ // Collect all dead connections and remove them.
+ List<List<Ref<Connection>>::Element *> dead_connections;
+
+ for (List<Ref<Connection>>::Element *E = connections.front(); E; E = E->next()) {
+ Ref<Connection> &c = E->get();
+
+ if (c->_cache.dirty) {
+ Node *from = get_node_or_null(NodePath(c->from_node));
+ GraphNode *gnode_from = Object::cast_to<GraphNode>(from);
+ if (!gnode_from) {
+ dead_connections.push_back(E);
+ continue;
+ }
+ Node *to = get_node_or_null(NodePath(c->to_node));
+ GraphNode *gnode_to = Object::cast_to<GraphNode>(to);
+
+ if (!gnode_to) {
+ dead_connections.push_back(E);
+ continue;
+ }
- if (!gnode_to) {
- to_erase.push_back(E);
+ const Vector2 from_pos = gnode_from->get_output_port_position(c->from_port) + gnode_from->get_position_offset();
+ const Vector2 to_pos = gnode_to->get_input_port_position(c->to_port) + gnode_to->get_position_offset();
+
+ const Color from_color = gnode_from->get_output_port_color(c->from_port);
+ const Color to_color = gnode_to->get_input_port_color(c->to_port);
+
+ const int from_type = gnode_from->get_output_port_type(c->from_port);
+ const int to_type = gnode_to->get_input_port_type(c->to_port);
+
+ c->_cache.from_pos = from_pos;
+ c->_cache.to_pos = to_pos;
+ c->_cache.from_color = from_color;
+ c->_cache.to_color = to_color;
+
+ PackedVector2Array line_points = get_connection_line(from_pos * zoom, to_pos * zoom);
+ c->_cache.line->set_points(line_points);
+
+ Ref<ShaderMaterial> line_material = c->_cache.line->get_material();
+ if (line_material.is_null()) {
+ line_material.instantiate();
+ c->_cache.line->set_material(line_material);
+ }
+
+ float line_width = _get_shader_line_width();
+ line_material->set_shader_parameter("line_width", line_width);
+ line_material->set_shader_parameter("from_type", from_type);
+ line_material->set_shader_parameter("to_type", to_type);
+ line_material->set_shader_parameter("rim_color", theme_cache.connection_rim_color);
+
+ // Compute bounding box of the line, including the line width.
+ c->_cache.aabb = Rect2(line_points[0], Vector2());
+ for (int i = 0; i < line_points.size(); i++) {
+ c->_cache.aabb.expand_to(line_points[i]);
+ }
+ c->_cache.aabb.grow_by(lines_thickness * 0.5);
+
+ c->_cache.dirty = false;
+ }
+
+ // Skip updating/drawing connections that are not visible.
+ Rect2 viewport_rect = get_viewport_rect();
+ viewport_rect.position += get_scroll_offset();
+ if (!c->_cache.aabb.intersects(viewport_rect)) {
continue;
}
- Vector2 frompos = gnode_from->get_output_port_position(c.from_port) * zoom + gnode_from->get_position_offset() * zoom;
- Color color = gnode_from->get_output_port_color(c.from_port);
- Vector2 topos = gnode_to->get_input_port_position(c.to_port) * zoom + gnode_to->get_position_offset() * zoom;
- Color tocolor = gnode_to->get_input_port_color(c.to_port);
+ Color from_color = c->_cache.from_color;
+ Color to_color = c->_cache.to_color;
+
+ if (c->activity > 0) {
+ from_color = from_color.lerp(theme_cache.activity_color, c->activity);
+ to_color = to_color.lerp(theme_cache.activity_color, c->activity);
+ }
- if (c.activity > 0) {
- color = color.lerp(theme_cache.activity_color, c.activity);
- tocolor = tocolor.lerp(theme_cache.activity_color, c.activity);
+ if (c == hovered_connection) {
+ from_color = from_color.blend(theme_cache.connection_hover_tint_color);
+ to_color = to_color.blend(theme_cache.connection_hover_tint_color);
}
- _draw_connection_line(connections_layer, frompos, topos, color, tocolor, lines_thickness, zoom);
+
+ // Update Line2D node.
+ Ref<Gradient> line_gradient = memnew(Gradient);
+
+ float line_width = _get_shader_line_width();
+ c->_cache.line->set_width(line_width);
+ line_gradient->set_color(0, from_color);
+ line_gradient->set_color(1, to_color);
+
+ c->_cache.line->set_gradient(line_gradient);
}
- for (List<Connection>::Element *&E : to_erase) {
- connections.erase(E);
+ for (const List<Ref<Connection>>::Element *E : dead_connections) {
+ List<Ref<Connection>> &connections_from = connection_map[E->get()->from_node];
+ List<Ref<Connection>> &connections_to = connection_map[E->get()->to_node];
+ connections_from.erase(E->get());
+ connections_to.erase(E->get());
+ E->get()->_cache.line->queue_free();
+
+ connections.erase(E->get());
}
}
void GraphEdit::_top_layer_draw() {
+ if (!box_selecting) {
+ return;
+ }
+
+ top_layer->draw_rect(box_selecting_rect, theme_cache.selection_fill);
+ top_layer->draw_rect(box_selecting_rect, theme_cache.selection_stroke, false);
+}
+
+void GraphEdit::_update_top_connection_layer() {
_update_scroll();
- if (connecting) {
- Node *node_from = get_node_or_null(NodePath(connecting_from));
- ERR_FAIL_NULL(node_from);
- GraphNode *graph_node_from = Object::cast_to<GraphNode>(node_from);
- ERR_FAIL_NULL(graph_node_from);
- Vector2 pos;
- if (connecting_out) {
- pos = graph_node_from->get_output_port_position(connecting_index) * zoom;
- } else {
- pos = graph_node_from->get_input_port_position(connecting_index) * zoom;
- }
- pos += graph_node_from->get_position();
+ if (!connecting) {
+ dragged_connection_line->clear_points();
- Vector2 to_pos = connecting_to;
- Color line_color = connecting_color;
+ return;
+ }
- // Draw the line to the mouse cursor brighter when it's over a valid target port.
- if (connecting_target) {
- line_color.r += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING;
- line_color.g += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING;
- line_color.b += CONNECTING_TARGET_LINE_COLOR_BRIGHTENING;
- }
+ GraphNode *graph_node_from = Object::cast_to<GraphNode>(get_node_or_null(NodePath(connecting_from_node)));
+ ERR_FAIL_NULL(graph_node_from);
- if (!connecting_out) {
- SWAP(pos, to_pos);
+ Vector2 from_pos = graph_node_from->get_position() / zoom;
+ Vector2 to_pos = connecting_to_point / zoom;
+ int from_type;
+ int to_type = connecting_type;
+ Color from_color;
+ Color to_color = connecting_color;
+
+ if (connecting_from_output) {
+ from_pos += graph_node_from->get_output_port_position(connecting_from_port_index);
+ from_type = graph_node_from->get_output_port_type(connecting_from_port_index);
+ from_color = graph_node_from->get_output_port_color(connecting_from_port_index);
+ } else {
+ from_pos += graph_node_from->get_input_port_position(connecting_from_port_index);
+ from_type = graph_node_from->get_input_port_type(connecting_from_port_index);
+ from_color = graph_node_from->get_input_port_color(connecting_from_port_index);
+ }
+
+ if (connecting_target_valid) {
+ GraphNode *graph_node_to = Object::cast_to<GraphNode>(get_node_or_null(NodePath(connecting_target_node)));
+ ERR_FAIL_NULL(graph_node_to);
+ if (connecting_from_output) {
+ to_type = graph_node_to->get_input_port_type(connecting_target_port_index);
+ to_color = graph_node_to->get_input_port_color(connecting_target_port_index);
+ } else {
+ to_type = graph_node_to->get_output_port_type(connecting_target_port_index);
+ to_color = graph_node_to->get_output_port_color(connecting_target_port_index);
}
- _draw_connection_line(top_layer, pos, to_pos, line_color, line_color, lines_thickness, zoom);
+
+ // Highlight the line to the mouse cursor when it's over a valid target port.
+ from_color = from_color.blend(theme_cache.connection_valid_target_tint_color);
+ to_color = to_color.blend(theme_cache.connection_valid_target_tint_color);
+ }
+
+ if (!connecting_from_output) {
+ SWAP(from_pos, to_pos);
+ SWAP(from_type, to_type);
+ SWAP(from_color, to_color);
}
- if (box_selecting) {
- top_layer->draw_rect(box_selecting_rect, theme_cache.selection_fill);
- top_layer->draw_rect(box_selecting_rect, theme_cache.selection_stroke, false);
+ PackedVector2Array line_points = get_connection_line(from_pos * zoom, to_pos * zoom);
+ dragged_connection_line->set_points(line_points);
+
+ Ref<ShaderMaterial> line_material = dragged_connection_line->get_material();
+ if (line_material.is_null()) {
+ line_material.instantiate();
+ line_material->set_shader(connections_shader);
+ dragged_connection_line->set_material(line_material);
}
+
+ float line_width = _get_shader_line_width();
+ line_material->set_shader_parameter("line_width", line_width);
+ line_material->set_shader_parameter("from_type", from_type);
+ line_material->set_shader_parameter("to_type", to_type);
+ line_material->set_shader_parameter("rim_color", theme_cache.connection_rim_color);
+
+ Ref<Gradient> line_gradient = memnew(Gradient);
+ dragged_connection_line->set_width(line_width);
+ line_gradient->set_color(0, from_color);
+ line_gradient->set_color(1, to_color);
+
+ dragged_connection_line->set_gradient(line_gradient);
}
void GraphEdit::_minimap_draw() {
@@ -1060,31 +1289,17 @@ void GraphEdit::_minimap_draw() {
}
// Draw node connections.
- for (const Connection &E : connections) {
- Node *from = get_node(NodePath(E.from_node));
- GraphNode *graph_node_from = Object::cast_to<GraphNode>(from);
- if (!graph_node_from) {
- continue;
- }
-
- Node *node_to = get_node(NodePath(E.to_node));
- GraphNode *graph_node_to = Object::cast_to<GraphNode>(node_to);
- if (!graph_node_to) {
- continue;
- }
-
- Vector2 from_port_position = graph_node_from->get_position_offset() * zoom + graph_node_from->get_output_port_position(E.from_port) * zoom;
- Vector2 from_position = minimap->_convert_from_graph_position(from_port_position - graph_offset) + minimap_offset;
- Color from_color = graph_node_from->get_output_port_color(E.from_port);
- Vector2 to_port_position = graph_node_to->get_position_offset() * zoom + graph_node_to->get_input_port_position(E.to_port) * zoom;
- Vector2 to_position = minimap->_convert_from_graph_position(to_port_position - graph_offset) + minimap_offset;
- Color to_color = graph_node_to->get_input_port_color(E.to_port);
-
- if (E.activity > 0) {
- from_color = from_color.lerp(theme_cache.activity_color, E.activity);
- to_color = to_color.lerp(theme_cache.activity_color, E.activity);
+ for (const Ref<Connection> &c : connections) {
+ Vector2 from_position = minimap->_convert_from_graph_position(c->_cache.from_pos * zoom - graph_offset) + minimap_offset;
+ Vector2 to_position = minimap->_convert_from_graph_position(c->_cache.to_pos * zoom - graph_offset) + minimap_offset;
+ Color from_color = c->_cache.from_color;
+ Color to_color = c->_cache.to_color;
+
+ if (c->activity > 0) {
+ from_color = from_color.lerp(theme_cache.activity_color, c->activity);
+ to_color = to_color.lerp(theme_cache.activity_color, c->activity);
}
- _draw_connection_line(minimap, from_position, to_position, from_color, to_color, 0.5, minimap->_convert_from_graph_position(Vector2(zoom, zoom)).length());
+ _draw_minimap_connection_line(minimap, from_position, to_position, from_color, to_color);
}
// Draw the "camera" viewport.
@@ -1175,7 +1390,15 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
return;
}
+ // Highlight the connection close to the mouse cursor.
Ref<InputEventMouseMotion> mm = p_ev;
+ if (mm.is_valid()) {
+ Ref<Connection> new_highlighted_connection = get_closest_connection_at_point(mm->get_position());
+ if (new_highlighted_connection != hovered_connection) {
+ connections_layer->queue_redraw();
+ }
+ hovered_connection = new_highlighted_connection;
+ }
if (mm.is_valid() && dragging) {
if (!moving_selection) {
@@ -1201,6 +1424,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
}
}
+ // Box selection logic.
if (mm.is_valid() && box_selecting) {
box_selecting_to = mm->get_position();
@@ -1281,10 +1505,10 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
dragging = false;
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
// Node selection logic.
@@ -1430,29 +1654,56 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) {
void GraphEdit::_pan_callback(Vector2 p_scroll_vec, Ref<InputEvent> p_event) {
h_scrollbar->set_value(h_scrollbar->get_value() - p_scroll_vec.x);
v_scrollbar->set_value(v_scrollbar->get_value() - p_scroll_vec.y);
+
+ connections_layer->queue_redraw();
}
void GraphEdit::_zoom_callback(float p_zoom_factor, Vector2 p_origin, Ref<InputEvent> p_event) {
+ // We need to invalidate all connections since we don't know whether
+ // the user is zooming/panning at the same time.
+ _invalidate_connection_line_cache();
+
set_zoom_custom(zoom * p_zoom_factor, p_origin);
}
void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity) {
- for (Connection &E : connections) {
- if (E.from_node == p_from && E.from_port == p_from_port && E.to_node == p_to && E.to_port == p_to_port) {
- if (!Math::is_equal_approx(E.activity, p_activity)) {
+ for (Ref<Connection> &c : connection_map[p_from]) {
+ if (c->from_node == p_from && c->from_port == p_from_port && c->to_node == p_to && c->to_port == p_to_port) {
+ if (!Math::is_equal_approx(c->activity, p_activity)) {
// Update only if changed.
- top_layer->queue_redraw();
minimap->queue_redraw();
+ c->_cache.dirty = true;
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
}
- E.activity = p_activity;
+ c->activity = p_activity;
return;
}
}
}
+void GraphEdit::reset_all_connection_activity() {
+ bool changed = false;
+ for (Ref<Connection> &conn : connections) {
+ if (conn->activity > 0) {
+ changed = true;
+ conn->_cache.dirty = true;
+ }
+ conn->activity = 0;
+ }
+ if (changed) {
+ connections_layer->queue_redraw();
+ }
+}
+
void GraphEdit::clear_connections() {
+ for (Ref<Connection> &c : connections) {
+ c->_cache.line->queue_free();
+ }
+
connections.clear();
+ connection_map.clear();
+
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
@@ -1462,10 +1713,10 @@ void GraphEdit::force_connection_drag_end() {
ERR_FAIL_COND_MSG(!connecting, "Drag end requested without active drag!");
connecting = false;
connecting_valid = false;
- top_layer->queue_redraw();
minimap->queue_redraw();
queue_redraw();
connections_layer->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
emit_signal(SNAME("connection_drag_ended"));
}
@@ -1497,7 +1748,8 @@ void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) {
Vector2 scrollbar_offset = (Vector2(h_scrollbar->get_value(), v_scrollbar->get_value()) + p_center) / zoom;
zoom = p_zoom;
- top_layer->queue_redraw();
+
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
zoom_minus_button->set_disabled(zoom == zoom_min);
zoom_plus_button->set_disabled(zoom == zoom_max);
@@ -1590,15 +1842,42 @@ void GraphEdit::remove_valid_left_disconnect_type(int p_type) {
}
TypedArray<Dictionary> GraphEdit::_get_connection_list() const {
- List<Connection> conns;
- get_connection_list(&conns);
+ List<Ref<Connection>> conns = get_connection_list();
+
TypedArray<Dictionary> arr;
- for (const Connection &E : conns) {
+ for (const Ref<Connection> &conn : conns) {
Dictionary d;
- d["from_node"] = E.from_node;
- d["from_port"] = E.from_port;
- d["to_node"] = E.to_node;
- d["to_port"] = E.to_port;
+ d["from_node"] = conn->from_node;
+ d["from_port"] = conn->from_port;
+ d["to_node"] = conn->to_node;
+ d["to_port"] = conn->to_port;
+ arr.push_back(d);
+ }
+ return arr;
+}
+
+Dictionary GraphEdit::_get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance) const {
+ Dictionary ret;
+ Ref<Connection> c = get_closest_connection_at_point(p_point, p_max_distance);
+ if (c.is_valid()) {
+ ret["from_node"] = c->from_node;
+ ret["from_port"] = c->from_port;
+ ret["to_node"] = c->to_node;
+ ret["to_port"] = c->to_port;
+ }
+ return ret;
+}
+
+TypedArray<Dictionary> GraphEdit::_get_connections_intersecting_with_rect(const Rect2 &p_rect) const {
+ List<Ref<Connection>> intersecting_connections = get_connections_intersecting_with_rect(p_rect);
+
+ TypedArray<Dictionary> arr;
+ for (const Ref<Connection> &conn : intersecting_connections) {
+ Dictionary d;
+ d["from_node"] = conn->from_node;
+ d["from_port"] = conn->from_port;
+ d["to_node"] = conn->to_node;
+ d["to_port"] = conn->to_port;
arr.push_back(d);
}
return arr;
@@ -1622,6 +1901,16 @@ void GraphEdit::_update_zoom_label() {
zoom_label->set_text(zoom_text);
}
+void GraphEdit::_invalidate_connection_line_cache() {
+ for (Ref<Connection> &c : connections) {
+ c->_cache.dirty = true;
+ }
+}
+
+float GraphEdit::_get_shader_line_width() {
+ return lines_thickness * theme_cache.base_scale + 4.0;
+}
+
void GraphEdit::add_valid_connection_type(int p_type, int p_with_type) {
ConnectionType ct(p_type, p_with_type);
valid_connection_types.insert(ct);
@@ -1806,6 +2095,15 @@ bool GraphEdit::is_showing_arrange_button() const {
return show_arrange_button;
}
+void GraphEdit::override_connections_shader(const Ref<Shader> &p_shader) {
+ connections_shader = p_shader;
+
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
+ minimap->queue_redraw();
+ callable_mp(this, &GraphEdit::_update_top_connection_layer).call_deferred();
+}
+
void GraphEdit::_minimap_toggled() {
if (is_minimap_enabled()) {
minimap->set_visible(true);
@@ -1817,6 +2115,8 @@ void GraphEdit::_minimap_toggled() {
void GraphEdit::set_connection_lines_curvature(float p_curvature) {
lines_curvature = p_curvature;
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
queue_redraw();
}
@@ -1825,10 +2125,13 @@ float GraphEdit::get_connection_lines_curvature() const {
}
void GraphEdit::set_connection_lines_thickness(float p_thickness) {
+ ERR_FAIL_COND_MSG(p_thickness < 0, "Connection lines thickness must be greater than or equal to 0.");
if (lines_thickness == p_thickness) {
return;
}
lines_thickness = p_thickness;
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
queue_redraw();
}
@@ -1841,6 +2144,8 @@ void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) {
return;
}
lines_antialiased = p_antialiased;
+ _invalidate_connection_line_cache();
+ connections_layer->queue_redraw();
queue_redraw();
}
@@ -1870,6 +2175,8 @@ void GraphEdit::_bind_methods() {
ClassDB::bind_method(D_METHOD("disconnect_node", "from_node", "from_port", "to_node", "to_port"), &GraphEdit::disconnect_node);
ClassDB::bind_method(D_METHOD("set_connection_activity", "from_node", "from_port", "to_node", "to_port", "amount"), &GraphEdit::set_connection_activity);
ClassDB::bind_method(D_METHOD("get_connection_list"), &GraphEdit::_get_connection_list);
+ ClassDB::bind_method(D_METHOD("get_closest_connection_at_point", "point", "max_distance"), &GraphEdit::_get_closest_connection_at_point, DEFVAL(4.0));
+ ClassDB::bind_method(D_METHOD("get_connections_intersecting_with_rect", "rect"), &GraphEdit::_get_connections_intersecting_with_rect);
ClassDB::bind_method(D_METHOD("clear_connections"), &GraphEdit::clear_connections);
ClassDB::bind_method(D_METHOD("force_connection_drag_end"), &GraphEdit::force_connection_drag_end);
ClassDB::bind_method(D_METHOD("get_scroll_offset"), &GraphEdit::get_scroll_offset);
@@ -1971,7 +2278,7 @@ void GraphEdit::_bind_methods() {
ADD_GROUP("Connection Lines", "connection_lines");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_curvature"), "set_connection_lines_curvature", "get_connection_lines_curvature");
- ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_NONE, "suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness", PROPERTY_HINT_RANGE, "0,100,0.1,suffix:px"), "set_connection_lines_thickness", "get_connection_lines_thickness");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "connection_lines_antialiased"), "set_connection_lines_antialiased", "is_connection_lines_antialiased");
ADD_GROUP("Zoom", "");
@@ -2025,6 +2332,9 @@ void GraphEdit::_bind_methods() {
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, grid_minor);
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_COLOR, GraphEdit, activity_color, "activity");
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_hover_tint_color);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_valid_target_tint_color);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, connection_rim_color);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, selection_fill);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, GraphEdit, selection_stroke);
@@ -2056,21 +2366,33 @@ GraphEdit::GraphEdit() {
panner.instantiate();
panner->set_callbacks(callable_mp(this, &GraphEdit::_pan_callback), callable_mp(this, &GraphEdit::_zoom_callback));
- top_layer = memnew(GraphEditFilter(this));
+ top_layer = memnew(Control);
add_child(top_layer, false, INTERNAL_MODE_BACK);
- top_layer->set_mouse_filter(MOUSE_FILTER_PASS);
+ top_layer->set_mouse_filter(MOUSE_FILTER_IGNORE);
top_layer->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
top_layer->connect("draw", callable_mp(this, &GraphEdit::_top_layer_draw));
- top_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_layer_input));
top_layer->connect("focus_exited", callable_mp(panner.ptr(), &ViewPanner::release_pan_key));
connections_layer = memnew(Control);
add_child(connections_layer, false, INTERNAL_MODE_FRONT);
- connections_layer->connect("draw", callable_mp(this, &GraphEdit::_connections_layer_draw));
+ connections_layer->connect("draw", callable_mp(this, &GraphEdit::_update_connections));
connections_layer->set_name("_connection_layer");
connections_layer->set_disable_visibility_clip(true); // Necessary, so it can draw freely and be offset.
connections_layer->set_mouse_filter(MOUSE_FILTER_IGNORE);
+ top_connection_layer = memnew(GraphEditFilter(this));
+ add_child(top_connection_layer, false, INTERNAL_MODE_BACK);
+
+ connections_shader = default_connections_shader;
+
+ top_connection_layer->set_mouse_filter(MOUSE_FILTER_PASS);
+ top_connection_layer->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+ top_connection_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_connection_layer_input));
+
+ dragged_connection_line = memnew(Line2D);
+ dragged_connection_line->set_texture_mode(Line2D::LINE_TEXTURE_STRETCH);
+ top_connection_layer->add_child(dragged_connection_line);
+
h_scrollbar = memnew(HScrollBar);
h_scrollbar->set_name("_h_scroll");
top_layer->add_child(h_scrollbar);
diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h
index 31cb495bf8..e24f039e84 100644
--- a/scene/gui/graph_edit.h
+++ b/scene/gui/graph_edit.h
@@ -39,6 +39,7 @@ class GraphEdit;
class GraphEditArranger;
class HScrollBar;
class Label;
+class Line2D;
class PanelContainer;
class SpinBox;
class ViewPanner;
@@ -112,12 +113,25 @@ class GraphEdit : public Control {
GDCLASS(GraphEdit, Control);
public:
- struct Connection {
+ struct Connection : RefCounted {
StringName from_node;
StringName to_node;
int from_port = 0;
int to_port = 0;
float activity = 0.0;
+
+ private:
+ struct Cache {
+ bool dirty = true;
+ Vector2 from_pos; // In graph space.
+ Vector2 to_pos; // In graph space.
+ Color from_color;
+ Color to_color;
+ Rect2 aabb; // In local screen space.
+ Line2D *line = nullptr; // In local screen space.
+ } _cache;
+
+ friend class GraphEdit;
};
// Should be in sync with ControlScheme in ViewPanner.
@@ -184,15 +198,15 @@ private:
GridPattern grid_pattern = GRID_PATTERN_LINES;
bool connecting = false;
- String connecting_from;
- bool connecting_out = false;
- int connecting_index = 0;
+ StringName connecting_from_node;
+ bool connecting_from_output = false;
int connecting_type = 0;
Color connecting_color;
- bool connecting_target = false;
- Vector2 connecting_to;
- StringName connecting_target_to;
- int connecting_target_index = 0;
+ Vector2 connecting_to_point; // In local screen space.
+ bool connecting_target_valid = false;
+ StringName connecting_target_node;
+ int connecting_from_port_index = 0;
+ int connecting_target_port_index = 0;
bool just_disconnected = false;
bool connecting_valid = false;
@@ -222,18 +236,28 @@ private:
bool right_disconnects = false;
bool updating = false;
bool awaiting_scroll_offset_update = false;
- List<Connection> connections;
- float lines_thickness = 2.0f;
+ List<Ref<Connection>> connections;
+ HashMap<StringName, List<Ref<Connection>>> connection_map;
+ Ref<Connection> hovered_connection;
+
+ float lines_thickness = 4.0f;
float lines_curvature = 0.5f;
bool lines_antialiased = true;
PanelContainer *menu_panel = nullptr;
HBoxContainer *menu_hbox = nullptr;
Control *connections_layer = nullptr;
- GraphEditFilter *top_layer = nullptr;
+
+ GraphEditFilter *top_connection_layer = nullptr; // Draws a dragged connection. Necessary since the connection line shader can't be applied to the whole top layer.
+ Line2D *dragged_connection_line = nullptr;
+ Control *top_layer = nullptr; // Used for drawing the box selection rect. Contains the minimap, menu panel and the scrollbars.
+
GraphEditMinimap *minimap = nullptr;
+ static Ref<Shader> default_connections_shader;
+ Ref<Shader> connections_shader;
+
Ref<GraphEditArranger> arranger;
HashSet<ConnectionType, ConnectionType> valid_connection_types;
@@ -248,6 +272,10 @@ private:
Color grid_minor;
Color activity_color;
+ Color connection_hover_tint_color;
+ Color connection_valid_target_tint_color;
+ Color connection_rim_color;
+
Color selection_fill;
Color selection_stroke;
@@ -274,30 +302,35 @@ private:
void _zoom_plus();
void _update_zoom_label();
- void _draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom);
-
void _graph_element_selected(Node *p_node);
void _graph_element_deselected(Node *p_node);
void _graph_element_moved_to_front(Node *p_node);
void _graph_element_resized(Vector2 p_new_minsize, Node *p_node);
void _graph_element_moved(Node *p_node);
void _graph_node_slot_updated(int p_index, Node *p_node);
+ void _graph_node_rect_changed(GraphNode *p_node);
void _update_scroll();
void _update_scroll_offset();
void _scroll_moved(double);
virtual void gui_input(const Ref<InputEvent> &p_ev) override;
- void _top_layer_input(const Ref<InputEvent> &p_ev);
+ void _top_connection_layer_input(const Ref<InputEvent> &p_ev);
- bool is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left);
+ float _get_shader_line_width();
+ void _draw_minimap_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color);
+ void _invalidate_connection_line_cache();
+ void _update_top_connection_layer();
+ void _update_connections();
void _top_layer_draw();
- void _connections_layer_draw();
void _minimap_draw();
-
void _draw_grid();
+ bool is_in_port_hotzone(const Vector2 &p_pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left);
+
TypedArray<Dictionary> _get_connection_list() const;
+ Dictionary _get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const;
+ TypedArray<Dictionary> _get_connections_intersecting_with_rect(const Rect2 &p_rect) const;
friend class GraphEditFilter;
bool _filter_input(const Point2 &p_point);
@@ -313,6 +346,7 @@ private:
#ifndef DISABLE_DEPRECATED
bool _is_arrange_nodes_button_hidden_bind_compat_81582() const;
void _set_arrange_nodes_button_hidden_bind_compat_81582(bool p_enable);
+ PackedVector2Array _get_connection_line_bind_compat_86158(const Vector2 &p_from, const Vector2 &p_to);
#endif
protected:
@@ -336,6 +370,9 @@ protected:
GDVIRTUAL4R(bool, _is_node_hover_valid, StringName, int, StringName, int);
public:
+ static void init_shaders();
+ static void finish_shaders();
+
virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override;
PackedStringArray get_configuration_warnings() const override;
@@ -344,12 +381,17 @@ public:
bool is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port);
void disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port);
void clear_connections();
+
void force_connection_drag_end();
+ const List<Ref<Connection>> &get_connection_list() const;
+ virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to) const;
+ Ref<Connection> get_closest_connection_at_point(const Vector2 &p_point, float p_max_distance = 4.0) const;
+ List<Ref<Connection>> get_connections_intersecting_with_rect(const Rect2 &p_rect) const;
- virtual PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to);
virtual bool is_node_hover_valid(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port);
void set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity);
+ void reset_all_connection_activity();
void add_valid_connection_type(int p_type, int p_with_type);
void remove_valid_connection_type(int p_type, int p_with_type);
@@ -392,10 +434,10 @@ public:
void set_show_arrange_button(bool p_hidden);
bool is_showing_arrange_button() const;
- GraphEditFilter *get_top_layer() const { return top_layer; }
+ Control *get_top_layer() const { return top_layer; }
GraphEditMinimap *get_minimap() const { return minimap; }
- void get_connection_list(List<Connection> *r_connections) const;
+ void override_connections_shader(const Ref<Shader> &p_shader);
void set_right_disconnects(bool p_enable);
bool is_right_disconnects_enabled() const;
diff --git a/scene/gui/graph_edit_arranger.cpp b/scene/gui/graph_edit_arranger.cpp
index 29c3056b3b..49998beb42 100644
--- a/scene/gui/graph_edit_arranger.cpp
+++ b/scene/gui/graph_edit_arranger.cpp
@@ -65,8 +65,7 @@ void GraphEditArranger::arrange_nodes() {
float gap_v = 100.0f;
float gap_h = 100.0f;
- List<GraphEdit::Connection> connection_list;
- graph_edit->get_connection_list(&connection_list);
+ List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
for (int i = graph_edit->get_child_count() - 1; i >= 0; i--) {
GraphNode *graph_element = Object::cast_to<GraphNode>(graph_edit->get_child(i));
@@ -77,15 +76,16 @@ void GraphEditArranger::arrange_nodes() {
if (graph_element->is_selected() || arrange_entire_graph) {
selected_nodes.insert(graph_element->get_name());
HashSet<StringName> s;
- for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
- GraphNode *p_from = Object::cast_to<GraphNode>(node_names[E->get().from_node]);
- if (E->get().to_node == graph_element->get_name() && (p_from->is_selected() || arrange_entire_graph) && E->get().to_node != E->get().from_node) {
+
+ for (const Ref<GraphEdit::Connection> &connection : connection_list) {
+ GraphNode *p_from = Object::cast_to<GraphNode>(node_names[connection->from_node]);
+ if (connection->to_node == graph_element->get_name() && (p_from->is_selected() || arrange_entire_graph) && connection->to_node != connection->from_node) {
if (!s.has(p_from->get_name())) {
s.insert(p_from->get_name());
}
- String s_connection = String(p_from->get_name()) + " " + String(E->get().to_node);
+ String s_connection = String(p_from->get_name()) + " " + String(connection->to_node);
StringName _connection(s_connection);
- Pair<int, int> ports(E->get().from_port, E->get().to_port);
+ Pair<int, int> ports(connection->from_port, connection->to_port);
port_info.insert(_connection, ports);
}
}
@@ -437,31 +437,30 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Strin
float threshold = p_current_threshold;
if (p_v == p_w) {
int min_order = MAX_ORDER;
- GraphEdit::Connection incoming;
- List<GraphEdit::Connection> connection_list;
- graph_edit->get_connection_list(&connection_list);
- for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
- if (E->get().to_node == p_w) {
- ORDER(E->get().from_node, r_layers);
+ Ref<GraphEdit::Connection> incoming;
+ List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
+ for (const Ref<GraphEdit::Connection> &connection : connection_list) {
+ if (connection->to_node == p_w) {
+ ORDER(connection->from_node, r_layers);
if (min_order > order) {
min_order = order;
- incoming = E->get();
+ incoming = connection;
}
}
}
- if (incoming.from_node != StringName()) {
- GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[incoming.from_node]);
+ if (incoming.is_valid()) {
+ GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[incoming->from_node]);
GraphNode *gnode_to = Object::cast_to<GraphNode>(r_node_names[p_w]);
- Vector2 pos_from = gnode_from->get_output_port_position(incoming.from_port) * graph_edit->get_zoom();
- Vector2 pos_to = gnode_to->get_input_port_position(incoming.to_port) * graph_edit->get_zoom();
+ Vector2 pos_from = gnode_from->get_output_port_position(incoming->from_port) * graph_edit->get_zoom();
+ Vector2 pos_to = gnode_to->get_input_port_position(incoming->to_port) * graph_edit->get_zoom();
// If connected block node is selected, calculate thershold or add current block to list.
if (gnode_from->is_selected()) {
- Vector2 connected_block_pos = r_node_positions[r_root[incoming.from_node]];
+ Vector2 connected_block_pos = r_node_positions[r_root[incoming->from_node]];
if (connected_block_pos.y != FLT_MAX) {
//Connected block is placed, calculate threshold.
- threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming.from_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
+ threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming->from_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
}
}
}
@@ -469,31 +468,30 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Strin
if (threshold == FLT_MIN && (StringName)r_align[p_w] == p_v) {
// This time, pick an outgoing edge and repeat as above!
int min_order = MAX_ORDER;
- GraphEdit::Connection outgoing;
- List<GraphEdit::Connection> connection_list;
- graph_edit->get_connection_list(&connection_list);
- for (List<GraphEdit::Connection>::Element *E = connection_list.front(); E; E = E->next()) {
- if (E->get().from_node == p_w) {
- ORDER(E->get().to_node, r_layers);
+ Ref<GraphEdit::Connection> outgoing;
+ List<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connection_list();
+ for (const Ref<GraphEdit::Connection> &connection : connection_list) {
+ if (connection->from_node == p_w) {
+ ORDER(connection->to_node, r_layers);
if (min_order > order) {
min_order = order;
- outgoing = E->get();
+ outgoing = connection;
}
}
}
- if (outgoing.to_node != StringName()) {
+ if (outgoing.is_valid()) {
GraphNode *gnode_from = Object::cast_to<GraphNode>(r_node_names[p_w]);
- GraphNode *gnode_to = Object::cast_to<GraphNode>(r_node_names[outgoing.to_node]);
- Vector2 pos_from = gnode_from->get_output_port_position(outgoing.from_port) * graph_edit->get_zoom();
- Vector2 pos_to = gnode_to->get_input_port_position(outgoing.to_port) * graph_edit->get_zoom();
+ GraphNode *gnode_to = Object::cast_to<GraphNode>(r_node_names[outgoing->to_node]);
+ Vector2 pos_from = gnode_from->get_output_port_position(outgoing->from_port) * graph_edit->get_zoom();
+ Vector2 pos_to = gnode_to->get_input_port_position(outgoing->to_port) * graph_edit->get_zoom();
// If connected block node is selected, calculate thershold or add current block to list.
if (gnode_to->is_selected()) {
- Vector2 connected_block_pos = r_node_positions[r_root[outgoing.to_node]];
+ Vector2 connected_block_pos = r_node_positions[r_root[outgoing->to_node]];
if (connected_block_pos.y != FLT_MAX) {
//Connected block is placed. Calculate threshold
- threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing.to_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
+ threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing->to_node] - (real_t)r_inner_shift[p_w] + pos_from.y - pos_to.y;
}
}
}
diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp
index 6386bb20c3..45cc9623be 100644
--- a/scene/gui/option_button.cpp
+++ b/scene/gui/option_button.cpp
@@ -60,7 +60,7 @@ Size2 OptionButton::get_minimum_size() const {
}
if (has_theme_icon(SNAME("arrow"))) {
- const Size2 padding = theme_cache.normal->get_minimum_size();
+ const Size2 padding = _get_current_stylebox()->get_minimum_size();
const Size2 arrow_size = theme_cache.arrow_icon->get_size();
Size2 content_size = minsize - padding;
@@ -605,6 +605,8 @@ void OptionButton::set_disable_shortcuts(bool p_disabled) {
OptionButton::OptionButton(const String &p_text) :
Button(p_text) {
+ _set_h_separation_is_valid_when_no_text(true);
+
set_toggle_mode(true);
set_process_shortcut_input(true);
set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT);
diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp
index a35ee17868..093b52b650 100644
--- a/scene/main/canvas_item.cpp
+++ b/scene/main/canvas_item.cpp
@@ -314,6 +314,7 @@ void CanvasItem::_notification(int p_what) {
}
}
+ _set_global_invalid(true);
_enter_canvas();
RenderingServer::get_singleton()->canvas_item_set_visible(canvas_item, is_visible_in_tree()); // The visibility of the parent may change.
diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h
index 36f0e17924..ce1dbce6c3 100644
--- a/scene/main/canvas_item.h
+++ b/scene/main/canvas_item.h
@@ -237,7 +237,7 @@ public:
Color get_modulate() const;
Color get_modulate_in_tree() const;
- void set_self_modulate(const Color &p_self_modulate);
+ virtual void set_self_modulate(const Color &p_self_modulate);
Color get_self_modulate() const;
void set_visibility_layer(uint32_t p_visibility_layer);
@@ -248,7 +248,7 @@ public:
/* ORDERING */
- void set_z_index(int p_z);
+ virtual void set_z_index(int p_z);
int get_z_index() const;
int get_effective_z_index() const;
diff --git a/scene/main/http_request.cpp b/scene/main/http_request.cpp
index 1972e62659..fa14ad5b3c 100644
--- a/scene/main/http_request.cpp
+++ b/scene/main/http_request.cpp
@@ -503,7 +503,9 @@ void HTTPRequest::_notification(int p_what) {
void HTTPRequest::set_use_threads(bool p_use) {
ERR_FAIL_COND(get_http_client_status() != HTTPClient::STATUS_DISCONNECTED);
+#ifdef THREADS_ENABLED
use_threads.set_to(p_use);
+#endif
}
bool HTTPRequest::is_using_threads() const {
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index a6b7ca8188..f7d695bf31 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -3088,6 +3088,10 @@ static void _add_nodes_to_options(const Node *p_base, const Node *p_node, List<S
if (p_node != p_base && !p_node->get_owner()) {
return;
}
+ if (p_node->is_unique_name_in_owner() && p_node->get_owner() == p_base) {
+ String n = "%" + p_node->get_name();
+ r_options->push_back(n.quote());
+ }
String n = p_base->get_path_to(p_node);
r_options->push_back(n.quote());
for (int i = 0; i < p_node->get_child_count(); i++) {
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 111d6447a0..64a1c72f9d 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -1184,7 +1184,9 @@ void register_scene_types() {
}
if (RenderingServer::get_singleton()) {
- ColorPicker::init_shaders(); // RenderingServer needs to exist for this to succeed.
+ // RenderingServer needs to exist for this to succeed.
+ ColorPicker::init_shaders();
+ GraphEdit::init_shaders();
}
SceneDebugger::initialize();
@@ -1236,6 +1238,7 @@ void unregister_scene_types() {
ParticleProcessMaterial::finish_shaders();
CanvasItemMaterial::finish_shaders();
ColorPicker::finish_shaders();
+ GraphEdit::finish_shaders();
SceneStringNames::free();
OS::get_singleton()->benchmark_end_measure("Scene", "Unregister Types");
diff --git a/scene/resources/tile_set.cpp b/scene/resources/tile_set.cpp
index a99102e847..66e1b7bcf9 100644
--- a/scene/resources/tile_set.cpp
+++ b/scene/resources/tile_set.cpp
@@ -1563,6 +1563,632 @@ void TileSet::draw_tile_shape(CanvasItem *p_canvas_item, Transform2D p_transform
}
}
+Vector2 TileSet::map_to_local(const Vector2i &p_pos) const {
+ // SHOULD RETURN THE CENTER OF THE CELL.
+ Vector2 ret = p_pos;
+
+ if (tile_shape == TileSet::TILE_SHAPE_HALF_OFFSET_SQUARE || tile_shape == TileSet::TILE_SHAPE_HEXAGON || tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ // Technically, those 3 shapes are equivalent, as they are basically half-offset, but with different levels or overlap.
+ // square = no overlap, hexagon = 0.25 overlap, isometric = 0.5 overlap.
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ switch (tile_layout) {
+ case TileSet::TILE_LAYOUT_STACKED:
+ ret = Vector2(ret.x + (Math::posmod(ret.y, 2) == 0 ? 0.0 : 0.5), ret.y);
+ break;
+ case TileSet::TILE_LAYOUT_STACKED_OFFSET:
+ ret = Vector2(ret.x + (Math::posmod(ret.y, 2) == 1 ? 0.0 : 0.5), ret.y);
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
+ ret = Vector2(ret.x + ret.y / 2, ret.y);
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_DOWN:
+ ret = Vector2(ret.x / 2, ret.y * 2 + ret.x);
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
+ ret = Vector2((ret.x + ret.y) / 2, ret.y - ret.x);
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
+ ret = Vector2((ret.x - ret.y) / 2, ret.y + ret.x);
+ break;
+ }
+ } else { // TILE_OFFSET_AXIS_VERTICAL.
+ switch (tile_layout) {
+ case TileSet::TILE_LAYOUT_STACKED:
+ ret = Vector2(ret.x, ret.y + (Math::posmod(ret.x, 2) == 0 ? 0.0 : 0.5));
+ break;
+ case TileSet::TILE_LAYOUT_STACKED_OFFSET:
+ ret = Vector2(ret.x, ret.y + (Math::posmod(ret.x, 2) == 1 ? 0.0 : 0.5));
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
+ ret = Vector2(ret.x * 2 + ret.y, ret.y / 2);
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_DOWN:
+ ret = Vector2(ret.x, ret.y + ret.x / 2);
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
+ ret = Vector2(ret.x + ret.y, (ret.y - ret.x) / 2);
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
+ ret = Vector2(ret.x - ret.y, (ret.y + ret.x) / 2);
+ break;
+ }
+ }
+ }
+
+ // Multiply by the overlapping ratio.
+ double overlapping_ratio = 1.0;
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ overlapping_ratio = 0.5;
+ } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
+ overlapping_ratio = 0.75;
+ }
+ ret.y *= overlapping_ratio;
+ } else { // TILE_OFFSET_AXIS_VERTICAL.
+ if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ overlapping_ratio = 0.5;
+ } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
+ overlapping_ratio = 0.75;
+ }
+ ret.x *= overlapping_ratio;
+ }
+
+ return (ret + Vector2(0.5, 0.5)) * tile_size;
+}
+
+Vector2i TileSet::local_to_map(const Vector2 &p_local_position) const {
+ Vector2 ret = p_local_position;
+ ret /= tile_size;
+
+ // Divide by the overlapping ratio.
+ double overlapping_ratio = 1.0;
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ overlapping_ratio = 0.5;
+ } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
+ overlapping_ratio = 0.75;
+ }
+ ret.y /= overlapping_ratio;
+ } else { // TILE_OFFSET_AXIS_VERTICAL.
+ if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ overlapping_ratio = 0.5;
+ } else if (tile_shape == TileSet::TILE_SHAPE_HEXAGON) {
+ overlapping_ratio = 0.75;
+ }
+ ret.x /= overlapping_ratio;
+ }
+
+ // For each half-offset shape, we check if we are in the corner of the tile, and thus should correct the local position accordingly.
+ if (tile_shape == TileSet::TILE_SHAPE_HALF_OFFSET_SQUARE || tile_shape == TileSet::TILE_SHAPE_HEXAGON || tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ // Technically, those 3 shapes are equivalent, as they are basically half-offset, but with different levels or overlap.
+ // square = no overlap, hexagon = 0.25 overlap, isometric = 0.5 overlap.
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ // Smart floor of the position
+ Vector2 raw_pos = ret;
+ if (Math::posmod(Math::floor(ret.y), 2) ^ (tile_layout == TileSet::TILE_LAYOUT_STACKED_OFFSET)) {
+ ret = Vector2(Math::floor(ret.x + 0.5) - 0.5, Math::floor(ret.y));
+ } else {
+ ret = ret.floor();
+ }
+
+ // Compute the tile offset, and if we might the output for a neighbor top tile.
+ Vector2 in_tile_pos = raw_pos - ret;
+ bool in_top_left_triangle = (in_tile_pos - Vector2(0.5, 0.0)).cross(Vector2(-0.5, 1.0 / overlapping_ratio - 1)) <= 0;
+ bool in_top_right_triangle = (in_tile_pos - Vector2(0.5, 0.0)).cross(Vector2(0.5, 1.0 / overlapping_ratio - 1)) > 0;
+
+ switch (tile_layout) {
+ case TileSet::TILE_LAYOUT_STACKED:
+ ret = ret.floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? 0 : -1, -1);
+ } else if (in_top_right_triangle) {
+ ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? 1 : 0, -1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_STACKED_OFFSET:
+ ret = ret.floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? -1 : 0, -1);
+ } else if (in_top_right_triangle) {
+ ret += Vector2i(Math::posmod(Math::floor(ret.y), 2) ? 0 : 1, -1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
+ ret = Vector2(ret.x - ret.y / 2, ret.y).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(0, -1);
+ } else if (in_top_right_triangle) {
+ ret += Vector2i(1, -1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_DOWN:
+ ret = Vector2(ret.x * 2, ret.y / 2 - ret.x).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(-1, 0);
+ } else if (in_top_right_triangle) {
+ ret += Vector2i(1, -1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
+ ret = Vector2(ret.x - ret.y / 2, ret.y / 2 + ret.x).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(0, -1);
+ } else if (in_top_right_triangle) {
+ ret += Vector2i(1, 0);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
+ ret = Vector2(ret.x + ret.y / 2, ret.y / 2 - ret.x).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(-1, 0);
+ } else if (in_top_right_triangle) {
+ ret += Vector2i(0, -1);
+ }
+ break;
+ }
+ } else { // TILE_OFFSET_AXIS_VERTICAL.
+ // Smart floor of the position.
+ Vector2 raw_pos = ret;
+ if (Math::posmod(Math::floor(ret.x), 2) ^ (tile_layout == TileSet::TILE_LAYOUT_STACKED_OFFSET)) {
+ ret = Vector2(Math::floor(ret.x), Math::floor(ret.y + 0.5) - 0.5);
+ } else {
+ ret = ret.floor();
+ }
+
+ // Compute the tile offset, and if we might the output for a neighbor top tile.
+ Vector2 in_tile_pos = raw_pos - ret;
+ bool in_top_left_triangle = (in_tile_pos - Vector2(0.0, 0.5)).cross(Vector2(1.0 / overlapping_ratio - 1, -0.5)) > 0;
+ bool in_bottom_left_triangle = (in_tile_pos - Vector2(0.0, 0.5)).cross(Vector2(1.0 / overlapping_ratio - 1, 0.5)) <= 0;
+
+ switch (tile_layout) {
+ case TileSet::TILE_LAYOUT_STACKED:
+ ret = ret.floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? 0 : -1);
+ } else if (in_bottom_left_triangle) {
+ ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? 1 : 0);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_STACKED_OFFSET:
+ ret = ret.floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? -1 : 0);
+ } else if (in_bottom_left_triangle) {
+ ret += Vector2i(-1, Math::posmod(Math::floor(ret.x), 2) ? 0 : 1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
+ ret = Vector2(ret.x / 2 - ret.y, ret.y * 2).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(0, -1);
+ } else if (in_bottom_left_triangle) {
+ ret += Vector2i(-1, 1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_STAIRS_DOWN:
+ ret = Vector2(ret.x, ret.y - ret.x / 2).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(-1, 0);
+ } else if (in_bottom_left_triangle) {
+ ret += Vector2i(-1, 1);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
+ ret = Vector2(ret.x / 2 - ret.y, ret.y + ret.x / 2).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(0, -1);
+ } else if (in_bottom_left_triangle) {
+ ret += Vector2i(-1, 0);
+ }
+ break;
+ case TileSet::TILE_LAYOUT_DIAMOND_DOWN:
+ ret = Vector2(ret.x / 2 + ret.y, ret.y - ret.x / 2).floor();
+ if (in_top_left_triangle) {
+ ret += Vector2i(-1, 0);
+ } else if (in_bottom_left_triangle) {
+ ret += Vector2i(0, 1);
+ }
+ break;
+ }
+ }
+ } else {
+ ret = (ret + Vector2(0.00005, 0.00005)).floor();
+ }
+ return Vector2i(ret);
+}
+
+bool TileSet::is_existing_neighbor(TileSet::CellNeighbor p_cell_neighbor) const {
+ if (tile_shape == TileSet::TILE_SHAPE_SQUARE) {
+ return p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER;
+
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC) {
+ return p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
+ } else {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ return p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
+ } else {
+ return p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE ||
+ p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE;
+ }
+ }
+}
+
+Vector2i TileSet::get_neighbor_cell(const Vector2i &p_coords, TileSet::CellNeighbor p_cell_neighbor) const {
+ if (tile_shape == TileSet::TILE_SHAPE_SQUARE) {
+ switch (p_cell_neighbor) {
+ case TileSet::CELL_NEIGHBOR_RIGHT_SIDE:
+ return p_coords + Vector2i(1, 0);
+ case TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER:
+ return p_coords + Vector2i(1, 1);
+ case TileSet::CELL_NEIGHBOR_BOTTOM_SIDE:
+ return p_coords + Vector2i(0, 1);
+ case TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_CORNER:
+ return p_coords + Vector2i(-1, 1);
+ case TileSet::CELL_NEIGHBOR_LEFT_SIDE:
+ return p_coords + Vector2i(-1, 0);
+ case TileSet::CELL_NEIGHBOR_TOP_LEFT_CORNER:
+ return p_coords + Vector2i(-1, -1);
+ case TileSet::CELL_NEIGHBOR_TOP_SIDE:
+ return p_coords + Vector2i(0, -1);
+ case TileSet::CELL_NEIGHBOR_TOP_RIGHT_CORNER:
+ return p_coords + Vector2i(1, -1);
+ default:
+ ERR_FAIL_V(p_coords);
+ }
+ } else { // Half-offset shapes (square and hexagon).
+ switch (tile_layout) {
+ case TileSet::TILE_LAYOUT_STACKED: {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ bool is_offset = p_coords.y % 2;
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
+ return p_coords + Vector2i(1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(is_offset ? 1 : 0, 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
+ return p_coords + Vector2i(0, 2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(is_offset ? 0 : -1, 1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(is_offset ? 0 : -1, -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
+ return p_coords + Vector2i(0, -2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(is_offset ? 1 : 0, -1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ } else {
+ bool is_offset = p_coords.x % 2;
+
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
+ return p_coords + Vector2i(0, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, is_offset ? 1 : 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
+ return p_coords + Vector2i(2, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, is_offset ? 0 : -1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
+ return p_coords + Vector2i(0, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, is_offset ? 0 : -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
+ return p_coords + Vector2i(-2, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, is_offset ? 1 : 0);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ }
+ } break;
+ case TileSet::TILE_LAYOUT_STACKED_OFFSET: {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ bool is_offset = p_coords.y % 2;
+
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
+ return p_coords + Vector2i(1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(is_offset ? 0 : 1, 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
+ return p_coords + Vector2i(0, 2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(is_offset ? -1 : 0, 1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(is_offset ? -1 : 0, -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
+ return p_coords + Vector2i(0, -2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(is_offset ? 0 : 1, -1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ } else {
+ bool is_offset = p_coords.x % 2;
+
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
+ return p_coords + Vector2i(0, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, is_offset ? 0 : 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
+ return p_coords + Vector2i(2, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, is_offset ? -1 : 0);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
+ return p_coords + Vector2i(0, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, is_offset ? -1 : 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
+ return p_coords + Vector2i(-2, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, is_offset ? 0 : 1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ }
+ } break;
+ case TileSet::TILE_LAYOUT_STAIRS_RIGHT:
+ case TileSet::TILE_LAYOUT_STAIRS_DOWN: {
+ if ((tile_layout == TileSet::TILE_LAYOUT_STAIRS_RIGHT) ^ (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_VERTICAL)) {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
+ return p_coords + Vector2i(1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(0, 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
+ return p_coords + Vector2i(-1, 2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(0, -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
+ return p_coords + Vector2i(1, -2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, -1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+
+ } else {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
+ return p_coords + Vector2i(0, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
+ return p_coords + Vector2i(2, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, -1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
+ return p_coords + Vector2i(0, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
+ return p_coords + Vector2i(-2, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ }
+ } else {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
+ return p_coords + Vector2i(2, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
+ return p_coords + Vector2i(0, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
+ return p_coords + Vector2i(-2, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
+ return p_coords + Vector2i(0, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, -1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+
+ } else {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
+ return p_coords + Vector2i(-1, 2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(0, 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
+ return p_coords + Vector2i(1, 0);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, -1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
+ return p_coords + Vector2i(1, -2);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(0, -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
+ return p_coords + Vector2i(-1, 0);
+
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ }
+ }
+ } break;
+ case TileSet::TILE_LAYOUT_DIAMOND_RIGHT:
+ case TileSet::TILE_LAYOUT_DIAMOND_DOWN: {
+ if ((tile_layout == TileSet::TILE_LAYOUT_DIAMOND_RIGHT) ^ (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_VERTICAL)) {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
+ return p_coords + Vector2i(1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(0, 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
+ return p_coords + Vector2i(-1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 0);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
+ return p_coords + Vector2i(-1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(0, -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
+ return p_coords + Vector2i(1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, 0);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+
+ } else {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
+ return p_coords + Vector2i(1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
+ return p_coords + Vector2i(1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(0, -1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
+ return p_coords + Vector2i(-1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
+ return p_coords + Vector2i(-1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(0, 1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ }
+ } else {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL) {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_SIDE)) {
+ return p_coords + Vector2i(1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) {
+ return p_coords + Vector2i(1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(0, 1);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_SIDE)) {
+ return p_coords + Vector2i(-1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 0);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) {
+ return p_coords + Vector2i(-1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(0, -1);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+
+ } else {
+ if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_SIDE)) {
+ return p_coords + Vector2i(-1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE) {
+ return p_coords + Vector2i(0, 1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_RIGHT_CORNER) {
+ return p_coords + Vector2i(1, 1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_RIGHT_SIDE) {
+ return p_coords + Vector2i(1, 0);
+ } else if ((tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_CORNER) ||
+ (tile_shape != TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_SIDE)) {
+ return p_coords + Vector2i(1, -1);
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_TOP_LEFT_SIDE) {
+ return p_coords + Vector2i(0, -1);
+ } else if (tile_shape == TileSet::TILE_SHAPE_ISOMETRIC && p_cell_neighbor == TileSet::CELL_NEIGHBOR_LEFT_CORNER) {
+ return p_coords + Vector2i(-1, -1);
+
+ } else if (p_cell_neighbor == TileSet::CELL_NEIGHBOR_BOTTOM_LEFT_SIDE) {
+ return p_coords + Vector2i(-1, 0);
+ } else {
+ ERR_FAIL_V(p_coords);
+ }
+ }
+ }
+ } break;
+ default:
+ ERR_FAIL_V(p_coords);
+ }
+ }
+}
+
+Vector2i TileSet::map_pattern(const Vector2i &p_position_in_tilemap, const Vector2i &p_coords_in_pattern, Ref<TileMapPattern> p_pattern) {
+ ERR_FAIL_COND_V(p_pattern.is_null(), Vector2i());
+ ERR_FAIL_COND_V(!p_pattern->has_cell(p_coords_in_pattern), Vector2i());
+
+ Vector2i output = p_position_in_tilemap + p_coords_in_pattern;
+ if (tile_shape != TileSet::TILE_SHAPE_SQUARE) {
+ if (tile_layout == TileSet::TILE_LAYOUT_STACKED) {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL && bool(p_position_in_tilemap.y % 2) && bool(p_coords_in_pattern.y % 2)) {
+ output.x += 1;
+ } else if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_VERTICAL && bool(p_position_in_tilemap.x % 2) && bool(p_coords_in_pattern.x % 2)) {
+ output.y += 1;
+ }
+ } else if (tile_layout == TileSet::TILE_LAYOUT_STACKED_OFFSET) {
+ if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_HORIZONTAL && bool(p_position_in_tilemap.y % 2) && bool(p_coords_in_pattern.y % 2)) {
+ output.x -= 1;
+ } else if (tile_offset_axis == TileSet::TILE_OFFSET_AXIS_VERTICAL && bool(p_position_in_tilemap.x % 2) && bool(p_coords_in_pattern.x % 2)) {
+ output.y -= 1;
+ }
+ }
+ }
+
+ return output;
+}
+
Vector<Point2> TileSet::get_terrain_polygon(int p_terrain_set) {
if (tile_shape == TileSet::TILE_SHAPE_SQUARE) {
return _get_square_terrain_polygon(tile_size);
diff --git a/scene/resources/tile_set.h b/scene/resources/tile_set.h
index a71982cd56..0a6d879047 100644
--- a/scene/resources/tile_set.h
+++ b/scene/resources/tile_set.h
@@ -530,6 +530,13 @@ public:
Vector<Vector2> get_tile_shape_polygon();
void draw_tile_shape(CanvasItem *p_canvas_item, Transform2D p_transform, Color p_color, bool p_filled = false, Ref<Texture2D> p_texture = Ref<Texture2D>());
+ // Used by TileMap/TileMapLayer
+ Vector2 map_to_local(const Vector2i &p_pos) const;
+ Vector2i local_to_map(const Vector2 &p_pos) const;
+ bool is_existing_neighbor(TileSet::CellNeighbor p_cell_neighbor) const;
+ Vector2i get_neighbor_cell(const Vector2i &p_coords, TileSet::CellNeighbor p_cell_neighbor) const;
+ Vector2i map_pattern(const Vector2i &p_position_in_tilemap, const Vector2i &p_coords_in_pattern, Ref<TileMapPattern> p_pattern);
+
Vector<Point2> get_terrain_polygon(int p_terrain_set);
Vector<Point2> get_terrain_peering_bit_polygon(int p_terrain_set, TileSet::CellNeighbor p_bit);
void draw_terrains(CanvasItem *p_canvas_item, Transform2D p_transform, const TileData *p_tile_data);
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index 005a88d391..fa83e06315 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -225,20 +225,20 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
// OptionButton
theme->set_stylebox("focus", "OptionButton", focus);
- Ref<StyleBox> sb_optbutton_normal = make_flat_stylebox(style_normal_color, 2 * default_margin, default_margin, 21, default_margin);
- Ref<StyleBox> sb_optbutton_hover = make_flat_stylebox(style_hover_color, 2 * default_margin, default_margin, 21, default_margin);
- Ref<StyleBox> sb_optbutton_pressed = make_flat_stylebox(style_pressed_color, 2 * default_margin, default_margin, 21, default_margin);
- Ref<StyleBox> sb_optbutton_disabled = make_flat_stylebox(style_disabled_color, 2 * default_margin, default_margin, 21, default_margin);
+ Ref<StyleBox> sb_optbutton_normal = make_flat_stylebox(style_normal_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_hover = make_flat_stylebox(style_hover_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_pressed = make_flat_stylebox(style_pressed_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_disabled = make_flat_stylebox(style_disabled_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
theme->set_stylebox("normal", "OptionButton", sb_optbutton_normal);
theme->set_stylebox("hover", "OptionButton", sb_optbutton_hover);
theme->set_stylebox("pressed", "OptionButton", sb_optbutton_pressed);
theme->set_stylebox("disabled", "OptionButton", sb_optbutton_disabled);
- Ref<StyleBox> sb_optbutton_normal_mirrored = make_flat_stylebox(style_normal_color, 21, default_margin, 2 * default_margin, default_margin);
- Ref<StyleBox> sb_optbutton_hover_mirrored = make_flat_stylebox(style_hover_color, 21, default_margin, 2 * default_margin, default_margin);
- Ref<StyleBox> sb_optbutton_pressed_mirrored = make_flat_stylebox(style_pressed_color, 21, default_margin, 2 * default_margin, default_margin);
- Ref<StyleBox> sb_optbutton_disabled_mirrored = make_flat_stylebox(style_disabled_color, 21, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_normal_mirrored = make_flat_stylebox(style_normal_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_hover_mirrored = make_flat_stylebox(style_hover_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_pressed_mirrored = make_flat_stylebox(style_pressed_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
+ Ref<StyleBox> sb_optbutton_disabled_mirrored = make_flat_stylebox(style_disabled_color, 2 * default_margin, default_margin, 2 * default_margin, default_margin);
theme->set_stylebox("normal_mirrored", "OptionButton", sb_optbutton_normal_mirrored);
theme->set_stylebox("hover_mirrored", "OptionButton", sb_optbutton_hover_mirrored);
@@ -1161,6 +1161,9 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
theme->set_color("selection_fill", "GraphEdit", Color(1, 1, 1, 0.3));
theme->set_color("selection_stroke", "GraphEdit", Color(1, 1, 1, 0.8));
theme->set_color("activity", "GraphEdit", Color(1, 1, 1));
+ theme->set_color("connection_hover_tint_color", "GraphEdit", Color(0, 0, 0, 0.3));
+ theme->set_color("connection_valid_target_tint_color", "GraphEdit", Color(1, 1, 1, 0.4));
+ theme->set_color("connection_rim_color", "GraphEdit", style_normal_color);
// Visual Node Ports
diff --git a/servers/register_server_types.cpp b/servers/register_server_types.cpp
index 2beab44f91..0ee2984a8c 100644
--- a/servers/register_server_types.cpp
+++ b/servers/register_server_types.cpp
@@ -88,7 +88,11 @@
ShaderTypes *shader_types = nullptr;
static PhysicsServer3D *_createGodotPhysics3DCallback() {
+#ifdef THREADS_ENABLED
bool using_threads = GLOBAL_GET("physics/3d/run_on_separate_thread");
+#else
+ bool using_threads = false;
+#endif
PhysicsServer3D *physics_server_3d = memnew(GodotPhysicsServer3D(using_threads));
@@ -96,7 +100,11 @@ static PhysicsServer3D *_createGodotPhysics3DCallback() {
}
static PhysicsServer2D *_createGodotPhysics2DCallback() {
+#ifdef THREADS_ENABLED
bool using_threads = GLOBAL_GET("physics/2d/run_on_separate_thread");
+#else
+ bool using_threads = false;
+#endif
PhysicsServer2D *physics_server_2d = memnew(GodotPhysicsServer2D(using_threads));
diff --git a/servers/rendering/rendering_server_default.cpp b/servers/rendering/rendering_server_default.cpp
index f7620ad5dc..bf8ab27722 100644
--- a/servers/rendering/rendering_server_default.cpp
+++ b/servers/rendering/rendering_server_default.cpp
@@ -395,15 +395,19 @@ RenderingServerDefault::RenderingServerDefault(bool p_create_thread) :
command_queue(p_create_thread) {
RenderingServer::init();
+#ifdef THREADS_ENABLED
create_thread = p_create_thread;
-
- if (!p_create_thread) {
+ if (!create_thread) {
server_thread = Thread::get_caller_id();
} else {
server_thread = 0;
}
+#else
+ create_thread = false;
+ server_thread = Thread::get_main_id();
+#endif
+ RSG::threaded = create_thread;
- RSG::threaded = p_create_thread;
RSG::canvas = memnew(RendererCanvasCull);
RSG::viewport = memnew(RendererViewport);
RendererSceneCull *sr = memnew(RendererSceneCull);
diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h
index 11ec670280..e8b20692f0 100644
--- a/servers/rendering/rendering_server_default.h
+++ b/servers/rendering/rendering_server_default.h
@@ -78,7 +78,7 @@ class RenderingServerDefault : public RenderingServer {
static void _thread_callback(void *_instance);
void _thread_loop();
- Thread::ID server_thread;
+ Thread::ID server_thread = 0;
SafeFlag exit;
Thread thread;
SafeFlag draw_thread_up;
diff --git a/servers/text_server.cpp b/servers/text_server.cpp
index b282c517d3..e05d55787a 100644
--- a/servers/text_server.cpp
+++ b/servers/text_server.cpp
@@ -1513,12 +1513,12 @@ void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, cons
int v_size = shaped_text_get_glyph_count(p_shaped);
const Glyph *glyphs = shaped_text_get_glyphs(p_shaped);
- Vector2 ofs = p_pos;
+ Vector2 ofs;
// Draw RTL ellipsis string when needed.
if (rtl && ellipsis_pos >= 0) {
for (int i = ellipsis_gl_size - 1; i >= 0; i--) {
for (int j = 0; j < ellipsis_glyphs[i].repeat; j++) {
- font_draw_glyph(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, ofs + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
+ font_draw_glyph(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, ofs + p_pos + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
if (orientation == ORIENTATION_HORIZONTAL) {
ofs.x += ellipsis_glyphs[i].advance;
} else {
@@ -1544,11 +1544,11 @@ void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, cons
if (p_clip_r > 0) {
// Clip right / bottom.
if (orientation == ORIENTATION_HORIZONTAL) {
- if (ofs.x - p_pos.x + glyphs[i].advance > p_clip_r) {
+ if (ofs.x + glyphs[i].advance > p_clip_r) {
return;
}
} else {
- if (ofs.y - p_pos.y + glyphs[i].advance > p_clip_r) {
+ if (ofs.y + glyphs[i].advance > p_clip_r) {
return;
}
}
@@ -1556,12 +1556,12 @@ void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, cons
if (p_clip_l > 0) {
// Clip left / top.
if (orientation == ORIENTATION_HORIZONTAL) {
- if (ofs.x - p_pos.x < p_clip_l) {
+ if (ofs.x < p_clip_l) {
ofs.x += glyphs[i].advance;
continue;
}
} else {
- if (ofs.y - p_pos.y < p_clip_l) {
+ if (ofs.y < p_clip_l) {
ofs.y += glyphs[i].advance;
continue;
}
@@ -1569,9 +1569,9 @@ void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, cons
}
if (glyphs[i].font_rid != RID()) {
- font_draw_glyph(glyphs[i].font_rid, p_canvas, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, p_color);
+ font_draw_glyph(glyphs[i].font_rid, p_canvas, glyphs[i].font_size, ofs + p_pos + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, p_color);
} else if (hex_codes && ((glyphs[i].flags & GRAPHEME_IS_VIRTUAL) != GRAPHEME_IS_VIRTUAL) && ((glyphs[i].flags & GRAPHEME_IS_EMBEDDED_OBJECT) != GRAPHEME_IS_EMBEDDED_OBJECT)) {
- TextServer::draw_hex_code_box(p_canvas, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, p_color);
+ TextServer::draw_hex_code_box(p_canvas, glyphs[i].font_size, ofs + p_pos + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, p_color);
}
if (orientation == ORIENTATION_HORIZONTAL) {
ofs.x += glyphs[i].advance;
@@ -1584,7 +1584,7 @@ void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, cons
if (!rtl && ellipsis_pos >= 0) {
for (int i = 0; i < ellipsis_gl_size; i++) {
for (int j = 0; j < ellipsis_glyphs[i].repeat; j++) {
- font_draw_glyph(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, ofs + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
+ font_draw_glyph(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, ofs + p_pos + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
if (orientation == ORIENTATION_HORIZONTAL) {
ofs.x += ellipsis_glyphs[i].advance;
} else {
@@ -1608,12 +1608,13 @@ void TextServer::shaped_text_draw_outline(const RID &p_shaped, const RID &p_canv
int v_size = shaped_text_get_glyph_count(p_shaped);
const Glyph *glyphs = shaped_text_get_glyphs(p_shaped);
- Vector2 ofs = p_pos;
+
+ Vector2 ofs;
// Draw RTL ellipsis string when needed.
if (rtl && ellipsis_pos >= 0) {
for (int i = ellipsis_gl_size - 1; i >= 0; i--) {
for (int j = 0; j < ellipsis_glyphs[i].repeat; j++) {
- font_draw_glyph_outline(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, p_outline_size, ofs + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
+ font_draw_glyph_outline(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, p_outline_size, ofs + p_pos + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
if (orientation == ORIENTATION_HORIZONTAL) {
ofs.x += ellipsis_glyphs[i].advance;
} else {
@@ -1639,11 +1640,11 @@ void TextServer::shaped_text_draw_outline(const RID &p_shaped, const RID &p_canv
if (p_clip_r > 0) {
// Clip right / bottom.
if (orientation == ORIENTATION_HORIZONTAL) {
- if (ofs.x - p_pos.x + glyphs[i].advance > p_clip_r) {
+ if (ofs.x + glyphs[i].advance > p_clip_r) {
return;
}
} else {
- if (ofs.y - p_pos.y + glyphs[i].advance > p_clip_r) {
+ if (ofs.y + glyphs[i].advance > p_clip_r) {
return;
}
}
@@ -1651,19 +1652,19 @@ void TextServer::shaped_text_draw_outline(const RID &p_shaped, const RID &p_canv
if (p_clip_l > 0) {
// Clip left / top.
if (orientation == ORIENTATION_HORIZONTAL) {
- if (ofs.x - p_pos.x < p_clip_l) {
+ if (ofs.x < p_clip_l) {
ofs.x += glyphs[i].advance;
continue;
}
} else {
- if (ofs.y - p_pos.y < p_clip_l) {
+ if (ofs.y < p_clip_l) {
ofs.y += glyphs[i].advance;
continue;
}
}
}
if (glyphs[i].font_rid != RID()) {
- font_draw_glyph_outline(glyphs[i].font_rid, p_canvas, glyphs[i].font_size, p_outline_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, p_color);
+ font_draw_glyph_outline(glyphs[i].font_rid, p_canvas, glyphs[i].font_size, p_outline_size, ofs + p_pos + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, p_color);
}
if (orientation == ORIENTATION_HORIZONTAL) {
ofs.x += glyphs[i].advance;
@@ -1676,7 +1677,7 @@ void TextServer::shaped_text_draw_outline(const RID &p_shaped, const RID &p_canv
if (!rtl && ellipsis_pos >= 0) {
for (int i = 0; i < ellipsis_gl_size; i++) {
for (int j = 0; j < ellipsis_glyphs[i].repeat; j++) {
- font_draw_glyph_outline(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, p_outline_size, ofs + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
+ font_draw_glyph_outline(ellipsis_glyphs[i].font_rid, p_canvas, ellipsis_glyphs[i].font_size, p_outline_size, ofs + p_pos + Vector2(ellipsis_glyphs[i].x_off, ellipsis_glyphs[i].y_off), ellipsis_glyphs[i].index, p_color);
if (orientation == ORIENTATION_HORIZONTAL) {
ofs.x += ellipsis_glyphs[i].advance;
} else {
diff --git a/tests/scene/test_audio_stream_wav.h b/tests/scene/test_audio_stream_wav.h
index e36f049136..ed1697929e 100644
--- a/tests/scene/test_audio_stream_wav.h
+++ b/tests/scene/test_audio_stream_wav.h
@@ -148,7 +148,7 @@ void run_test(String file_name, AudioStreamWAV::Format data_format, bool stereo,
Ref<FileAccess> wav_file = FileAccess::open(save_path, FileAccess::READ, &error);
REQUIRE(error == OK);
-#if TOOLS_ENABLED
+#ifdef TOOLS_ENABLED
// The WAV importer can be used if enabled to check that the saved file is valid.
Ref<ResourceImporterWAV> wav_importer = memnew(ResourceImporterWAV);
diff --git a/tests/scene/test_node_2d.h b/tests/scene/test_node_2d.h
index 7e93c77e22..8cf6408438 100644
--- a/tests/scene/test_node_2d.h
+++ b/tests/scene/test_node_2d.h
@@ -32,6 +32,7 @@
#define TEST_NODE_2D_H
#include "scene/2d/node_2d.h"
+#include "scene/main/window.h"
#include "tests/test_macros.h"
@@ -56,6 +57,33 @@ TEST_CASE("[SceneTree][Node2D]") {
memdelete(test_child);
memdelete(test_node);
}
+
+ SUBCASE("[Node2D][Global Transform] Global Transform should be correct after inserting node from detached tree into SceneTree.") { // GH-86841
+ Node2D *main = memnew(Node2D);
+ Node2D *outer = memnew(Node2D);
+ Node2D *inner = memnew(Node2D);
+ SceneTree::get_singleton()->get_root()->add_child(main);
+
+ main->set_position(Point2(100, 100));
+ outer->set_position(Point2(10, 0));
+ inner->set_position(Point2(0, 10));
+
+ outer->add_child(inner);
+ // `inner` is still detached.
+ CHECK_EQ(inner->get_global_position(), Point2(10, 10));
+
+ main->add_child(outer);
+ // `inner` is in scene tree.
+ CHECK_EQ(inner->get_global_position(), Point2(110, 110));
+
+ main->remove_child(outer);
+ // `inner` is detached again.
+ CHECK_EQ(inner->get_global_position(), Point2(10, 10));
+
+ memdelete(inner);
+ memdelete(outer);
+ memdelete(main);
+ }
}
} // namespace TestNode2D