summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/classes/EditorSettings.xml12
-rw-r--r--doc/classes/Label.xml2
-rw-r--r--doc/classes/Label3D.xml2
-rw-r--r--doc/classes/ProjectSettings.xml3
-rw-r--r--doc/classes/RichTextLabel.xml9
-rw-r--r--doc/classes/TextMesh.xml2
-rw-r--r--doc/classes/TextParagraph.xml2
-rw-r--r--drivers/windows/dir_access_windows.cpp190
-rw-r--r--drivers/windows/file_access_windows.cpp72
-rw-r--r--editor/editor_file_system.cpp18
-rw-r--r--editor/editor_properties_array_dict.cpp8
-rw-r--r--editor/editor_settings.cpp6
-rw-r--r--editor/import/resource_importer_wav.cpp11
-rw-r--r--editor/plugins/node_3d_editor_plugin.cpp11
-rw-r--r--editor/progress_dialog.cpp8
-rw-r--r--editor/progress_dialog.h2
-rw-r--r--godot.manifest5
-rw-r--r--methods.py2
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp20
-rw-r--r--modules/gdscript/gdscript_warning.cpp3
-rw-r--r--modules/gdscript/gdscript_warning.h2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd7
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd1
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out1
-rw-r--r--platform/android/java/editor/src/main/AndroidManifest.xml5
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt192
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt23
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt153
-rw-r--r--platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt93
-rw-r--r--platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml25
-rw-r--r--platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml12
-rw-r--r--platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml10
-rw-r--r--platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml9
-rw-r--r--platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml10
-rw-r--r--platform/android/java/editor/src/main/res/layout/godot_game_layout.xml25
-rw-r--r--platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml5
-rw-r--r--platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.pngbin0 -> 1954 bytes
-rw-r--r--platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.pngbin0 -> 1350 bytes
-rw-r--r--platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.pngbin0 -> 2617 bytes
-rw-r--r--platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.pngbin0 -> 3923 bytes
-rw-r--r--platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.pngbin0 -> 5480 bytes
-rw-r--r--platform/android/java/editor/src/main/res/values/dimens.xml2
-rw-r--r--platform/android/java/editor/src/main/res/values/strings.xml2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt8
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java27
-rw-r--r--platform/linuxbsd/freedesktop_portal_desktop.cpp17
-rw-r--r--platform/windows/console_wrapper_windows.cpp4
-rw-r--r--platform/windows/detect.py35
-rw-r--r--platform/windows/display_server_windows.cpp27
-rw-r--r--platform/windows/godot_res_wrap.rc5
-rw-r--r--platform/windows/os_windows.cpp110
-rw-r--r--scene/gui/rich_text_label.cpp131
-rw-r--r--scene/gui/rich_text_label.h10
-rw-r--r--scene/gui/tree.h3
-rw-r--r--scene/resources/packed_scene.cpp2
-rw-r--r--tests/scene/test_tree.h24
-rw-r--r--thirdparty/README.md4
-rw-r--r--thirdparty/misc/patches/qoa-min-fix.patch14
-rw-r--r--thirdparty/misc/qoa.h6
61 files changed, 1116 insertions, 294 deletions
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index 28f1964aa9..b6565b81f2 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -963,7 +963,17 @@
If [code]true[/code], on Linux/BSD, the editor will check for Wayland first instead of X11 (if available).
</member>
<member name="run/window_placement/android_window" type="int" setter="" getter="">
- The Android window to display the project on when starting the project from the editor.
+ Specifies how the Play window is launched relative to the Android editor.
+ - [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets.
+ - [b]Same as Editor[/b] will launch the Play window in the same window as the Editor.
+ - [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window.
+ [b]Note:[/b] Only available in the Android editor.
+ </member>
+ <member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter="">
+ Specifies the picture-in-picture (PiP) mode for the Play window.
+ - [b]Disabled:[/b] PiP is disabled for the Play window.
+ - [b]Enabled:[/b] If the device supports it, PiP is always enabled for the Play window. The Play window will contain a button to enter PiP mode.
+ - [b]Enabled when Play window is same as Editor[/b] (default for Android editor): If the device supports it, PiP is enabled when the Play window is the same as the Editor. The Play window will contain a button to enter PiP mode.
[b]Note:[/b] Only available in the Android editor.
</member>
<member name="run/window_placement/rect" type="int" setter="" getter="">
diff --git a/doc/classes/Label.xml b/doc/classes/Label.xml
index 8acd05cbd1..e6eba30ab7 100644
--- a/doc/classes/Label.xml
+++ b/doc/classes/Label.xml
@@ -59,7 +59,7 @@
Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants.
</member>
<member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163">
- Line fill alignment rules. For more info see [enum TextServer.JustificationFlag].
+ Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information.
</member>
<member name="label_settings" type="LabelSettings" setter="set_label_settings" getter="get_label_settings">
A [LabelSettings] resource that can be shared between multiple [Label] nodes. Takes priority over theme properties.
diff --git a/doc/classes/Label3D.xml b/doc/classes/Label3D.xml
index 4c70897452..ff26c5490d 100644
--- a/doc/classes/Label3D.xml
+++ b/doc/classes/Label3D.xml
@@ -73,7 +73,7 @@
Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants.
</member>
<member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163">
- Line fill alignment rules. For more info see [enum TextServer.JustificationFlag].
+ Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information.
</member>
<member name="language" type="String" setter="set_language" getter="get_language" default="&quot;&quot;">
Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 30cbbbf799..1f31fef5ca 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -528,6 +528,9 @@
<member name="debug/gdscript/warnings/integer_division" type="int" setter="" getter="" default="1">
When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when dividing an integer by another integer (the decimal part will be discarded).
</member>
+ <member name="debug/gdscript/warnings/missing_tool" type="int" setter="" getter="" default="1">
+ When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the base class script has the [code]@tool[/code] annotation, but the current class script does not have it.
+ </member>
<member name="debug/gdscript/warnings/narrowing_conversion" type="int" setter="" getter="" default="1">
When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when passing a floating-point value to a function that expects an integer (it will be converted and lose precision).
</member>
diff --git a/doc/classes/RichTextLabel.xml b/doc/classes/RichTextLabel.xml
index 9a772835a6..4a2cbbc3d8 100644
--- a/doc/classes/RichTextLabel.xml
+++ b/doc/classes/RichTextLabel.xml
@@ -631,6 +631,12 @@
<member name="hint_underlined" type="bool" setter="set_hint_underline" getter="is_hint_underlined" default="true">
If [code]true[/code], the label underlines hint tags such as [code skip-lint][hint=description]{text}[/hint][/code].
</member>
+ <member name="horizontal_alignment" type="int" setter="set_horizontal_alignment" getter="get_horizontal_alignment" enum="HorizontalAlignment" default="0">
+ Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants.
+ </member>
+ <member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163">
+ Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information.
+ </member>
<member name="language" type="String" setter="set_language" getter="get_language" default="&quot;&quot;">
Language code used for line-breaking and text shaping algorithms, if left empty current locale is used instead.
</member>
@@ -662,6 +668,9 @@
<member name="tab_size" type="int" setter="set_tab_size" getter="get_tab_size" default="4">
The number of spaces associated with a single tab length. Does not affect [code]\t[/code] in text tags, only indent tags.
</member>
+ <member name="tab_stops" type="PackedFloat32Array" setter="set_tab_stops" getter="get_tab_stops" default="PackedFloat32Array()">
+ Aligns text to the given tab-stops.
+ </member>
<member name="text" type="String" setter="set_text" getter="get_text" default="&quot;&quot;">
The label's text in BBCode format. Is not representative of manual modifications to the internal tag stack. Erases changes made by other methods when edited.
[b]Note:[/b] If [member bbcode_enabled] is [code]true[/code], it is unadvised to use the [code]+=[/code] operator with [member text] (e.g. [code]text += "some string"[/code]) as it replaces the whole text and can cause slowdowns. It will also erase all BBCode that was added to stack using [code]push_*[/code] methods. Use [method append_text] for adding text instead, unless you absolutely need to close a tag that was opened in an earlier method call.
diff --git a/doc/classes/TextMesh.xml b/doc/classes/TextMesh.xml
index 9e705311c5..898d19aed3 100644
--- a/doc/classes/TextMesh.xml
+++ b/doc/classes/TextMesh.xml
@@ -31,7 +31,7 @@
Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants.
</member>
<member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163">
- Line fill alignment rules. For more info see [enum TextServer.JustificationFlag].
+ Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information.
</member>
<member name="language" type="String" setter="set_language" getter="get_language" default="&quot;&quot;">
Language code used for text shaping algorithms, if left empty current locale is used instead.
diff --git a/doc/classes/TextParagraph.xml b/doc/classes/TextParagraph.xml
index c6511a2b8e..46197f19b8 100644
--- a/doc/classes/TextParagraph.xml
+++ b/doc/classes/TextParagraph.xml
@@ -278,7 +278,7 @@
Ellipsis character used for text clipping.
</member>
<member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163">
- Line fill alignment rules. For more info see [enum TextServer.JustificationFlag].
+ Line fill alignment rules. See [enum TextServer.JustificationFlag] for more information.
</member>
<member name="max_lines_visible" type="int" setter="set_max_lines_visible" getter="get_max_lines_visible" default="-1">
Limits the lines of text shown.
diff --git a/drivers/windows/dir_access_windows.cpp b/drivers/windows/dir_access_windows.cpp
index 63ba6a6c96..1d1f2ce415 100644
--- a/drivers/windows/dir_access_windows.cpp
+++ b/drivers/windows/dir_access_windows.cpp
@@ -35,6 +35,7 @@
#include "core/config/project_settings.h"
#include "core/os/memory.h"
+#include "core/os/os.h"
#include "core/string/print_string.h"
#include <stdio.h>
@@ -69,9 +70,17 @@ struct DirAccessWindowsPrivate {
};
String DirAccessWindows::fix_path(const String &p_path) const {
- String r_path = DirAccess::fix_path(p_path);
- if (r_path.is_absolute_path() && !r_path.is_network_share_path() && r_path.length() > MAX_PATH) {
- r_path = "\\\\?\\" + r_path.replace("/", "\\");
+ String r_path = DirAccess::fix_path(p_path.trim_prefix(R"(\\?\)").replace("\\", "/"));
+
+ if (r_path.is_relative_path()) {
+ r_path = current_dir.trim_prefix(R"(\\?\)").replace("\\", "/").path_join(r_path);
+ } else if (r_path == ".") {
+ r_path = current_dir.trim_prefix(R"(\\?\)").replace("\\", "/");
+ }
+ r_path = r_path.simplify_path();
+ r_path = r_path.replace("/", "\\");
+ if (!r_path.is_network_share_path() && !r_path.begins_with(R"(\\?\)")) {
+ r_path = R"(\\?\)" + r_path;
}
return r_path;
}
@@ -140,28 +149,33 @@ String DirAccessWindows::get_drive(int p_drive) {
Error DirAccessWindows::change_dir(String p_dir) {
GLOBAL_LOCK_FUNCTION
- p_dir = fix_path(p_dir);
+ String dir = fix_path(p_dir);
- WCHAR real_current_dir_name[2048];
- GetCurrentDirectoryW(2048, real_current_dir_name);
- String prev_dir = String::utf16((const char16_t *)real_current_dir_name);
+ Char16String real_current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ real_current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw());
+ String prev_dir = String::utf16((const char16_t *)real_current_dir_name.get_data());
SetCurrentDirectoryW((LPCWSTR)(current_dir.utf16().get_data()));
- bool worked = (SetCurrentDirectoryW((LPCWSTR)(p_dir.utf16().get_data())) != 0);
+ bool worked = (SetCurrentDirectoryW((LPCWSTR)(dir.utf16().get_data())) != 0);
String base = _get_root_path();
if (!base.is_empty()) {
- GetCurrentDirectoryW(2048, real_current_dir_name);
- String new_dir = String::utf16((const char16_t *)real_current_dir_name).replace("\\", "/");
+ str_len = GetCurrentDirectoryW(0, nullptr);
+ real_current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw());
+ String new_dir = String::utf16((const char16_t *)real_current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/");
if (!new_dir.begins_with(base)) {
worked = false;
}
}
if (worked) {
- GetCurrentDirectoryW(2048, real_current_dir_name);
- current_dir = String::utf16((const char16_t *)real_current_dir_name);
- current_dir = current_dir.replace("\\", "/");
+ str_len = GetCurrentDirectoryW(0, nullptr);
+ real_current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw());
+ current_dir = String::utf16((const char16_t *)real_current_dir_name.get_data());
}
SetCurrentDirectoryW((LPCWSTR)(prev_dir.utf16().get_data()));
@@ -172,12 +186,6 @@ Error DirAccessWindows::change_dir(String p_dir) {
Error DirAccessWindows::make_dir(String p_dir) {
GLOBAL_LOCK_FUNCTION
- p_dir = fix_path(p_dir);
- if (p_dir.is_relative_path()) {
- p_dir = current_dir.path_join(p_dir);
- p_dir = fix_path(p_dir);
- }
-
if (FileAccessWindows::is_path_invalid(p_dir)) {
#ifdef DEBUG_ENABLED
WARN_PRINT("The path :" + p_dir + " is a reserved Windows system pipe, so it can't be used for creating directories.");
@@ -185,12 +193,12 @@ Error DirAccessWindows::make_dir(String p_dir) {
return ERR_INVALID_PARAMETER;
}
- p_dir = p_dir.simplify_path().replace("/", "\\");
+ String dir = fix_path(p_dir);
bool success;
int err;
- success = CreateDirectoryW((LPCWSTR)(p_dir.utf16().get_data()), nullptr);
+ success = CreateDirectoryW((LPCWSTR)(dir.utf16().get_data()), nullptr);
err = GetLastError();
if (success) {
@@ -205,9 +213,10 @@ Error DirAccessWindows::make_dir(String p_dir) {
}
String DirAccessWindows::get_current_dir(bool p_include_drive) const {
+ String cdir = current_dir.trim_prefix(R"(\\?\)").replace("\\", "/");
String base = _get_root_path();
if (!base.is_empty()) {
- String bd = current_dir.replace("\\", "/").replace_first(base, "");
+ String bd = cdir.replace_first(base, "");
if (bd.begins_with("/")) {
return _get_root_string() + bd.substr(1, bd.length());
} else {
@@ -216,30 +225,25 @@ String DirAccessWindows::get_current_dir(bool p_include_drive) const {
}
if (p_include_drive) {
- return current_dir;
+ return cdir;
} else {
if (_get_root_string().is_empty()) {
- int pos = current_dir.find(":");
+ int pos = cdir.find(":");
if (pos != -1) {
- return current_dir.substr(pos + 1);
+ return cdir.substr(pos + 1);
}
}
- return current_dir;
+ return cdir;
}
}
bool DirAccessWindows::file_exists(String p_file) {
GLOBAL_LOCK_FUNCTION
- if (!p_file.is_absolute_path()) {
- p_file = get_current_dir().path_join(p_file);
- }
-
- p_file = fix_path(p_file);
+ String file = fix_path(p_file);
DWORD fileAttr;
-
- fileAttr = GetFileAttributesW((LPCWSTR)(p_file.utf16().get_data()));
+ fileAttr = GetFileAttributesW((LPCWSTR)(file.utf16().get_data()));
if (INVALID_FILE_ATTRIBUTES == fileAttr) {
return false;
}
@@ -250,14 +254,10 @@ bool DirAccessWindows::file_exists(String p_file) {
bool DirAccessWindows::dir_exists(String p_dir) {
GLOBAL_LOCK_FUNCTION
- if (p_dir.is_relative_path()) {
- p_dir = get_current_dir().path_join(p_dir);
- }
-
- p_dir = fix_path(p_dir);
+ String dir = fix_path(p_dir);
DWORD fileAttr;
- fileAttr = GetFileAttributesW((LPCWSTR)(p_dir.utf16().get_data()));
+ fileAttr = GetFileAttributesW((LPCWSTR)(dir.utf16().get_data()));
if (INVALID_FILE_ATTRIBUTES == fileAttr) {
return false;
}
@@ -265,66 +265,63 @@ bool DirAccessWindows::dir_exists(String p_dir) {
}
Error DirAccessWindows::rename(String p_path, String p_new_path) {
- if (p_path.is_relative_path()) {
- p_path = get_current_dir().path_join(p_path);
- }
-
- p_path = fix_path(p_path);
-
- if (p_new_path.is_relative_path()) {
- p_new_path = get_current_dir().path_join(p_new_path);
- }
-
- p_new_path = fix_path(p_new_path);
+ String path = fix_path(p_path);
+ String new_path = fix_path(p_new_path);
// If we're only changing file name case we need to do a little juggling
- if (p_path.to_lower() == p_new_path.to_lower()) {
- if (dir_exists(p_path)) {
+ if (path.to_lower() == new_path.to_lower()) {
+ if (dir_exists(path)) {
// The path is a dir; just rename
- return ::_wrename((LPCWSTR)(p_path.utf16().get_data()), (LPCWSTR)(p_new_path.utf16().get_data())) == 0 ? OK : FAILED;
+ return MoveFileW((LPCWSTR)(path.utf16().get_data()), (LPCWSTR)(new_path.utf16().get_data())) != 0 ? OK : FAILED;
}
// The path is a file; juggle
- WCHAR tmpfile[MAX_PATH];
-
- if (!GetTempFileNameW((LPCWSTR)(fix_path(get_current_dir()).utf16().get_data()), nullptr, 0, tmpfile)) {
- return FAILED;
+ // Note: do not use GetTempFileNameW, it's not long path aware!
+ Char16String tmpfile_utf16;
+ uint64_t id = OS::get_singleton()->get_ticks_usec();
+ while (true) {
+ tmpfile_utf16 = (path + itos(id++) + ".tmp").utf16();
+ HANDLE handle = CreateFileW((LPCWSTR)tmpfile_utf16.get_data(), GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, 0);
+ if (handle != INVALID_HANDLE_VALUE) {
+ CloseHandle(handle);
+ break;
+ }
+ if (GetLastError() != ERROR_FILE_EXISTS && GetLastError() != ERROR_SHARING_VIOLATION) {
+ return FAILED;
+ }
}
- if (!::ReplaceFileW(tmpfile, (LPCWSTR)(p_path.utf16().get_data()), nullptr, 0, nullptr, nullptr)) {
- DeleteFileW(tmpfile);
+ if (!::ReplaceFileW((LPCWSTR)tmpfile_utf16.get_data(), (LPCWSTR)(path.utf16().get_data()), nullptr, 0, nullptr, nullptr)) {
+ DeleteFileW((LPCWSTR)tmpfile_utf16.get_data());
return FAILED;
}
- return ::_wrename(tmpfile, (LPCWSTR)(p_new_path.utf16().get_data())) == 0 ? OK : FAILED;
+ return MoveFileW((LPCWSTR)tmpfile_utf16.get_data(), (LPCWSTR)(new_path.utf16().get_data())) != 0 ? OK : FAILED;
} else {
- if (file_exists(p_new_path)) {
- if (remove(p_new_path) != OK) {
+ if (file_exists(new_path)) {
+ if (remove(new_path) != OK) {
return FAILED;
}
}
- return ::_wrename((LPCWSTR)(p_path.utf16().get_data()), (LPCWSTR)(p_new_path.utf16().get_data())) == 0 ? OK : FAILED;
+ return MoveFileW((LPCWSTR)(path.utf16().get_data()), (LPCWSTR)(p_new_path.utf16().get_data())) != 0 ? OK : FAILED;
}
}
Error DirAccessWindows::remove(String p_path) {
- if (p_path.is_relative_path()) {
- p_path = get_current_dir().path_join(p_path);
- }
-
- p_path = fix_path(p_path);
+ String path = fix_path(p_path);
+ const Char16String &path_utf16 = path.utf16();
DWORD fileAttr;
- fileAttr = GetFileAttributesW((LPCWSTR)(p_path.utf16().get_data()));
+ fileAttr = GetFileAttributesW((LPCWSTR)(path_utf16.get_data()));
if (INVALID_FILE_ATTRIBUTES == fileAttr) {
return FAILED;
}
if ((fileAttr & FILE_ATTRIBUTE_DIRECTORY)) {
- return ::_wrmdir((LPCWSTR)(p_path.utf16().get_data())) == 0 ? OK : FAILED;
+ return RemoveDirectoryW((LPCWSTR)(path_utf16.get_data())) != 0 ? OK : FAILED;
} else {
- return ::_wunlink((LPCWSTR)(p_path.utf16().get_data())) == 0 ? OK : FAILED;
+ return DeleteFileW((LPCWSTR)(path_utf16.get_data())) != 0 ? OK : FAILED;
}
}
@@ -339,16 +336,16 @@ uint64_t DirAccessWindows::get_space_left() {
}
String DirAccessWindows::get_filesystem_type() const {
- String path = fix_path(const_cast<DirAccessWindows *>(this)->get_current_dir());
-
- int unit_end = path.find(":");
- ERR_FAIL_COND_V(unit_end == -1, String());
- String unit = path.substr(0, unit_end + 1) + "\\";
+ String path = current_dir.trim_prefix(R"(\\?\)");
if (path.is_network_share_path()) {
return "Network Share";
}
+ int unit_end = path.find(":");
+ ERR_FAIL_COND_V(unit_end == -1, String());
+ String unit = path.substr(0, unit_end + 1) + "\\";
+
WCHAR szVolumeName[100];
WCHAR szFileSystemName[10];
DWORD dwSerialNumber = 0;
@@ -370,11 +367,7 @@ String DirAccessWindows::get_filesystem_type() const {
}
bool DirAccessWindows::is_case_sensitive(const String &p_path) const {
- String f = p_path;
- if (!f.is_absolute_path()) {
- f = get_current_dir().path_join(f);
- }
- f = fix_path(f);
+ String f = fix_path(p_path);
HANDLE h_file = ::CreateFileW((LPCWSTR)(f.utf16().get_data()), 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
@@ -397,12 +390,7 @@ bool DirAccessWindows::is_case_sensitive(const String &p_path) const {
}
bool DirAccessWindows::is_link(String p_file) {
- String f = p_file;
-
- if (!f.is_absolute_path()) {
- f = get_current_dir().path_join(f);
- }
- f = fix_path(f);
+ String f = fix_path(p_file);
DWORD attr = GetFileAttributesW((LPCWSTR)(f.utf16().get_data()));
if (attr == INVALID_FILE_ATTRIBUTES) {
@@ -413,12 +401,7 @@ bool DirAccessWindows::is_link(String p_file) {
}
String DirAccessWindows::read_link(String p_file) {
- String f = p_file;
-
- if (!f.is_absolute_path()) {
- f = get_current_dir().path_join(f);
- }
- f = fix_path(f);
+ String f = fix_path(p_file);
HANDLE hfile = CreateFileW((LPCWSTR)(f.utf16().get_data()), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
if (hfile == INVALID_HANDLE_VALUE) {
@@ -434,22 +417,18 @@ String DirAccessWindows::read_link(String p_file) {
GetFinalPathNameByHandleW(hfile, (LPWSTR)cs.ptrw(), ret, VOLUME_NAME_DOS | FILE_NAME_NORMALIZED);
CloseHandle(hfile);
- return String::utf16((const char16_t *)cs.ptr(), ret).trim_prefix(R"(\\?\)");
+ return String::utf16((const char16_t *)cs.ptr(), ret).trim_prefix(R"(\\?\)").replace("\\", "/");
}
Error DirAccessWindows::create_link(String p_source, String p_target) {
- if (p_target.is_relative_path()) {
- p_target = get_current_dir().path_join(p_target);
- }
+ String source = fix_path(p_source);
+ String target = fix_path(p_target);
- p_source = fix_path(p_source);
- p_target = fix_path(p_target);
-
- DWORD file_attr = GetFileAttributesW((LPCWSTR)(p_source.utf16().get_data()));
+ DWORD file_attr = GetFileAttributesW((LPCWSTR)(source.utf16().get_data()));
bool is_dir = (file_attr & FILE_ATTRIBUTE_DIRECTORY);
DWORD flags = ((is_dir) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0) | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
- if (CreateSymbolicLinkW((LPCWSTR)p_target.utf16().get_data(), (LPCWSTR)p_source.utf16().get_data(), flags) != 0) {
+ if (CreateSymbolicLinkW((LPCWSTR)target.utf16().get_data(), (LPCWSTR)source.utf16().get_data(), flags) != 0) {
return OK;
} else {
return FAILED;
@@ -459,7 +438,12 @@ Error DirAccessWindows::create_link(String p_source, String p_target) {
DirAccessWindows::DirAccessWindows() {
p = memnew(DirAccessWindowsPrivate);
p->h = INVALID_HANDLE_VALUE;
- current_dir = ".";
+
+ Char16String real_current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ real_current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(real_current_dir_name.size(), (LPWSTR)real_current_dir_name.ptrw());
+ current_dir = String::utf16((const char16_t *)real_current_dir_name.get_data());
DWORD mask = GetLogicalDrives();
diff --git a/drivers/windows/file_access_windows.cpp b/drivers/windows/file_access_windows.cpp
index 9885d9d7ee..f6f721639c 100644
--- a/drivers/windows/file_access_windows.cpp
+++ b/drivers/windows/file_access_windows.cpp
@@ -73,8 +73,18 @@ bool FileAccessWindows::is_path_invalid(const String &p_path) {
String FileAccessWindows::fix_path(const String &p_path) const {
String r_path = FileAccess::fix_path(p_path);
- if (r_path.is_absolute_path() && !r_path.is_network_share_path() && r_path.length() > MAX_PATH) {
- r_path = "\\\\?\\" + r_path.replace("/", "\\");
+
+ if (r_path.is_relative_path()) {
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+ r_path = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/").path_join(r_path);
+ }
+ r_path = r_path.simplify_path();
+ r_path = r_path.replace("/", "\\");
+ if (!r_path.is_network_share_path() && !r_path.begins_with(R"(\\?\)")) {
+ r_path = R"(\\?\)" + r_path;
}
return r_path;
}
@@ -108,9 +118,6 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) {
return ERR_INVALID_PARAMETER;
}
- /* Pretty much every implementation that uses fopen as primary
- backend supports utf8 encoding. */
-
struct _stat st;
if (_wstat((LPCWSTR)(path.utf16().get_data()), &st) == 0) {
if (!S_ISREG(st.st_mode)) {
@@ -125,7 +132,7 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) {
// platforms), we only check for relative paths, or paths in res:// or user://,
// other paths aren't likely to be portable anyway.
if (p_mode_flags == READ && (p_path.is_relative_path() || get_access_type() != ACCESS_FILESYSTEM)) {
- String base_path = path;
+ String base_path = p_path;
String working_path;
String proper_path;
@@ -144,23 +151,17 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) {
}
proper_path = "user://";
}
+ working_path = fix_path(working_path);
WIN32_FIND_DATAW d;
- Vector<String> parts = base_path.split("/");
+ Vector<String> parts = base_path.simplify_path().split("/");
bool mismatch = false;
for (const String &part : parts) {
- working_path = working_path.path_join(part);
-
- // Skip if relative.
- if (part == "." || part == "..") {
- proper_path = proper_path.path_join(part);
- continue;
- }
+ working_path = working_path + "\\" + part;
HANDLE fnd = FindFirstFileW((LPCWSTR)(working_path.utf16().get_data()), &d);
-
if (fnd == INVALID_HANDLE_VALUE) {
mismatch = false;
break;
@@ -186,12 +187,22 @@ Error FileAccessWindows::open_internal(const String &p_path, int p_mode_flags) {
if (is_backup_save_enabled() && p_mode_flags == WRITE) {
save_path = path;
// Create a temporary file in the same directory as the target file.
- WCHAR tmpFileName[MAX_PATH];
- if (GetTempFileNameW((LPCWSTR)(path.get_base_dir().utf16().get_data()), (LPCWSTR)(path.get_file().utf16().get_data()), 0, tmpFileName) == 0) {
- last_error = ERR_FILE_CANT_OPEN;
- return last_error;
+ // Note: do not use GetTempFileNameW, it's not long path aware!
+ String tmpfile;
+ uint64_t id = OS::get_singleton()->get_ticks_usec();
+ while (true) {
+ tmpfile = path + itos(id++) + ".tmp";
+ HANDLE handle = CreateFileW((LPCWSTR)tmpfile.utf16().get_data(), GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, 0);
+ if (handle != INVALID_HANDLE_VALUE) {
+ CloseHandle(handle);
+ break;
+ }
+ if (GetLastError() != ERROR_FILE_EXISTS && GetLastError() != ERROR_SHARING_VIOLATION) {
+ last_error = ERR_FILE_CANT_WRITE;
+ return FAILED;
+ }
}
- path = tmpFileName;
+ path = tmpfile;
}
f = _wfsopen((LPCWSTR)(path.utf16().get_data()), mode_string, is_backup_save_enabled() ? _SH_SECURE : _SH_DENYNO);
@@ -235,7 +246,7 @@ void FileAccessWindows::_close() {
} else {
// Either the target exists and is locked (temporarily, hopefully)
// or it doesn't exist; let's assume the latter before re-trying.
- rename_error = _wrename((LPCWSTR)(path_utf16.get_data()), (LPCWSTR)(save_path_utf16.get_data())) != 0;
+ rename_error = MoveFileW((LPCWSTR)(path_utf16.get_data()), (LPCWSTR)(save_path_utf16.get_data())) == 0;
}
if (!rename_error) {
@@ -262,7 +273,7 @@ String FileAccessWindows::get_path() const {
}
String FileAccessWindows::get_path_absolute() const {
- return path;
+ return path.trim_prefix(R"(\\?\)").replace("\\", "/");
}
bool FileAccessWindows::is_open() const {
@@ -548,10 +559,11 @@ uint64_t FileAccessWindows::_get_modified_time(const String &p_file) {
return 0;
}
- String file = fix_path(p_file);
+ String file = p_file;
if (file.ends_with("/") && file != "/") {
file = file.substr(0, file.length() - 1);
}
+ file = fix_path(file);
struct _stat st;
int rv = _wstat((LPCWSTR)(file.utf16().get_data()), &st);
@@ -582,14 +594,15 @@ bool FileAccessWindows::_get_hidden_attribute(const String &p_file) {
Error FileAccessWindows::_set_hidden_attribute(const String &p_file, bool p_hidden) {
String file = fix_path(p_file);
+ const Char16String &file_utf16 = file.utf16();
- DWORD attrib = GetFileAttributesW((LPCWSTR)file.utf16().get_data());
+ DWORD attrib = GetFileAttributesW((LPCWSTR)file_utf16.get_data());
ERR_FAIL_COND_V_MSG(attrib == INVALID_FILE_ATTRIBUTES, FAILED, "Failed to get attributes for: " + p_file);
BOOL ok;
if (p_hidden) {
- ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib | FILE_ATTRIBUTE_HIDDEN);
+ ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib | FILE_ATTRIBUTE_HIDDEN);
} else {
- ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib & ~FILE_ATTRIBUTE_HIDDEN);
+ ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib & ~FILE_ATTRIBUTE_HIDDEN);
}
ERR_FAIL_COND_V_MSG(!ok, FAILED, "Failed to set attributes for: " + p_file);
@@ -606,14 +619,15 @@ bool FileAccessWindows::_get_read_only_attribute(const String &p_file) {
Error FileAccessWindows::_set_read_only_attribute(const String &p_file, bool p_ro) {
String file = fix_path(p_file);
+ const Char16String &file_utf16 = file.utf16();
- DWORD attrib = GetFileAttributesW((LPCWSTR)file.utf16().get_data());
+ DWORD attrib = GetFileAttributesW((LPCWSTR)file_utf16.get_data());
ERR_FAIL_COND_V_MSG(attrib == INVALID_FILE_ATTRIBUTES, FAILED, "Failed to get attributes for: " + p_file);
BOOL ok;
if (p_ro) {
- ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib | FILE_ATTRIBUTE_READONLY);
+ ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib | FILE_ATTRIBUTE_READONLY);
} else {
- ok = SetFileAttributesW((LPCWSTR)file.utf16().get_data(), attrib & ~FILE_ATTRIBUTE_READONLY);
+ ok = SetFileAttributesW((LPCWSTR)file_utf16.get_data(), attrib & ~FILE_ATTRIBUTE_READONLY);
}
ERR_FAIL_COND_V_MSG(!ok, FAILED, "Failed to set attributes for: " + p_file);
diff --git a/editor/editor_file_system.cpp b/editor/editor_file_system.cpp
index bcfc29f7a3..474a45cf2b 100644
--- a/editor/editor_file_system.cpp
+++ b/editor/editor_file_system.cpp
@@ -1988,7 +1988,7 @@ void EditorFileSystem::_update_scene_groups() {
}
if (ep) {
- ep->step(efd->files[index]->file, step_count++);
+ ep->step(efd->files[index]->file, step_count++, false);
}
}
@@ -2706,6 +2706,16 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
EditorProgress *ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size()));
+ // The method reimport_files runs on the main thread, and if VSync is enabled
+ // or Update Continuously is disabled, Main::Iteration takes longer each frame.
+ // Each EditorProgress::step can trigger a redraw, and when there are many files to import,
+ // this could lead to a slow import process, especially when the editor is unfocused.
+ // Temporarily disabling VSync and low_processor_usage_mode while reimporting fixes this.
+ const bool old_low_processor_usage_mode = OS::get_singleton()->is_in_low_processor_usage_mode();
+ const DisplayServer::VSyncMode old_vsync_mode = DisplayServer::get_singleton()->window_get_vsync_mode(DisplayServer::MAIN_WINDOW_ID);
+ OS::get_singleton()->set_low_processor_usage_mode(false);
+ DisplayServer::get_singleton()->window_set_vsync_mode(DisplayServer::VSyncMode::VSYNC_DISABLED);
+
Vector<ImportFile> reimport_files;
HashSet<String> groups_to_reimport;
@@ -2836,6 +2846,7 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
}
}
}
+ ep->step(TTR("Finalizing Asset Import..."), p_files.size());
ResourceUID::get_singleton()->update_cache(); // After reimporting, update the cache.
_save_filesystem_cache();
@@ -2843,6 +2854,11 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
memdelete_notnull(ep);
_process_update_pending();
+
+ // Revert to previous values to restore editor settings for VSync and Update Continuously.
+ OS::get_singleton()->set_low_processor_usage_mode(old_low_processor_usage_mode);
+ DisplayServer::get_singleton()->window_set_vsync_mode(old_vsync_mode);
+
importing = false;
ep = memnew(EditorProgress("reimport", TTR("(Re)Importing Assets"), p_files.size()));
diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp
index d58d0520cc..f5d016629f 100644
--- a/editor/editor_properties_array_dict.cpp
+++ b/editor/editor_properties_array_dict.cpp
@@ -644,6 +644,8 @@ void EditorPropertyArray::_notification(int p_what) {
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_ENTER_TREE: {
change_type->clear();
+ change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX);
+ change_type->add_separator();
for (int i = 0; i < Variant::VARIANT_MAX; i++) {
if (i == Variant::CALLABLE || i == Variant::SIGNAL || i == Variant::RID) {
// These types can't be constructed or serialized properly, so skip them.
@@ -653,8 +655,6 @@ void EditorPropertyArray::_notification(int p_what) {
String type = Variant::get_type_name(Variant::Type(i));
change_type->add_icon_item(get_editor_theme_icon(type), type, i);
}
- change_type->add_separator();
- change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX);
if (button_add_item) {
button_add_item->set_icon(get_editor_theme_icon(SNAME("Add")));
@@ -1117,6 +1117,8 @@ void EditorPropertyDictionary::_notification(int p_what) {
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_ENTER_TREE: {
change_type->clear();
+ change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX);
+ change_type->add_separator();
for (int i = 0; i < Variant::VARIANT_MAX; i++) {
if (i == Variant::CALLABLE || i == Variant::SIGNAL || i == Variant::RID) {
// These types can't be constructed or serialized properly, so skip them.
@@ -1126,8 +1128,6 @@ void EditorPropertyDictionary::_notification(int p_what) {
String type = Variant::get_type_name(Variant::Type(i));
change_type->add_icon_item(get_editor_theme_icon(type), type, i);
}
- change_type->add_separator();
- change_type->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Remove Item"), Variant::VARIANT_MAX);
if (button_add_item) {
button_add_item->set_icon(get_editor_theme_icon(SNAME("Add")));
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index c6ac8050ac..b5c11b574e 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -827,6 +827,12 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
+ int default_play_window_pip_mode = 0;
+#ifdef ANDROID_ENABLED
+ default_play_window_pip_mode = 2;
+#endif
+ EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2")
+
// Auto save
_initial_set("run/auto_save/save_before_running", true);
diff --git a/editor/import/resource_importer_wav.cpp b/editor/import/resource_importer_wav.cpp
index 6d3d474cee..ce3411bf41 100644
--- a/editor/import/resource_importer_wav.cpp
+++ b/editor/import/resource_importer_wav.cpp
@@ -517,16 +517,19 @@ Error ResourceImporterWAV::import(const String &p_source_file, const String &p_s
Vector<uint8_t> dst_data;
if (compression == 2) {
dst_format = AudioStreamWAV::FORMAT_QOA;
- qoa_desc desc = { 0, 0, 0, { { { 0 }, { 0 } } } };
+ qoa_desc desc = {};
uint32_t qoa_len = 0;
desc.samplerate = rate;
desc.samples = frames;
desc.channels = format_channels;
- void *encoded = qoa_encode((short *)pcm_data.ptrw(), &desc, &qoa_len);
- dst_data.resize(qoa_len);
- memcpy(dst_data.ptrw(), encoded, qoa_len);
+ void *encoded = qoa_encode((short *)pcm_data.ptr(), &desc, &qoa_len);
+ if (encoded) {
+ dst_data.resize(qoa_len);
+ memcpy(dst_data.ptrw(), encoded, qoa_len);
+ QOA_FREE(encoded);
+ }
} else {
dst_data = pcm_data;
}
diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp
index c58109427b..f0be8791d3 100644
--- a/editor/plugins/node_3d_editor_plugin.cpp
+++ b/editor/plugins/node_3d_editor_plugin.cpp
@@ -770,7 +770,7 @@ void Node3DEditorViewport::_select_clicked(bool p_allow_locked) {
}
}
- if (p_allow_locked || !_is_node_locked(selected)) {
+ if (p_allow_locked || (selected != nullptr && !_is_node_locked(selected))) {
if (clicked_wants_append) {
if (editor_selection->is_selected(selected)) {
editor_selection->remove_node(selected);
@@ -4058,6 +4058,14 @@ void Node3DEditorViewport::set_state(const Dictionary &p_state) {
_menu_option(VIEW_GIZMOS);
}
}
+ if (p_state.has("transform_gizmo")) {
+ bool transform_gizmo = p_state["transform_gizmo"];
+
+ int idx = view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO);
+ if (view_menu->get_popup()->is_item_checked(idx) != transform_gizmo) {
+ _menu_option(VIEW_TRANSFORM_GIZMO);
+ }
+ }
if (p_state.has("grid")) {
bool grid = p_state["grid"];
@@ -4144,6 +4152,7 @@ Dictionary Node3DEditorViewport::get_state() const {
d["listener"] = viewport->is_audio_listener_3d();
d["doppler"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_AUDIO_DOPPLER));
d["gizmos"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_GIZMOS));
+ d["transform_gizmo"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_TRANSFORM_GIZMO));
d["grid"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_GRID));
d["information"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_INFORMATION));
d["frame_time"] = view_menu->get_popup()->is_item_checked(view_menu->get_popup()->get_item_index(VIEW_FRAME_TIME));
diff --git a/editor/progress_dialog.cpp b/editor/progress_dialog.cpp
index c21723d1ba..2f345e5161 100644
--- a/editor/progress_dialog.cpp
+++ b/editor/progress_dialog.cpp
@@ -202,14 +202,13 @@ void ProgressDialog::add_task(const String &p_task, const String &p_label, int p
bool ProgressDialog::task_step(const String &p_task, const String &p_state, int p_step, bool p_force_redraw) {
ERR_FAIL_COND_V(!tasks.has(p_task), canceled);
+ Task &t = tasks[p_task];
if (!p_force_redraw) {
uint64_t tus = OS::get_singleton()->get_ticks_usec();
- if (tus - last_progress_tick < 200000) { //200ms
+ if (tus - t.last_progress_tick < 200000) { //200ms
return canceled;
}
}
-
- Task &t = tasks[p_task];
if (p_step < 0) {
t.progress->set_value(t.progress->get_value() + 1);
} else {
@@ -217,7 +216,7 @@ bool ProgressDialog::task_step(const String &p_task, const String &p_state, int
}
t.state->set_text(p_state);
- last_progress_tick = OS::get_singleton()->get_ticks_usec();
+ t.last_progress_tick = OS::get_singleton()->get_ticks_usec();
_update_ui();
return canceled;
@@ -252,7 +251,6 @@ ProgressDialog::ProgressDialog() {
main->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
set_exclusive(true);
set_flag(Window::FLAG_POPUP, false);
- last_progress_tick = 0;
singleton = this;
cancel_hb = memnew(HBoxContainer);
main->add_child(cancel_hb);
diff --git a/editor/progress_dialog.h b/editor/progress_dialog.h
index 82d59219da..355812b0b7 100644
--- a/editor/progress_dialog.h
+++ b/editor/progress_dialog.h
@@ -71,13 +71,13 @@ class ProgressDialog : public PopupPanel {
VBoxContainer *vb = nullptr;
ProgressBar *progress = nullptr;
Label *state = nullptr;
+ uint64_t last_progress_tick = 0;
};
HBoxContainer *cancel_hb = nullptr;
Button *cancel = nullptr;
HashMap<String, Task> tasks;
VBoxContainer *main = nullptr;
- uint64_t last_progress_tick;
LocalVector<Window *> host_windows;
diff --git a/godot.manifest b/godot.manifest
index 30b80aff25..17b588cafd 100644
--- a/godot.manifest
+++ b/godot.manifest
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
+ <ws2:longPathAware>true</ws2:longPathAware>
+ </windowsSettings>
+ </application>
<dependency>
<dependentAssembly>
<assemblyIdentity
diff --git a/methods.py b/methods.py
index 16ef0e126e..6d81b35aff 100644
--- a/methods.py
+++ b/methods.py
@@ -495,6 +495,8 @@ def use_windows_spawn_fix(self, platform=None):
rv = proc.wait()
if rv:
print_error(err)
+ elif len(err) > 0 and not err.isspace():
+ print(err)
return rv
def mySpawn(sh, escape, cmd, args, env):
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index e98cae765b..7e29a9c0fe 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -418,6 +418,12 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c
return err;
}
+#ifdef DEBUG_ENABLED
+ if (!parser->_is_tool && ext_parser->get_parser()->_is_tool) {
+ parser->push_warning(p_class, GDScriptWarning::MISSING_TOOL);
+ }
+#endif
+
base = ext_parser->get_parser()->head->get_datatype();
} else {
if (p_class->extends.is_empty()) {
@@ -445,6 +451,13 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c
push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), id);
return err;
}
+
+#ifdef DEBUG_ENABLED
+ if (!parser->_is_tool && base_parser->get_parser()->_is_tool) {
+ parser->push_warning(p_class, GDScriptWarning::MISSING_TOOL);
+ }
+#endif
+
base = base_parser->get_parser()->head->get_datatype();
}
} else if (ProjectSettings::get_singleton()->has_autoload(name) && ProjectSettings::get_singleton()->get_autoload(name).is_singleton) {
@@ -465,6 +478,13 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c
push_error(vformat(R"(Could not resolve super class inheritance from "%s".)", name), id);
return err;
}
+
+#ifdef DEBUG_ENABLED
+ if (!parser->_is_tool && info_parser->get_parser()->_is_tool) {
+ parser->push_warning(p_class, GDScriptWarning::MISSING_TOOL);
+ }
+#endif
+
base = info_parser->get_parser()->head->get_datatype();
} else if (class_exists(name)) {
if (Engine::get_singleton()->has_singleton(name)) {
diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp
index e8fb1d94b3..4ffb4bd9d1 100644
--- a/modules/gdscript/gdscript_warning.cpp
+++ b/modules/gdscript/gdscript_warning.cpp
@@ -109,6 +109,8 @@ String GDScriptWarning::get_message() const {
case STATIC_CALLED_ON_INSTANCE:
CHECK_SYMBOLS(2);
return vformat(R"*(The function "%s()" is a static function but was called from an instance. Instead, it should be directly called from the type: "%s.%s()".)*", symbols[0], symbols[1], symbols[0]);
+ case MISSING_TOOL:
+ return R"(The base class script has the "@tool" annotation, but this script does not have it.)";
case REDUNDANT_STATIC_UNLOAD:
return R"(The "@static_unload" annotation is redundant because the file does not have a class with static variables.)";
case REDUNDANT_AWAIT:
@@ -219,6 +221,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
"UNSAFE_VOID_RETURN",
"RETURN_VALUE_DISCARDED",
"STATIC_CALLED_ON_INSTANCE",
+ "MISSING_TOOL",
"REDUNDANT_STATIC_UNLOAD",
"REDUNDANT_AWAIT",
"ASSERT_ALWAYS_TRUE",
diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h
index 1c806bb4e2..ffcf00a830 100644
--- a/modules/gdscript/gdscript_warning.h
+++ b/modules/gdscript/gdscript_warning.h
@@ -70,6 +70,7 @@ public:
UNSAFE_VOID_RETURN, // Function returns void but returned a call to a function that can't be type checked.
RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used.
STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself.
+ MISSING_TOOL, // The base class script has the "@tool" annotation, but this script does not have it.
REDUNDANT_STATIC_UNLOAD, // The `@static_unload` annotation is used but the class does not have static data.
REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine).
ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true.
@@ -123,6 +124,7 @@ public:
WARN, // UNSAFE_VOID_RETURN
IGNORE, // RETURN_VALUE_DISCARDED // Too spammy by default on common cases (connect, Tween, etc.).
WARN, // STATIC_CALLED_ON_INSTANCE
+ WARN, // MISSING_TOOL
WARN, // REDUNDANT_STATIC_UNLOAD
WARN, // REDUNDANT_AWAIT
WARN, // ASSERT_ALWAYS_TRUE
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd
new file mode 100644
index 0000000000..95d497c3f3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.gd
@@ -0,0 +1,7 @@
+extends "./non_tool_extends_tool.notest.gd"
+
+class InnerClass extends "./non_tool_extends_tool.notest.gd":
+ pass
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd
new file mode 100644
index 0000000000..07427846d1
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.notest.gd
@@ -0,0 +1 @@
+@tool
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out
new file mode 100644
index 0000000000..f65caf5222
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool.out
@@ -0,0 +1,9 @@
+GDTEST_OK
+>> WARNING
+>> Line: 1
+>> MISSING_TOOL
+>> The base class script has the "@tool" annotation, but this script does not have it.
+>> WARNING
+>> Line: 3
+>> MISSING_TOOL
+>> The base class script has the "@tool" annotation, but this script does not have it.
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd
new file mode 100644
index 0000000000..a452307d99
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.gd
@@ -0,0 +1,9 @@
+@warning_ignore("missing_tool")
+extends "./non_tool_extends_tool.notest.gd"
+
+@warning_ignore("missing_tool")
+class InnerClass extends "./non_tool_extends_tool.notest.gd":
+ pass
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out
new file mode 100644
index 0000000000..d73c5eb7cd
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/non_tool_extends_tool_ignored.out
@@ -0,0 +1 @@
+GDTEST_OK
diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml
index c7d14a3f49..a875745860 100644
--- a/platform/android/java/editor/src/main/AndroidManifest.xml
+++ b/platform/android/java/editor/src/main/AndroidManifest.xml
@@ -42,6 +42,7 @@
android:name=".GodotEditor"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:exported="true"
+ android:icon="@mipmap/icon"
android:launchMode="singleTask"
android:screenOrientation="userLandscape">
<layout
@@ -59,9 +60,11 @@
android:name=".GodotGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:exported="false"
- android:label="@string/godot_project_name_string"
+ android:icon="@mipmap/ic_play_window"
+ android:label="@string/godot_game_activity_name"
android:launchMode="singleTask"
android:process=":GodotGame"
+ android:supportsPictureInPicture="true"
android:screenOrientation="userLandscape">
<layout
android:defaultWidth="@dimen/editor_default_window_width"
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
new file mode 100644
index 0000000000..ba1185d647
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
@@ -0,0 +1,192 @@
+/**************************************************************************/
+/* EditorMessageDispatcher.kt */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+package org.godotengine.editor
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Message
+import android.os.Messenger
+import android.os.RemoteException
+import android.util.Log
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Used by the [GodotEditor] classes to dispatch messages across processes.
+ */
+internal class EditorMessageDispatcher(private val editor: GodotEditor) {
+
+ companion object {
+ private val TAG = EditorMessageDispatcher::class.java.simpleName
+
+ /**
+ * Extra used to pass the message dispatcher payload through an [Intent]
+ */
+ const val EXTRA_MSG_DISPATCHER_PAYLOAD = "message_dispatcher_payload"
+
+ /**
+ * Key used to pass the editor id through a [Bundle]
+ */
+ private const val KEY_EDITOR_ID = "editor_id"
+
+ /**
+ * Key used to pass the editor messenger through a [Bundle]
+ */
+ private const val KEY_EDITOR_MESSENGER = "editor_messenger"
+
+ /**
+ * Requests the recipient to quit right away.
+ */
+ private const val MSG_FORCE_QUIT = 0
+
+ /**
+ * Requests the recipient to store the passed [android.os.Messenger] instance.
+ */
+ private const val MSG_REGISTER_MESSENGER = 1
+ }
+
+ private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>()
+
+ @SuppressLint("HandlerLeak")
+ private val dispatcherHandler = object : Handler() {
+ override fun handleMessage(msg: Message) {
+ when (msg.what) {
+ MSG_FORCE_QUIT -> editor.finish()
+
+ MSG_REGISTER_MESSENGER -> {
+ val editorId = msg.arg1
+ val messenger = msg.replyTo
+ registerMessenger(editorId, messenger)
+ }
+
+ else -> super.handleMessage(msg)
+ }
+ }
+ }
+
+ /**
+ * Request the window with the given [editorId] to force quit.
+ */
+ fun requestForceQuit(editorId: Int): Boolean {
+ val messenger = recipientsMessengers[editorId] ?: return false
+ return try {
+ Log.v(TAG, "Requesting 'forceQuit' for $editorId")
+ val msg = Message.obtain(null, MSG_FORCE_QUIT)
+ messenger.send(msg)
+ true
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
+ recipientsMessengers.remove(editorId)
+ false
+ }
+ }
+
+ /**
+ * Utility method to register a receiver messenger.
+ */
+ private fun registerMessenger(editorId: Int, messenger: Messenger?, messengerDeathCallback: Runnable? = null) {
+ try {
+ if (messenger == null) {
+ Log.w(TAG, "Invalid 'replyTo' payload")
+ } else if (messenger.binder.isBinderAlive) {
+ messenger.binder.linkToDeath({
+ Log.v(TAG, "Removing messenger for $editorId")
+ recipientsMessengers.remove(editorId)
+ messengerDeathCallback?.run()
+ }, 0)
+ recipientsMessengers[editorId] = messenger
+ }
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Unable to register messenger from $editorId", e)
+ recipientsMessengers.remove(editorId)
+ }
+ }
+
+ /**
+ * Utility method to register a [Messenger] attached to this handler with a host.
+ *
+ * This is done so that the host can send request to the editor instance attached to this handle.
+ *
+ * Note that this is only done when the editor instance is internal (not exported) to prevent
+ * arbitrary apps from having the ability to send requests.
+ */
+ private fun registerSelfTo(pm: PackageManager, host: Messenger?, selfId: Int) {
+ try {
+ if (host == null || !host.binder.isBinderAlive) {
+ Log.v(TAG, "Host is unavailable")
+ return
+ }
+
+ val activityInfo = pm.getActivityInfo(editor.componentName, 0)
+ if (activityInfo.exported) {
+ Log.v(TAG, "Not registering self to host as we're exported")
+ return
+ }
+
+ Log.v(TAG, "Registering self $selfId to host")
+ val msg = Message.obtain(null, MSG_REGISTER_MESSENGER)
+ msg.arg1 = selfId
+ msg.replyTo = Messenger(dispatcherHandler)
+ host.send(msg)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Unable to register self with host", e)
+ }
+ }
+
+ /**
+ * Parses the starting intent and retrieve an editor messenger if available
+ */
+ fun parseStartIntent(pm: PackageManager, intent: Intent) {
+ val messengerBundle = intent.getBundleExtra(EXTRA_MSG_DISPATCHER_PAYLOAD) ?: return
+
+ // Retrieve the sender messenger payload and store it. This can be used to communicate back
+ // to the sender.
+ val senderId = messengerBundle.getInt(KEY_EDITOR_ID)
+ val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER)
+ registerMessenger(senderId, senderMessenger)
+
+ // Register ourselves to the sender so that it can communicate with us.
+ registerSelfTo(pm, senderMessenger, editor.getEditorId())
+ }
+
+ /**
+ * Returns the payload used by the [EditorMessageDispatcher] class to establish an IPC bridge
+ * across editor instances.
+ */
+ fun getMessageDispatcherPayload(): Bundle {
+ return Bundle().apply {
+ putInt(KEY_EDITOR_ID, editor.getEditorId())
+ putParcelable(KEY_EDITOR_MESSENGER, Messenger(dispatcherHandler))
+ }
+ }
+}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
index 0da1d01aed..d3daa1dbbc 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
@@ -31,23 +31,24 @@
package org.godotengine.editor
/**
- * Specifies the policy for adjacent launches.
+ * Specifies the policy for launches.
*/
-enum class LaunchAdjacentPolicy {
+enum class LaunchPolicy {
/**
- * Adjacent launches are disabled.
+ * Launch policy is determined by the editor settings or based on the device and screen metrics.
*/
- DISABLED,
+ AUTO,
+
/**
- * Adjacent launches are enabled / disabled based on the device and screen metrics.
+ * Launches happen in the same window.
*/
- AUTO,
+ SAME,
/**
* Adjacent launches are enabled.
*/
- ENABLED
+ ADJACENT
}
/**
@@ -57,12 +58,14 @@ data class EditorWindowInfo(
val windowClassName: String,
val windowId: Int,
val processNameSuffix: String,
- val launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED
+ val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
+ val supportsPiPMode: Boolean = false
) {
constructor(
windowClass: Class<*>,
windowId: Int,
processNameSuffix: String,
- launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED
- ) : this(windowClass.name, windowId, processNameSuffix, launchAdjacentPolicy)
+ launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
+ supportsPiPMode: Boolean = false
+ ) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode)
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
index 9cc133046b..5d6da06f97 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
@@ -32,6 +32,7 @@ package org.godotengine.editor
import android.Manifest
import android.app.ActivityManager
+import android.app.ActivityOptions
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -69,17 +70,24 @@ open class GodotEditor : GodotActivity() {
private const val WAIT_FOR_DEBUGGER = false
- private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
+ @JvmStatic
+ protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
+ @JvmStatic
+ protected val EXTRA_PIP_AVAILABLE = "pip_available"
+ @JvmStatic
+ protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested"
// Command line arguments
private const val EDITOR_ARG = "--editor"
private const val EDITOR_ARG_SHORT = "-e"
private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
+ private const val BREAKPOINTS_ARG = "--breakpoints"
+ private const val BREAKPOINTS_ARG_SHORT = "-b"
// Info for the various classes used by the editor
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
- internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchAdjacentPolicy.AUTO)
+ internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true)
/**
* Sets of constants to specify the window to use to run the project.
@@ -90,13 +98,26 @@ open class GodotEditor : GodotActivity() {
private const val ANDROID_WINDOW_AUTO = 0
private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1
private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2
+
+ /**
+ * Sets of constants to specify the Play window PiP mode.
+ *
+ * Should match the values in `editor/editor_settings.cpp'` for the
+ * 'run/window_placement/play_window_pip_mode' setting.
+ */
+ private const val PLAY_WINDOW_PIP_DISABLED = 0
+ private const val PLAY_WINDOW_PIP_ENABLED = 1
+ private const val PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR = 2
}
+ private val editorMessageDispatcher = EditorMessageDispatcher(this)
private val commandLineParams = ArrayList<String>()
private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
override fun getGodotAppLayout() = R.layout.godot_editor_layout
+ internal open fun getEditorId() = EDITOR_MAIN_INFO.windowId
+
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@@ -108,6 +129,8 @@ open class GodotEditor : GodotActivity() {
Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
updateCommandLineParams(params?.asList() ?: emptyList())
+ editorMessageDispatcher.parseStartIntent(packageManager, intent)
+
if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) {
Debug.waitForDebugger()
}
@@ -189,35 +212,67 @@ open class GodotEditor : GodotActivity() {
}
}
- override fun onNewGodotInstanceRequested(args: Array<String>): Int {
- val editorWindowInfo = getEditorWindowInfo(args)
-
- // Launch a new activity
+ protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent {
val newInstance = Intent()
.setComponent(ComponentName(this, editorWindowInfo.windowClassName))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- if (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.ENABLED ||
- (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.AUTO && shouldGameLaunchAdjacent())) {
+
+ val launchPolicy = resolveLaunchPolicyIfNeeded(editorWindowInfo.launchPolicy)
+ val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) {
+ val pipMode = getPlayWindowPiPMode()
+ pipMode == PLAY_WINDOW_PIP_ENABLED ||
+ (pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && launchPolicy == LaunchPolicy.SAME)
+ } else {
+ false
+ }
+ newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable)
+
+ if (launchPolicy == LaunchPolicy.ADJACENT) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Log.v(TAG, "Adding flag for adjacent launch")
newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
}
+ } else if (launchPolicy == LaunchPolicy.SAME) {
+ if (isPiPAvailable &&
+ (args.contains(BREAKPOINTS_ARG) || args.contains(BREAKPOINTS_ARG_SHORT))) {
+ Log.v(TAG, "Launching in PiP mode because of breakpoints")
+ newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, true)
+ }
}
+
+ return newInstance
+ }
+
+ override fun onNewGodotInstanceRequested(args: Array<String>): Int {
+ val editorWindowInfo = getEditorWindowInfo(args)
+
+ // Launch a new activity
+ val sourceView = godotFragment?.view
+ val activityOptions = if (sourceView == null) {
+ null
+ } else {
+ val startX = sourceView.width / 2
+ val startY = sourceView.height / 2
+ ActivityOptions.makeScaleUpAnimation(sourceView, startX, startY, 0, 0)
+ }
+
+ val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args)
if (editorWindowInfo.windowClassName == javaClass.name) {
Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
val godot = godot
if (godot != null) {
godot.destroyAndKillProcess {
- ProcessPhoenix.triggerRebirth(this, newInstance)
+ ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance)
}
} else {
- ProcessPhoenix.triggerRebirth(this, newInstance)
+ ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance)
}
} else {
Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
newInstance.putExtra(EXTRA_NEW_LAUNCH, true)
- startActivity(newInstance)
+ .putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, editorMessageDispatcher.getMessageDispatcherPayload())
+ startActivity(newInstance, activityOptions?.toBundle())
}
return editorWindowInfo.windowId
}
@@ -231,6 +286,12 @@ open class GodotEditor : GodotActivity() {
return true
}
+ // Send an inter-process message to request the target editor window to force quit.
+ if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) {
+ return true
+ }
+
+ // Fallback to killing the target process.
val processName = packageName + editorWindowInfo.processNameSuffix
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningProcesses = activityManager.runningAppProcesses
@@ -285,29 +346,65 @@ open class GodotEditor : GodotActivity() {
java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures"))
/**
- * Whether we should launch the new godot instance in an adjacent window
- * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT
+ * Retrieves the play window pip mode editor setting.
+ */
+ private fun getPlayWindowPiPMode(): Int {
+ return try {
+ Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/play_window_pip_mode"))
+ } catch (e: NumberFormatException) {
+ PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR
+ }
+ }
+
+ /**
+ * If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the
+ * editor setting or device and screen metrics.
+ *
+ * If the launch policy is [LaunchPolicy.PIP] but PIP is not supported, fallback to the default
+ * launch policy.
*/
- private fun shouldGameLaunchAdjacent(): Boolean {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- try {
- when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
- ANDROID_WINDOW_SAME_AS_EDITOR -> false
- ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> true
- else -> {
- // ANDROID_WINDOW_AUTO
- isInMultiWindowMode || isLargeScreen
+ private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy {
+ val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ isInMultiWindowMode
+ } else {
+ false
+ }
+ val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen) {
+ LaunchPolicy.ADJACENT
+ } else {
+ LaunchPolicy.SAME
+ }
+
+ return when (policy) {
+ LaunchPolicy.AUTO -> {
+ try {
+ when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
+ ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
+ ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
+ else -> {
+ // ANDROID_WINDOW_AUTO
+ defaultLaunchPolicy
+ }
}
+ } catch (e: NumberFormatException) {
+ Log.w(TAG, "Error parsing the Android window placement editor setting", e)
+ // Fall-back to the default launch policy
+ defaultLaunchPolicy
}
- } catch (e: NumberFormatException) {
- // Fall-back to the 'Auto' behavior
- isInMultiWindowMode || isLargeScreen
}
- } else {
- false
+
+ else -> {
+ policy
+ }
}
}
+ /**
+ * Returns true the if the device supports picture-in-picture (PiP)
+ */
+ protected open fun hasPiPSystemFeature() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+ packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Check if we got the MANAGE_EXTERNAL_STORAGE permission
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
index 2bcfba559c..33fcbf9030 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
@@ -30,6 +30,14 @@
package org.godotengine.editor
+import android.annotation.SuppressLint
+import android.app.PictureInPictureParams
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.View
import org.godotengine.godot.GodotLib
/**
@@ -37,7 +45,90 @@ import org.godotengine.godot.GodotLib
*/
class GodotGame : GodotEditor() {
- override fun getGodotAppLayout() = org.godotengine.godot.R.layout.godot_app_layout
+ companion object {
+ private val TAG = GodotGame::class.java.simpleName
+ }
+
+ private val gameViewSourceRectHint = Rect()
+ private val pipButton: View? by lazy {
+ findViewById(R.id.godot_pip_button)
+ }
+
+ private var pipAvailable = false
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val gameView = findViewById<View>(R.id.godot_fragment_container)
+ gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
+ gameView.getGlobalVisibleRect(gameViewSourceRectHint)
+ }
+ }
+
+ pipButton?.setOnClickListener { enterPiPMode() }
+
+ handleStartIntent(intent)
+ }
+
+ override fun onNewIntent(newIntent: Intent) {
+ super.onNewIntent(newIntent)
+ handleStartIntent(newIntent)
+ }
+
+ private fun handleStartIntent(intent: Intent) {
+ pipAvailable = intent.getBooleanExtra(EXTRA_PIP_AVAILABLE, pipAvailable)
+ updatePiPButtonVisibility()
+
+ val pipLaunchRequested = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false)
+ if (pipLaunchRequested) {
+ enterPiPMode()
+ }
+ }
+
+ private fun updatePiPButtonVisibility() {
+ pipButton?.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable && !isInPictureInPictureMode) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+
+ private fun enterPiPMode() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ builder.setSeamlessResizeEnabled(false)
+ }
+ setPictureInPictureParams(builder.build())
+ }
+
+ Log.v(TAG, "Entering PiP mode")
+ enterPictureInPictureMode()
+ }
+ }
+
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode)
+ Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
+ updatePiPButtonVisibility()
+ }
+
+ override fun onStop() {
+ super.onStop()
+
+ val isInPiPMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode
+ if (isInPiPMode && !isFinishing) {
+ // We get in this state when PiP is closed, so we terminate the activity.
+ finish()
+ }
+ }
+
+ override fun getGodotAppLayout() = R.layout.godot_game_layout
+
+ override fun getEditorId() = RUN_GAME_INFO.windowId
override fun overrideOrientationRequest() = false
diff --git a/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml b/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml
new file mode 100644
index 0000000000..41bc5475c8
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/ic_play_window_foreground.xml
@@ -0,0 +1,25 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:tint="#FFFFFF"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group
+ android:scaleX="0.522"
+ android:scaleY="0.522"
+ android:translateX="5.736"
+ android:translateY="5.736">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M21.58,16.09l-1.09,-7.66C20.21,6.46 18.52,5 16.53,5H7.47C5.48,5 3.79,6.46 3.51,8.43l-1.09,7.66C2.2,17.63 3.39,19 4.94,19h0c0.68,0 1.32,-0.27 1.8,-0.75L9,16h6l2.25,2.25c0.48,0.48 1.13,0.75 1.8,0.75h0C20.61,19 21.8,17.63 21.58,16.09zM19.48,16.81C19.4,16.9 19.27,17 19.06,17c-0.15,0 -0.29,-0.06 -0.39,-0.16L15.83,14H8.17l-2.84,2.84C5.23,16.94 5.09,17 4.94,17c-0.21,0 -0.34,-0.1 -0.42,-0.19c-0.08,-0.09 -0.16,-0.23 -0.13,-0.44l1.09,-7.66C5.63,7.74 6.48,7 7.47,7h9.06c0.99,0 1.84,0.74 1.98,1.72l1.09,7.66C19.63,16.58 19.55,16.72 19.48,16.81z" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,8l-1,0l0,2l-2,0l0,1l2,0l0,2l1,0l0,-2l2,0l0,-1l-2,0z" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M17,12m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M15,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
+ </group>
+</vector>
diff --git a/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml
new file mode 100644
index 0000000000..c8b5a15d19
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:tint="#FFFFFF"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" />
+
+</vector>
diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml
new file mode 100644
index 0000000000..aeaa96ce54
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <size
+ android:width="60dp"
+ android:height="60dp" />
+
+ <solid android:color="#44000000" />
+</shape>
diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml
new file mode 100644
index 0000000000..e9b2959275
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_pressed="true" />
+ <item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_hovered="true" />
+
+ <item android:drawable="@drawable/pip_button_default_bg_drawable" />
+
+</selector>
diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml
new file mode 100644
index 0000000000..a8919689fe
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <size
+ android:width="60dp"
+ android:height="60dp" />
+
+ <solid android:color="#13000000" />
+</shape>
diff --git a/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml b/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml
new file mode 100644
index 0000000000..d53787c87e
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/layout/godot_game_layout.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/godot_fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <ImageView
+ android:id="@+id/godot_pip_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="36dp"
+ android:contentDescription="@string/pip_button_description"
+ android:background="@drawable/pip_button_bg_drawable"
+ android:scaleType="center"
+ android:src="@drawable/outline_fullscreen_exit_48"
+ android:visibility="gone"
+ android:layout_gravity="end|top"
+ tools:visibility="visible" />
+
+</FrameLayout>
diff --git a/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml b/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml
new file mode 100644
index 0000000000..a3aabf2ee0
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/mipmap-anydpi-v26/ic_play_window.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@mipmap/icon_background"/>
+ <foreground android:drawable="@drawable/ic_play_window_foreground"/>
+</adaptive-icon>
diff --git a/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png
new file mode 100644
index 0000000000..a5ce40241f
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png
Binary files differ
diff --git a/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png
new file mode 100644
index 0000000000..147adb6127
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png
Binary files differ
diff --git a/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png
new file mode 100644
index 0000000000..0b1db1b923
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png
Binary files differ
diff --git a/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png
new file mode 100644
index 0000000000..39d7450390
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png
Binary files differ
diff --git a/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png b/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png
new file mode 100644
index 0000000000..b7a09a15b5
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png
Binary files differ
diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml
index 98bfe40179..1e486872e6 100644
--- a/platform/android/java/editor/src/main/res/values/dimens.xml
+++ b/platform/android/java/editor/src/main/res/values/dimens.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <dimen name="editor_default_window_height">600dp</dimen>
+ <dimen name="editor_default_window_height">640dp</dimen>
<dimen name="editor_default_window_width">1024dp</dimen>
</resources>
diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml
index 909711ab18..0ad54ac3a1 100644
--- a/platform/android/java/editor/src/main/res/values/strings.xml
+++ b/platform/android/java/editor/src/main/res/values/strings.xml
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+ <string name="godot_game_activity_name">Godot Play window</string>
<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
+ <string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string>
</resources>
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
index 913e3d04c5..474c6e9b2f 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt
@@ -53,8 +53,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
private val TAG = GodotActivity::class.java.simpleName
@JvmStatic
- protected val EXTRA_FORCE_QUIT = "force_quit_requested"
- @JvmStatic
protected val EXTRA_NEW_LAUNCH = "new_launch_requested"
}
@@ -128,12 +126,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
}
private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
- val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false)
- if (forceQuitRequested) {
- Log.d(TAG, "Force quit requested, terminating..")
- ProcessPhoenix.forceQuit(this)
- return
- }
if (!newLaunch) {
val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false)
if (newLaunchRequested) {
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java
index b1bce45fbb..d9afdf90b1 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java
@@ -24,6 +24,7 @@ package org.godotengine.godot.utils;
import android.app.Activity;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@@ -44,6 +45,9 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
*/
public final class ProcessPhoenix extends Activity {
private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
+ // -- GODOT start --
+ private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options";
+ // -- GODOT end --
private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
/**
@@ -56,12 +60,23 @@ public final class ProcessPhoenix extends Activity {
triggerRebirth(context, getRestartIntent(context));
}
+ // -- GODOT start --
/**
* Call to restart the application process using the specified intents.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerRebirth(Context context, Intent... nextIntents) {
+ triggerRebirth(context, null, nextIntents);
+ }
+
+ /**
+ * Call to restart the application process using the specified intents launched with the given
+ * {@link ActivityOptions}.
+ * <p>
+ * Behavior of the current process after invoking this method is undefined.
+ */
+ public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
@@ -72,10 +87,12 @@ public final class ProcessPhoenix extends Activity {
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
+ if (activityOptions != null) {
+ intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions);
+ }
context.startActivity(intent);
}
- // -- GODOT start --
/**
* Finish the activity and kill its process
*/
@@ -112,9 +129,11 @@ public final class ProcessPhoenix extends Activity {
super.onCreate(savedInstanceState);
// -- GODOT start --
- ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
- startActivities(intents.toArray(new Intent[intents.size()]));
- forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1));
+ Intent launchIntent = getIntent();
+ ArrayList<Intent> intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS);
+ Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS);
+ startActivities(intents.toArray(new Intent[intents.size()]), activityOptions);
+ forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1));
// -- GODOT end --
}
diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp
index 671da7fc2a..2b98fda0d5 100644
--- a/platform/linuxbsd/freedesktop_portal_desktop.cpp
+++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp
@@ -377,17 +377,26 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo
String flt = tokens[0].strip_edges();
if (!flt.is_empty()) {
if (tokens.size() == 2) {
- filter_exts.push_back(flt);
+ if (flt == "*.*") {
+ filter_exts.push_back("*");
+ } else {
+ filter_exts.push_back(flt);
+ }
filter_names.push_back(tokens[1]);
} else {
- filter_exts.push_back(flt);
- filter_names.push_back(flt);
+ if (flt == "*.*") {
+ filter_exts.push_back("*");
+ filter_names.push_back(RTR("All Files"));
+ } else {
+ filter_exts.push_back(flt);
+ filter_names.push_back(flt);
+ }
}
}
}
}
if (filter_names.is_empty()) {
- filter_exts.push_back("*.*");
+ filter_exts.push_back("*");
filter_names.push_back(RTR("All Files"));
}
diff --git a/platform/windows/console_wrapper_windows.cpp b/platform/windows/console_wrapper_windows.cpp
index 133711a9ea..1ba09b236b 100644
--- a/platform/windows/console_wrapper_windows.cpp
+++ b/platform/windows/console_wrapper_windows.cpp
@@ -40,8 +40,8 @@
int main(int argc, char *argv[]) {
// Get executable name.
- WCHAR exe_name[MAX_PATH] = {};
- if (!GetModuleFileNameW(nullptr, exe_name, MAX_PATH)) {
+ WCHAR exe_name[32767] = {};
+ if (!GetModuleFileNameW(nullptr, exe_name, 32767)) {
wprintf(L"GetModuleFileName failed, error %d\n", GetLastError());
return -1;
}
diff --git a/platform/windows/detect.py b/platform/windows/detect.py
index 40a8067000..82a98655c0 100644
--- a/platform/windows/detect.py
+++ b/platform/windows/detect.py
@@ -20,34 +20,26 @@ def get_name():
return "Windows"
-def try_cmd(test, prefix, arch):
+def try_cmd(test, prefix, arch, check_clang=False):
+ archs = ["x86_64", "x86_32", "arm64", "arm32"]
if arch:
+ archs = [arch]
+
+ for a in archs:
try:
out = subprocess.Popen(
- get_mingw_bin_prefix(prefix, arch) + test,
+ get_mingw_bin_prefix(prefix, a) + test,
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
- out.communicate()
+ outs, errs = out.communicate()
if out.returncode == 0:
+ if check_clang and not outs.startswith(b"clang"):
+ return False
return True
except Exception:
pass
- else:
- for a in ["x86_64", "x86_32", "arm64", "arm32"]:
- try:
- out = subprocess.Popen(
- get_mingw_bin_prefix(prefix, a) + test,
- shell=True,
- stderr=subprocess.PIPE,
- stdout=subprocess.PIPE,
- )
- out.communicate()
- if out.returncode == 0:
- return True
- except Exception:
- pass
return False
@@ -651,6 +643,10 @@ def configure_mingw(env: "SConsEnvironment"):
if env["use_llvm"] and not try_cmd("clang --version", env["mingw_prefix"], env["arch"]):
env["use_llvm"] = False
+ if not env["use_llvm"] and try_cmd("gcc --version", env["mingw_prefix"], env["arch"], True):
+ print("Detected GCC to be a wrapper for Clang.")
+ env["use_llvm"] = True
+
# TODO: Re-evaluate the need for this / streamline with common config.
if env["target"] == "template_release":
if env["arch"] != "arm64":
@@ -763,6 +759,11 @@ def configure_mingw(env: "SConsEnvironment"):
env.Append(CCFLAGS=san_flags)
env.Append(LINKFLAGS=san_flags)
+ if env["use_llvm"] and os.name == "nt" and methods._colorize:
+ env.Append(CCFLAGS=["$(-fansi-escape-codes$)", "$(-fcolor-diagnostics$)"])
+
+ env.Append(ARFLAGS=["--thin"])
+
env.Append(CPPDEFINES=["WINDOWS_ENABLED", "WASAPI_ENABLED", "WINMIDI_ENABLED"])
env.Append(
CPPDEFINES=[
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index 270112e624..8bcd556c22 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -317,7 +317,7 @@ public:
if (!lpw_path) {
return S_FALSE;
}
- String path = String::utf16((const char16_t *)lpw_path).simplify_path();
+ String path = String::utf16((const char16_t *)lpw_path).replace("\\", "/").trim_prefix(R"(\\?\)").simplify_path();
if (!path.begins_with(root.simplify_path())) {
return S_FALSE;
}
@@ -542,7 +542,26 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
pfd->SetOptions(flags | FOS_FORCEFILESYSTEM);
pfd->SetTitle((LPCWSTR)fd->title.utf16().ptr());
- String dir = fd->current_directory.replace("/", "\\");
+ String dir = ProjectSettings::get_singleton()->globalize_path(fd->current_directory);
+ if (dir == ".") {
+ dir = OS::get_singleton()->get_executable_path().get_base_dir();
+ }
+ if (dir.is_relative_path() || dir == ".") {
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+ if (dir == ".") {
+ dir = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/");
+ } else {
+ dir = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/").path_join(dir);
+ }
+ }
+ dir = dir.simplify_path();
+ dir = dir.replace("/", "\\");
+ if (!dir.is_network_share_path() && !dir.begins_with(R"(\\?\)")) {
+ dir = R"(\\?\)" + dir;
+ }
IShellItem *shellitem = nullptr;
hr = SHCreateItemFromParsingName((LPCWSTR)dir.utf16().ptr(), nullptr, IID_IShellItem, (void **)&shellitem);
@@ -585,7 +604,7 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
PWSTR file_path = nullptr;
hr = result->GetDisplayName(SIGDN_FILESYSPATH, &file_path);
if (SUCCEEDED(hr)) {
- file_names.push_back(String::utf16((const char16_t *)file_path));
+ file_names.push_back(String::utf16((const char16_t *)file_path).replace("\\", "/").trim_prefix(R"(\\?\)"));
CoTaskMemFree(file_path);
}
result->Release();
@@ -599,7 +618,7 @@ void DisplayServerWindows::_thread_fd_monitor(void *p_ud) {
PWSTR file_path = nullptr;
hr = result->GetDisplayName(SIGDN_FILESYSPATH, &file_path);
if (SUCCEEDED(hr)) {
- file_names.push_back(String::utf16((const char16_t *)file_path));
+ file_names.push_back(String::utf16((const char16_t *)file_path).replace("\\", "/").trim_prefix(R"(\\?\)"));
CoTaskMemFree(file_path);
}
result->Release();
diff --git a/platform/windows/godot_res_wrap.rc b/platform/windows/godot_res_wrap.rc
index 27ad26cbc5..61e6100497 100644
--- a/platform/windows/godot_res_wrap.rc
+++ b/platform/windows/godot_res_wrap.rc
@@ -1,6 +1,11 @@
#include "core/version.h"
+#ifndef RT_MANIFEST
+#define RT_MANIFEST 24
+#endif
+
GODOT_ICON ICON platform/windows/godot_console.ico
+1 RT_MANIFEST "godot.manifest"
1 VERSIONINFO
FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0
diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp
index 7316992b60..bc42b234be 100644
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -90,6 +90,23 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
#define GetProcAddress (void *)GetProcAddress
#endif
+static String fix_path(const String &p_path) {
+ String path = p_path;
+ if (p_path.is_relative_path()) {
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+ path = String::utf16((const char16_t *)current_dir_name.get_data()).trim_prefix(R"(\\?\)").replace("\\", "/").path_join(path);
+ }
+ path = path.simplify_path();
+ path = path.replace("/", "\\");
+ if (!path.is_network_share_path() && !path.begins_with(R"(\\?\)")) {
+ path = R"(\\?\)" + path;
+ }
+ return path;
+}
+
static String format_error_message(DWORD id) {
LPWSTR messageBuffer = nullptr;
size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
@@ -300,7 +317,7 @@ Error OS_Windows::get_entropy(uint8_t *r_buffer, int p_bytes) {
}
#ifdef DEBUG_ENABLED
-void debug_dynamic_library_check_dependencies(const String &p_root_path, const String &p_path, HashSet<String> &r_checked, HashSet<String> &r_missing) {
+void debug_dynamic_library_check_dependencies(const String &p_path, HashSet<String> &r_checked, HashSet<String> &r_missing) {
if (r_checked.has(p_path)) {
return;
}
@@ -342,15 +359,15 @@ void debug_dynamic_library_check_dependencies(const String &p_root_path, const S
const IMAGE_IMPORT_DESCRIPTOR *import_desc = (const IMAGE_IMPORT_DESCRIPTOR *)ImageDirectoryEntryToData((HMODULE)loaded_image.MappedAddress, false, IMAGE_DIRECTORY_ENTRY_IMPORT, &size);
if (import_desc) {
for (; import_desc->Name && import_desc->FirstThunk; import_desc++) {
- char16_t full_name_wc[MAX_PATH];
+ char16_t full_name_wc[32767];
const char *name_cs = (const char *)ImageRvaToVa(loaded_image.FileHeader, loaded_image.MappedAddress, import_desc->Name, nullptr);
String name = String(name_cs);
if (name.begins_with("api-ms-win-")) {
r_checked.insert(name);
- } else if (SearchPathW(nullptr, (LPCWSTR)name.utf16().get_data(), nullptr, MAX_PATH, (LPWSTR)full_name_wc, nullptr)) {
- debug_dynamic_library_check_dependencies(p_root_path, String::utf16(full_name_wc), r_checked, r_missing);
- } else if (SearchPathW((LPCWSTR)(p_path.get_base_dir().utf16().get_data()), (LPCWSTR)name.utf16().get_data(), nullptr, MAX_PATH, (LPWSTR)full_name_wc, nullptr)) {
- debug_dynamic_library_check_dependencies(p_root_path, String::utf16(full_name_wc), r_checked, r_missing);
+ } else if (SearchPathW(nullptr, (LPCWSTR)name.utf16().get_data(), nullptr, 32767, (LPWSTR)full_name_wc, nullptr)) {
+ debug_dynamic_library_check_dependencies(String::utf16(full_name_wc), r_checked, r_missing);
+ } else if (SearchPathW((LPCWSTR)(p_path.get_base_dir().utf16().get_data()), (LPCWSTR)name.utf16().get_data(), nullptr, 32767, (LPWSTR)full_name_wc, nullptr)) {
+ debug_dynamic_library_check_dependencies(String::utf16(full_name_wc), r_checked, r_missing);
} else {
r_missing.insert(name);
}
@@ -367,7 +384,7 @@ void debug_dynamic_library_check_dependencies(const String &p_root_path, const S
#endif
Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) {
- String path = p_path.replace("/", "\\");
+ String path = p_path;
if (!FileAccess::exists(path)) {
//this code exists so gdextension can load .dll files from within the executable path
@@ -413,11 +430,13 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha
bool has_dll_directory_api = ((add_dll_directory != nullptr) && (remove_dll_directory != nullptr));
DLL_DIRECTORY_COOKIE cookie = nullptr;
+ String dll_dir = ProjectSettings::get_singleton()->globalize_path(load_path.get_base_dir());
+ String wpath = fix_path(dll_dir);
if (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) {
- cookie = add_dll_directory((LPCWSTR)(load_path.get_base_dir().utf16().get_data()));
+ cookie = add_dll_directory((LPCWSTR)(wpath.get_base_dir().utf16().get_data()));
}
- p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(load_path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0);
+ p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(wpath.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0);
if (!p_library_handle) {
if (p_data != nullptr && p_data->generate_temp_files) {
DirAccess::remove_absolute(load_path);
@@ -428,7 +447,7 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha
HashSet<String> checked_libs;
HashSet<String> missing_libs;
- debug_dynamic_library_check_dependencies(load_path, load_path, checked_libs, missing_libs);
+ debug_dynamic_library_check_dependencies(wpath, checked_libs, missing_libs);
if (!missing_libs.is_empty()) {
String missing;
for (const String &E : missing_libs) {
@@ -882,7 +901,7 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String
Dictionary ret;
- String path = p_path.replace("/", "\\");
+ String path = p_path.is_absolute_path() ? fix_path(p_path) : p_path;
String command = _quote_command_line_argument(path);
for (const String &E : p_arguments) {
command += " " + _quote_command_line_argument(E);
@@ -931,7 +950,19 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String
DWORD creation_flags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW;
- if (!CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, true, creation_flags, nullptr, nullptr, si_w, &pi.pi)) {
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+ if (current_dir_name.size() >= MAX_PATH) {
+ Char16String current_short_dir_name;
+ str_len = GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), nullptr, 0);
+ current_short_dir_name.resize(str_len);
+ GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), (LPWSTR)current_short_dir_name.ptrw(), current_short_dir_name.size());
+ current_dir_name = current_short_dir_name;
+ }
+
+ if (!CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, true, creation_flags, nullptr, (LPWSTR)current_dir_name.ptr(), si_w, &pi.pi)) {
CLEAN_PIPES
ERR_FAIL_V_MSG(ret, "Could not create child process: " + command);
}
@@ -961,7 +992,7 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String
}
Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) {
- String path = p_path.replace("/", "\\");
+ String path = p_path.is_absolute_path() ? fix_path(p_path) : p_path;
String command = _quote_command_line_argument(path);
for (const String &E : p_arguments) {
command += " " + _quote_command_line_argument(E);
@@ -999,7 +1030,19 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments,
creation_flags |= CREATE_NO_WINDOW;
}
- int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, inherit_handles, creation_flags, nullptr, nullptr, si_w, &pi.pi);
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+ if (current_dir_name.size() >= MAX_PATH) {
+ Char16String current_short_dir_name;
+ str_len = GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), nullptr, 0);
+ current_short_dir_name.resize(str_len);
+ GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), (LPWSTR)current_short_dir_name.ptrw(), current_short_dir_name.size());
+ current_dir_name = current_short_dir_name;
+ }
+
+ int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, inherit_handles, creation_flags, nullptr, (LPWSTR)current_dir_name.ptr(), si_w, &pi.pi);
if (!ret && r_pipe) {
CloseHandle(pipe[0]); // Cleanup pipe handles.
CloseHandle(pipe[1]);
@@ -1063,7 +1106,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments,
}
Error OS_Windows::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) {
- String path = p_path.replace("/", "\\");
+ String path = p_path.is_absolute_path() ? fix_path(p_path) : p_path;
String command = _quote_command_line_argument(path);
for (const String &E : p_arguments) {
command += " " + _quote_command_line_argument(E);
@@ -1082,7 +1125,19 @@ Error OS_Windows::create_process(const String &p_path, const List<String> &p_arg
creation_flags |= CREATE_NO_WINDOW;
}
- int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, false, creation_flags, nullptr, nullptr, si_w, &pi.pi);
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+ if (current_dir_name.size() >= MAX_PATH) {
+ Char16String current_short_dir_name;
+ str_len = GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), nullptr, 0);
+ current_short_dir_name.resize(str_len);
+ GetShortPathNameW((LPCWSTR)current_dir_name.ptr(), (LPWSTR)current_short_dir_name.ptrw(), current_short_dir_name.size());
+ current_dir_name = current_short_dir_name;
+ }
+
+ int ret = CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, false, creation_flags, nullptr, (LPWSTR)current_dir_name.ptr(), si_w, &pi.pi);
ERR_FAIL_COND_V_MSG(ret == 0, ERR_CANT_FORK, "Could not create child process: " + command);
ProcessID pid = pi.pi.dwProcessId;
@@ -1450,8 +1505,8 @@ Vector<String> OS_Windows::get_system_font_path_for_text(const String &p_font_na
continue;
}
- WCHAR file_path[MAX_PATH];
- hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], MAX_PATH);
+ WCHAR file_path[32767];
+ hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], 32767);
if (FAILED(hr)) {
continue;
}
@@ -1529,8 +1584,8 @@ String OS_Windows::get_system_font_path(const String &p_font_name, int p_weight,
continue;
}
- WCHAR file_path[MAX_PATH];
- hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], MAX_PATH);
+ WCHAR file_path[32767];
+ hr = loader->GetFilePathFromKey(reference_key, reference_key_size, &file_path[0], 32767);
if (FAILED(hr)) {
continue;
}
@@ -1636,7 +1691,7 @@ Error OS_Windows::shell_show_in_file_manager(String p_path, bool p_open_folder)
if (!p_path.is_quoted()) {
p_path = p_path.quote();
}
- p_path = p_path.replace("/", "\\");
+ p_path = fix_path(p_path);
INT_PTR ret = OK;
if (open_folder) {
@@ -1961,6 +2016,19 @@ String OS_Windows::get_system_ca_certificates() {
OS_Windows::OS_Windows(HINSTANCE _hInstance) {
hInstance = _hInstance;
+ // Reset CWD to ensure long path is used.
+ Char16String current_dir_name;
+ size_t str_len = GetCurrentDirectoryW(0, nullptr);
+ current_dir_name.resize(str_len + 1);
+ GetCurrentDirectoryW(current_dir_name.size(), (LPWSTR)current_dir_name.ptrw());
+
+ Char16String new_current_dir_name;
+ str_len = GetLongPathNameW((LPCWSTR)current_dir_name.get_data(), nullptr, 0);
+ new_current_dir_name.resize(str_len + 1);
+ GetLongPathNameW((LPCWSTR)current_dir_name.get_data(), (LPWSTR)new_current_dir_name.ptrw(), new_current_dir_name.size());
+
+ SetCurrentDirectoryW((LPCWSTR)new_current_dir_name.get_data());
+
#ifndef WINDOWS_SUBSYSTEM_CONSOLE
RedirectIOToConsole();
#endif
diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp
index 457392fb2c..9bb6130742 100644
--- a/scene/gui/rich_text_label.cpp
+++ b/scene/gui/rich_text_label.cpp
@@ -1800,13 +1800,13 @@ void RichTextLabel::_notification(int p_what) {
case NOTIFICATION_RESIZED: {
_stop_thread();
- main->first_resized_line.store(0); //invalidate ALL
+ main->first_resized_line.store(0); // Invalidate all lines.
queue_redraw();
} break;
case NOTIFICATION_THEME_CHANGED: {
_stop_thread();
- main->first_invalid_font_line.store(0); //invalidate ALL
+ main->first_invalid_font_line.store(0); // Invalidate all lines.
queue_redraw();
} break;
@@ -1816,7 +1816,7 @@ void RichTextLabel::_notification(int p_what) {
set_text(text);
}
- main->first_invalid_line.store(0); //invalidate ALL
+ main->first_invalid_line.store(0); // Invalidate all lines.
queue_redraw();
} break;
@@ -2528,7 +2528,7 @@ PackedFloat32Array RichTextLabel::_find_tab_stops(Item *p_item) {
item = item->parent;
}
- return PackedFloat32Array();
+ return default_tab_stops;
}
HorizontalAlignment RichTextLabel::_find_alignment(Item *p_item) {
@@ -4444,19 +4444,19 @@ void RichTextLabel::append_text(const String &p_bbcode) {
add_text(String::chr(0x00AD));
pos = brk_end + 1;
} else if (tag == "center") {
- push_paragraph(HORIZONTAL_ALIGNMENT_CENTER);
+ push_paragraph(HORIZONTAL_ALIGNMENT_CENTER, text_direction, language, st_parser, default_jst_flags, default_tab_stops);
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag == "fill") {
- push_paragraph(HORIZONTAL_ALIGNMENT_FILL);
+ push_paragraph(HORIZONTAL_ALIGNMENT_FILL, text_direction, language, st_parser, default_jst_flags, default_tab_stops);
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag == "left") {
- push_paragraph(HORIZONTAL_ALIGNMENT_LEFT);
+ push_paragraph(HORIZONTAL_ALIGNMENT_LEFT, text_direction, language, st_parser, default_jst_flags, default_tab_stops);
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag == "right") {
- push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT);
+ push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT, text_direction, language, st_parser, default_jst_flags, default_tab_stops);
pos = brk_end + 1;
tag_stack.push_front(tag);
} else if (tag == "ul") {
@@ -4515,8 +4515,8 @@ void RichTextLabel::append_text(const String &p_bbcode) {
HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT;
Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED;
- String lang;
- PackedFloat32Array tab_stops;
+ String lang = language;
+ PackedFloat32Array tab_stops = default_tab_stops;
TextServer::StructuredTextParser st_parser_type = TextServer::STRUCTURED_TEXT_DEFAULT;
BitField<TextServer::JustificationFlag> jst_flags = default_jst_flags;
for (int i = 0; i < subtag.size(); i++) {
@@ -5734,19 +5734,89 @@ void RichTextLabel::set_text_direction(Control::TextDirection p_text_direction)
if (text_direction != p_text_direction) {
text_direction = p_text_direction;
- main->first_invalid_line.store(0); //invalidate ALL
- _validate_line_caches();
+ if (!text.is_empty()) {
+ _apply_translation();
+ } else {
+ main->first_invalid_line.store(0); // Invalidate all lines.
+ _validate_line_caches();
+ }
+ queue_redraw();
+ }
+}
+
+Control::TextDirection RichTextLabel::get_text_direction() const {
+ return text_direction;
+}
+
+void RichTextLabel::set_horizontal_alignment(HorizontalAlignment p_alignment) {
+ ERR_FAIL_INDEX((int)p_alignment, 4);
+ _stop_thread();
+
+ if (default_alignment != p_alignment) {
+ default_alignment = p_alignment;
+ if (!text.is_empty()) {
+ _apply_translation();
+ } else {
+ main->first_invalid_line.store(0); // Invalidate all lines.
+ _validate_line_caches();
+ }
+ queue_redraw();
+ }
+}
+
+HorizontalAlignment RichTextLabel::get_horizontal_alignment() const {
+ return default_alignment;
+}
+
+void RichTextLabel::set_justification_flags(BitField<TextServer::JustificationFlag> p_flags) {
+ _stop_thread();
+
+ if (default_jst_flags != p_flags) {
+ default_jst_flags = p_flags;
+ if (!text.is_empty()) {
+ _apply_translation();
+ } else {
+ main->first_invalid_line.store(0); // Invalidate all lines.
+ _validate_line_caches();
+ }
queue_redraw();
}
}
+BitField<TextServer::JustificationFlag> RichTextLabel::get_justification_flags() const {
+ return default_jst_flags;
+}
+
+void RichTextLabel::set_tab_stops(const PackedFloat32Array &p_tab_stops) {
+ _stop_thread();
+
+ if (default_tab_stops != p_tab_stops) {
+ default_tab_stops = p_tab_stops;
+ if (!text.is_empty()) {
+ _apply_translation();
+ } else {
+ main->first_invalid_line.store(0); // Invalidate all lines.
+ _validate_line_caches();
+ }
+ queue_redraw();
+ }
+}
+
+PackedFloat32Array RichTextLabel::get_tab_stops() const {
+ return default_tab_stops;
+}
+
void RichTextLabel::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) {
if (st_parser != p_parser) {
_stop_thread();
st_parser = p_parser;
- main->first_invalid_line.store(0); //invalidate ALL
- _validate_line_caches();
+ if (!text.is_empty()) {
+ _apply_translation();
+ } else {
+ main->first_invalid_line.store(0); // Invalidate all lines.
+ _validate_line_caches();
+ }
queue_redraw();
}
}
@@ -5760,7 +5830,7 @@ void RichTextLabel::set_structured_text_bidi_override_options(Array p_args) {
_stop_thread();
st_args = p_args;
- main->first_invalid_line.store(0); //invalidate ALL
+ main->first_invalid_line.store(0); // Invalidate all lines.
_validate_line_caches();
queue_redraw();
}
@@ -5770,17 +5840,17 @@ Array RichTextLabel::get_structured_text_bidi_override_options() const {
return st_args;
}
-Control::TextDirection RichTextLabel::get_text_direction() const {
- return text_direction;
-}
-
void RichTextLabel::set_language(const String &p_language) {
if (language != p_language) {
_stop_thread();
language = p_language;
- main->first_invalid_line.store(0); //invalidate ALL
- _validate_line_caches();
+ if (!text.is_empty()) {
+ _apply_translation();
+ } else {
+ main->first_invalid_line.store(0); // Invalidate all lines.
+ _validate_line_caches();
+ }
queue_redraw();
}
}
@@ -5794,7 +5864,7 @@ void RichTextLabel::set_autowrap_mode(TextServer::AutowrapMode p_mode) {
_stop_thread();
autowrap_mode = p_mode;
- main->first_invalid_line = 0; //invalidate ALL
+ main->first_invalid_line = 0; // Invalidate all lines.
_validate_line_caches();
queue_redraw();
}
@@ -5820,7 +5890,7 @@ void RichTextLabel::set_visible_ratio(float p_ratio) {
}
if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) {
- main->first_invalid_line.store(0); // Invalidate ALL.
+ main->first_invalid_line.store(0); // Invalidate all lines..
_validate_line_caches();
}
queue_redraw();
@@ -5948,6 +6018,13 @@ void RichTextLabel::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_language", "language"), &RichTextLabel::set_language);
ClassDB::bind_method(D_METHOD("get_language"), &RichTextLabel::get_language);
+ ClassDB::bind_method(D_METHOD("set_horizontal_alignment", "alignment"), &RichTextLabel::set_horizontal_alignment);
+ ClassDB::bind_method(D_METHOD("get_horizontal_alignment"), &RichTextLabel::get_horizontal_alignment);
+ ClassDB::bind_method(D_METHOD("set_justification_flags", "justification_flags"), &RichTextLabel::set_justification_flags);
+ ClassDB::bind_method(D_METHOD("get_justification_flags"), &RichTextLabel::get_justification_flags);
+ ClassDB::bind_method(D_METHOD("set_tab_stops", "tab_stops"), &RichTextLabel::set_tab_stops);
+ ClassDB::bind_method(D_METHOD("get_tab_stops"), &RichTextLabel::get_tab_stops);
+
ClassDB::bind_method(D_METHOD("set_autowrap_mode", "autowrap_mode"), &RichTextLabel::set_autowrap_mode);
ClassDB::bind_method(D_METHOD("get_autowrap_mode"), &RichTextLabel::get_autowrap_mode);
@@ -6068,6 +6145,10 @@ void RichTextLabel::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "horizontal_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_horizontal_alignment", "get_horizontal_alignment");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "justification_flags", PROPERTY_HINT_FLAGS, "Kashida Justification:1,Word Justification:2,Justify Only After Last Tab:8,Skip Last Line:32,Skip Last Line With Visible Characters:64,Do Not Skip Single Line:128"), "set_justification_flags", "get_justification_flags");
+ ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "tab_stops"), "set_tab_stops", "get_tab_stops");
+
ADD_GROUP("Markup", "");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("RichTextEffect"), (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_effects", "get_effects");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "meta_underlined"), "set_meta_underline", "is_meta_underlined");
@@ -6170,7 +6251,7 @@ void RichTextLabel::set_visible_characters_behavior(TextServer::VisibleCharacter
_stop_thread();
visible_chars_behavior = p_behavior;
- main->first_invalid_line.store(0); //invalidate ALL
+ main->first_invalid_line.store(0); // Invalidate all lines.
_validate_line_caches();
queue_redraw();
}
@@ -6190,7 +6271,7 @@ void RichTextLabel::set_visible_characters(int p_visible) {
}
}
if (visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) {
- main->first_invalid_line.store(0); //invalidate ALL
+ main->first_invalid_line.store(0); // Invalidate all lines.
_validate_line_caches();
}
queue_redraw();
diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h
index 9f81674454..6da13e7b2d 100644
--- a/scene/gui/rich_text_label.h
+++ b/scene/gui/rich_text_label.h
@@ -482,6 +482,7 @@ private:
HorizontalAlignment default_alignment = HORIZONTAL_ALIGNMENT_LEFT;
BitField<TextServer::JustificationFlag> default_jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE;
+ PackedFloat32Array default_tab_stops;
ItemMeta *meta_hovering = nullptr;
Variant current_meta;
@@ -808,6 +809,15 @@ public:
void set_text(const String &p_bbcode);
String get_text() const;
+ void set_horizontal_alignment(HorizontalAlignment p_alignment);
+ HorizontalAlignment get_horizontal_alignment() const;
+
+ void set_justification_flags(BitField<TextServer::JustificationFlag> p_flags);
+ BitField<TextServer::JustificationFlag> get_justification_flags() const;
+
+ void set_tab_stops(const PackedFloat32Array &p_tab_stops);
+ PackedFloat32Array get_tab_stops() const;
+
void set_text_direction(TextDirection p_text_direction);
TextDirection get_text_direction() const;
diff --git a/scene/gui/tree.h b/scene/gui/tree.h
index 9b1541f4b9..4518708685 100644
--- a/scene/gui/tree.h
+++ b/scene/gui/tree.h
@@ -178,6 +178,9 @@ private:
if (parent->first_child == this) {
parent->first_child = next;
}
+ if (parent->last_child == this) {
+ parent->last_child = prev;
+ }
}
}
diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp
index 0874872cc9..27db65bb1a 100644
--- a/scene/resources/packed_scene.cpp
+++ b/scene/resources/packed_scene.cpp
@@ -2209,7 +2209,7 @@ void PackedScene::_bind_methods() {
ClassDB::bind_method(D_METHOD("_get_bundled_scene"), &PackedScene::_get_bundled_scene);
ClassDB::bind_method(D_METHOD("get_state"), &PackedScene::get_state);
- ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_bundled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_INTERNAL), "_set_bundled_scene", "_get_bundled_scene");
+ ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "_bundled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL), "_set_bundled_scene", "_get_bundled_scene");
BIND_ENUM_CONSTANT(GEN_EDIT_STATE_DISABLED);
BIND_ENUM_CONSTANT(GEN_EDIT_STATE_INSTANCE);
diff --git a/tests/scene/test_tree.h b/tests/scene/test_tree.h
index 41ef39d621..e19f8311e2 100644
--- a/tests/scene/test_tree.h
+++ b/tests/scene/test_tree.h
@@ -108,6 +108,30 @@ TEST_CASE("[SceneTree][Tree]") {
memdelete(tree);
}
+ // https://github.com/godotengine/godot/issues/96205
+ SUBCASE("[Tree] Get last item after removal.") {
+ Tree *tree = memnew(Tree);
+ TreeItem *root = tree->create_item();
+
+ TreeItem *child1 = tree->create_item(root);
+ TreeItem *child2 = tree->create_item(root);
+
+ CHECK_EQ(root->get_child_count(), 2);
+ CHECK_EQ(tree->get_last_item(), child2);
+
+ root->remove_child(child2);
+
+ CHECK_EQ(root->get_child_count(), 1);
+ CHECK_EQ(tree->get_last_item(), child1);
+
+ root->add_child(child2);
+
+ CHECK_EQ(root->get_child_count(), 2);
+ CHECK_EQ(tree->get_last_item(), child2);
+
+ memdelete(tree);
+ }
+
SUBCASE("[Tree] Previous and Next items.") {
Tree *tree = memnew(Tree);
TreeItem *root = tree->create_item();
diff --git a/thirdparty/README.md b/thirdparty/README.md
index 799eb97cd2..d87c1c3c33 100644
--- a/thirdparty/README.md
+++ b/thirdparty/README.md
@@ -692,8 +692,8 @@ Collection of single-file libraries used in Godot components.
* License: MIT
- `qoa.h`
* Upstream: https://github.com/phoboslab/qoa
- * Version: git (5c2a86d615661f34636cf179abf4fa278d3257e0, 2024)
- * Modifications: Inlined functions, patched uninitialized variables and untyped mallocs.
+ * Version: git (e0c69447d4d3945c3c92ac1751e4cdc9803a8303, 2024)
+ * Modifications: Added a few modifiers to comply with C++ nature.
* License: MIT
- `r128.{c,h}`
* Upstream: https://github.com/fahickman/r128
diff --git a/thirdparty/misc/patches/qoa-min-fix.patch b/thirdparty/misc/patches/qoa-min-fix.patch
index 38303a1521..6008b5f8bc 100644
--- a/thirdparty/misc/patches/qoa-min-fix.patch
+++ b/thirdparty/misc/patches/qoa-min-fix.patch
@@ -1,5 +1,5 @@
diff --git a/qoa.h b/qoa.h
-index 592082933a..c890b88bd6 100644
+index cfed266bef..23612bb0bf 100644
--- a/qoa.h
+++ b/qoa.h
@@ -140,14 +140,14 @@ typedef struct {
@@ -24,19 +24,15 @@ index 592082933a..c890b88bd6 100644
#ifndef QOA_NO_STDIO
-@@ -394,9 +394,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
- #ifdef QOA_RECORD_TOTAL_ERROR
+@@ -395,7 +395,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
qoa_uint64_t best_error = -1;
#endif
-- qoa_uint64_t best_slice;
+ qoa_uint64_t best_slice = 0;
- qoa_lms_t best_lms;
-- int best_scalefactor;
-+ qoa_uint64_t best_slice = -1;
-+ qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}};
-+ int best_scalefactor = -1;
++ qoa_lms_t best_lms = {};
+ int best_scalefactor = 0;
for (int sfi = 0; sfi < 16; sfi++) {
- /* There is a strong correlation between the scalefactors of
@@ -500,7 +500,7 @@ void *qoa_encode(const short *sample_data, qoa_desc *qoa, unsigned int *out_len)
num_frames * QOA_LMS_LEN * 4 * qoa->channels + /* 4 * 4 bytes lms state per channel */
num_slices * 8 * qoa->channels; /* 8 byte slices */
diff --git a/thirdparty/misc/qoa.h b/thirdparty/misc/qoa.h
index c890b88bd6..23612bb0bf 100644
--- a/thirdparty/misc/qoa.h
+++ b/thirdparty/misc/qoa.h
@@ -394,9 +394,9 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned
#ifdef QOA_RECORD_TOTAL_ERROR
qoa_uint64_t best_error = -1;
#endif
- qoa_uint64_t best_slice = -1;
- qoa_lms_t best_lms = {{-1, -1, -1, -1}, {-1, -1, -1, -1}};
- int best_scalefactor = -1;
+ qoa_uint64_t best_slice = 0;
+ qoa_lms_t best_lms = {};
+ int best_scalefactor = 0;
for (int sfi = 0; sfi < 16; sfi++) {
/* There is a strong correlation between the scalefactors of