diff options
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=""""> 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=""""> 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=""""> 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=""""> 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 Binary files differnew file mode 100644 index 0000000000..a5ce40241f --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-hdpi/ic_play_window.png 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 Binary files differnew file mode 100644 index 0000000000..147adb6127 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-mdpi/ic_play_window.png 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 Binary files differnew file mode 100644 index 0000000000..0b1db1b923 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-xhdpi/ic_play_window.png 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 Binary files differnew file mode 100644 index 0000000000..39d7450390 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-xxhdpi/ic_play_window.png 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 Binary files differnew file mode 100644 index 0000000000..b7a09a15b5 --- /dev/null +++ b/platform/android/java/editor/src/main/res/mipmap-xxxhdpi/ic_play_window.png 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 |