summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/web_builds.yml30
-rw-r--r--SConstruct8
-rw-r--r--core/input/input.cpp4
-rw-r--r--core/io/packet_peer.cpp6
-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/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--core/templates/cowdata.h152
-rw-r--r--core/templates/ring_buffer.h2
-rw-r--r--core/templates/vector.h69
-rw-r--r--core/variant/variant_call.cpp2
-rw-r--r--doc/classes/GraphEdit.xml39
-rw-r--r--doc/classes/RayCast2D.xml2
-rw-r--r--doc/classes/RayCast3D.xml2
-rw-r--r--drivers/unix/os_unix.cpp2
-rw-r--r--editor/SCsub1
-rw-r--r--editor/debugger/editor_performance_profiler.cpp4
-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_node.cpp5
-rw-r--r--editor/editor_settings.cpp8
-rw-r--r--editor/editor_settings.h2
-rw-r--r--editor/filesystem_dock.cpp5
-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/canvas_item_editor_plugin.h1
-rw-r--r--editor/plugins/particle_process_material_editor_plugin.cpp3
-rw-r--r--editor/plugins/script_text_editor.cpp1
-rw-r--r--editor/plugins/visual_shader_editor_plugin.cpp227
-rw-r--r--editor/plugins/visual_shader_editor_plugin.h14
-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/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.cpp225
-rw-r--r--editor/themes/editor_theme_manager.h36
-rw-r--r--main/main.cpp18
-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/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj18
-rw-r--r--misc/extension_api_validation/4.2-stable.expected7
-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.cpp122
-rw-r--r--modules/gdscript/gdscript.h6
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp15
-rw-r--r--modules/gdscript/gdscript_compiler.cpp5
-rw-r--r--modules/gdscript/gdscript_editor.cpp15
-rw-r--r--modules/gdscript/gdscript_parser.cpp38
-rw-r--r--modules/gdscript/gdscript_parser.h4
-rw-r--r--modules/gdscript/gdscript_vm.cpp2
-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_gdscript_uid.h115
-rw-r--r--modules/gltf/doc_classes/GLTFPhysicsBody.xml19
-rw-r--r--modules/gltf/doc_classes/GLTFPhysicsShape.xml7
-rw-r--r--modules/gltf/extensions/physics/gltf_document_extension_physics.cpp316
-rw-r--r--modules/gltf/extensions/physics/gltf_physics_body.cpp300
-rw-r--r--modules/gltf/extensions/physics/gltf_physics_body.h34
-rw-r--r--modules/gltf/extensions/physics/gltf_physics_shape.cpp86
-rw-r--r--modules/gltf/extensions/physics/gltf_physics_shape.h5
-rw-r--r--modules/gltf/structures/gltf_buffer_view.cpp1
-rw-r--r--modules/lightmapper_rd/lightmapper_rd.cpp2
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs20
-rw-r--r--modules/multiplayer/scene_cache_interface.cpp164
-rw-r--r--modules/multiplayer/scene_cache_interface.h27
-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/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.py6
-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/3d/audio_stream_player_3d.cpp260
-rw-r--r--scene/3d/audio_stream_player_3d.h33
-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.cpp2
-rw-r--r--scene/main/http_request.cpp2
-rw-r--r--scene/main/viewport.cpp10
-rw-r--r--scene/register_scene_types.cpp5
-rw-r--r--scene/resources/visual_shader.cpp14
-rw-r--r--scene/resources/visual_shader.h1
-rw-r--r--scene/theme/default_theme.cpp19
-rw-r--r--servers/register_server_types.cpp8
-rw-r--r--servers/rendering/renderer_rd/storage_rd/light_storage.cpp2
-rw-r--r--servers/rendering/rendering_device_graph.cpp1
-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_node_2d.h28
149 files changed, 7203 insertions, 4671 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/input/input.cpp b/core/input/input.cpp
index 2ba4b1d1e8..4e33d3087d 100644
--- a/core/input/input.cpp
+++ b/core/input/input.cpp
@@ -865,6 +865,8 @@ Point2i Input::warp_mouse_motion(const Ref<InputEventMouseMotion> &p_motion, con
}
void Input::action_press(const StringName &p_action, float p_strength) {
+ ERR_FAIL_COND_MSG(!InputMap::get_singleton()->has_action(p_action), InputMap::get_singleton()->suggest_actions(p_action));
+
// Create or retrieve existing action.
ActionState &action_state = action_states[p_action];
@@ -879,6 +881,8 @@ void Input::action_press(const StringName &p_action, float p_strength) {
}
void Input::action_release(const StringName &p_action) {
+ ERR_FAIL_COND_MSG(!InputMap::get_singleton()->has_action(p_action), InputMap::get_singleton()->suggest_actions(p_action));
+
// Create or retrieve existing action.
ActionState &action_state = action_states[p_action];
action_state.cache.pressed = 0;
diff --git a/core/io/packet_peer.cpp b/core/io/packet_peer.cpp
index 48806fba73..0329ace313 100644
--- a/core/io/packet_peer.cpp
+++ b/core/io/packet_peer.cpp
@@ -318,9 +318,9 @@ int PacketPeerStream::get_output_buffer_max_size() const {
}
PacketPeerStream::PacketPeerStream() {
- int rbsize = GLOBAL_GET("network/limits/packet_peer_stream/max_buffer_po2");
+ int64_t rbsize = GLOBAL_GET("network/limits/packet_peer_stream/max_buffer_po2");
ring_buffer.resize(rbsize);
- input_buffer.resize(1 << rbsize);
- output_buffer.resize(1 << rbsize);
+ input_buffer.resize(int64_t(1) << rbsize);
+ output_buffer.resize(int64_t(1) << rbsize);
}
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/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/core/templates/cowdata.h b/core/templates/cowdata.h
index 46d9797d6c..d43cf8107f 100644
--- a/core/templates/cowdata.h
+++ b/core/templates/cowdata.h
@@ -46,7 +46,7 @@ class CharString;
template <class T, class V>
class VMap;
-SAFE_NUMERIC_TYPE_PUN_GUARANTEES(uint32_t)
+SAFE_NUMERIC_TYPE_PUN_GUARANTEES(uint64_t)
// Silence a false positive warning (see GH-52119).
#if defined(__GNUC__) && !defined(__clang__)
@@ -64,45 +64,71 @@ class CowData {
template <class TV, class VV>
friend class VMap;
+public:
+ typedef int64_t Size;
+ typedef uint64_t USize;
+ static constexpr USize MAX_INT = INT64_MAX;
+
private:
+ // Function to find the next power of 2 to an integer.
+ static _FORCE_INLINE_ USize next_po2(USize x) {
+ if (x == 0) {
+ return 0;
+ }
+
+ --x;
+ x |= x >> 1;
+ x |= x >> 2;
+ x |= x >> 4;
+ x |= x >> 8;
+ x |= x >> 16;
+ if (sizeof(USize) == 8) {
+ x |= x >> 32;
+ }
+
+ return ++x;
+ }
+
+ static constexpr USize ALLOC_PAD = sizeof(USize) * 2; // For size and atomic refcount.
+
mutable T *_ptr = nullptr;
// internal helpers
- _FORCE_INLINE_ SafeNumeric<uint32_t> *_get_refcount() const {
+ _FORCE_INLINE_ SafeNumeric<USize> *_get_refcount() const {
if (!_ptr) {
return nullptr;
}
- return reinterpret_cast<SafeNumeric<uint32_t> *>(_ptr) - 2;
+ return reinterpret_cast<SafeNumeric<USize> *>(_ptr) - 2;
}
- _FORCE_INLINE_ uint32_t *_get_size() const {
+ _FORCE_INLINE_ USize *_get_size() const {
if (!_ptr) {
return nullptr;
}
- return reinterpret_cast<uint32_t *>(_ptr) - 1;
+ return reinterpret_cast<USize *>(_ptr) - 1;
}
- _FORCE_INLINE_ size_t _get_alloc_size(size_t p_elements) const {
- return next_power_of_2(p_elements * sizeof(T));
+ _FORCE_INLINE_ USize _get_alloc_size(USize p_elements) const {
+ return next_po2(p_elements * sizeof(T));
}
- _FORCE_INLINE_ bool _get_alloc_size_checked(size_t p_elements, size_t *out) const {
+ _FORCE_INLINE_ bool _get_alloc_size_checked(USize p_elements, USize *out) const {
if (unlikely(p_elements == 0)) {
*out = 0;
return true;
}
-#if defined(__GNUC__)
- size_t o;
- size_t p;
+#if defined(__GNUC__) && defined(IS_32_BIT)
+ USize o;
+ USize p;
if (__builtin_mul_overflow(p_elements, sizeof(T), &o)) {
*out = 0;
return false;
}
- *out = next_power_of_2(o);
- if (__builtin_add_overflow(o, static_cast<size_t>(32), &p)) {
+ *out = next_po2(o);
+ if (__builtin_add_overflow(o, static_cast<USize>(32), &p)) {
return false; // No longer allocated here.
}
#else
@@ -116,7 +142,7 @@ private:
void _unref(void *p_data);
void _ref(const CowData *p_from);
void _ref(const CowData &p_from);
- uint32_t _copy_on_write();
+ USize _copy_on_write();
public:
void operator=(const CowData<T> &p_from) { _ref(p_from); }
@@ -130,8 +156,8 @@ public:
return _ptr;
}
- _FORCE_INLINE_ int size() const {
- uint32_t *size = (uint32_t *)_get_size();
+ _FORCE_INLINE_ Size size() const {
+ USize *size = (USize *)_get_size();
if (size) {
return *size;
} else {
@@ -142,42 +168,42 @@ public:
_FORCE_INLINE_ void clear() { resize(0); }
_FORCE_INLINE_ bool is_empty() const { return _ptr == nullptr; }
- _FORCE_INLINE_ void set(int p_index, const T &p_elem) {
+ _FORCE_INLINE_ void set(Size p_index, const T &p_elem) {
ERR_FAIL_INDEX(p_index, size());
_copy_on_write();
_ptr[p_index] = p_elem;
}
- _FORCE_INLINE_ T &get_m(int p_index) {
+ _FORCE_INLINE_ T &get_m(Size p_index) {
CRASH_BAD_INDEX(p_index, size());
_copy_on_write();
return _ptr[p_index];
}
- _FORCE_INLINE_ const T &get(int p_index) const {
+ _FORCE_INLINE_ const T &get(Size p_index) const {
CRASH_BAD_INDEX(p_index, size());
return _ptr[p_index];
}
template <bool p_ensure_zero = false>
- Error resize(int p_size);
+ Error resize(Size p_size);
- _FORCE_INLINE_ void remove_at(int p_index) {
+ _FORCE_INLINE_ void remove_at(Size p_index) {
ERR_FAIL_INDEX(p_index, size());
T *p = ptrw();
- int len = size();
- for (int i = p_index; i < len - 1; i++) {
+ Size len = size();
+ for (Size i = p_index; i < len - 1; i++) {
p[i] = p[i + 1];
}
resize(len - 1);
}
- Error insert(int p_pos, const T &p_val) {
+ Error insert(Size p_pos, const T &p_val) {
ERR_FAIL_INDEX_V(p_pos, size() + 1, ERR_INVALID_PARAMETER);
resize(size() + 1);
- for (int i = (size() - 1); i > p_pos; i--) {
+ for (Size i = (size() - 1); i > p_pos; i--) {
set(i, get(i - 1));
}
set(p_pos, p_val);
@@ -185,9 +211,9 @@ public:
return OK;
}
- int find(const T &p_val, int p_from = 0) const;
- int rfind(const T &p_val, int p_from = -1) const;
- int count(const T &p_val) const;
+ Size find(const T &p_val, Size p_from = 0) const;
+ Size rfind(const T &p_val, Size p_from = -1) const;
+ Size count(const T &p_val) const;
_FORCE_INLINE_ CowData() {}
_FORCE_INLINE_ ~CowData();
@@ -200,7 +226,7 @@ void CowData<T>::_unref(void *p_data) {
return;
}
- SafeNumeric<uint32_t> *refc = _get_refcount();
+ SafeNumeric<USize> *refc = _get_refcount();
if (refc->decrement() > 0) {
return; // still in use
@@ -208,35 +234,36 @@ void CowData<T>::_unref(void *p_data) {
// clean up
if (!std::is_trivially_destructible<T>::value) {
- uint32_t *count = _get_size();
+ USize *count = _get_size();
T *data = (T *)(count + 1);
- for (uint32_t i = 0; i < *count; ++i) {
+ for (USize i = 0; i < *count; ++i) {
// call destructors
data[i].~T();
}
}
// free mem
- Memory::free_static((uint8_t *)p_data, true);
+ Memory::free_static(((uint8_t *)p_data) - ALLOC_PAD, false);
}
template <class T>
-uint32_t CowData<T>::_copy_on_write() {
+typename CowData<T>::USize CowData<T>::_copy_on_write() {
if (!_ptr) {
return 0;
}
- SafeNumeric<uint32_t> *refc = _get_refcount();
+ SafeNumeric<USize> *refc = _get_refcount();
- uint32_t rc = refc->get();
+ USize rc = refc->get();
if (unlikely(rc > 1)) {
/* in use by more than me */
- uint32_t current_size = *_get_size();
+ USize current_size = *_get_size();
- uint32_t *mem_new = (uint32_t *)Memory::alloc_static(_get_alloc_size(current_size), true);
+ USize *mem_new = (USize *)Memory::alloc_static(_get_alloc_size(current_size) + ALLOC_PAD, false);
+ mem_new += 2;
- new (mem_new - 2) SafeNumeric<uint32_t>(1); //refcount
+ new (mem_new - 2) SafeNumeric<USize>(1); //refcount
*(mem_new - 1) = current_size; //size
T *_data = (T *)(mem_new);
@@ -246,7 +273,7 @@ uint32_t CowData<T>::_copy_on_write() {
memcpy(mem_new, _ptr, current_size * sizeof(T));
} else {
- for (uint32_t i = 0; i < current_size; i++) {
+ for (USize i = 0; i < current_size; i++) {
memnew_placement(&_data[i], T(_ptr[i]));
}
}
@@ -261,10 +288,10 @@ uint32_t CowData<T>::_copy_on_write() {
template <class T>
template <bool p_ensure_zero>
-Error CowData<T>::resize(int p_size) {
+Error CowData<T>::resize(Size p_size) {
ERR_FAIL_COND_V(p_size < 0, ERR_INVALID_PARAMETER);
- int current_size = size();
+ Size current_size = size();
if (p_size == current_size) {
return OK;
@@ -278,27 +305,29 @@ Error CowData<T>::resize(int p_size) {
}
// possibly changing size, copy on write
- uint32_t rc = _copy_on_write();
+ USize rc = _copy_on_write();
- size_t current_alloc_size = _get_alloc_size(current_size);
- size_t alloc_size;
+ USize current_alloc_size = _get_alloc_size(current_size);
+ USize alloc_size;
ERR_FAIL_COND_V(!_get_alloc_size_checked(p_size, &alloc_size), ERR_OUT_OF_MEMORY);
if (p_size > current_size) {
if (alloc_size != current_alloc_size) {
if (current_size == 0) {
// alloc from scratch
- uint32_t *ptr = (uint32_t *)Memory::alloc_static(alloc_size, true);
+ USize *ptr = (USize *)Memory::alloc_static(alloc_size + ALLOC_PAD, false);
+ ptr += 2;
ERR_FAIL_NULL_V(ptr, ERR_OUT_OF_MEMORY);
*(ptr - 1) = 0; //size, currently none
- new (ptr - 2) SafeNumeric<uint32_t>(1); //refcount
+ new (ptr - 2) SafeNumeric<USize>(1); //refcount
_ptr = (T *)ptr;
} else {
- uint32_t *_ptrnew = (uint32_t *)Memory::realloc_static(_ptr, alloc_size, true);
+ USize *_ptrnew = (USize *)Memory::realloc_static(((uint8_t *)_ptr) - ALLOC_PAD, alloc_size + ALLOC_PAD, false);
ERR_FAIL_NULL_V(_ptrnew, ERR_OUT_OF_MEMORY);
- new (_ptrnew - 2) SafeNumeric<uint32_t>(rc); //refcount
+ _ptrnew += 2;
+ new (_ptrnew - 2) SafeNumeric<USize>(rc); //refcount
_ptr = (T *)(_ptrnew);
}
@@ -307,7 +336,7 @@ Error CowData<T>::resize(int p_size) {
// construct the newly created elements
if (!std::is_trivially_constructible<T>::value) {
- for (int i = *_get_size(); i < p_size; i++) {
+ for (Size i = *_get_size(); i < p_size; i++) {
memnew_placement(&_ptr[i], T);
}
} else if (p_ensure_zero) {
@@ -319,16 +348,17 @@ Error CowData<T>::resize(int p_size) {
} else if (p_size < current_size) {
if (!std::is_trivially_destructible<T>::value) {
// deinitialize no longer needed elements
- for (uint32_t i = p_size; i < *_get_size(); i++) {
+ for (USize i = p_size; i < *_get_size(); i++) {
T *t = &_ptr[i];
t->~T();
}
}
if (alloc_size != current_alloc_size) {
- uint32_t *_ptrnew = (uint32_t *)Memory::realloc_static(_ptr, alloc_size, true);
+ USize *_ptrnew = (USize *)Memory::realloc_static(((uint8_t *)_ptr) - ALLOC_PAD, alloc_size + ALLOC_PAD, false);
ERR_FAIL_NULL_V(_ptrnew, ERR_OUT_OF_MEMORY);
- new (_ptrnew - 2) SafeNumeric<uint32_t>(rc); //refcount
+ _ptrnew += 2;
+ new (_ptrnew - 2) SafeNumeric<USize>(rc); //refcount
_ptr = (T *)(_ptrnew);
}
@@ -340,14 +370,14 @@ Error CowData<T>::resize(int p_size) {
}
template <class T>
-int CowData<T>::find(const T &p_val, int p_from) const {
- int ret = -1;
+typename CowData<T>::Size CowData<T>::find(const T &p_val, Size p_from) const {
+ Size ret = -1;
if (p_from < 0 || size() == 0) {
return ret;
}
- for (int i = p_from; i < size(); i++) {
+ for (Size i = p_from; i < size(); i++) {
if (get(i) == p_val) {
ret = i;
break;
@@ -358,8 +388,8 @@ int CowData<T>::find(const T &p_val, int p_from) const {
}
template <class T>
-int CowData<T>::rfind(const T &p_val, int p_from) const {
- const int s = size();
+typename CowData<T>::Size CowData<T>::rfind(const T &p_val, Size p_from) const {
+ const Size s = size();
if (p_from < 0) {
p_from = s + p_from;
@@ -368,7 +398,7 @@ int CowData<T>::rfind(const T &p_val, int p_from) const {
p_from = s - 1;
}
- for (int i = p_from; i >= 0; i--) {
+ for (Size i = p_from; i >= 0; i--) {
if (get(i) == p_val) {
return i;
}
@@ -377,9 +407,9 @@ int CowData<T>::rfind(const T &p_val, int p_from) const {
}
template <class T>
-int CowData<T>::count(const T &p_val) const {
- int amount = 0;
- for (int i = 0; i < size(); i++) {
+typename CowData<T>::Size CowData<T>::count(const T &p_val) const {
+ Size amount = 0;
+ for (Size i = 0; i < size(); i++) {
if (get(i) == p_val) {
amount++;
}
diff --git a/core/templates/ring_buffer.h b/core/templates/ring_buffer.h
index d878894946..54148a59bf 100644
--- a/core/templates/ring_buffer.h
+++ b/core/templates/ring_buffer.h
@@ -197,7 +197,7 @@ public:
int old_size = size();
int new_size = 1 << p_power;
int mask = new_size - 1;
- data.resize(1 << p_power);
+ data.resize(int64_t(1) << int64_t(p_power));
if (old_size < new_size && read_pos > write_pos) {
for (int i = 0; i < write_pos; i++) {
data.write[(old_size + i) & mask] = data[i];
diff --git a/core/templates/vector.h b/core/templates/vector.h
index d8bac0870f..361a7f56d5 100644
--- a/core/templates/vector.h
+++ b/core/templates/vector.h
@@ -48,7 +48,7 @@
template <class T>
class VectorWriteProxy {
public:
- _FORCE_INLINE_ T &operator[](int p_index) {
+ _FORCE_INLINE_ T &operator[](typename CowData<T>::Size p_index) {
CRASH_BAD_INDEX(p_index, ((Vector<T> *)(this))->_cowdata.size());
return ((Vector<T> *)(this))->_cowdata.ptrw()[p_index];
@@ -61,6 +61,7 @@ class Vector {
public:
VectorWriteProxy<T> write;
+ typedef typename CowData<T>::Size Size;
private:
CowData<T> _cowdata;
@@ -70,9 +71,9 @@ public:
_FORCE_INLINE_ bool append(const T &p_elem) { return push_back(p_elem); } //alias
void fill(T p_elem);
- void remove_at(int p_index) { _cowdata.remove_at(p_index); }
+ void remove_at(Size p_index) { _cowdata.remove_at(p_index); }
_FORCE_INLINE_ bool erase(const T &p_val) {
- int idx = find(p_val);
+ Size idx = find(p_val);
if (idx >= 0) {
remove_at(idx);
return true;
@@ -87,17 +88,17 @@ public:
_FORCE_INLINE_ void clear() { resize(0); }
_FORCE_INLINE_ bool is_empty() const { return _cowdata.is_empty(); }
- _FORCE_INLINE_ T get(int p_index) { return _cowdata.get(p_index); }
- _FORCE_INLINE_ const T &get(int p_index) const { return _cowdata.get(p_index); }
- _FORCE_INLINE_ void set(int p_index, const T &p_elem) { _cowdata.set(p_index, p_elem); }
- _FORCE_INLINE_ int size() const { return _cowdata.size(); }
- Error resize(int p_size) { return _cowdata.resize(p_size); }
- Error resize_zeroed(int p_size) { return _cowdata.template resize<true>(p_size); }
- _FORCE_INLINE_ const T &operator[](int p_index) const { return _cowdata.get(p_index); }
- Error insert(int p_pos, T p_val) { return _cowdata.insert(p_pos, p_val); }
- int find(const T &p_val, int p_from = 0) const { return _cowdata.find(p_val, p_from); }
- int rfind(const T &p_val, int p_from = -1) const { return _cowdata.rfind(p_val, p_from); }
- int count(const T &p_val) const { return _cowdata.count(p_val); }
+ _FORCE_INLINE_ T get(Size p_index) { return _cowdata.get(p_index); }
+ _FORCE_INLINE_ const T &get(Size p_index) const { return _cowdata.get(p_index); }
+ _FORCE_INLINE_ void set(Size p_index, const T &p_elem) { _cowdata.set(p_index, p_elem); }
+ _FORCE_INLINE_ Size size() const { return _cowdata.size(); }
+ Error resize(Size p_size) { return _cowdata.resize(p_size); }
+ Error resize_zeroed(Size p_size) { return _cowdata.template resize<true>(p_size); }
+ _FORCE_INLINE_ const T &operator[](Size p_index) const { return _cowdata.get(p_index); }
+ Error insert(Size p_pos, T p_val) { return _cowdata.insert(p_pos, p_val); }
+ Size find(const T &p_val, Size p_from = 0) const { return _cowdata.find(p_val, p_from); }
+ Size rfind(const T &p_val, Size p_from = -1) const { return _cowdata.rfind(p_val, p_from); }
+ Size count(const T &p_val) const { return _cowdata.count(p_val); }
void append_array(Vector<T> p_other);
@@ -109,7 +110,7 @@ public:
template <class Comparator, bool Validate = SORT_ARRAY_VALIDATE_ENABLED, class... Args>
void sort_custom(Args &&...args) {
- int len = _cowdata.size();
+ Size len = _cowdata.size();
if (len == 0) {
return;
}
@@ -119,12 +120,12 @@ public:
sorter.sort(data, len);
}
- int bsearch(const T &p_value, bool p_before) {
+ Size bsearch(const T &p_value, bool p_before) {
return bsearch_custom<_DefaultComparator<T>>(p_value, p_before);
}
template <class Comparator, class Value, class... Args>
- int bsearch_custom(const Value &p_value, bool p_before, Args &&...args) {
+ Size bsearch_custom(const Value &p_value, bool p_before, Args &&...args) {
SearchArray<T, Comparator> search{ args... };
return search.bisect(ptrw(), size(), p_value, p_before);
}
@@ -134,7 +135,7 @@ public:
}
void ordered_insert(const T &p_val) {
- int i;
+ Size i;
for (i = 0; i < _cowdata.size(); i++) {
if (p_val < operator[](i)) {
break;
@@ -157,28 +158,28 @@ public:
return ret;
}
- Vector<T> slice(int p_begin, int p_end = INT_MAX) const {
+ Vector<T> slice(Size p_begin, Size p_end = CowData<T>::MAX_INT) const {
Vector<T> result;
- const int s = size();
+ const Size s = size();
- int begin = CLAMP(p_begin, -s, s);
+ Size begin = CLAMP(p_begin, -s, s);
if (begin < 0) {
begin += s;
}
- int end = CLAMP(p_end, -s, s);
+ Size end = CLAMP(p_end, -s, s);
if (end < 0) {
end += s;
}
ERR_FAIL_COND_V(begin > end, result);
- int result_size = end - begin;
+ Size result_size = end - begin;
result.resize(result_size);
const T *const r = ptr();
T *const w = result.ptrw();
- for (int i = 0; i < result_size; ++i) {
+ for (Size i = 0; i < result_size; ++i) {
w[i] = r[begin + i];
}
@@ -186,11 +187,11 @@ public:
}
bool operator==(const Vector<T> &p_arr) const {
- int s = size();
+ Size s = size();
if (s != p_arr.size()) {
return false;
}
- for (int i = 0; i < s; i++) {
+ for (Size i = 0; i < s; i++) {
if (operator[](i) != p_arr[i]) {
return false;
}
@@ -199,11 +200,11 @@ public:
}
bool operator!=(const Vector<T> &p_arr) const {
- int s = size();
+ Size s = size();
if (s != p_arr.size()) {
return true;
}
- for (int i = 0; i < s; i++) {
+ for (Size i = 0; i < s; i++) {
if (operator[](i) != p_arr[i]) {
return true;
}
@@ -280,7 +281,7 @@ public:
Error err = _cowdata.resize(p_init.size());
ERR_FAIL_COND(err);
- int i = 0;
+ Size i = 0;
for (const T &element : p_init) {
_cowdata.set(i++, element);
}
@@ -292,7 +293,7 @@ public:
template <class T>
void Vector<T>::reverse() {
- for (int i = 0; i < size() / 2; i++) {
+ for (Size i = 0; i < size() / 2; i++) {
T *p = ptrw();
SWAP(p[i], p[size() - i - 1]);
}
@@ -300,13 +301,13 @@ void Vector<T>::reverse() {
template <class T>
void Vector<T>::append_array(Vector<T> p_other) {
- const int ds = p_other.size();
+ const Size ds = p_other.size();
if (ds == 0) {
return;
}
- const int bs = size();
+ const Size bs = size();
resize(bs + ds);
- for (int i = 0; i < ds; ++i) {
+ for (Size i = 0; i < ds; ++i) {
ptrw()[bs + i] = p_other[i];
}
}
@@ -323,7 +324,7 @@ bool Vector<T>::push_back(T p_elem) {
template <class T>
void Vector<T>::fill(T p_elem) {
T *p = ptrw();
- for (int i = 0; i < size(); i++) {
+ for (Size i = 0; i < size(); i++) {
p[i] = p_elem;
}
}
diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp
index 03836985f3..90f7e7b44a 100644
--- a/core/variant/variant_call.cpp
+++ b/core/variant/variant_call.cpp
@@ -666,7 +666,7 @@ struct _VariantCall {
CharString cs;
cs.resize(p_instance->size() + 1);
memcpy(cs.ptrw(), r, p_instance->size());
- cs[p_instance->size()] = 0;
+ cs[(int)p_instance->size()] = 0;
s = cs.get_data();
}
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/RayCast2D.xml b/doc/classes/RayCast2D.xml
index 6144fd8f0b..22e3ae07ba 100644
--- a/doc/classes/RayCast2D.xml
+++ b/doc/classes/RayCast2D.xml
@@ -74,7 +74,7 @@
<method name="get_collision_point" qualifiers="const">
<return type="Vector2" />
<description>
- Returns the collision point at which the ray intersects the closest object.
+ Returns the collision point at which the ray intersects the closest object. If [member hit_from_inside] is [code]true[/code] and the ray starts inside of a collision shape, this function will return the origin point of the ray.
[b]Note:[/b] This point is in the [b]global[/b] coordinate system.
</description>
</method>
diff --git a/doc/classes/RayCast3D.xml b/doc/classes/RayCast3D.xml
index 7157ec9b5f..406fed107f 100644
--- a/doc/classes/RayCast3D.xml
+++ b/doc/classes/RayCast3D.xml
@@ -81,7 +81,7 @@
<method name="get_collision_point" qualifiers="const">
<return type="Vector3" />
<description>
- Returns the collision point at which the ray intersects the closest object.
+ Returns the collision point at which the ray intersects the closest object. If [member hit_from_inside] is [code]true[/code] and the ray starts inside of a collision shape, this function will return the origin point of the ray.
[b]Note:[/b] This point is in the [b]global[/b] coordinate system.
</description>
</method>
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/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/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_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 0aa9a3bfee..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;
@@ -3752,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/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/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h
index 7fe63e6282..bf36b6ec1d 100644
--- a/editor/plugins/canvas_item_editor_plugin.h
+++ b/editor/plugins/canvas_item_editor_plugin.h
@@ -56,7 +56,6 @@ class CanvasItemEditorSelectedItem : public Object {
public:
Transform2D prev_xform;
- real_t prev_rot = 0;
Rect2 prev_rect;
Vector2 prev_pivot;
real_t prev_anchors[4] = { (real_t)0.0 };
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_text_editor.cpp b/editor/plugins/script_text_editor.cpp
index 5bd6f83616..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();
diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp
index 6de37172b3..89fff008ea 100644
--- a/editor/plugins/visual_shader_editor_plugin.cpp
+++ b/editor/plugins/visual_shader_editor_plugin.cpp
@@ -3191,6 +3191,15 @@ void VisualShaderEditor::_add_node(int p_idx, const Vector<Variant> &p_ops, Stri
bool created_expression_port = false;
+ // A node is inserted in an already present connection.
+ if (from_node != -1 && from_slot != -1 && to_node != -1 && to_slot != -1) {
+ undo_redo->add_do_method(visual_shader.ptr(), "disconnect_nodes", type, from_node, from_slot, to_node, to_slot);
+ undo_redo->add_undo_method(visual_shader.ptr(), "connect_nodes", type, from_node, from_slot, to_node, to_slot);
+ undo_redo->add_do_method(graph_plugin.ptr(), "disconnect_nodes", type, from_node, from_slot, to_node, to_slot);
+ undo_redo->add_undo_method(graph_plugin.ptr(), "connect_nodes", type, from_node, from_slot, to_node, to_slot);
+ }
+
+ // Create a connection from the new node to an input port of an existing one.
if (to_node != -1 && to_slot != -1) {
VisualShaderNode::PortType input_port_type = visual_shader->get_node(type, to_node)->get_input_port_type(to_slot);
@@ -3260,7 +3269,10 @@ void VisualShaderEditor::_add_node(int p_idx, const Vector<Variant> &p_ops, Stri
}
}
}
- } else if (from_node != -1 && from_slot != -1) {
+ }
+
+ // Create a connection from the output port of an existing node to the new one.
+ if (from_node != -1 && from_slot != -1) {
VisualShaderNode::PortType output_port_type = visual_shader->get_node(type, from_node)->get_output_port_type(from_slot);
if (expr && expr->is_editable()) {
@@ -3483,8 +3495,11 @@ void VisualShaderEditor::_nodes_dragged() {
undo_redo->add_undo_method(graph_plugin.ptr(), "set_node_position", E.type, E.node, E.from);
}
- drag_buffer.clear();
undo_redo->commit_action();
+
+ _handle_node_drop_on_connection();
+
+ drag_buffer.clear();
}
void VisualShaderEditor::_connection_request(const String &p_from, int p_from_index, const String &p_to, int p_to_index) {
@@ -3564,6 +3579,132 @@ void VisualShaderEditor::_connection_from_empty(const String &p_to, int p_to_slo
_show_members_dialog(true, input_port_type, output_port_type);
}
+bool VisualShaderEditor::_check_node_drop_on_connection(const Vector2 &p_position, Ref<GraphEdit::Connection> *r_closest_connection, int *r_from_port, int *r_to_port) {
+ VisualShader::Type shader_type = get_current_shader_type();
+
+ // Get selected graph node.
+ Ref<VisualShaderNode> selected_vsnode;
+ int selected_node_id = -1;
+ int selected_node_count = 0;
+ Rect2 selected_node_rect;
+
+ for (int i = 0; i < graph->get_child_count(); i++) {
+ GraphNode *graph_node = Object::cast_to<GraphNode>(graph->get_child(i));
+ if (graph_node && graph_node->is_selected()) {
+ selected_node_id = String(graph_node->get_name()).to_int();
+ Ref<VisualShaderNode> vsnode = visual_shader->get_node(shader_type, selected_node_id);
+ if (!vsnode->is_closable()) {
+ continue;
+ }
+
+ selected_node_count += 1;
+
+ Ref<VisualShaderNode> node = visual_shader->get_node(shader_type, selected_node_id);
+ selected_vsnode = node;
+ selected_node_rect = graph_node->get_rect();
+ }
+ }
+
+ // Only a single node - which has both input and output ports but is not connected yet - can be inserted.
+ if (selected_node_count != 1 || !selected_vsnode.is_valid()) {
+ return false;
+ }
+
+ // Check whether the dragged node was dropped over a connection.
+ List<Ref<GraphEdit::Connection>> intersecting_connections = graph->get_connections_intersecting_with_rect(selected_node_rect);
+
+ if (intersecting_connections.is_empty()) {
+ return false;
+ }
+
+ Ref<GraphEdit::Connection> intersecting_connection = intersecting_connections.front()->get();
+
+ if (selected_vsnode->is_any_port_connected() || selected_vsnode->get_input_port_count() == 0 || selected_vsnode->get_output_port_count() == 0) {
+ return false;
+ }
+
+ VisualShaderNode::PortType original_port_type_from = visual_shader->get_node(shader_type, String(intersecting_connection->from_node).to_int())->get_output_port_type(intersecting_connection->from_port);
+ VisualShaderNode::PortType original_port_type_to = visual_shader->get_node(shader_type, String(intersecting_connection->to_node).to_int())->get_input_port_type(intersecting_connection->to_port);
+
+ // Searching for the default port or the first compatible input port of the node to insert.
+ int _to_port = -1;
+ for (int i = 0; i < selected_vsnode->get_input_port_count(); i++) {
+ if (visual_shader->is_port_types_compatible(original_port_type_from, selected_vsnode->get_input_port_type(i))) {
+ if (i == selected_vsnode->get_default_input_port(original_port_type_from)) {
+ _to_port = i;
+ break;
+ } else if (_to_port == -1) {
+ _to_port = i;
+ }
+ }
+ }
+
+ // Searching for the first compatible output port of the node to insert.
+ int _from_port = -1;
+ for (int i = 0; i < selected_vsnode->get_output_port_count(); i++) {
+ if (visual_shader->is_port_types_compatible(selected_vsnode->get_output_port_type(i), original_port_type_to)) {
+ _from_port = i;
+ break;
+ }
+ }
+
+ if (_to_port == -1 || _from_port == -1) {
+ return false;
+ }
+
+ if (r_closest_connection != nullptr) {
+ *r_closest_connection = intersecting_connection;
+ }
+ if (r_from_port != nullptr) {
+ *r_from_port = _from_port;
+ }
+ if (r_to_port != nullptr) {
+ *r_to_port = _to_port;
+ }
+
+ return true;
+}
+
+void VisualShaderEditor::_handle_node_drop_on_connection() {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Insert node"));
+
+ // Check whether the dragged node was dropped over a connection.
+ Ref<GraphEdit::Connection> closest_connection;
+ int _from_port = -1;
+ int _to_port = -1;
+
+ if (!_check_node_drop_on_connection(graph->get_local_mouse_position(), &closest_connection, &_from_port, &_to_port)) {
+ return;
+ }
+
+ int selected_node_id = drag_buffer[0].node;
+ VisualShader::Type shader_type = get_current_shader_type();
+ Ref<VisualShaderNode> selected_vsnode = visual_shader->get_node(shader_type, selected_node_id);
+
+ // Delete the old connection.
+ undo_redo->add_do_method(visual_shader.ptr(), "disconnect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+ undo_redo->add_undo_method(visual_shader.ptr(), "connect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+ undo_redo->add_do_method(graph_plugin.ptr(), "disconnect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+ undo_redo->add_undo_method(graph_plugin.ptr(), "connect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+
+ // Add the connection to the dropped node.
+ undo_redo->add_do_method(visual_shader.ptr(), "connect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, selected_node_id, _to_port);
+ undo_redo->add_undo_method(visual_shader.ptr(), "disconnect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, selected_node_id, _to_port);
+ undo_redo->add_do_method(graph_plugin.ptr(), "connect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, selected_node_id, _to_port);
+ undo_redo->add_undo_method(graph_plugin.ptr(), "disconnect_nodes", shader_type, String(closest_connection->from_node).to_int(), closest_connection->from_port, selected_node_id, _to_port);
+
+ // Add the connection from the dropped node.
+ undo_redo->add_do_method(visual_shader.ptr(), "connect_nodes", shader_type, selected_node_id, _from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+ undo_redo->add_undo_method(visual_shader.ptr(), "disconnect_nodes", shader_type, selected_node_id, _from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+ undo_redo->add_do_method(graph_plugin.ptr(), "connect_nodes", shader_type, selected_node_id, _from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+ undo_redo->add_undo_method(graph_plugin.ptr(), "disconnect_nodes", shader_type, selected_node_id, _from_port, String(closest_connection->to_node).to_int(), closest_connection->to_port);
+
+ undo_redo->commit_action();
+
+ call_deferred(SNAME("_update_graph"));
+}
+
void VisualShaderEditor::_delete_nodes(int p_type, const List<int> &p_nodes) {
VisualShader::Type type = VisualShader::Type(p_type);
List<VisualShader::Connection> conns;
@@ -3923,9 +4064,19 @@ void VisualShaderEditor::_node_selected(Object *p_node) {
}
void VisualShaderEditor::_graph_gui_input(const Ref<InputEvent> &p_event) {
+ Ref<InputEventMouseMotion> mm = p_event;
Ref<InputEventMouseButton> mb = p_event;
VisualShader::Type type = get_current_shader_type();
+ // Highlight valid connection on which a node can be dropped.
+ if (mm.is_valid() && mm->get_button_mask().has_flag(MouseButtonMask::LEFT)) {
+ Ref<GraphEdit::Connection> closest_connection;
+ graph->reset_all_connection_activity();
+ if (_check_node_drop_on_connection(graph->get_local_mouse_position(), &closest_connection)) {
+ graph->set_connection_activity(closest_connection->from_node, closest_connection->from_port, closest_connection->to_node, closest_connection->to_port, 1.0);
+ }
+ }
+
Ref<VisualShaderNode> selected_vsnode;
// Right click actions.
if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::RIGHT) {
@@ -3981,7 +4132,16 @@ void VisualShaderEditor::_graph_gui_input(const Ref<InputEvent> &p_event) {
}
}
- if (selected_closable_graph_elements.is_empty() && copy_buffer_empty) {
+ menu_point = graph->get_local_mouse_position();
+ Point2 gpos = get_screen_position() + get_local_mouse_position();
+
+ Ref<GraphEdit::Connection> closest_connection = graph->get_closest_connection_at_point(menu_point);
+ if (closest_connection.is_valid()) {
+ clicked_connection = closest_connection;
+ connection_popup_menu->set_position(gpos);
+ connection_popup_menu->reset_size();
+ connection_popup_menu->popup();
+ } else if (selected_closable_graph_elements.is_empty() && copy_buffer_empty) {
_show_members_dialog(true);
} else {
popup_menu->set_item_disabled(NodeMenuOptions::CUT, selected_closable_graph_elements.is_empty());
@@ -4053,8 +4213,6 @@ void VisualShaderEditor::_graph_gui_input(const Ref<InputEvent> &p_event) {
popup_menu->add_item(TTR("Set Comment Description"), NodeMenuOptions::SET_COMMENT_DESCRIPTION);
}
- menu_point = graph->get_local_mouse_position();
- Point2 gpos = get_screen_position() + get_local_mouse_position();
popup_menu->set_position(gpos);
popup_menu->reset_size();
popup_menu->popup();
@@ -4757,6 +4915,27 @@ void VisualShaderEditor::_member_create() {
TreeItem *item = members->get_selected();
if (item != nullptr && item->has_meta("id")) {
int idx = members->get_selected()->get_meta("id");
+ if (connection_node_insert_requested) {
+ from_node = String(clicked_connection->from_node).to_int();
+ from_slot = clicked_connection->from_port;
+ to_node = String(clicked_connection->to_node).to_int();
+ to_slot = clicked_connection->to_port;
+
+ connection_node_insert_requested = false;
+
+ saved_node_pos_dirty = true;
+
+ // Find both graph nodes and get their positions.
+ GraphNode *from_graph_element = Object::cast_to<GraphNode>(graph->get_node(itos(from_node)));
+ GraphNode *to_graph_element = Object::cast_to<GraphNode>(graph->get_node(itos(to_node)));
+
+ ERR_FAIL_NULL(from_graph_element);
+ ERR_FAIL_NULL(to_graph_element);
+
+ // Since the size of the node to add is not known yet, it's not possible to center it exactly.
+ float zoom = graph->get_zoom();
+ saved_node_pos = 0.5 * (from_graph_element->get_position() + zoom * from_graph_element->get_output_port_position(from_slot) + to_graph_element->get_position() + zoom * to_graph_element->get_input_port_position(to_slot));
+ }
_add_node(idx, add_options[idx].ops);
members_dialog->hide();
}
@@ -4767,6 +4946,7 @@ void VisualShaderEditor::_member_cancel() {
to_slot = -1;
from_node = -1;
from_slot = -1;
+ connection_node_insert_requested = false;
}
void VisualShaderEditor::_update_varying_tree() {
@@ -4938,6 +5118,37 @@ void VisualShaderEditor::_node_menu_id_pressed(int p_idx) {
}
}
+void VisualShaderEditor::_connection_menu_id_pressed(int p_idx) {
+ switch (p_idx) {
+ case ConnectionMenuOptions::DISCONNECT: {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ undo_redo->create_action(TTR("Disconnect"));
+ undo_redo->add_do_method(visual_shader.ptr(), "disconnect_nodes", get_current_shader_type(), String(clicked_connection->from_node).to_int(), clicked_connection->from_port, String(clicked_connection->to_node).to_int(), clicked_connection->to_port);
+ undo_redo->add_undo_method(visual_shader.ptr(), "connect_nodes", get_current_shader_type(), String(clicked_connection->from_node).to_int(), clicked_connection->from_port, String(clicked_connection->to_node).to_int(), clicked_connection->to_port);
+ undo_redo->add_do_method(graph_plugin.ptr(), "disconnect_nodes", get_current_shader_type(), String(clicked_connection->from_node).to_int(), clicked_connection->from_port, String(clicked_connection->to_node).to_int(), clicked_connection->to_port);
+ undo_redo->add_undo_method(graph_plugin.ptr(), "connect_nodes", get_current_shader_type(), String(clicked_connection->from_node).to_int(), clicked_connection->from_port, String(clicked_connection->to_node).to_int(), clicked_connection->to_port);
+ undo_redo->commit_action();
+ } break;
+ case ConnectionMenuOptions::INSERT_NEW_NODE: {
+ VisualShaderNode::PortType input_port_type = VisualShaderNode::PORT_TYPE_MAX;
+ VisualShaderNode::PortType output_port_type = VisualShaderNode::PORT_TYPE_MAX;
+ Ref<VisualShaderNode> node1 = visual_shader->get_node(get_current_shader_type(), String(clicked_connection->from_node).to_int());
+ if (node1.is_valid()) {
+ output_port_type = node1->get_output_port_type(from_slot);
+ }
+ Ref<VisualShaderNode> node2 = visual_shader->get_node(get_current_shader_type(), String(clicked_connection->to_node).to_int());
+ if (node2.is_valid()) {
+ input_port_type = node2->get_input_port_type(to_slot);
+ }
+
+ connection_node_insert_requested = true;
+ _show_members_dialog(true, input_port_type, output_port_type);
+ } break;
+ default:
+ break;
+ }
+}
+
Variant VisualShaderEditor::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
if (p_from == members) {
TreeItem *it = members->get_item_at_position(p_point);
@@ -5417,6 +5628,12 @@ VisualShaderEditor::VisualShaderEditor() {
popup_menu->add_item(TTR("Clear Copy Buffer"), NodeMenuOptions::CLEAR_COPY_BUFFER);
popup_menu->connect("id_pressed", callable_mp(this, &VisualShaderEditor::_node_menu_id_pressed));
+ connection_popup_menu = memnew(PopupMenu);
+ add_child(connection_popup_menu);
+ connection_popup_menu->add_item(TTR("Disconnect"), ConnectionMenuOptions::DISCONNECT);
+ connection_popup_menu->add_item(TTR("Insert New Node"), ConnectionMenuOptions::INSERT_NEW_NODE);
+ connection_popup_menu->connect("id_pressed", callable_mp(this, &VisualShaderEditor::_connection_menu_id_pressed));
+
///////////////////////////////////////
// SHADER NODES TREE
///////////////////////////////////////
diff --git a/editor/plugins/visual_shader_editor_plugin.h b/editor/plugins/visual_shader_editor_plugin.h
index 5f1fde3a52..39e721f226 100644
--- a/editor/plugins/visual_shader_editor_plugin.h
+++ b/editor/plugins/visual_shader_editor_plugin.h
@@ -34,13 +34,13 @@
#include "editor/editor_plugin.h"
#include "editor/editor_properties.h"
#include "editor/plugins/editor_resource_conversion_plugin.h"
+#include "scene/gui/graph_edit.h"
#include "scene/resources/syntax_highlighter.h"
#include "scene/resources/visual_shader.h"
class CodeEdit;
class ColorPicker;
class CurveEditor;
-class GraphEdit;
class GraphElement;
class MenuButton;
class PopupPanel;
@@ -203,6 +203,7 @@ class VisualShaderEditor : public VBoxContainer {
VisualShaderNode::PortType members_input_port_type = VisualShaderNode::PORT_TYPE_MAX;
VisualShaderNode::PortType members_output_port_type = VisualShaderNode::PORT_TYPE_MAX;
PopupMenu *popup_menu = nullptr;
+ PopupMenu *connection_popup_menu = nullptr;
PopupMenu *constants_submenu = nullptr;
MenuButton *tools = nullptr;
@@ -282,6 +283,11 @@ class VisualShaderEditor : public VBoxContainer {
SET_COMMENT_DESCRIPTION,
};
+ enum ConnectionMenuOptions {
+ INSERT_NEW_NODE,
+ DISCONNECT,
+ };
+
enum class VaryingMenuOptions {
ADD,
REMOVE,
@@ -397,6 +403,9 @@ class VisualShaderEditor : public VBoxContainer {
int from_node = -1;
int from_slot = -1;
+ Ref<GraphEdit::Connection> clicked_connection;
+ bool connection_node_insert_requested = false;
+
HashSet<int> selected_constants;
HashSet<int> selected_parameters;
int selected_comment = -1;
@@ -409,6 +418,8 @@ class VisualShaderEditor : public VBoxContainer {
void _connection_to_empty(const String &p_from, int p_from_slot, const Vector2 &p_release_position);
void _connection_from_empty(const String &p_to, int p_to_slot, const Vector2 &p_release_position);
+ bool _check_node_drop_on_connection(const Vector2 &p_position, Ref<GraphEdit::Connection> *r_closest_connection, int *r_node_id = nullptr, int *r_to_port = nullptr);
+ void _handle_node_drop_on_connection();
void _comment_title_popup_show(const Point2 &p_position, int p_node_id);
void _comment_title_popup_hide();
@@ -501,6 +512,7 @@ class VisualShaderEditor : public VBoxContainer {
Vector2 menu_point;
void _node_menu_id_pressed(int p_idx);
+ void _connection_menu_id_pressed(int p_idx);
Variant get_drag_data_fw(const Point2 &p_point, Control *p_from);
bool can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const;
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/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..bccfe6d786 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.
@@ -1365,7 +1440,11 @@ void EditorThemeManager::_populate_standard_styles(const Ref<Theme> &p_theme, Th
}
p_theme->set_color("selection_fill", "GraphEdit", p_theme->get_color(SNAME("box_selection_fill_color"), EditorStringName(Editor)));
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("activity", "GraphEdit", p_config.dark_theme ? Color(1, 1, 1) : Color(0, 0, 0));
+
+ 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)));
@@ -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 50ec7d82b4..dbe186d63a 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -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/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj
index bd2560c280..e9efea8809 100644
--- a/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj
+++ b/misc/dist/ios_xcode/godot_ios.xcodeproj/project.pbxproj
@@ -255,13 +255,10 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = $min_version;
- "LD_CLASSIC_1000" = "";
- "LD_CLASSIC_1100" = "";
- "LD_CLASSIC_1200" = "";
- "LD_CLASSIC_1300" = "";
- "LD_CLASSIC_1400" = "";
"LD_CLASSIC_1500" = "-ld_classic";
- OTHER_LDFLAGS = "$(LD_CLASSIC_$(XCODE_VERSION_MAJOR)) $linker_flags";
+ "LD_CLASSIC_1501" = "-ld_classic";
+ "LD_CLASSIC_1510" = "-ld_classic";
+ OTHER_LDFLAGS = "$(LD_CLASSIC_$(XCODE_VERSION_ACTUAL)) $linker_flags";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "$targeted_device_family";
};
@@ -300,13 +297,10 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = $min_version;
- "LD_CLASSIC_1000" = "";
- "LD_CLASSIC_1100" = "";
- "LD_CLASSIC_1200" = "";
- "LD_CLASSIC_1300" = "";
- "LD_CLASSIC_1400" = "";
"LD_CLASSIC_1500" = "-ld_classic";
- OTHER_LDFLAGS = "$(LD_CLASSIC_$(XCODE_VERSION_MAJOR)) $linker_flags";
+ "LD_CLASSIC_1501" = "-ld_classic";
+ "LD_CLASSIC_1510" = "-ld_classic";
+ OTHER_LDFLAGS = "$(LD_CLASSIC_$(XCODE_VERSION_ACTUAL)) $linker_flags";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "$targeted_device_family";
VALIDATE_PRODUCT = YES;
diff --git a/misc/extension_api_validation/4.2-stable.expected b/misc/extension_api_validation/4.2-stable.expected
index 25094dda77..2c18b43948 100644
--- a/misc/extension_api_validation/4.2-stable.expected
+++ b/misc/extension_api_validation/4.2-stable.expected
@@ -59,3 +59,10 @@ Validate extension JSON: Error: Field 'classes/TileMap/methods/get_collision_vis
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 920aa63fbe..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()) {
@@ -2593,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.
@@ -2817,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 + "'.");
@@ -2841,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()) {
@@ -2870,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 2da9b89eb9..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;
@@ -616,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);
};
@@ -624,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_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index f6633f8bf6..13ed66710c 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);
@@ -1375,7 +1376,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
return GDScriptCodeGenerator::Address();
}
- codegen.script->lambda_info.insert(function, { lambda->captures.size(), lambda->use_self });
+ codegen.script->lambda_info.insert(function, { (int)lambda->captures.size(), lambda->use_self });
gen->write_lambda(result, function, captures, lambda->use_self);
for (int i = 0; i < captures.size(); i++) {
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index c330037bd6..210e2c3898 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -1432,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;
}
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/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp
index 7b03ac74d6..1a8c22cc11 100644
--- a/modules/gdscript/gdscript_vm.cpp
+++ b/modules/gdscript/gdscript_vm.cpp
@@ -655,7 +655,7 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
}
bool exit_ok = false;
bool awaited = false;
- int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? p_instance->members.size() : 0 };
+ int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? (int)p_instance->members.size() : 0 };
#endif
Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr };
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_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/doc_classes/GLTFPhysicsBody.xml b/modules/gltf/doc_classes/GLTFPhysicsBody.xml
index cf39721ce8..ca66cd54b0 100644
--- a/modules/gltf/doc_classes/GLTFPhysicsBody.xml
+++ b/modules/gltf/doc_classes/GLTFPhysicsBody.xml
@@ -4,7 +4,7 @@
Represents a GLTF physics body.
</brief_description>
<description>
- Represents a physics body as defined by the [code]OMI_physics_body[/code] GLTF extension. This class is an intermediary between the GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future.
+ Represents a physics body as an intermediary between the [code]OMI_physics_body[/code] GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future.
</description>
<tutorials>
<link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link>
@@ -15,7 +15,7 @@
<return type="GLTFPhysicsBody" />
<param index="0" name="dictionary" type="Dictionary" />
<description>
- Creates a new GLTFPhysicsBody instance by parsing the given [Dictionary].
+ Creates a new GLTFPhysicsBody instance by parsing the given [Dictionary] in the [code]OMI_physics_body[/code] GLTF extension format.
</description>
</method>
<method name="from_node" qualifiers="static">
@@ -28,7 +28,7 @@
<method name="to_dictionary" qualifiers="const">
<return type="Dictionary" />
<description>
- Serializes this GLTFPhysicsBody instance into a [Dictionary].
+ Serializes this GLTFPhysicsBody instance into a [Dictionary]. It will be in the format expected by the [code]OMI_physics_body[/code] GLTF extension.
</description>
</method>
<method name="to_node" qualifiers="const">
@@ -42,13 +42,20 @@
<member name="angular_velocity" type="Vector3" setter="set_angular_velocity" getter="get_angular_velocity" default="Vector3(0, 0, 0)">
The angular velocity of the physics body, in radians per second. This is only used when the body type is "rigid" or "vehicle".
</member>
- <member name="body_type" type="String" setter="set_body_type" getter="get_body_type" default="&quot;static&quot;">
- The type of the body. When importing, this controls what type of [CollisionObject3D] node Godot should generate. Valid values are "static", "kinematic", "character", "rigid", "vehicle", and "trigger".
+ <member name="body_type" type="String" setter="set_body_type" getter="get_body_type" default="&quot;rigid&quot;">
+ The type of the body. When importing, this controls what type of [CollisionObject3D] node Godot should generate. Valid values are "static", "animatable", "character", "rigid", "vehicle", and "trigger". When exporting, this will be squashed down to one of "static", "kinematic", or "dynamic" motion types, or the "trigger" property.
</member>
<member name="center_of_mass" type="Vector3" setter="set_center_of_mass" getter="get_center_of_mass" default="Vector3(0, 0, 0)">
The center of mass of the body, in meters. This is in local space relative to the body. By default, the center of the mass is the body's origin.
</member>
- <member name="inertia_tensor" type="Basis" setter="set_inertia_tensor" getter="get_inertia_tensor" default="Basis(0, 0, 0, 0, 0, 0, 0, 0, 0)">
+ <member name="inertia_diagonal" type="Vector3" setter="set_inertia_diagonal" getter="get_inertia_diagonal" default="Vector3(0, 0, 0)">
+ The inertia strength of the physics body, in kilogram meter squared (kg⋅m²). This represents the inertia around the principle axes, the diagonal of the inertia tensor matrix. This is only used when the body type is "rigid" or "vehicle".
+ When converted to a Godot [RigidBody3D] node, if this value is zero, then the inertia will be calculated automatically.
+ </member>
+ <member name="inertia_orientation" type="Quaternion" setter="set_inertia_orientation" getter="get_inertia_orientation" default="Quaternion(0, 0, 0, 1)">
+ The inertia orientation of the physics body. This defines the rotation of the inertia's principle axes relative to the object's local axes. This is only used when the body type is "rigid" or "vehicle" and [member inertia_diagonal] is set to a non-zero value.
+ </member>
+ <member name="inertia_tensor" type="Basis" setter="set_inertia_tensor" getter="get_inertia_tensor" default="Basis(0, 0, 0, 0, 0, 0, 0, 0, 0)" is_deprecated="true">
The inertia tensor of the physics body, in kilogram meter squared (kg⋅m²). This is only used when the body type is "rigid" or "vehicle".
When converted to a Godot [RigidBody3D] node, if this value is zero, then the inertia will be calculated automatically.
</member>
diff --git a/modules/gltf/doc_classes/GLTFPhysicsShape.xml b/modules/gltf/doc_classes/GLTFPhysicsShape.xml
index 67382f3295..c397c660d9 100644
--- a/modules/gltf/doc_classes/GLTFPhysicsShape.xml
+++ b/modules/gltf/doc_classes/GLTFPhysicsShape.xml
@@ -4,11 +4,12 @@
Represents a GLTF physics shape.
</brief_description>
<description>
- Represents a physics shape as defined by the [code]OMI_collider[/code] GLTF extension. This class is an intermediary between the GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future.
+ Represents a physics shape as defined by the [code]OMI_physics_shape[/code] or [code]OMI_collider[/code] GLTF extensions. This class is an intermediary between the GLTF data and Godot's nodes, and it's abstracted in a way that allows adding support for different GLTF physics extensions in the future.
</description>
<tutorials>
<link title="Runtime file loading and saving">$DOCS_URL/tutorials/io/runtime_file_loading_and_saving.html</link>
- <link title="OMI_collider GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_collider</link>
+ <link title="OMI_physics_shape GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_shape</link>
+ <link title="OMI_collider GLTF extension">https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/Archived/OMI_collider</link>
</tutorials>
<methods>
<method name="from_dictionary" qualifiers="static">
@@ -28,7 +29,7 @@
<method name="to_dictionary" qualifiers="const">
<return type="Dictionary" />
<description>
- Serializes this GLTFPhysicsShape instance into a [Dictionary].
+ Serializes this GLTFPhysicsShape instance into a [Dictionary] in the format defined by [code]OMI_physics_shape[/code].
</description>
</method>
<method name="to_node">
diff --git a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp
index 2ba5123c31..37b8ae0634 100644
--- a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp
+++ b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp
@@ -34,13 +34,26 @@
// Import process.
Error GLTFDocumentExtensionPhysics::import_preflight(Ref<GLTFState> p_state, Vector<String> p_extensions) {
- if (!p_extensions.has("OMI_collider") && !p_extensions.has("OMI_physics_body")) {
+ if (!p_extensions.has("OMI_collider") && !p_extensions.has("OMI_physics_body") && !p_extensions.has("OMI_physics_shape")) {
return ERR_SKIP;
}
Dictionary state_json = p_state->get_json();
if (state_json.has("extensions")) {
Dictionary state_extensions = state_json["extensions"];
- if (state_extensions.has("OMI_collider")) {
+ if (state_extensions.has("OMI_physics_shape")) {
+ Dictionary omi_physics_shape_ext = state_extensions["OMI_physics_shape"];
+ if (omi_physics_shape_ext.has("shapes")) {
+ Array state_shape_dicts = omi_physics_shape_ext["shapes"];
+ if (state_shape_dicts.size() > 0) {
+ Array state_shapes;
+ for (int i = 0; i < state_shape_dicts.size(); i++) {
+ state_shapes.push_back(GLTFPhysicsShape::from_dictionary(state_shape_dicts[i]));
+ }
+ p_state->set_additional_data(StringName("GLTFPhysicsShapes"), state_shapes);
+ }
+ }
+#ifndef DISABLE_DEPRECATED
+ } else if (state_extensions.has("OMI_collider")) {
Dictionary omi_collider_ext = state_extensions["OMI_collider"];
if (omi_collider_ext.has("colliders")) {
Array state_collider_dicts = omi_collider_ext["colliders"];
@@ -49,9 +62,10 @@ Error GLTFDocumentExtensionPhysics::import_preflight(Ref<GLTFState> p_state, Vec
for (int i = 0; i < state_collider_dicts.size(); i++) {
state_colliders.push_back(GLTFPhysicsShape::from_dictionary(state_collider_dicts[i]));
}
- p_state->set_additional_data("GLTFPhysicsShapes", state_colliders);
+ p_state->set_additional_data(StringName("GLTFPhysicsShapes"), state_colliders);
}
}
+#endif // DISABLE_DEPRECATED
}
}
return OK;
@@ -61,49 +75,87 @@ Vector<String> GLTFDocumentExtensionPhysics::get_supported_extensions() {
Vector<String> ret;
ret.push_back("OMI_collider");
ret.push_back("OMI_physics_body");
+ ret.push_back("OMI_physics_shape");
return ret;
}
Error GLTFDocumentExtensionPhysics::parse_node_extensions(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &p_extensions) {
+#ifndef DISABLE_DEPRECATED
if (p_extensions.has("OMI_collider")) {
Dictionary node_collider_ext = p_extensions["OMI_collider"];
if (node_collider_ext.has("collider")) {
// "collider" is the index of the collider in the state colliders array.
int node_collider_index = node_collider_ext["collider"];
- Array state_colliders = p_state->get_additional_data("GLTFPhysicsShapes");
+ Array state_colliders = p_state->get_additional_data(StringName("GLTFPhysicsShapes"));
ERR_FAIL_INDEX_V_MSG(node_collider_index, state_colliders.size(), Error::ERR_FILE_CORRUPT, "GLTF Physics: On node " + p_gltf_node->get_name() + ", the collider index " + itos(node_collider_index) + " is not in the state colliders (size: " + itos(state_colliders.size()) + ").");
p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), state_colliders[node_collider_index]);
} else {
- p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), GLTFPhysicsShape::from_dictionary(p_extensions["OMI_collider"]));
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), GLTFPhysicsShape::from_dictionary(node_collider_ext));
}
}
+#endif // DISABLE_DEPRECATED
if (p_extensions.has("OMI_physics_body")) {
- p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_dictionary(p_extensions["OMI_physics_body"]));
+ Dictionary physics_body_ext = p_extensions["OMI_physics_body"];
+ if (physics_body_ext.has("collider")) {
+ Dictionary node_collider = physics_body_ext["collider"];
+ // "shape" is the index of the shape in the state shapes array.
+ int node_shape_index = node_collider.get("shape", -1);
+ if (node_shape_index != -1) {
+ Array state_shapes = p_state->get_additional_data(StringName("GLTFPhysicsShapes"));
+ ERR_FAIL_INDEX_V_MSG(node_shape_index, state_shapes.size(), Error::ERR_FILE_CORRUPT, "GLTF Physics: On node " + p_gltf_node->get_name() + ", the shape index " + itos(node_shape_index) + " is not in the state shapes (size: " + itos(state_shapes.size()) + ").");
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShape"), state_shapes[node_shape_index]);
+ } else {
+ // If this node is a collider but does not have a collider
+ // shape, then it only serves to combine together shapes.
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsCompoundCollider"), true);
+ }
+ }
+ if (physics_body_ext.has("trigger")) {
+ Dictionary node_trigger = physics_body_ext["trigger"];
+ // "shape" is the index of the shape in the state shapes array.
+ int node_shape_index = node_trigger.get("shape", -1);
+ if (node_shape_index != -1) {
+ Array state_shapes = p_state->get_additional_data(StringName("GLTFPhysicsShapes"));
+ ERR_FAIL_INDEX_V_MSG(node_shape_index, state_shapes.size(), Error::ERR_FILE_CORRUPT, "GLTF Physics: On node " + p_gltf_node->get_name() + ", the shape index " + itos(node_shape_index) + " is not in the state shapes (size: " + itos(state_shapes.size()) + ").");
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShape"), state_shapes[node_shape_index]);
+ } else {
+ // If this node is a trigger but does not have a trigger shape,
+ // then it's a trigger body, what Godot calls an Area3D node.
+ Ref<GLTFPhysicsBody> trigger_body;
+ trigger_body.instantiate();
+ trigger_body->set_body_type("trigger");
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), trigger_body);
+ }
+ }
+ if (physics_body_ext.has("motion") || physics_body_ext.has("type")) {
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_dictionary(physics_body_ext));
+ }
}
return OK;
}
-void _setup_collider_mesh_resource_from_index_if_needed(Ref<GLTFState> p_state, Ref<GLTFPhysicsShape> p_collider) {
- GLTFMeshIndex collider_mesh_index = p_collider->get_mesh_index();
- if (collider_mesh_index == -1) {
- return; // No mesh for this collider.
+void _setup_shape_mesh_resource_from_index_if_needed(Ref<GLTFState> p_state, Ref<GLTFPhysicsShape> p_gltf_shape) {
+ GLTFMeshIndex shape_mesh_index = p_gltf_shape->get_mesh_index();
+ if (shape_mesh_index == -1) {
+ return; // No mesh for this shape.
}
- Ref<ImporterMesh> importer_mesh = p_collider->get_importer_mesh();
+ Ref<ImporterMesh> importer_mesh = p_gltf_shape->get_importer_mesh();
if (importer_mesh.is_valid()) {
return; // The mesh resource is already set up.
}
TypedArray<GLTFMesh> state_meshes = p_state->get_meshes();
- ERR_FAIL_INDEX_MSG(collider_mesh_index, state_meshes.size(), "GLTF Physics: When importing '" + p_state->get_scene_name() + "', the collider mesh index " + itos(collider_mesh_index) + " is not in the state meshes (size: " + itos(state_meshes.size()) + ").");
- Ref<GLTFMesh> gltf_mesh = state_meshes[collider_mesh_index];
+ ERR_FAIL_INDEX_MSG(shape_mesh_index, state_meshes.size(), "GLTF Physics: When importing '" + p_state->get_scene_name() + "', the shape mesh index " + itos(shape_mesh_index) + " is not in the state meshes (size: " + itos(state_meshes.size()) + ").");
+ Ref<GLTFMesh> gltf_mesh = state_meshes[shape_mesh_index];
ERR_FAIL_COND(gltf_mesh.is_null());
importer_mesh = gltf_mesh->get_mesh();
ERR_FAIL_COND(importer_mesh.is_null());
- p_collider->set_importer_mesh(importer_mesh);
+ p_gltf_shape->set_importer_mesh(importer_mesh);
}
-CollisionObject3D *_generate_collision_with_body(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Ref<GLTFPhysicsShape> p_collider, Ref<GLTFPhysicsBody> p_physics_body) {
- print_verbose("glTF: Creating collision for: " + p_gltf_node->get_name());
- bool is_trigger = p_collider->get_is_trigger();
+#ifndef DISABLE_DEPRECATED
+CollisionObject3D *_generate_shape_with_body(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Ref<GLTFPhysicsShape> p_physics_shape, Ref<GLTFPhysicsBody> p_physics_body) {
+ print_verbose("glTF: Creating shape with body for: " + p_gltf_node->get_name());
+ bool is_trigger = p_physics_shape->get_is_trigger();
// This method is used for the case where we must generate a parent body.
// This is can happen for multiple reasons. One possibility is that this
// GLTF file is using OMI_collider but not OMI_physics_body, or at least
@@ -113,10 +165,10 @@ CollisionObject3D *_generate_collision_with_body(Ref<GLTFState> p_state, Ref<GLT
if (p_physics_body.is_valid()) {
// This code is run when the physics body is on the same GLTF node.
body = p_physics_body->to_node();
- if (is_trigger != (p_physics_body->get_body_type() == "trigger")) {
+ if (is_trigger && (p_physics_body->get_body_type() != "trigger")) {
// Edge case: If the body's trigger and the collider's trigger
// are in disagreement, we need to create another new body.
- CollisionObject3D *child = _generate_collision_with_body(p_state, p_gltf_node, p_collider, nullptr);
+ CollisionObject3D *child = _generate_shape_with_body(p_state, p_gltf_node, p_physics_shape, nullptr);
child->set_name(p_gltf_node->get_name() + (is_trigger ? String("Trigger") : String("Solid")));
body->add_child(child);
return body;
@@ -126,33 +178,131 @@ CollisionObject3D *_generate_collision_with_body(Ref<GLTFState> p_state, Ref<GLT
} else {
body = memnew(StaticBody3D);
}
- CollisionShape3D *shape = p_collider->to_node();
+ CollisionShape3D *shape = p_physics_shape->to_node();
shape->set_name(p_gltf_node->get_name() + "Shape");
body->add_child(shape);
return body;
}
+#endif // DISABLE_DEPRECATED
+
+CollisionObject3D *_get_ancestor_collision_object(Node *p_scene_parent) {
+ // Note: Despite the name of the method, at the moment this only checks
+ // the direct parent. Only check more later if Godot adds support for it.
+ if (p_scene_parent) {
+ CollisionObject3D *co = Object::cast_to<CollisionObject3D>(p_scene_parent);
+ if (likely(co)) {
+ return co;
+ }
+ }
+ return nullptr;
+}
+
+Node3D *_generate_shape_node_and_body_if_needed(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Ref<GLTFPhysicsShape> p_physics_shape, CollisionObject3D *p_col_object, bool p_is_trigger) {
+ // If we need to generate a body node, do so.
+ CollisionObject3D *body_node = nullptr;
+ if (p_is_trigger || p_physics_shape->get_is_trigger()) {
+ // If the shape wants to be a trigger but it doesn't
+ // have an Area3D parent, we need to make one.
+ if (!Object::cast_to<Area3D>(p_col_object)) {
+ body_node = memnew(Area3D);
+ }
+ } else {
+ if (!Object::cast_to<PhysicsBody3D>(p_col_object)) {
+ body_node = memnew(StaticBody3D);
+ }
+ }
+ // Generate the shape node.
+ _setup_shape_mesh_resource_from_index_if_needed(p_state, p_physics_shape);
+ CollisionShape3D *shape_node = p_physics_shape->to_node(true);
+ if (body_node) {
+ shape_node->set_name(p_gltf_node->get_name() + "Shape");
+ body_node->add_child(shape_node);
+ return body_node;
+ }
+ return shape_node;
+}
+
+// Either add the child to the parent, or return the child if there is no parent.
+Node3D *_add_physics_node_to_given_node(Node3D *p_current_node, Node3D *p_child, Ref<GLTFNode> p_gltf_node) {
+ if (!p_current_node) {
+ return p_child;
+ }
+ String suffix;
+ if (Object::cast_to<CollisionShape3D>(p_child)) {
+ suffix = "Shape";
+ } else if (Object::cast_to<Area3D>(p_child)) {
+ suffix = "Trigger";
+ } else {
+ suffix = "Collider";
+ }
+ p_child->set_name(p_gltf_node->get_name() + suffix);
+ p_current_node->add_child(p_child);
+ return p_current_node;
+}
Node3D *GLTFDocumentExtensionPhysics::generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent) {
- Ref<GLTFPhysicsBody> physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody"));
- Ref<GLTFPhysicsShape> collider = p_gltf_node->get_additional_data(StringName("GLTFPhysicsShape"));
- if (collider.is_valid()) {
- _setup_collider_mesh_resource_from_index_if_needed(p_state, collider);
- // If the collider has the correct type of parent, we just return one node.
- if (collider->get_is_trigger()) {
- if (Object::cast_to<Area3D>(p_scene_parent)) {
- return collider->to_node(true);
+ Ref<GLTFPhysicsBody> gltf_physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody"));
+#ifndef DISABLE_DEPRECATED
+ // This deprecated code handles OMI_collider (which we internally name "GLTFPhysicsShape").
+ Ref<GLTFPhysicsShape> gltf_physics_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsShape"));
+ if (gltf_physics_shape.is_valid()) {
+ _setup_shape_mesh_resource_from_index_if_needed(p_state, gltf_physics_shape);
+ // If this GLTF node specifies both a shape and a body, generate both.
+ if (gltf_physics_body.is_valid()) {
+ return _generate_shape_with_body(p_state, p_gltf_node, gltf_physics_shape, gltf_physics_body);
+ }
+ CollisionObject3D *ancestor_col_obj = _get_ancestor_collision_object(p_scene_parent);
+ if (gltf_physics_shape->get_is_trigger()) {
+ // If the shape wants to be a trigger and it already has a
+ // trigger parent, we only need to make the shape node.
+ if (Object::cast_to<Area3D>(ancestor_col_obj)) {
+ return gltf_physics_shape->to_node(true);
}
- } else {
- if (Object::cast_to<PhysicsBody3D>(p_scene_parent)) {
- return collider->to_node(true);
+ } else if (ancestor_col_obj != nullptr) {
+ // If the shape has a valid parent, only make the shape node.
+ return gltf_physics_shape->to_node(true);
+ }
+ // Otherwise, we need to create a new body.
+ return _generate_shape_with_body(p_state, p_gltf_node, gltf_physics_shape, nullptr);
+ }
+#endif // DISABLE_DEPRECATED
+ Node3D *ret = nullptr;
+ CollisionObject3D *ancestor_col_obj = nullptr;
+ if (gltf_physics_body.is_valid()) {
+ ancestor_col_obj = gltf_physics_body->to_node();
+ ret = ancestor_col_obj;
+ } else {
+ ancestor_col_obj = _get_ancestor_collision_object(p_scene_parent);
+ if (!Object::cast_to<PhysicsBody3D>(ancestor_col_obj)) {
+ if (p_gltf_node->get_additional_data(StringName("GLTFPhysicsCompoundCollider"))) {
+ // If the GLTF file wants this node to group solid shapes together,
+ // and there is no parent body, we need to create a static body.
+ ancestor_col_obj = memnew(StaticBody3D);
+ ret = ancestor_col_obj;
}
}
- return _generate_collision_with_body(p_state, p_gltf_node, collider, physics_body);
}
- if (physics_body.is_valid()) {
- return physics_body->to_node();
+ // Add the shapes to the tree. When an ancestor body is present, use it.
+ // If an explicit body was specified, it has already been generated and
+ // set above. If there is no ancestor body, we will either generate an
+ // Area3D or StaticBody3D implicitly, so prefer an Area3D as the base
+ // node for best compatibility with signal connections to this node.
+ Ref<GLTFPhysicsShape> gltf_physics_collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape"));
+ Ref<GLTFPhysicsShape> gltf_physics_trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape"));
+ bool is_ancestor_col_obj_solid = Object::cast_to<PhysicsBody3D>(ancestor_col_obj);
+ if (is_ancestor_col_obj_solid && gltf_physics_collider_shape.is_valid()) {
+ Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_collider_shape, ancestor_col_obj, false);
+ ret = _add_physics_node_to_given_node(ret, child, p_gltf_node);
}
- return nullptr;
+ if (gltf_physics_trigger_shape.is_valid()) {
+ Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_trigger_shape, ancestor_col_obj, true);
+ ret = _add_physics_node_to_given_node(ret, child, p_gltf_node);
+ }
+ if (!is_ancestor_col_obj_solid && gltf_physics_collider_shape.is_valid()) {
+ Node3D *child = _generate_shape_node_and_body_if_needed(p_state, p_gltf_node, gltf_physics_collider_shape, ancestor_col_obj, false);
+ ret = _add_physics_node_to_given_node(ret, child, p_gltf_node);
+ }
+ return ret;
}
// Export process.
@@ -202,22 +352,26 @@ GLTFMeshIndex _get_or_insert_mesh_in_state(Ref<GLTFState> p_state, Ref<ImporterM
void GLTFDocumentExtensionPhysics::convert_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_node) {
if (cast_to<CollisionShape3D>(p_scene_node)) {
- CollisionShape3D *shape = Object::cast_to<CollisionShape3D>(p_scene_node);
- Ref<GLTFPhysicsShape> collider = GLTFPhysicsShape::from_node(shape);
+ CollisionShape3D *godot_shape = Object::cast_to<CollisionShape3D>(p_scene_node);
+ Ref<GLTFPhysicsShape> gltf_shape = GLTFPhysicsShape::from_node(godot_shape);
{
- Ref<ImporterMesh> importer_mesh = collider->get_importer_mesh();
+ Ref<ImporterMesh> importer_mesh = gltf_shape->get_importer_mesh();
if (importer_mesh.is_valid()) {
- collider->set_mesh_index(_get_or_insert_mesh_in_state(p_state, importer_mesh));
+ gltf_shape->set_mesh_index(_get_or_insert_mesh_in_state(p_state, importer_mesh));
}
}
- p_gltf_node->set_additional_data(StringName("GLTFPhysicsShape"), collider);
+ if (cast_to<Area3D>(_get_ancestor_collision_object(p_scene_node))) {
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsTriggerShape"), gltf_shape);
+ } else {
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsColliderShape"), gltf_shape);
+ }
} else if (cast_to<CollisionObject3D>(p_scene_node)) {
- CollisionObject3D *body = Object::cast_to<CollisionObject3D>(p_scene_node);
- p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_node(body));
+ CollisionObject3D *godot_body = Object::cast_to<CollisionObject3D>(p_scene_node);
+ p_gltf_node->set_additional_data(StringName("GLTFPhysicsBody"), GLTFPhysicsBody::from_node(godot_body));
}
}
-Array _get_or_create_state_colliders_in_state(Ref<GLTFState> p_state) {
+Array _get_or_create_state_shapes_in_state(Ref<GLTFState> p_state) {
Dictionary state_json = p_state->get_json();
Dictionary state_extensions;
if (state_json.has("extensions")) {
@@ -225,48 +379,60 @@ Array _get_or_create_state_colliders_in_state(Ref<GLTFState> p_state) {
} else {
state_json["extensions"] = state_extensions;
}
- Dictionary omi_collider_ext;
- if (state_extensions.has("OMI_collider")) {
- omi_collider_ext = state_extensions["OMI_collider"];
+ Dictionary omi_physics_shape_ext;
+ if (state_extensions.has("OMI_physics_shape")) {
+ omi_physics_shape_ext = state_extensions["OMI_physics_shape"];
} else {
- state_extensions["OMI_collider"] = omi_collider_ext;
- p_state->add_used_extension("OMI_collider");
+ state_extensions["OMI_physics_shape"] = omi_physics_shape_ext;
+ p_state->add_used_extension("OMI_physics_shape");
}
- Array state_colliders;
- if (omi_collider_ext.has("colliders")) {
- state_colliders = omi_collider_ext["colliders"];
+ Array state_shapes;
+ if (omi_physics_shape_ext.has("shapes")) {
+ state_shapes = omi_physics_shape_ext["shapes"];
} else {
- omi_collider_ext["colliders"] = state_colliders;
+ omi_physics_shape_ext["shapes"] = state_shapes;
+ }
+ return state_shapes;
+}
+
+Dictionary _export_node_shape(Ref<GLTFState> p_state, Ref<GLTFPhysicsShape> p_physics_shape) {
+ Array state_shapes = _get_or_create_state_shapes_in_state(p_state);
+ int size = state_shapes.size();
+ Dictionary shape_property;
+ Dictionary shape_dict = p_physics_shape->to_dictionary();
+ for (int i = 0; i < size; i++) {
+ Dictionary other = state_shapes[i];
+ if (other == shape_dict) {
+ // De-duplication: If we already have an identical shape,
+ // set the shape index to the existing one and return.
+ shape_property["shape"] = i;
+ return shape_property;
+ }
}
- return state_colliders;
+ // If we don't have an identical shape, add it to the array.
+ state_shapes.push_back(shape_dict);
+ shape_property["shape"] = size;
+ return shape_property;
}
Error GLTFDocumentExtensionPhysics::export_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_node_json, Node *p_node) {
- Dictionary node_extensions = r_node_json["extensions"];
+ Dictionary physics_body_ext;
Ref<GLTFPhysicsBody> physics_body = p_gltf_node->get_additional_data(StringName("GLTFPhysicsBody"));
if (physics_body.is_valid()) {
- node_extensions["OMI_physics_body"] = physics_body->to_dictionary();
- p_state->add_used_extension("OMI_physics_body");
+ physics_body_ext = physics_body->to_dictionary();
}
- Ref<GLTFPhysicsShape> collider = p_gltf_node->get_additional_data(StringName("GLTFPhysicsShape"));
- if (collider.is_valid()) {
- Array state_colliders = _get_or_create_state_colliders_in_state(p_state);
- int size = state_colliders.size();
- Dictionary omi_collider_ext;
- node_extensions["OMI_collider"] = omi_collider_ext;
- Dictionary collider_dict = collider->to_dictionary();
- for (int i = 0; i < size; i++) {
- Dictionary other = state_colliders[i];
- if (other == collider_dict) {
- // De-duplication: If we already have an identical collider,
- // set the collider index to the existing one and return.
- omi_collider_ext["collider"] = i;
- return OK;
- }
- }
- // If we don't have an identical collider, add it to the array.
- state_colliders.push_back(collider_dict);
- omi_collider_ext["collider"] = size;
+ Ref<GLTFPhysicsShape> collider_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsColliderShape"));
+ if (collider_shape.is_valid()) {
+ physics_body_ext["collider"] = _export_node_shape(p_state, collider_shape);
+ }
+ Ref<GLTFPhysicsShape> trigger_shape = p_gltf_node->get_additional_data(StringName("GLTFPhysicsTriggerShape"));
+ if (trigger_shape.is_valid()) {
+ physics_body_ext["trigger"] = _export_node_shape(p_state, trigger_shape);
+ }
+ if (!physics_body_ext.is_empty()) {
+ Dictionary node_extensions = r_node_json["extensions"];
+ node_extensions["OMI_physics_body"] = physics_body_ext;
+ p_state->add_used_extension("OMI_physics_body");
}
return OK;
}
diff --git a/modules/gltf/extensions/physics/gltf_physics_body.cpp b/modules/gltf/extensions/physics/gltf_physics_body.cpp
index b80f4348c2..271bb9b332 100644
--- a/modules/gltf/extensions/physics/gltf_physics_body.cpp
+++ b/modules/gltf/extensions/physics/gltf_physics_body.cpp
@@ -50,22 +50,70 @@ void GLTFPhysicsBody::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_angular_velocity", "angular_velocity"), &GLTFPhysicsBody::set_angular_velocity);
ClassDB::bind_method(D_METHOD("get_center_of_mass"), &GLTFPhysicsBody::get_center_of_mass);
ClassDB::bind_method(D_METHOD("set_center_of_mass", "center_of_mass"), &GLTFPhysicsBody::set_center_of_mass);
+ ClassDB::bind_method(D_METHOD("get_inertia_diagonal"), &GLTFPhysicsBody::get_inertia_diagonal);
+ ClassDB::bind_method(D_METHOD("set_inertia_diagonal", "inertia_diagonal"), &GLTFPhysicsBody::set_inertia_diagonal);
+ ClassDB::bind_method(D_METHOD("get_inertia_orientation"), &GLTFPhysicsBody::get_inertia_orientation);
+ ClassDB::bind_method(D_METHOD("set_inertia_orientation", "inertia_orientation"), &GLTFPhysicsBody::set_inertia_orientation);
+#ifndef DISABLE_DEPRECATED
ClassDB::bind_method(D_METHOD("get_inertia_tensor"), &GLTFPhysicsBody::get_inertia_tensor);
ClassDB::bind_method(D_METHOD("set_inertia_tensor", "inertia_tensor"), &GLTFPhysicsBody::set_inertia_tensor);
+#endif // DISABLE_DEPRECATED
ADD_PROPERTY(PropertyInfo(Variant::STRING, "body_type"), "set_body_type", "get_body_type");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mass"), "set_mass", "get_mass");
ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "linear_velocity"), "set_linear_velocity", "get_linear_velocity");
ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "angular_velocity"), "set_angular_velocity", "get_angular_velocity");
ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "center_of_mass"), "set_center_of_mass", "get_center_of_mass");
+ ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "inertia_diagonal"), "set_inertia_diagonal", "get_inertia_diagonal");
+ ADD_PROPERTY(PropertyInfo(Variant::QUATERNION, "inertia_orientation"), "set_inertia_orientation", "get_inertia_orientation");
+#ifndef DISABLE_DEPRECATED
ADD_PROPERTY(PropertyInfo(Variant::BASIS, "inertia_tensor"), "set_inertia_tensor", "get_inertia_tensor");
+#endif // DISABLE_DEPRECATED
}
String GLTFPhysicsBody::get_body_type() const {
- return body_type;
+ switch (body_type) {
+ case PhysicsBodyType::STATIC:
+ return "static";
+ case PhysicsBodyType::ANIMATABLE:
+ return "animatable";
+ case PhysicsBodyType::CHARACTER:
+ return "character";
+ case PhysicsBodyType::RIGID:
+ return "rigid";
+ case PhysicsBodyType::VEHICLE:
+ return "vehicle";
+ case PhysicsBodyType::TRIGGER:
+ return "trigger";
+ }
+ // Unreachable, the switch cases handle all values the enum can take.
+ // Omitting this works on Clang but not GCC or MSVC. If reached, it's UB.
+ return "rigid";
}
void GLTFPhysicsBody::set_body_type(String p_body_type) {
+ if (p_body_type == "static") {
+ body_type = PhysicsBodyType::STATIC;
+ } else if (p_body_type == "animatable") {
+ body_type = PhysicsBodyType::ANIMATABLE;
+ } else if (p_body_type == "character") {
+ body_type = PhysicsBodyType::CHARACTER;
+ } else if (p_body_type == "rigid") {
+ body_type = PhysicsBodyType::RIGID;
+ } else if (p_body_type == "vehicle") {
+ body_type = PhysicsBodyType::VEHICLE;
+ } else if (p_body_type == "trigger") {
+ body_type = PhysicsBodyType::TRIGGER;
+ } else {
+ ERR_PRINT("Error setting GLTF physics body type: The body type must be one of \"static\", \"animatable\", \"character\", \"rigid\", \"vehicle\", or \"trigger\".");
+ }
+}
+
+GLTFPhysicsBody::PhysicsBodyType GLTFPhysicsBody::get_physics_body_type() const {
+ return body_type;
+}
+
+void GLTFPhysicsBody::set_physics_body_type(PhysicsBodyType p_body_type) {
body_type = p_body_type;
}
@@ -101,140 +149,215 @@ void GLTFPhysicsBody::set_center_of_mass(const Vector3 &p_center_of_mass) {
center_of_mass = p_center_of_mass;
}
+Vector3 GLTFPhysicsBody::get_inertia_diagonal() const {
+ return inertia_diagonal;
+}
+
+void GLTFPhysicsBody::set_inertia_diagonal(const Vector3 &p_inertia_diagonal) {
+ inertia_diagonal = p_inertia_diagonal;
+}
+
+Quaternion GLTFPhysicsBody::get_inertia_orientation() const {
+ return inertia_orientation;
+}
+
+void GLTFPhysicsBody::set_inertia_orientation(const Quaternion &p_inertia_orientation) {
+ inertia_orientation = p_inertia_orientation;
+}
+
+#ifndef DISABLE_DEPRECATED
Basis GLTFPhysicsBody::get_inertia_tensor() const {
- return inertia_tensor;
+ return Basis::from_scale(inertia_diagonal);
}
void GLTFPhysicsBody::set_inertia_tensor(Basis p_inertia_tensor) {
- inertia_tensor = p_inertia_tensor;
+ inertia_diagonal = p_inertia_tensor.get_main_diagonal();
}
+#endif // DISABLE_DEPRECATED
Ref<GLTFPhysicsBody> GLTFPhysicsBody::from_node(const CollisionObject3D *p_body_node) {
Ref<GLTFPhysicsBody> physics_body;
physics_body.instantiate();
ERR_FAIL_NULL_V_MSG(p_body_node, physics_body, "Tried to create a GLTFPhysicsBody from a CollisionObject3D node, but the given node was null.");
if (cast_to<CharacterBody3D>(p_body_node)) {
- physics_body->body_type = "character";
+ physics_body->body_type = PhysicsBodyType::CHARACTER;
} else if (cast_to<AnimatableBody3D>(p_body_node)) {
- physics_body->body_type = "kinematic";
+ physics_body->body_type = PhysicsBodyType::ANIMATABLE;
} else if (cast_to<RigidBody3D>(p_body_node)) {
const RigidBody3D *body = cast_to<const RigidBody3D>(p_body_node);
physics_body->mass = body->get_mass();
physics_body->linear_velocity = body->get_linear_velocity();
physics_body->angular_velocity = body->get_angular_velocity();
physics_body->center_of_mass = body->get_center_of_mass();
- Vector3 inertia_diagonal = body->get_inertia();
- physics_body->inertia_tensor = Basis(inertia_diagonal.x, 0, 0, 0, inertia_diagonal.y, 0, 0, 0, inertia_diagonal.z);
+ physics_body->inertia_diagonal = body->get_inertia();
if (body->get_center_of_mass() != Vector3()) {
WARN_PRINT("GLTFPhysicsBody: This rigid body has a center of mass offset from the origin, which will be ignored when exporting to GLTF.");
}
if (cast_to<VehicleBody3D>(p_body_node)) {
- physics_body->body_type = "vehicle";
+ physics_body->body_type = PhysicsBodyType::VEHICLE;
} else {
- physics_body->body_type = "rigid";
+ physics_body->body_type = PhysicsBodyType::RIGID;
}
} else if (cast_to<StaticBody3D>(p_body_node)) {
- physics_body->body_type = "static";
+ physics_body->body_type = PhysicsBodyType::STATIC;
} else if (cast_to<Area3D>(p_body_node)) {
- physics_body->body_type = "trigger";
+ physics_body->body_type = PhysicsBodyType::TRIGGER;
}
return physics_body;
}
CollisionObject3D *GLTFPhysicsBody::to_node() const {
- if (body_type == "character") {
- CharacterBody3D *body = memnew(CharacterBody3D);
- return body;
- }
- if (body_type == "kinematic") {
- AnimatableBody3D *body = memnew(AnimatableBody3D);
- return body;
- }
- if (body_type == "vehicle") {
- VehicleBody3D *body = memnew(VehicleBody3D);
- body->set_mass(mass);
- body->set_linear_velocity(linear_velocity);
- body->set_angular_velocity(angular_velocity);
- body->set_inertia(inertia_tensor.get_main_diagonal());
- body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM);
- body->set_center_of_mass(center_of_mass);
- return body;
- }
- if (body_type == "rigid") {
- RigidBody3D *body = memnew(RigidBody3D);
- body->set_mass(mass);
- body->set_linear_velocity(linear_velocity);
- body->set_angular_velocity(angular_velocity);
- body->set_inertia(inertia_tensor.get_main_diagonal());
- body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM);
- body->set_center_of_mass(center_of_mass);
- return body;
- }
- if (body_type == "static") {
- StaticBody3D *body = memnew(StaticBody3D);
- return body;
- }
- if (body_type == "trigger") {
- Area3D *body = memnew(Area3D);
- return body;
+ switch (body_type) {
+ case PhysicsBodyType::CHARACTER: {
+ CharacterBody3D *body = memnew(CharacterBody3D);
+ return body;
+ }
+ case PhysicsBodyType::ANIMATABLE: {
+ AnimatableBody3D *body = memnew(AnimatableBody3D);
+ return body;
+ }
+ case PhysicsBodyType::VEHICLE: {
+ VehicleBody3D *body = memnew(VehicleBody3D);
+ body->set_mass(mass);
+ body->set_linear_velocity(linear_velocity);
+ body->set_angular_velocity(angular_velocity);
+ body->set_inertia(inertia_diagonal);
+ body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM);
+ body->set_center_of_mass(center_of_mass);
+ return body;
+ }
+ case PhysicsBodyType::RIGID: {
+ RigidBody3D *body = memnew(RigidBody3D);
+ body->set_mass(mass);
+ body->set_linear_velocity(linear_velocity);
+ body->set_angular_velocity(angular_velocity);
+ body->set_inertia(inertia_diagonal);
+ body->set_center_of_mass_mode(RigidBody3D::CENTER_OF_MASS_MODE_CUSTOM);
+ body->set_center_of_mass(center_of_mass);
+ return body;
+ }
+ case PhysicsBodyType::STATIC: {
+ StaticBody3D *body = memnew(StaticBody3D);
+ return body;
+ }
+ case PhysicsBodyType::TRIGGER: {
+ Area3D *body = memnew(Area3D);
+ return body;
+ }
}
- ERR_FAIL_V_MSG(nullptr, "Error converting GLTFPhysicsBody to a node: Body type '" + body_type + "' is unknown.");
+ // Unreachable, the switch cases handle all values the enum can take.
+ // Omitting this works on Clang but not GCC or MSVC. If reached, it's UB.
+ return nullptr;
}
Ref<GLTFPhysicsBody> GLTFPhysicsBody::from_dictionary(const Dictionary p_dictionary) {
Ref<GLTFPhysicsBody> physics_body;
physics_body.instantiate();
- ERR_FAIL_COND_V_MSG(!p_dictionary.has("type"), physics_body, "Failed to parse GLTF physics body, missing required field 'type'.");
- const String &body_type = p_dictionary["type"];
- physics_body->body_type = body_type;
-
- if (p_dictionary.has("mass")) {
- physics_body->mass = p_dictionary["mass"];
+ Dictionary motion;
+ if (p_dictionary.has("motion")) {
+ motion = p_dictionary["motion"];
+#ifndef DISABLE_DEPRECATED
+ } else {
+ motion = p_dictionary;
+#endif // DISABLE_DEPRECATED
}
- if (p_dictionary.has("linearVelocity")) {
- const Array &arr = p_dictionary["linearVelocity"];
+ if (motion.has("type")) {
+ // Read the body type. This representation sits between glTF's and Godot's physics nodes.
+ // While we may only read "static", "kinematic", or "dynamic" from a valid glTF file, we
+ // want to allow another extension to override this to another Godot node type mid-import.
+ // For example, a vehicle extension may want to override the body type to "vehicle"
+ // so Godot generates a VehicleBody3D node. Therefore we distinguish by importing
+ // "dynamic" as "rigid", and "kinematic" as "animatable", in the GLTFPhysicsBody code.
+ String body_type_string = motion["type"];
+ if (body_type_string == "static") {
+ physics_body->body_type = PhysicsBodyType::STATIC;
+ } else if (body_type_string == "kinematic") {
+ physics_body->body_type = PhysicsBodyType::ANIMATABLE;
+ } else if (body_type_string == "dynamic") {
+ physics_body->body_type = PhysicsBodyType::RIGID;
+#ifndef DISABLE_DEPRECATED
+ } else if (body_type_string == "character") {
+ physics_body->body_type = PhysicsBodyType::CHARACTER;
+ } else if (body_type_string == "rigid") {
+ physics_body->body_type = PhysicsBodyType::RIGID;
+ } else if (body_type_string == "vehicle") {
+ physics_body->body_type = PhysicsBodyType::VEHICLE;
+ } else if (body_type_string == "trigger") {
+ physics_body->body_type = PhysicsBodyType::TRIGGER;
+#endif // DISABLE_DEPRECATED
+ } else {
+ ERR_PRINT("Error parsing GLTF physics body: The body type in the GLTF file \"" + body_type_string + "\" was not recognized.");
+ }
+ }
+ if (motion.has("mass")) {
+ physics_body->mass = motion["mass"];
+ }
+ if (motion.has("linearVelocity")) {
+ const Array &arr = motion["linearVelocity"];
if (arr.size() == 3) {
physics_body->set_linear_velocity(Vector3(arr[0], arr[1], arr[2]));
} else {
ERR_PRINT("Error parsing GLTF physics body: The linear velocity vector must have exactly 3 numbers.");
}
}
- if (p_dictionary.has("angularVelocity")) {
- const Array &arr = p_dictionary["angularVelocity"];
+ if (motion.has("angularVelocity")) {
+ const Array &arr = motion["angularVelocity"];
if (arr.size() == 3) {
physics_body->set_angular_velocity(Vector3(arr[0], arr[1], arr[2]));
} else {
ERR_PRINT("Error parsing GLTF physics body: The angular velocity vector must have exactly 3 numbers.");
}
}
- if (p_dictionary.has("centerOfMass")) {
- const Array &arr = p_dictionary["centerOfMass"];
+ if (motion.has("centerOfMass")) {
+ const Array &arr = motion["centerOfMass"];
if (arr.size() == 3) {
physics_body->set_center_of_mass(Vector3(arr[0], arr[1], arr[2]));
} else {
ERR_PRINT("Error parsing GLTF physics body: The center of mass vector must have exactly 3 numbers.");
}
}
- if (p_dictionary.has("inertiaTensor")) {
- const Array &arr = p_dictionary["inertiaTensor"];
- if (arr.size() == 9) {
- // Only use the diagonal elements of the inertia tensor matrix (principal axes).
- physics_body->set_inertia_tensor(Basis(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7], arr[8]));
+ if (motion.has("inertiaDiagonal")) {
+ const Array &arr = motion["inertiaDiagonal"];
+ if (arr.size() == 3) {
+ physics_body->set_inertia_diagonal(Vector3(arr[0], arr[1], arr[2]));
} else {
- ERR_PRINT("Error parsing GLTF physics body: The inertia tensor must be a 3x3 matrix (9 number array).");
+ ERR_PRINT("Error parsing GLTF physics body: The inertia diagonal vector must have exactly 3 numbers.");
}
}
- if (body_type != "character" && body_type != "kinematic" && body_type != "rigid" && body_type != "static" && body_type != "trigger" && body_type != "vehicle") {
- ERR_PRINT("Error parsing GLTF physics body: Body type '" + body_type + "' is unknown.");
+ if (motion.has("inertiaOrientation")) {
+ const Array &arr = motion["inertiaOrientation"];
+ if (arr.size() == 4) {
+ physics_body->set_inertia_orientation(Quaternion(arr[0], arr[1], arr[2], arr[3]));
+ } else {
+ ERR_PRINT("Error parsing GLTF physics body: The inertia orientation quaternion must have exactly 4 numbers.");
+ }
}
return physics_body;
}
Dictionary GLTFPhysicsBody::to_dictionary() const {
- Dictionary d;
- d["type"] = body_type;
+ Dictionary ret;
+ if (body_type == PhysicsBodyType::TRIGGER) {
+ // The equivalent of a Godot Area3D node in glTF is a node that
+ // defines that it is a trigger, but does not have a shape.
+ Dictionary trigger;
+ ret["trigger"] = trigger;
+ return ret;
+ }
+ // All non-trigger body types are defined using the motion property.
+ Dictionary motion;
+ // When stored in memory, the body type can correspond to a Godot
+ // node type. However, when exporting to glTF, we need to squash
+ // this down to one of "static", "kinematic", or "dynamic".
+ if (body_type == PhysicsBodyType::STATIC) {
+ motion["type"] = "static";
+ } else if (body_type == PhysicsBodyType::ANIMATABLE || body_type == PhysicsBodyType::CHARACTER) {
+ motion["type"] = "kinematic";
+ } else {
+ motion["type"] = "dynamic";
+ }
if (mass != 1.0) {
- d["mass"] = mass;
+ motion["mass"] = mass;
}
if (linear_velocity != Vector3()) {
Array velocity_array;
@@ -242,7 +365,7 @@ Dictionary GLTFPhysicsBody::to_dictionary() const {
velocity_array[0] = linear_velocity.x;
velocity_array[1] = linear_velocity.y;
velocity_array[2] = linear_velocity.z;
- d["linearVelocity"] = velocity_array;
+ motion["linearVelocity"] = velocity_array;
}
if (angular_velocity != Vector3()) {
Array velocity_array;
@@ -250,7 +373,7 @@ Dictionary GLTFPhysicsBody::to_dictionary() const {
velocity_array[0] = angular_velocity.x;
velocity_array[1] = angular_velocity.y;
velocity_array[2] = angular_velocity.z;
- d["angularVelocity"] = velocity_array;
+ motion["angularVelocity"] = velocity_array;
}
if (center_of_mass != Vector3()) {
Array center_of_mass_array;
@@ -258,22 +381,25 @@ Dictionary GLTFPhysicsBody::to_dictionary() const {
center_of_mass_array[0] = center_of_mass.x;
center_of_mass_array[1] = center_of_mass.y;
center_of_mass_array[2] = center_of_mass.z;
- d["centerOfMass"] = center_of_mass_array;
+ motion["centerOfMass"] = center_of_mass_array;
+ }
+ if (inertia_diagonal != Vector3()) {
+ Array inertia_array;
+ inertia_array.resize(3);
+ inertia_array[0] = inertia_diagonal[0];
+ inertia_array[1] = inertia_diagonal[1];
+ inertia_array[2] = inertia_diagonal[2];
+ motion["inertiaDiagonal"] = inertia_array;
}
- if (inertia_tensor != Basis(0, 0, 0, 0, 0, 0, 0, 0, 0)) {
+ if (inertia_orientation != Quaternion()) {
Array inertia_array;
- inertia_array.resize(9);
- inertia_array.fill(0.0);
- inertia_array[0] = inertia_tensor[0][0];
- inertia_array[1] = inertia_tensor[0][1];
- inertia_array[2] = inertia_tensor[0][2];
- inertia_array[3] = inertia_tensor[1][0];
- inertia_array[4] = inertia_tensor[1][1];
- inertia_array[5] = inertia_tensor[1][2];
- inertia_array[6] = inertia_tensor[2][0];
- inertia_array[7] = inertia_tensor[2][1];
- inertia_array[8] = inertia_tensor[2][2];
- d["inertiaTensor"] = inertia_array;
+ inertia_array.resize(4);
+ inertia_array[0] = inertia_orientation[0];
+ inertia_array[1] = inertia_orientation[1];
+ inertia_array[2] = inertia_orientation[2];
+ inertia_array[3] = inertia_orientation[3];
+ motion["inertiaDiagonal"] = inertia_array;
}
- return d;
+ ret["motion"] = motion;
+ return ret;
}
diff --git a/modules/gltf/extensions/physics/gltf_physics_body.h b/modules/gltf/extensions/physics/gltf_physics_body.h
index 391b4b873f..6b21639a7b 100644
--- a/modules/gltf/extensions/physics/gltf_physics_body.h
+++ b/modules/gltf/extensions/physics/gltf_physics_body.h
@@ -33,27 +33,47 @@
#include "scene/3d/physics_body_3d.h"
-// GLTFPhysicsBody is an intermediary between OMI_physics_body and Godot's physics body nodes.
+// GLTFPhysicsBody is an intermediary between Godot's physics body nodes
+// and the OMI_physics_body extension.
// https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_body
class GLTFPhysicsBody : public Resource {
GDCLASS(GLTFPhysicsBody, Resource)
+public:
+ // These values map to Godot's physics body types.
+ // When importing, the body type will be set to the closest match, and
+ // user code can change this to make Godot generate a different node type.
+ // When exporting, this will be squashed down to one of "static",
+ // "kinematic", or "dynamic" motion types, or the "trigger" property.
+ enum class PhysicsBodyType {
+ STATIC,
+ ANIMATABLE,
+ CHARACTER,
+ RIGID,
+ VEHICLE,
+ TRIGGER,
+ };
+
protected:
static void _bind_methods();
private:
- String body_type = "static";
+ PhysicsBodyType body_type = PhysicsBodyType::RIGID;
real_t mass = 1.0;
Vector3 linear_velocity;
Vector3 angular_velocity;
Vector3 center_of_mass;
- Basis inertia_tensor = Basis(0, 0, 0, 0, 0, 0, 0, 0, 0);
+ Vector3 inertia_diagonal;
+ Quaternion inertia_orientation;
public:
String get_body_type() const;
void set_body_type(String p_body_type);
+ PhysicsBodyType get_physics_body_type() const;
+ void set_physics_body_type(PhysicsBodyType p_body_type);
+
real_t get_mass() const;
void set_mass(real_t p_mass);
@@ -66,8 +86,16 @@ public:
Vector3 get_center_of_mass() const;
void set_center_of_mass(const Vector3 &p_center_of_mass);
+ Vector3 get_inertia_diagonal() const;
+ void set_inertia_diagonal(const Vector3 &p_inertia_diagonal);
+
+ Quaternion get_inertia_orientation() const;
+ void set_inertia_orientation(const Quaternion &p_inertia_orientation);
+
+#ifndef DISABLE_DEPRECATED
Basis get_inertia_tensor() const;
void set_inertia_tensor(Basis p_inertia_tensor);
+#endif // DISABLE_DEPRECATED
static Ref<GLTFPhysicsBody> from_node(const CollisionObject3D *p_body_node);
CollisionObject3D *to_node() const;
diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.cpp b/modules/gltf/extensions/physics/gltf_physics_shape.cpp
index d3c56c0da9..af4ac10313 100644
--- a/modules/gltf/extensions/physics/gltf_physics_shape.cpp
+++ b/modules/gltf/extensions/physics/gltf_physics_shape.cpp
@@ -129,16 +129,16 @@ void GLTFPhysicsShape::set_importer_mesh(Ref<ImporterMesh> p_importer_mesh) {
importer_mesh = p_importer_mesh;
}
-Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_collider_node) {
+Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_godot_shape_node) {
Ref<GLTFPhysicsShape> gltf_shape;
gltf_shape.instantiate();
- ERR_FAIL_NULL_V_MSG(p_collider_node, gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node was null.");
- Node *parent = p_collider_node->get_parent();
+ ERR_FAIL_NULL_V_MSG(p_godot_shape_node, gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node was null.");
+ Node *parent = p_godot_shape_node->get_parent();
if (cast_to<const Area3D>(parent)) {
gltf_shape->set_is_trigger(true);
}
// All the code for working with the shape is below this comment.
- Ref<Shape3D> shape_resource = p_collider_node->get_shape();
+ Ref<Shape3D> shape_resource = p_godot_shape_node->get_shape();
ERR_FAIL_COND_V_MSG(shape_resource.is_null(), gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node had a null shape.");
gltf_shape->_shape_cache = shape_resource;
if (cast_to<BoxShape3D>(shape_resource.ptr())) {
@@ -160,7 +160,7 @@ Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_coll
Ref<SphereShape3D> sphere = shape_resource;
gltf_shape->set_radius(sphere->get_radius());
} else if (cast_to<const ConvexPolygonShape3D>(shape_resource.ptr())) {
- gltf_shape->shape_type = "hull";
+ gltf_shape->shape_type = "convex";
Ref<ConvexPolygonShape3D> convex = shape_resource;
Vector<Vector3> hull_points = convex->get_points();
ERR_FAIL_COND_V_MSG(hull_points.size() < 3, gltf_shape, "GLTFPhysicsShape: Convex hull has fewer points (" + itos(hull_points.size()) + ") than the minimum of 3. At least 3 points are required in order to save to GLTF, since it uses a mesh to represent convex hulls.");
@@ -206,7 +206,7 @@ Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_coll
}
CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) {
- CollisionShape3D *gltf_shape = memnew(CollisionShape3D);
+ CollisionShape3D *godot_shape_node = memnew(CollisionShape3D);
if (!p_cache_shapes || _shape_cache == nullptr) {
if (shape_type == "box") {
Ref<BoxShape3D> box;
@@ -230,80 +230,88 @@ CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) {
sphere.instantiate();
sphere->set_radius(radius);
_shape_cache = sphere;
- } else if (shape_type == "hull") {
- ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), gltf_shape, "GLTFPhysicsShape: Error converting convex hull shape to a node: The mesh resource is null.");
+ } else if (shape_type == "convex") {
+ ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), godot_shape_node, "GLTFPhysicsShape: Error converting convex hull shape to a node: The mesh resource is null.");
Ref<ConvexPolygonShape3D> convex = importer_mesh->get_mesh()->create_convex_shape();
_shape_cache = convex;
} else if (shape_type == "trimesh") {
- ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), gltf_shape, "GLTFPhysicsShape: Error converting concave mesh shape to a node: The mesh resource is null.");
+ ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), godot_shape_node, "GLTFPhysicsShape: Error converting concave mesh shape to a node: The mesh resource is null.");
Ref<ConcavePolygonShape3D> concave = importer_mesh->create_trimesh_shape();
_shape_cache = concave;
} else {
ERR_PRINT("GLTFPhysicsShape: Error converting to a node: Shape type '" + shape_type + "' is unknown.");
}
}
- gltf_shape->set_shape(_shape_cache);
- return gltf_shape;
+ godot_shape_node->set_shape(_shape_cache);
+ return godot_shape_node;
}
Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_dictionary(const Dictionary p_dictionary) {
ERR_FAIL_COND_V_MSG(!p_dictionary.has("type"), Ref<GLTFPhysicsShape>(), "Failed to parse GLTFPhysicsShape, missing required field 'type'.");
Ref<GLTFPhysicsShape> gltf_shape;
gltf_shape.instantiate();
- const String &shape_type = p_dictionary["type"];
+ String shape_type = p_dictionary["type"];
+ if (shape_type == "hull") {
+ shape_type = "convex";
+ }
gltf_shape->shape_type = shape_type;
- if (shape_type != "box" && shape_type != "capsule" && shape_type != "cylinder" && shape_type != "sphere" && shape_type != "hull" && shape_type != "trimesh") {
- ERR_PRINT("GLTFPhysicsShape: Error parsing unknown shape type '" + shape_type + "'. Only box, capsule, cylinder, sphere, hull, and trimesh are supported.");
+ if (shape_type != "box" && shape_type != "capsule" && shape_type != "cylinder" && shape_type != "sphere" && shape_type != "convex" && shape_type != "trimesh") {
+ ERR_PRINT("GLTFPhysicsShape: Error parsing unknown shape type '" + shape_type + "'. Only box, capsule, cylinder, sphere, convex, and trimesh are supported.");
+ }
+ Dictionary properties;
+ if (p_dictionary.has(shape_type)) {
+ properties = p_dictionary[shape_type];
+ } else {
+ properties = p_dictionary;
}
- if (p_dictionary.has("radius")) {
- gltf_shape->set_radius(p_dictionary["radius"]);
+ if (properties.has("radius")) {
+ gltf_shape->set_radius(properties["radius"]);
}
- if (p_dictionary.has("height")) {
- gltf_shape->set_height(p_dictionary["height"]);
+ if (properties.has("height")) {
+ gltf_shape->set_height(properties["height"]);
}
- if (p_dictionary.has("size")) {
- const Array &arr = p_dictionary["size"];
+ if (properties.has("size")) {
+ const Array &arr = properties["size"];
if (arr.size() == 3) {
gltf_shape->set_size(Vector3(arr[0], arr[1], arr[2]));
} else {
ERR_PRINT("GLTFPhysicsShape: Error parsing the size, it must have exactly 3 numbers.");
}
}
- if (p_dictionary.has("isTrigger")) {
- gltf_shape->set_is_trigger(p_dictionary["isTrigger"]);
+ if (properties.has("isTrigger")) {
+ gltf_shape->set_is_trigger(properties["isTrigger"]);
}
- if (p_dictionary.has("mesh")) {
- gltf_shape->set_mesh_index(p_dictionary["mesh"]);
+ if (properties.has("mesh")) {
+ gltf_shape->set_mesh_index(properties["mesh"]);
}
- if (unlikely(gltf_shape->get_mesh_index() < 0 && (shape_type == "hull" || shape_type == "trimesh"))) {
+ if (unlikely(gltf_shape->get_mesh_index() < 0 && (shape_type == "convex" || shape_type == "trimesh"))) {
ERR_PRINT("Error parsing GLTFPhysicsShape: The mesh-based shape type '" + shape_type + "' does not have a valid mesh index.");
}
return gltf_shape;
}
Dictionary GLTFPhysicsShape::to_dictionary() const {
- Dictionary d;
- d["type"] = shape_type;
+ Dictionary gltf_shape;
+ gltf_shape["type"] = shape_type;
+ Dictionary sub;
if (shape_type == "box") {
Array size_array;
size_array.resize(3);
size_array[0] = size.x;
size_array[1] = size.y;
size_array[2] = size.z;
- d["size"] = size_array;
+ sub["size"] = size_array;
} else if (shape_type == "capsule") {
- d["radius"] = get_radius();
- d["height"] = get_height();
+ sub["radius"] = get_radius();
+ sub["height"] = get_height();
} else if (shape_type == "cylinder") {
- d["radius"] = get_radius();
- d["height"] = get_height();
+ sub["radius"] = get_radius();
+ sub["height"] = get_height();
} else if (shape_type == "sphere") {
- d["radius"] = get_radius();
- } else if (shape_type == "trimesh" || shape_type == "hull") {
- d["mesh"] = get_mesh_index();
- }
- if (is_trigger) {
- d["isTrigger"] = is_trigger;
+ sub["radius"] = get_radius();
+ } else if (shape_type == "trimesh" || shape_type == "convex") {
+ sub["mesh"] = get_mesh_index();
}
- return d;
+ gltf_shape[shape_type] = sub;
+ return gltf_shape;
}
diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.h b/modules/gltf/extensions/physics/gltf_physics_shape.h
index efecf27e1b..4f7ac39292 100644
--- a/modules/gltf/extensions/physics/gltf_physics_shape.h
+++ b/modules/gltf/extensions/physics/gltf_physics_shape.h
@@ -37,8 +37,9 @@
class ImporterMesh;
-// GLTFPhysicsShape is an intermediary between OMI_collider and Godot's collision shape nodes.
-// https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_collider
+// GLTFPhysicsShape is an intermediary between Godot's collision shape nodes
+// and the OMI_physics_shape extension.
+// https://github.com/omigroup/gltf-extensions/tree/main/extensions/2.0/OMI_physics_shape
class GLTFPhysicsShape : public Resource {
GDCLASS(GLTFPhysicsShape, Resource)
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/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp
index 4746ffb79b..d2fe8a7534 100644
--- a/modules/lightmapper_rd/lightmapper_rd.cpp
+++ b/modules/lightmapper_rd/lightmapper_rd.cpp
@@ -1711,7 +1711,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
push_constant.ray_from = i * max_rays;
push_constant.ray_to = MIN((i + 1) * max_rays, int32_t(push_constant.ray_count));
rd->compute_list_set_push_constant(compute_list, &push_constant, sizeof(PushConstant));
- rd->compute_list_dispatch(compute_list, Math::division_round_up(probe_positions.size(), 64), 1, 1);
+ rd->compute_list_dispatch(compute_list, Math::division_round_up((int)probe_positions.size(), 64), 1, 1);
rd->compute_list_end(); //done
rd->submit();
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs
index d5d9404ed1..c806263edb 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/InteropStructs.cs
@@ -474,7 +474,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != IntPtr.Zero ? *((int*)_ptr - 1) : 0;
+ get => _ptr != IntPtr.Zero ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -725,7 +725,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -875,7 +875,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -939,7 +939,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -971,7 +971,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -1003,7 +1003,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -1035,7 +1035,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -1067,7 +1067,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -1099,7 +1099,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
@@ -1131,7 +1131,7 @@ namespace Godot.NativeInterop
public readonly unsafe int Size
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => _ptr != null ? *((int*)_ptr - 1) : 0;
+ get => _ptr != null ? (int)(*((ulong*)_ptr - 1)) : 0;
}
}
diff --git a/modules/multiplayer/scene_cache_interface.cpp b/modules/multiplayer/scene_cache_interface.cpp
index 56cd0bec18..33b05d4cc2 100644
--- a/modules/multiplayer/scene_cache_interface.cpp
+++ b/modules/multiplayer/scene_cache_interface.cpp
@@ -35,25 +35,61 @@
#include "core/io/marshalls.h"
#include "scene/main/node.h"
#include "scene/main/window.h"
+#include "scene/scene_string_names.h"
+
+SceneCacheInterface::NodeCache &SceneCacheInterface::_track(Node *p_node) {
+ const ObjectID oid = p_node->get_instance_id();
+ NodeCache *nc = nodes_cache.getptr(oid);
+ if (!nc) {
+ nodes_cache[oid] = NodeCache();
+ p_node->connect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneCacheInterface::_remove_node_cache).bind(oid), Object::CONNECT_ONE_SHOT);
+ }
+ return nodes_cache[oid];
+}
+
+void SceneCacheInterface::_remove_node_cache(ObjectID p_oid) {
+ NodeCache *nc = nodes_cache.getptr(p_oid);
+ if (!nc) {
+ return;
+ }
+ for (KeyValue<int, int> &E : nc->recv_ids) {
+ PeerInfo *pinfo = peers_info.getptr(E.key);
+ ERR_CONTINUE(!pinfo);
+ pinfo->recv_nodes.erase(E.value);
+ }
+ for (KeyValue<int, bool> &E : nc->confirmed_peers) {
+ PeerInfo *pinfo = peers_info.getptr(E.key);
+ ERR_CONTINUE(!pinfo);
+ pinfo->sent_nodes.erase(p_oid);
+ }
+ nodes_cache.erase(p_oid);
+}
void SceneCacheInterface::on_peer_change(int p_id, bool p_connected) {
if (p_connected) {
- path_get_cache.insert(p_id, PathGetCache());
+ peers_info.insert(p_id, PeerInfo());
} else {
- // Cleanup get cache.
- path_get_cache.erase(p_id);
- // Cleanup sent cache.
- // Some refactoring is needed to make this faster and do paths GC.
- for (KeyValue<ObjectID, PathSentCache> &E : path_send_cache) {
- E.value.confirmed_peers.erase(p_id);
+ PeerInfo *pinfo = peers_info.getptr(p_id);
+ ERR_FAIL_NULL(pinfo); // Bug.
+ for (KeyValue<int, ObjectID> E : pinfo->recv_nodes) {
+ NodeCache *nc = nodes_cache.getptr(E.value);
+ ERR_CONTINUE(!nc);
+ nc->recv_ids.erase(E.key);
+ }
+ for (const ObjectID &oid : pinfo->sent_nodes) {
+ NodeCache *nc = nodes_cache.getptr(oid);
+ ERR_CONTINUE(!nc);
+ nc->confirmed_peers.erase(p_id);
}
+ peers_info.erase(p_id);
}
}
void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_packet, int p_packet_len) {
+ ERR_FAIL_COND(!peers_info.has(p_from)); // Bug.
+ ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small.");
Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
ERR_FAIL_NULL(root_node);
- ERR_FAIL_COND_MSG(p_packet_len < 38, "Invalid packet received. Size too small.");
int ofs = 1;
String methods_md5;
@@ -63,15 +99,13 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac
int id = decode_uint32(&p_packet[ofs]);
ofs += 4;
+ ERR_FAIL_COND_MSG(peers_info[p_from].recv_nodes.has(id), vformat("Duplicate remote cache ID %d for peer %d", id, p_from));
+
String paths;
paths.parse_utf8((const char *)(p_packet + ofs), p_packet_len - ofs);
const NodePath path = paths;
- if (!path_get_cache.has(p_from)) {
- path_get_cache[p_from] = PathGetCache();
- }
-
Node *node = root_node->get_node(path);
ERR_FAIL_NULL(node);
const bool valid_rpc_checksum = multiplayer->get_rpc_md5(node) == methods_md5;
@@ -79,10 +113,9 @@ void SceneCacheInterface::process_simplify_path(int p_from, const uint8_t *p_pac
ERR_PRINT("The rpc node checksum failed. Make sure to have the same methods on both nodes. Node path: " + path);
}
- PathGetCache::NodeInfo ni;
- ni.path = node->get_path();
-
- path_get_cache[p_from].nodes[id] = ni;
+ peers_info[p_from].recv_nodes.insert(id, node->get_instance_id());
+ NodeCache &cache = _track(node);
+ cache.recv_ids.insert(p_from, id);
// Encode path to send ack.
CharString pname = String(path).utf8();
@@ -122,15 +155,15 @@ void SceneCacheInterface::process_confirm_path(int p_from, const uint8_t *p_pack
Node *node = root_node->get_node(path);
ERR_FAIL_NULL(node);
- PathSentCache *psc = path_send_cache.getptr(node->get_instance_id());
- ERR_FAIL_NULL_MSG(psc, "Invalid packet received. Tries to confirm a path which was not found in cache.");
+ NodeCache *cache = nodes_cache.getptr(node->get_instance_id());
+ ERR_FAIL_NULL_MSG(cache, "Invalid packet received. Tries to confirm a node which was not requested.");
- HashMap<int, bool>::Iterator E = psc->confirmed_peers.find(p_from);
- ERR_FAIL_COND_MSG(!E, "Invalid packet received. Source peer was not found in cache for the given path.");
- E->value = true;
+ bool *confirmed = cache->confirmed_peers.getptr(p_from);
+ ERR_FAIL_NULL_MSG(confirmed, "Invalid packet received. Tries to confirm a node which was not requested.");
+ *confirmed = true;
}
-Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc, const List<int> &p_peers) {
+Error SceneCacheInterface::_send_confirm_path(Node *p_node, NodeCache &p_cache, const List<int> &p_peers) {
// Encode function name.
const CharString path = String(multiplayer->get_root_path().rel_path_to(p_node->get_path())).utf8();
const int path_len = encode_cstring(path.get_data(), nullptr);
@@ -148,7 +181,7 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc,
ofs += encode_cstring(methods_md5.utf8().get_data(), &packet.write[ofs]);
- ofs += encode_uint32(psc->id, &packet.write[ofs]);
+ ofs += encode_uint32(p_cache.cache_id, &packet.write[ofs]);
ofs += encode_cstring(path.get_data(), &packet.write[ofs]);
@@ -162,80 +195,74 @@ Error SceneCacheInterface::_send_confirm_path(Node *p_node, PathSentCache *psc,
err = multiplayer->send_command(peer_id, packet.ptr(), packet.size());
ERR_FAIL_COND_V(err != OK, err);
// Insert into confirmed, but as false since it was not confirmed.
- psc->confirmed_peers.insert(peer_id, false);
+ p_cache.confirmed_peers.insert(peer_id, false);
+ ERR_CONTINUE(!peers_info.has(peer_id));
+ peers_info[peer_id].sent_nodes.insert(p_node->get_instance_id());
}
return err;
}
bool SceneCacheInterface::is_cache_confirmed(Node *p_node, int p_peer) {
ERR_FAIL_NULL_V(p_node, false);
- const PathSentCache *psc = path_send_cache.getptr(p_node->get_instance_id());
- ERR_FAIL_NULL_V(psc, false);
- HashMap<int, bool>::ConstIterator F = psc->confirmed_peers.find(p_peer);
- ERR_FAIL_COND_V(!F, false); // Should never happen.
- return F->value;
+ const ObjectID oid = p_node->get_instance_id();
+ NodeCache *cache = nodes_cache.getptr(oid);
+ bool *confirmed = cache ? cache->confirmed_peers.getptr(p_peer) : nullptr;
+ return confirmed && *confirmed;
}
int SceneCacheInterface::make_object_cache(Object *p_obj) {
Node *node = Object::cast_to<Node>(p_obj);
ERR_FAIL_NULL_V(node, -1);
- const ObjectID oid = node->get_instance_id();
- // See if the path is cached.
- PathSentCache *psc = path_send_cache.getptr(oid);
- if (!psc) {
- // Path is not cached, create.
- path_send_cache[oid] = PathSentCache();
- psc = path_send_cache.getptr(oid);
- psc->id = last_send_cache_id++;
+ NodeCache &cache = _track(node);
+ if (cache.cache_id == 0) {
+ cache.cache_id = last_send_cache_id++;
}
- return psc->id;
+ return cache.cache_id;
}
bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r_id) {
Node *node = Object::cast_to<Node>(p_obj);
ERR_FAIL_NULL_V(node, false);
- const ObjectID oid = node->get_instance_id();
// See if the path is cached.
- PathSentCache *psc = path_send_cache.getptr(oid);
- if (!psc) {
- // Path is not cached, create.
- path_send_cache[oid] = PathSentCache();
- psc = path_send_cache.getptr(oid);
- psc->id = last_send_cache_id++;
+ NodeCache &cache = _track(node);
+ if (cache.cache_id == 0) {
+ cache.cache_id = last_send_cache_id++;
}
- r_id = psc->id;
+ r_id = cache.cache_id;
bool has_all_peers = true;
List<int> peers_to_add; // If one is missing, take note to add it.
if (p_peer_id > 0) {
// Fast single peer check.
- HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(p_peer_id);
- if (!F) {
+ ERR_FAIL_COND_V_MSG(!peers_info.has(p_peer_id), false, "Peer doesn't exist: " + itos(p_peer_id));
+
+ bool *confirmed = cache.confirmed_peers.getptr(p_peer_id);
+ if (!confirmed) {
peers_to_add.push_back(p_peer_id); // Need to also be notified.
has_all_peers = false;
- } else if (!F->value) {
+ } else if (!(*confirmed)) {
has_all_peers = false;
}
} else {
// Long and painful.
- for (const int &E : multiplayer->get_connected_peers()) {
- if (p_peer_id < 0 && E == -p_peer_id) {
+ for (KeyValue<int, PeerInfo> &E : peers_info) {
+ if (p_peer_id < 0 && E.key == -p_peer_id) {
continue; // Continue, excluded.
}
- HashMap<int, bool>::Iterator F = psc->confirmed_peers.find(E);
- if (!F) {
- peers_to_add.push_back(E); // Need to also be notified.
+ bool *confirmed = cache.confirmed_peers.getptr(E.key);
+ if (!confirmed) {
+ peers_to_add.push_back(E.key); // Need to also be notified.
has_all_peers = false;
- } else if (!F->value) {
+ } else if (!(*confirmed)) {
has_all_peers = false;
}
}
}
if (peers_to_add.size()) {
- _send_confirm_path(node, psc, peers_to_add);
+ _send_confirm_path(node, cache, peers_to_add);
}
return has_all_peers;
@@ -244,22 +271,23 @@ bool SceneCacheInterface::send_object_cache(Object *p_obj, int p_peer_id, int &r
Object *SceneCacheInterface::get_cached_object(int p_from, uint32_t p_cache_id) {
Node *root_node = SceneTree::get_singleton()->get_root()->get_node(multiplayer->get_root_path());
ERR_FAIL_NULL_V(root_node, nullptr);
- HashMap<int, PathGetCache>::Iterator E = path_get_cache.find(p_from);
- ERR_FAIL_COND_V_MSG(!E, nullptr, vformat("No cache found for peer %d.", p_from));
-
- HashMap<int, PathGetCache::NodeInfo>::Iterator F = E->value.nodes.find(p_cache_id);
- ERR_FAIL_COND_V_MSG(!F, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from));
+ PeerInfo *pinfo = peers_info.getptr(p_from);
+ ERR_FAIL_NULL_V(pinfo, nullptr);
- PathGetCache::NodeInfo *ni = &F->value;
- Node *node = root_node->get_node(ni->path);
- if (!node) {
- ERR_PRINT("Failed to get cached path: " + String(ni->path) + ".");
- }
+ const ObjectID *oid = pinfo->recv_nodes.getptr(p_cache_id);
+ ERR_FAIL_NULL_V_MSG(oid, nullptr, vformat("ID %d not found in cache of peer %d.", p_cache_id, p_from));
+ Node *node = Object::cast_to<Node>(ObjectDB::get_instance(*oid));
+ ERR_FAIL_NULL_V_MSG(node, nullptr, vformat("Failed to get cached node from peer %d with cache ID %d.", p_from, p_cache_id));
return node;
}
void SceneCacheInterface::clear() {
- path_get_cache.clear();
- path_send_cache.clear();
+ for (KeyValue<ObjectID, NodeCache> &E : nodes_cache) {
+ Object *obj = ObjectDB::get_instance(E.key);
+ ERR_CONTINUE(!obj);
+ obj->disconnect(SceneStringNames::get_singleton()->tree_exited, callable_mp(this, &SceneCacheInterface::_remove_node_cache));
+ }
+ peers_info.clear();
+ nodes_cache.clear();
last_send_cache_id = 1;
}
diff --git a/modules/multiplayer/scene_cache_interface.h b/modules/multiplayer/scene_cache_interface.h
index e63beb5f84..ab4a20c078 100644
--- a/modules/multiplayer/scene_cache_interface.h
+++ b/modules/multiplayer/scene_cache_interface.h
@@ -43,27 +43,26 @@ private:
SceneMultiplayer *multiplayer = nullptr;
//path sent caches
- struct PathSentCache {
- HashMap<int, bool> confirmed_peers;
- int id;
+ struct NodeCache {
+ int cache_id;
+ HashMap<int, int> recv_ids; // peer id, remote cache id
+ HashMap<int, bool> confirmed_peers; // peer id, confirmed
};
- //path get caches
- struct PathGetCache {
- struct NodeInfo {
- NodePath path;
- ObjectID instance;
- };
-
- HashMap<int, NodeInfo> nodes;
+ struct PeerInfo {
+ HashMap<int, ObjectID> recv_nodes; // remote cache id, ObjectID
+ HashSet<ObjectID> sent_nodes;
};
- HashMap<ObjectID, PathSentCache> path_send_cache;
- HashMap<int, PathGetCache> path_get_cache;
+ HashMap<ObjectID, NodeCache> nodes_cache;
+ HashMap<int, PeerInfo> peers_info;
int last_send_cache_id = 1;
+ void _remove_node_cache(ObjectID p_oid);
+ NodeCache &_track(Node *p_node);
+
protected:
- Error _send_confirm_path(Node *p_node, PathSentCache *psc, const List<int> &p_peers);
+ Error _send_confirm_path(Node *p_node, NodeCache &p_cache, const List<int> &p_peers);
public:
void clear();
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/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 bc14d233bb..4a8e9cd956 100644
--- a/platform/macos/detect.py
+++ b/platform/macos/detect.py
@@ -130,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"])
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/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/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 7fff2f90fb..093b52b650 100644
--- a/scene/main/canvas_item.cpp
+++ b/scene/main/canvas_item.cpp
@@ -277,8 +277,6 @@ void CanvasItem::_notification(int p_what) {
ERR_MAIN_THREAD_GUARD;
ERR_FAIL_COND(!is_inside_tree());
- _set_global_invalid(true);
-
Node *parent = get_parent();
if (parent) {
CanvasItem *ci = Object::cast_to<CanvasItem>(parent);
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/viewport.cpp b/scene/main/viewport.cpp
index fe02d97586..f92ab76753 100644
--- a/scene/main/viewport.cpp
+++ b/scene/main/viewport.cpp
@@ -3316,6 +3316,16 @@ void Viewport::push_input(const Ref<InputEvent> &p_event, bool p_local_coords) {
}
local_input_handled = false;
+ if (!handle_input_locally) {
+ Viewport *vp = this;
+ while (true) {
+ if (Object::cast_to<Window>(vp) || !vp->get_parent()) {
+ break;
+ }
+ vp = vp->get_parent()->get_viewport();
+ }
+ vp->local_input_handled = false;
+ }
Ref<InputEvent> ev;
if (!p_local_coords) {
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/visual_shader.cpp b/scene/resources/visual_shader.cpp
index 8ff5b54fbe..41660767ab 100644
--- a/scene/resources/visual_shader.cpp
+++ b/scene/resources/visual_shader.cpp
@@ -246,6 +246,20 @@ void VisualShaderNode::set_input_port_connected(int p_port, bool p_connected) {
connected_input_ports[p_port] = p_connected;
}
+bool VisualShaderNode::is_any_port_connected() const {
+ for (const KeyValue<int, bool> &E : connected_input_ports) {
+ if (E.value) {
+ return true;
+ }
+ }
+ for (const KeyValue<int, int> &E : connected_output_ports) {
+ if (E.value > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
bool VisualShaderNode::is_generate_input_var(int p_port) const {
return true;
}
diff --git a/scene/resources/visual_shader.h b/scene/resources/visual_shader.h
index 501a538c86..7faebb86ab 100644
--- a/scene/resources/visual_shader.h
+++ b/scene/resources/visual_shader.h
@@ -314,6 +314,7 @@ public:
void set_output_port_connected(int p_port, bool p_connected);
bool is_input_port_connected(int p_port) const;
void set_input_port_connected(int p_port, bool p_connected);
+ bool is_any_port_connected() const;
virtual bool is_generate_input_var(int p_port) const;
virtual bool has_output_port_preview(int p_port) const;
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/renderer_rd/storage_rd/light_storage.cpp b/servers/rendering/renderer_rd/storage_rd/light_storage.cpp
index f0e3d0090f..2786af65eb 100644
--- a/servers/rendering/renderer_rd/storage_rd/light_storage.cpp
+++ b/servers/rendering/renderer_rd/storage_rd/light_storage.cpp
@@ -2003,7 +2003,7 @@ void LightStorage::shadow_atlas_set_size(RID p_atlas, int p_size, bool p_16_bits
for (int i = 0; i < 4; i++) {
//clear subdivisions
shadow_atlas->quadrants[i].shadows.clear();
- shadow_atlas->quadrants[i].shadows.resize(1 << shadow_atlas->quadrants[i].subdivision);
+ shadow_atlas->quadrants[i].shadows.resize(int64_t(1) << int64_t(shadow_atlas->quadrants[i].subdivision));
}
//erase shadow atlas reference from lights
diff --git a/servers/rendering/rendering_device_graph.cpp b/servers/rendering/rendering_device_graph.cpp
index bfacd38065..28526b8f58 100644
--- a/servers/rendering/rendering_device_graph.cpp
+++ b/servers/rendering/rendering_device_graph.cpp
@@ -1197,6 +1197,7 @@ void RenderingDeviceGraph::begin() {
command_data_offsets.clear();
command_normalization_barriers.clear();
command_transition_barriers.clear();
+ command_buffer_barriers.clear();
command_label_chars.clear();
command_label_colors.clear();
command_label_offsets.clear();
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 0d6da27c30..9ad1d8b8dc 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_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