diff options
Diffstat (limited to 'platform')
55 files changed, 1406 insertions, 643 deletions
diff --git a/platform/android/README.md b/platform/android/README.md index 343e588553..f6aabab708 100644 --- a/platform/android/README.md +++ b/platform/android/README.md @@ -3,6 +3,13 @@ This folder contains the Java and C++ (JNI) code for the Android platform port, using [Gradle](https://gradle.org/) as a build system. +## Documentation + +- [Compiling for Android](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_android.html) + - Instructions on building this platform port from source. +- [Exporting for Android](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_android.html) + - Instructions on using the compiled export templates to export a project. + ## Artwork license [`logo.png`](logo.png) and [`run_icon.png`](run_icon.png) are licensed under diff --git a/platform/android/SCsub b/platform/android/SCsub index d370a4d18d..344ca036de 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -41,13 +41,13 @@ lib = env_android.add_shared_library("#bin/libgodot", [android_objects], SHLIBSU env.Depends(lib, thirdparty_obj) lib_arch_dir = "" -if env["android_arch"] == "armv7": +if env["arch"] == "arm32": lib_arch_dir = "armeabi-v7a" -elif env["android_arch"] == "arm64v8": +elif env["arch"] == "arm64": lib_arch_dir = "arm64-v8a" -elif env["android_arch"] == "x86": +elif env["arch"] == "x86_32": lib_arch_dir = "x86" -elif env["android_arch"] == "x86_64": +elif env["arch"] == "x86_64": lib_arch_dir = "x86_64" else: print("WARN: Architecture not suitable for embedding into APK; keeping .so at \\bin") diff --git a/platform/android/detect.py b/platform/android/detect.py index 2ff5bf59ea..ad63821162 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -22,7 +22,6 @@ def get_opts(): return [ ("ANDROID_SDK_ROOT", "Path to the Android SDK", get_env_android_sdk_root()), ("ndk_platform", 'Target platform (android-<api>, e.g. "android-24")', "android-24"), - EnumVariable("android_arch", "Target architecture", "arm64v8", ("armv7", "arm64v8", "x86", "x86_64")), ] @@ -46,6 +45,7 @@ def get_ndk_version(): def get_flags(): return [ + ("arch", "arm64"), # Default for convenience. ("tools", False), ] @@ -75,35 +75,37 @@ def install_ndk_if_needed(env): def configure(env): + # Validate arch. + supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] + if env["arch"] not in supported_arches: + print( + 'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) + ) + sys.exit() + install_ndk_if_needed(env) ndk_root = env["ANDROID_NDK_ROOT"] # Architecture - if env["android_arch"] not in ["armv7", "arm64v8", "x86", "x86_64"]: - env["android_arch"] = "arm64v8" - - print("Building for Android, platform " + env["ndk_platform"] + " (" + env["android_arch"] + ")") - - if get_min_sdk_version(env["ndk_platform"]) < 21: - if env["android_arch"] == "x86_64" or env["android_arch"] == "arm64v8": - print( - "WARNING: android_arch=" - + env["android_arch"] - + " is not supported by ndk_platform lower than android-21; setting ndk_platform=android-21" - ) - env["ndk_platform"] = "android-21" + if get_min_sdk_version(env["ndk_platform"]) < 21 and env["arch"] in ["x86_64", "arm64"]: + print( + 'WARNING: arch="%s" is not supported with "ndk_platform" lower than "android-21". Forcing platform 21.' + % env["arch"] + ) + env["ndk_platform"] = "android-21" - if env["android_arch"] == "armv7": + if env["arch"] == "arm32": target_triple = "armv7a-linux-androideabi" env.extra_suffix = ".armv7" + env.extra_suffix - elif env["android_arch"] == "arm64v8": + elif env["arch"] == "arm64": target_triple = "aarch64-linux-android" env.extra_suffix = ".armv8" + env.extra_suffix - elif env["android_arch"] == "x86": + elif env["arch"] == "x86_32": target_triple = "i686-linux-android" env.extra_suffix = ".x86" + env.extra_suffix - elif env["android_arch"] == "x86_64": + elif env["arch"] == "x86_64": target_triple = "x86_64-linux-android" env.extra_suffix = ".x86_64" + env.extra_suffix @@ -176,14 +178,14 @@ def configure(env): if get_min_sdk_version(env["ndk_platform"]) >= 24: env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)]) - if env["android_arch"] == "x86": + if env["arch"] == "x86_32": # The NDK adds this if targeting API < 24, so we can drop it when Godot targets it at least env.Append(CCFLAGS=["-mstackrealign"]) - elif env["android_arch"] == "armv7": + elif env["arch"] == "arm32": env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split()) env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"]) env.Append(CPPDEFINES=["__ARM_NEON__"]) - elif env["android_arch"] == "arm64v8": + elif env["arch"] == "arm64": env.Append(CCFLAGS=["-mfix-cortex-a53-835769"]) env.Append(CPPDEFINES=["__ARM_ARCH_8A__"]) diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index eb344d3b43..428135de56 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -135,6 +135,20 @@ String DirAccessJAndroid::get_drive(int p_drive) { } } +String DirAccessJAndroid::get_current_dir(bool p_include_drive) const { + String base = _get_root_path(); + String bd = current_dir; + if (!base.is_empty()) { + bd = current_dir.replace_first(base, ""); + } + + if (bd.begins_with("/")) { + return _get_root_string() + bd.substr(1, bd.length()); + } else { + return _get_root_string() + bd; + } +} + Error DirAccessJAndroid::change_dir(String p_dir) { String new_dir = get_absolute_path(p_dir); if (new_dir == current_dir) { diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index d469c9d317..5b7b4a9c4d 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -67,6 +67,7 @@ public: virtual int get_drive_count() override; virtual String get_drive(int p_drive) override; + virtual String get_current_dir(bool p_include_drive = true) const override; ///< return current dir location virtual Error change_dir(String p_dir) override; ///< can be relative or absolute, return false on success diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index b51dd18af6..d3bce12de1 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -83,7 +83,7 @@ bool DisplayServerAndroid::tts_is_paused() const { return TTS_Android::is_paused(); } -Array DisplayServerAndroid::tts_get_voices() const { +TypedArray<Dictionary> DisplayServerAndroid::tts_get_voices() const { return TTS_Android::get_voices(); } @@ -136,7 +136,7 @@ bool DisplayServerAndroid::clipboard_has() const { } } -Array DisplayServerAndroid::get_display_cutouts() const { +TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); ERR_FAIL_NULL_V(godot_io_java, Array()); return godot_io_java->get_display_cutouts(); diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 2f30642319..6e14ba3e23 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -93,7 +93,7 @@ public: virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; - virtual Array tts_get_voices() const override; + virtual TypedArray<Dictionary> tts_get_voices() const override; virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; virtual void tts_pause() override; @@ -104,7 +104,7 @@ public: virtual String clipboard_get() const override; virtual bool clipboard_has() const override; - virtual Array get_display_cutouts() const override; + virtual TypedArray<Rect2> get_display_cutouts() const override; virtual Rect2i get_display_safe_area() const override; virtual void screen_set_keep_on(bool p_enable) override; diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 6f1b4bde40..685b1f01af 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -248,6 +248,7 @@ static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/instal static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' +#ifndef ANDROID_ENABLED void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud); @@ -277,7 +278,6 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } -#ifndef ANDROID_ENABLED // Check for devices updates String adb = get_adb_path(); if (FileAccess::exists(adb)) { @@ -389,7 +389,6 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { ea->devices_changed.set(); } } -#endif uint64_t sleep = 200; uint64_t wait = 3000000; @@ -402,7 +401,6 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } -#ifndef ANDROID_ENABLED if (EditorSettings::get_singleton()->get("export/android/shutdown_adb_on_exit")) { String adb = get_adb_path(); if (!FileAccess::exists(adb)) { @@ -413,8 +411,8 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { args.push_back("kill-server"); OS::get_singleton()->execute(adb, args); } -#endif } +#endif String EditorExportPlatformAndroid::get_project_name(const String &p_name) const { String aname; @@ -2049,7 +2047,7 @@ String EditorExportPlatformAndroid::get_apksigner_path() { return apksigner_path; } -bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { String err; bool valid = false; const bool custom_build_enabled = p_preset->get("custom_build/use_custom_build"); @@ -2097,7 +2095,7 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr valid = installed_android_build_template && !r_missing_templates; } - // Validate the rest of the configuration. + // Validate the rest of the export configuration. String dk = p_preset->get("keystore/debug"); String dk_user = p_preset->get("keystore/debug_user"); @@ -2173,6 +2171,19 @@ bool EditorExportPlatformAndroid::can_export(const Ref<EditorExportPreset> &p_pr } } + if (!err.is_empty()) { + r_error = err; + } + + return valid; +} + +bool EditorExportPlatformAndroid::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { + String err; + bool valid = true; + const bool custom_build_enabled = p_preset->get("custom_build/use_custom_build"); + + // Validate the project configuration. bool apk_expansion = p_preset->get("apk_expansion/enable"); if (apk_expansion) { @@ -3125,10 +3136,14 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() { devices_changed.set(); plugins_changed.set(); +#ifndef ANDROID_ENABLED check_for_changes_thread.start(_check_for_changes_poll_thread, this); +#endif } EditorExportPlatformAndroid::~EditorExportPlatformAndroid() { +#ifndef ANDROID_ENABLED quit_request.set(); check_for_changes_thread.wait_to_finish(); +#endif } diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 1da3f68f9a..46012bd46c 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -80,10 +80,12 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { Vector<Device> devices; SafeFlag devices_changed; Mutex device_lock; +#ifndef ANDROID_ENABLED Thread check_for_changes_thread; SafeFlag quit_request; static void _check_for_changes_poll_thread(void *ud); +#endif String get_project_name(const String &p_name) const; @@ -186,7 +188,8 @@ public: static String get_apksigner_path(); - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index 6b21c18d59..56561cb616 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -46,6 +46,7 @@ jmethodID FileAccessFilesystemJAndroid::_file_seek_end = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_read = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_tell = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_eof = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_set_eof = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_close = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr; @@ -162,6 +163,16 @@ bool FileAccessFilesystemJAndroid::eof_reached() const { } } +void FileAccessFilesystemJAndroid::_set_eof(bool eof) { + if (_file_set_eof) { + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(file_access_handler, _file_set_eof, id, eof); + } +} + uint8_t FileAccessFilesystemJAndroid::get_8() const { ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); uint8_t byte; @@ -184,6 +195,7 @@ String FileAccessFilesystemJAndroid::get_line() const { while (true) { size_t line_buffer_size = MIN(buffer_size_limit, file_size - get_position()); if (line_buffer_size <= 0) { + const_cast<FileAccessFilesystemJAndroid *>(this)->_set_eof(true); break; } @@ -310,6 +322,7 @@ void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) { _file_get_size = env->GetMethodID(cls, "fileGetSize", "(I)J"); _file_tell = env->GetMethodID(cls, "fileGetPosition", "(I)J"); _file_eof = env->GetMethodID(cls, "isFileEof", "(I)Z"); + _file_set_eof = env->GetMethodID(cls, "setFileEof", "(IZ)V"); _file_seek = env->GetMethodID(cls, "fileSeek", "(IJ)V"); _file_seek_end = env->GetMethodID(cls, "fileSeekFromEnd", "(IJ)V"); _file_read = env->GetMethodID(cls, "fileRead", "(ILjava/nio/ByteBuffer;)I"); diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index 7deb8de37b..76d7db6e3a 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -44,6 +44,7 @@ class FileAccessFilesystemJAndroid : public FileAccess { static jmethodID _file_seek_end; static jmethodID _file_tell; static jmethodID _file_eof; + static jmethodID _file_set_eof; static jmethodID _file_read; static jmethodID _file_write; static jmethodID _file_flush; @@ -56,6 +57,7 @@ class FileAccessFilesystemJAndroid : public FileAccess { String path_src; void _close(); ///< close a file + void _set_eof(bool eof); public: virtual Error _open(const String &p_path, int p_mode_flags) override; ///< open a file diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index da30bd3a95..81c7130c03 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -28,7 +28,7 @@ allprojects { } ext { - supportedAbis = ["armv7", "arm64v8", "x86", "x86_64"] + supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedTargetsMap = [release: "release", dev: "debug", debug: "release_debug"] supportedFlavors = ["editor", "template"] @@ -37,7 +37,7 @@ ext { // If building manually on the command line, it's recommended to use the // `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s). // The {selectedAbis} values must be from the {supportedAbis} values. - selectedAbis = ["arm64v8"] + selectedAbis = ["arm64"] } def rootDir = "../../.." diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 6b82326a27..318ae1143f 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -159,7 +159,7 @@ android { def taskName = getSconsTaskName(flavorName, buildType, selectedAbi) tasks.create(name: taskName, type: Exec) { executable sconsExecutableFile.absolutePath - args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "android_arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() + args "--directory=${pathToRootDir}", "platform=android", "tools=${toolsFlag}", "target=${sconsTarget}", "arch=${selectedAbi}", "-j" + Runtime.runtime.availableProcessors() } // Schedule the tasks so the generated libs are present before the aar file is packaged. diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt index 463dabfb23..f23537a29e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -104,7 +104,6 @@ internal abstract class DataAccess(private val filePath: String) { protected abstract val fileChannel: FileChannel internal var endOfFile = false - private set fun close() { try { @@ -125,9 +124,7 @@ internal abstract class DataAccess(private val filePath: String) { fun seek(position: Long) { try { fileChannel.position(position) - if (position <= size()) { - endOfFile = false - } + endOfFile = position >= fileChannel.size() } catch (e: Exception) { Log.w(TAG, "Exception when seeking file $filePath.", e) } @@ -161,8 +158,7 @@ internal abstract class DataAccess(private val filePath: String) { fun read(buffer: ByteBuffer): Int { return try { val readBytes = fileChannel.read(buffer) - endOfFile = readBytes == -1 - || (fileChannel.position() >= fileChannel.size() && fileChannel.size() > 0) + endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size()) if (readBytes == -1) { 0 } else { diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index 04b6772c45..83da3a24b3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -194,6 +194,11 @@ class FileAccessHandler(val context: Context) { return files[fileId].endOfFile } + fun setFileEof(fileId: Int, eof: Boolean) { + val file = files[fileId] ?: return + file.endOfFile = eof + } + fun fileClose(fileId: Int) { if (hasFileId(fileId)) { files[fileId].close() diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 5877c15114..cea64a7f22 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -165,8 +165,8 @@ float GodotIOJavaWrapper::get_screen_refresh_rate(float fallback) { return fallback; } -Array GodotIOJavaWrapper::get_display_cutouts() { - Array result; +TypedArray<Rect2> GodotIOJavaWrapper::get_display_cutouts() { + TypedArray<Rect2> result; ERR_FAIL_NULL_V(_get_display_cutouts, result); JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, result); diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index dc68f4d90d..9a1a877b6f 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -38,7 +38,7 @@ #include <jni.h> #include "core/math/rect2i.h" -#include "core/variant/array.h" +#include "core/variant/typed_array.h" #include "string_android.h" // Class that makes functions in java/src/org/godotengine/godot/GodotIO.java callable from C++ @@ -78,7 +78,7 @@ public: int get_screen_dpi(); float get_scaled_density(); float get_screen_refresh_rate(float fallback); - Array get_display_cutouts(); + TypedArray<Rect2> get_display_cutouts(); Rect2i get_display_safe_area(); String get_unique_id(); bool has_vk(); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 0f551e7f4f..f94614c741 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -370,15 +370,15 @@ bool OS_Android::_check_internal_feature_support(const String &p_feature) { return true; } #if defined(__aarch64__) - if (p_feature == "arm64-v8a") { + if (p_feature == "arm64-v8a" || p_feature == "arm64") { return true; } #elif defined(__ARM_ARCH_7A__) - if (p_feature == "armeabi-v7a" || p_feature == "armeabi") { + if (p_feature == "armeabi-v7a" || p_feature == "armeabi" || p_feature == "arm32") { return true; } #elif defined(__arm__) - if (p_feature == "armeabi") { + if (p_feature == "armeabi" || p_feature == "arm") { return true; } #endif diff --git a/platform/ios/README.md b/platform/ios/README.md new file mode 100644 index 0000000000..82c275ad31 --- /dev/null +++ b/platform/ios/README.md @@ -0,0 +1,14 @@ +# iOS platform port + +This folder contains the C++, Objective-C and Objective-C++ code for the iOS +platform port. + +See also [`misc/dist/ios_xcode`](/misc/dist/ios_xcode) folder for the Xcode +project template used for packaging the iOS export templates. + +## Documentation + +- [Compiling for iOS](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_ios.html) + - Instructions on building this platform port from source. +- [Exporting for iOS](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_ios.html) + - Instructions on using the compiled export templates to export a project. diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 67c90b10a0..1a8d24d12d 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -36,12 +36,22 @@ def get_opts(): def get_flags(): return [ + ("arch", "arm64"), # Default for convenience. ("tools", False), ("use_volk", False), ] def configure(env): + # Validate arch. + supported_arches = ["x86_64", "arm64"] + if env["arch"] not in supported_arches: + print( + 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) + ) + sys.exit() + ## Build type if env["target"].startswith("release"): @@ -64,11 +74,6 @@ def configure(env): env.Append(CCFLAGS=["-flto"]) env.Append(LINKFLAGS=["-flto"]) - ## Architecture - env["bits"] = "64" - if env["arch"] != "x86_64": - env["arch"] = "arm64" - ## Compiler configuration # Save this in environment for use by other modules diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index bbb2dd3ab3..f3624f24ab 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -127,7 +127,7 @@ public: virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; - virtual Array tts_get_voices() const override; + virtual TypedArray<Dictionary> tts_get_voices() const override; virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; virtual void tts_pause() override; diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index 6ce7e676a2..74d6bc2e97 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -336,8 +336,8 @@ bool DisplayServerIOS::tts_is_paused() const { return [tts isPaused]; } -Array DisplayServerIOS::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, Array()); +TypedArray<Dictionary> DisplayServerIOS::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, TypedArray<Dictionary>()); return [tts getVoices]; } diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 001c9b803d..f49cf7a88d 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -140,27 +140,27 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photolibrary_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need access to the photo library"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photolibrary_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_120x120", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPhone/iPod Touch with Retina display - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_180x180", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPhone with Retina HD display + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_120x120", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPhone/iPod Touch with Retina display + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/iphone_180x180", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPhone with Retina HD display - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/ipad_76x76", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPad - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/ipad_152x152", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPad with Retina display - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/ipad_167x167", PROPERTY_HINT_FILE, "*.png"), "")); // Home screen on iPad Pro + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/ipad_76x76", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPad + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/ipad_152x152", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPad with Retina display + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/ipad_167x167", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Home screen on iPad Pro - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/app_store_1024x1024", PROPERTY_HINT_FILE, "*.png"), "")); // App Store + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/app_store_1024x1024", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // App Store - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/spotlight_40x40", PROPERTY_HINT_FILE, "*.png"), "")); // Spotlight - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/spotlight_80x80", PROPERTY_HINT_FILE, "*.png"), "")); // Spotlight on devices with Retina display + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/spotlight_40x40", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Spotlight + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "icons/spotlight_80x80", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); // Spotlight on devices with Retina display r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_launch_screen_storyboard"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "storyboard/image_scale_mode", PROPERTY_HINT_ENUM, "Same as Logo,Center,Scale to Fit,Scale to Fill,Scale"), 0)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE, "*.png"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE, "*.png"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@2x", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "storyboard/custom_image@3x", PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "storyboard/use_custom_bg_color"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::COLOR, "storyboard/custom_bg_color"), Color())); for (uint64_t i = 0; i < sizeof(loading_screen_infos) / sizeof(loading_screen_infos[0]); ++i) { - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, loading_screen_infos[i].preset_key, PROPERTY_HINT_FILE, "*.png"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, loading_screen_infos[i].preset_key, PROPERTY_HINT_FILE, "*.png,*.jpg,*.jpeg"), "")); } } @@ -387,13 +387,17 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ String locale_files; Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); if (translations.size() > 0) { - int index = 0; + HashSet<String> languages; for (const String &E : translations) { Ref<Translation> tr = ResourceLoader::load(E); - if (tr.is_valid()) { - String lang = tr->get_locale(); - locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = " + lang + "; path = " + lang + ".lproj/InfoPlist.strings; sourceTree = \"<group>\"; };"; + if (tr.is_valid() && tr->get_locale() != "en") { + languages.insert(tr->get_locale()); } + } + + int index = 0; + for (const String &lang : languages) { + locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = " + lang + "; path = " + lang + ".lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n"; index++; } } @@ -402,13 +406,17 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ String locale_files; Vector<String> translations = ProjectSettings::get_singleton()->get("internationalization/locale/translations"); if (translations.size() > 0) { - int index = 0; + HashSet<String> languages; for (const String &E : translations) { Ref<Translation> tr = ResourceLoader::load(E); - if (tr.is_valid()) { - String lang = tr->get_locale(); - locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */,"; + if (tr.is_valid() && tr->get_locale() != "en") { + languages.insert(tr->get_locale()); } + } + + int index = 0; + for (const String &lang : languages) { + locale_files += "D0BCFE4518AEBDA2004A" + itos(index).pad_zeros(4) + " /* " + lang + " */,\n"; index++; } } @@ -531,26 +539,27 @@ struct IconInfo { const char *actual_size_side; const char *scale; const char *unscaled_size; + const bool force_opaque; }; static const IconInfo icon_infos[] = { // Home screen on iPhone - { "icons/iphone_120x120", "iphone", "Icon-120.png", "120", "2x", "60x60" }, - { "icons/iphone_120x120", "iphone", "Icon-120.png", "120", "3x", "40x40" }, - { "icons/iphone_180x180", "iphone", "Icon-180.png", "180", "3x", "60x60" }, + { "icons/iphone_120x120", "iphone", "Icon-120.png", "120", "2x", "60x60", false }, + { "icons/iphone_120x120", "iphone", "Icon-120.png", "120", "3x", "40x40", false }, + { "icons/iphone_180x180", "iphone", "Icon-180.png", "180", "3x", "60x60", false }, // Home screen on iPad - { "icons/ipad_76x76", "ipad", "Icon-76.png", "76", "1x", "76x76" }, - { "icons/ipad_152x152", "ipad", "Icon-152.png", "152", "2x", "76x76" }, - { "icons/ipad_167x167", "ipad", "Icon-167.png", "167", "2x", "83.5x83.5" }, + { "icons/ipad_76x76", "ipad", "Icon-76.png", "76", "1x", "76x76", false }, + { "icons/ipad_152x152", "ipad", "Icon-152.png", "152", "2x", "76x76", false }, + { "icons/ipad_167x167", "ipad", "Icon-167.png", "167", "2x", "83.5x83.5", false }, // App Store - { "icons/app_store_1024x1024", "ios-marketing", "Icon-1024.png", "1024", "1x", "1024x1024" }, + { "icons/app_store_1024x1024", "ios-marketing", "Icon-1024.png", "1024", "1x", "1024x1024", true }, // Spotlight - { "icons/spotlight_40x40", "ipad", "Icon-40.png", "40", "1x", "40x40" }, - { "icons/spotlight_80x80", "iphone", "Icon-80.png", "80", "2x", "40x40" }, - { "icons/spotlight_80x80", "ipad", "Icon-80.png", "80", "2x", "40x40" } + { "icons/spotlight_40x40", "ipad", "Icon-40.png", "40", "1x", "40x40", false }, + { "icons/spotlight_80x80", "iphone", "Icon-80.png", "80", "2x", "40x40", false }, + { "icons/spotlight_80x80", "ipad", "Icon-80.png", "80", "2x", "40x40", false } }; Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_preset, const String &p_iconset_dir) { @@ -570,14 +579,17 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr Ref<Image> img = memnew(Image); Error err = ImageLoader::load_image(icon_path, img); if (err != OK) { - ERR_PRINT("Invalid icon (" + String(info.preset_key) + "): '" + icon_path + "'."); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path)); + return ERR_UNCONFIGURED; + } + if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key)); return ERR_UNCONFIGURED; } img->resize(side_size, side_size); err = img->save_png(p_iconset_dir + info.export_name); if (err) { - String err_str = String("Failed to export icon(" + String(info.preset_key) + "): '" + icon_path + "'."); - ERR_PRINT(err_str.utf8().get_data()); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path)); return err; } } else { @@ -585,11 +597,15 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr Ref<Image> img = memnew(Image); Error err = ImageLoader::load_image(icon_path, img); if (err != OK) { - ERR_PRINT("Invalid icon (" + String(info.preset_key) + "): '" + icon_path + "'."); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Invalid icon (%s): '%s'.", info.preset_key, icon_path)); + return ERR_UNCONFIGURED; + } + if (info.force_opaque && img->detect_alpha() != Image::ALPHA_NONE) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Icon (%s) must be opaque.", info.preset_key)); return ERR_UNCONFIGURED; } if (img->get_width() != side_size || img->get_height() != side_size) { - WARN_PRINT("Icon (" + String(info.preset_key) + "): '" + icon_path + "' has incorrect size (" + String::num_int64(img->get_width()) + "x" + String::num_int64(img->get_height()) + ") and was automatically resized to " + String::num_int64(side_size) + "x" + String::num_int64(side_size) + "."); + add_message(EXPORT_MESSAGE_WARNING, TTR("Export Icons"), vformat("Icon (%s): '%s' has incorrect size %s and was automatically resized to %s.", info.preset_key, icon_path, img->get_size(), Vector2i(side_size, side_size))); img->resize(side_size, side_size); err = img->save_png(p_iconset_dir + info.export_name); } else { @@ -597,8 +613,7 @@ Error EditorExportPlatformIOS::_export_icons(const Ref<EditorExportPreset> &p_pr } if (err) { - String err_str = String("Failed to export icon(" + String(info.preset_key) + "): '" + icon_path + "'."); - ERR_PRINT(err_str.utf8().get_data()); + add_message(EXPORT_MESSAGE_ERROR, TTR("Export Icons"), vformat("Failed to export icon (%s): '%s'.", info.preset_key, icon_path)); return err; } } @@ -1651,27 +1666,31 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p f->store_line("NSPhotoLibraryUsageDescription = \"" + p_preset->get("privacy/photolibrary_usage_description").operator String() + "\";"); } + HashSet<String> languages; for (const String &E : translations) { Ref<Translation> tr = ResourceLoader::load(E); - if (tr.is_valid()) { - String lang = tr->get_locale(); - String fname = dest_dir + binary_name + "/" + lang + ".lproj"; - tmp_app_path->make_dir_recursive(fname); - Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); - f->store_line("/* Localized versions of Info.plist keys */"); - f->store_line(""); - if (appnames.has(lang)) { - f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); - } - if (camera_usage_descriptions.has(lang)) { - f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); - } - if (microphone_usage_descriptions.has(lang)) { - f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); - } - if (photolibrary_usage_descriptions.has(lang)) { - f->store_line("NSPhotoLibraryUsageDescription = \"" + photolibrary_usage_descriptions[lang].operator String() + "\";"); - } + if (tr.is_valid() && tr->get_locale() != "en") { + languages.insert(tr->get_locale()); + } + } + + for (const String &lang : languages) { + String fname = dest_dir + binary_name + "/" + lang + ".lproj"; + tmp_app_path->make_dir_recursive(fname); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + if (appnames.has(lang)) { + f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); + } + if (camera_usage_descriptions.has(lang)) { + f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); + } + if (microphone_usage_descriptions.has(lang)) { + f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); + } + if (photolibrary_usage_descriptions.has(lang)) { + f->store_line("NSPhotoLibraryUsageDescription = \"" + photolibrary_usage_descriptions[lang].operator String() + "\";"); } } } @@ -1817,7 +1836,7 @@ Error EditorExportPlatformIOS::export_project(const Ref<EditorExportPreset> &p_p return OK; } -bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { String err; bool valid = false; @@ -1842,7 +1861,18 @@ bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset valid = dvalid || rvalid; r_missing_templates = !valid; - // Validate the rest of the configuration. + if (!err.is_empty()) { + r_error = err; + } + + return valid; +} + +bool EditorExportPlatformIOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { + String err; + bool valid = true; + + // Validate the project configuration. String team_id = p_preset->get("application/app_store_team_id"); if (team_id.length() == 0) { @@ -1873,10 +1903,14 @@ bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset EditorExportPlatformIOS::EditorExportPlatformIOS() { logo = ImageTexture::create_from_image(memnew(Image(_ios_logo))); plugins_changed.set(); +#ifndef ANDROID_ENABLED check_for_changes_thread.start(_check_for_changes_poll_thread, this); +#endif } EditorExportPlatformIOS::~EditorExportPlatformIOS() { +#ifndef ANDROID_ENABLED quit_request.set(); check_for_changes_thread.wait_to_finish(); +#endif } diff --git a/platform/ios/export/export_plugin.h b/platform/ios/export/export_plugin.h index e32aef82dd..abda8e218a 100644 --- a/platform/ios/export/export_plugin.h +++ b/platform/ios/export/export_plugin.h @@ -57,8 +57,10 @@ class EditorExportPlatformIOS : public EditorExportPlatform { // Plugins SafeFlag plugins_changed; +#ifndef ANDROID_ENABLED Thread check_for_changes_thread; SafeFlag quit_request; +#endif Mutex plugins_lock; Vector<PluginConfigIOS> plugins; @@ -139,6 +141,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { return true; } +#ifndef ANDROID_ENABLED static void _check_for_changes_poll_thread(void *ud) { EditorExportPlatformIOS *ea = static_cast<EditorExportPlatformIOS *>(ud); @@ -172,6 +175,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { } } } +#endif protected: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; @@ -198,7 +202,8 @@ public: } virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; virtual void get_platform_features(List<String> *r_features) const override { r_features->push_back("mobile"); diff --git a/platform/javascript/README.md b/platform/javascript/README.md index f181bea9e0..812ab6778b 100644 --- a/platform/javascript/README.md +++ b/platform/javascript/README.md @@ -5,8 +5,15 @@ compiled using [Emscripten](https://emscripten.org/). It also contains a ESLint linting setup (see [`package.json`](package.json)). -See also [`misc/dist/html`](/misc/dist/html) folder for files used by this platform -such as the HTML5 shell. +See also [`misc/dist/html`](/misc/dist/html) folder for additional files used by +this platform such as the HTML5 shell. + +## Documentation + +- [Compiling for the Web](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_web.html) + - Instructions on building this platform port from source. +- [Exporting for the Web](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_web.html) + - Instructions on using the compiled export templates to export a project. ## Artwork license diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index a769260f01..048c9c2eb4 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -46,6 +46,7 @@ def get_opts(): def get_flags(): return [ + ("arch", "wasm32"), ("tools", False), ("builtin_pcre2_with_jit", False), ("vulkan", False), @@ -53,6 +54,15 @@ def get_flags(): def configure(env): + # Validate arch. + supported_arches = ["wasm32"] + if env["arch"] not in supported_arches: + print( + 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) + ) + sys.exit() + try: env["initial_memory"] = int(env["initial_memory"]) except Exception: diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 48f637fcfe..30240ad2db 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -296,7 +296,7 @@ void DisplayServerJavaScript::update_voices_callback(int p_size, const char **p_ } } -Array DisplayServerJavaScript::tts_get_voices() const { +TypedArray<Dictionary> DisplayServerJavaScript::tts_get_voices() const { godot_js_tts_get_voices(update_voices_callback); return voices; } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index fb7f5d02a8..cbb91477b7 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -124,7 +124,7 @@ public: // tts virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; - virtual Array tts_get_voices() const override; + virtual TypedArray<Dictionary> tts_get_voices() const override; virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; virtual void tts_pause() override; diff --git a/platform/javascript/export/export_plugin.cpp b/platform/javascript/export/export_plugin.cpp index b99f88d067..0bdee11018 100644 --- a/platform/javascript/export/export_plugin.cpp +++ b/platform/javascript/export/export_plugin.cpp @@ -362,7 +362,7 @@ Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const { return logo; } -bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformJavaScript::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { #ifndef DEV_ENABLED // We don't provide export templates for the HTML5 platform currently as there // is no suitable renderer to use with them. So we forbid exporting and tell @@ -396,7 +396,27 @@ bool EditorExportPlatformJavaScript::can_export(const Ref<EditorExportPreset> &p valid = dvalid || rvalid; r_missing_templates = !valid; - // Validate the rest of the configuration. + if (!err.is_empty()) { + r_error = err; + } + + return valid; +} + +bool EditorExportPlatformJavaScript::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { +#ifndef DEV_ENABLED + // We don't provide export templates for the HTML5 platform currently as there + // is no suitable renderer to use with them. So we forbid exporting and tell + // users why. This is skipped in DEV_ENABLED so that contributors can still test + // the pipeline once we start having WebGL or WebGPU support. + r_error = "The HTML5 platform is currently not supported in Godot 4.0, as there is no suitable renderer for it.\n"; + return false; +#endif + + String err; + bool valid = true; + + // Validate the project configuration. if (p_preset->get("vram_texture_compression/for_mobile")) { String etc_error = test_etc2(); diff --git a/platform/javascript/export/export_plugin.h b/platform/javascript/export/export_plugin.h index fbaa3615cb..16bab02d54 100644 --- a/platform/javascript/export/export_plugin.h +++ b/platform/javascript/export/export_plugin.h @@ -118,7 +118,8 @@ public: virtual String get_os_name() const override; virtual Ref<Texture2D> get_logo() const override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; diff --git a/platform/javascript/js/libs/library_godot_display.js b/platform/javascript/js/libs/library_godot_display.js index c7729a8c5b..768eaf9e1d 100644 --- a/platform/javascript/js/libs/library_godot_display.js +++ b/platform/javascript/js/libs/library_godot_display.js @@ -336,26 +336,12 @@ const GodotDisplay = { $GodotDisplay__deps: ['$GodotConfig', '$GodotRuntime', '$GodotDisplayCursor', '$GodotEventListeners', '$GodotDisplayScreen', '$GodotDisplayVK'], $GodotDisplay: { window_icon: '', - findDPI: function () { - function testDPI(dpi) { - return window.matchMedia(`(max-resolution: ${dpi}dpi)`).matches; - } - function bisect(low, high, func) { - const mid = parseInt(((high - low) / 2) + low, 10); - if (high - low <= 1) { - return func(high) ? high : low; - } - if (func(mid)) { - return bisect(low, mid, func); - } - return bisect(mid, high, func); - } - try { - const dpi = bisect(0, 800, testDPI); - return dpi >= 96 ? dpi : 96; - } catch (e) { - return 96; - } + getDPI: function () { + // devicePixelRatio is given in dppx + // https://drafts.csswg.org/css-values/#resolution + // > due to the 1:96 fixed ratio of CSS *in* to CSS *px*, 1dppx is equivalent to 96dpi. + const dpi = Math.round(window.devicePixelRatio * 96); + return dpi >= 96 ? dpi : 96; }, }, @@ -461,7 +447,7 @@ const GodotDisplay = { godot_js_display_screen_dpi_get__sig: 'i', godot_js_display_screen_dpi_get: function () { - return GodotDisplay.findDPI(); + return GodotDisplay.getDPI(); }, godot_js_display_pixel_ratio_get__sig: 'f', diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 35e13c94fc..d932745177 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -98,7 +98,6 @@ public: String get_user_data_dir() const override; bool is_userfs_persistent() const override; - bool is_single_window() const override { return true; } void alert(const String &p_alert, const String &p_title = "ALERT!") override; diff --git a/platform/linuxbsd/README.md b/platform/linuxbsd/README.md index 0d3fb37be5..efa8682062 100644 --- a/platform/linuxbsd/README.md +++ b/platform/linuxbsd/README.md @@ -2,10 +2,20 @@ This folder contains the C++ code for the Linux/*BSD platform port. +See also [`misc/dist/linux`](/misc/dist/linux) folder for additional files +used by this platform. + +## Documentation + +- [Compiling for Linux/*BSD](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_linuxbsd.html) + - Instructions on building this platform port from source. +- [Exporting for Linux/*BSD](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_linux.html) + - Instructions on using the compiled export templates to export a project. + ## Artwork license [`logo.png`](logo.png) is derived from the [Linux logo](https://isc.tamu.edu/~lewing/linux/): > Permission to use and/or modify this image is granted provided you acknowledge me - <lewing@isc.tamu.edu> and [The GIMP](https://isc.tamu.edu/~lewing/gimp/) - if someone asks. +> <lewing@isc.tamu.edu> and [The GIMP](https://isc.tamu.edu/~lewing/gimp/) +> if someone asks. diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 00e2b9e6eb..dd829bdb9b 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -2,6 +2,7 @@ import os import platform import sys from methods import get_compiler_version, using_gcc +from platform_methods import detect_arch def is_active(): @@ -52,10 +53,21 @@ def get_opts(): def get_flags(): - return [] + return [ + ("arch", detect_arch()), + ] def configure(env): + # Validate arch. + supported_arches = ["x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64"] + if env["arch"] not in supported_arches: + print( + 'Unsupported CPU architecture "%s" for Linux / *BSD. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) + ) + sys.exit() + ## Build type if env["target"] == "release": @@ -80,23 +92,7 @@ def configure(env): env.Prepend(CCFLAGS=["-g3"]) env.Append(LINKFLAGS=["-rdynamic"]) - ## Architecture - - is64 = sys.maxsize > 2**32 - if env["bits"] == "default": - env["bits"] = "64" if is64 else "32" - - machines = { - "riscv64": "rv64", - "ppc64le": "ppc64", - "ppc64": "ppc64", - "ppcle": "ppc", - "ppc": "ppc", - } - - if env["arch"] == "" and platform.machine() in machines: - env["arch"] = machines[platform.machine()] - + # CPU architecture flags. if env["arch"] == "rv64": # G = General-purpose extensions, C = Compression extension (very common). env.Append(CCFLAGS=["-march=rv64gc"]) @@ -262,8 +258,7 @@ def configure(env): env["builtin_libvorbis"] = False # Needed to link against system libtheora env.ParseConfig("pkg-config theora theoradec --cflags --libs") else: - list_of_x86 = ["x86_64", "x86", "i386", "i586"] - if any(platform.machine() in s for s in list_of_x86): + if env["arch"] in ["x86_64", "x86_32"]: env["x86_libtheora_opt_gcc"] = True if not env["builtin_libvorbis"]: @@ -392,7 +387,9 @@ def configure(env): import subprocess import re - linker_version_str = subprocess.check_output([env.subst(env["LINK"]), "-Wl,--version"]).decode("utf-8") + linker_version_str = subprocess.check_output( + [env.subst(env["LINK"]), "-Wl,--version"] + env.subst(env["LINKFLAGS"]) + ).decode("utf-8") gnu_ld_version = re.search("^GNU ld [^$]*(\d+\.\d+)$", linker_version_str, re.MULTILINE) if not gnu_ld_version: print( @@ -405,11 +402,12 @@ def configure(env): env.Append(LINKFLAGS=["-T", "platform/linuxbsd/pck_embed.legacy.ld"]) ## Cross-compilation - - if is64 and env["bits"] == "32": + # TODO: Support cross-compilation on architectures other than x86. + host_is_64_bit = sys.maxsize > 2**32 + if host_is_64_bit and env["arch"] == "x86_32": env.Append(CCFLAGS=["-m32"]) env.Append(LINKFLAGS=["-m32", "-L/usr/lib/i386-linux-gnu"]) - elif not is64 and env["bits"] == "64": + elif not host_is_64_bit and env["arch"] == "x86_64": env.Append(CCFLAGS=["-m64"]) env.Append(LINKFLAGS=["-m64", "-L/usr/lib/i686-linux-gnu"]) diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index 8efbd2e3c5..63998e2fde 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -322,8 +322,8 @@ bool DisplayServerX11::tts_is_paused() const { return tts->is_paused(); } -Array DisplayServerX11::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, Array()); +TypedArray<Dictionary> DisplayServerX11::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, TypedArray<Dictionary>()); return tts->get_voices(); } diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index 9ce6a557db..0174cfb881 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -308,7 +308,7 @@ public: #ifdef SPEECHD_ENABLED virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; - virtual Array tts_get_voices() const override; + virtual TypedArray<Dictionary> tts_get_voices() const override; virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; virtual void tts_pause() override; diff --git a/platform/macos/README.md b/platform/macos/README.md new file mode 100644 index 0000000000..feead80736 --- /dev/null +++ b/platform/macos/README.md @@ -0,0 +1,19 @@ +# macOS platform port + +This folder contains the C++, Objective-C and Objective-C++ code for the macOS +platform port. + +See also [`misc/dist/macos`](/misc/dist/macos) folder for additional files used +by this platform. [`misc/dist/macos_tools.app`](/misc/dist/macos_tools.app) is +an `.app` bundle template used for packaging the macOS editor, while +[`misc/dist/macos_template.app`](/misc/dist/macos_template.app) is used for +packaging macOS export templates. + +## Documentation + +- [Compiling for macOS](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_macos.html) + - Instructions on building this platform port from source. +- [Exporting for macOS](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_macos.html) + - Instructions on using the compiled export templates to export a project. +- [Running Godot apps on macOS](https://docs.godotengine.org/en/latest/tutorials/export/running_on_macos.html) + - Instructions on running Godot projects on macOS. diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 20e7afa772..e5bcb46b02 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -1,6 +1,7 @@ import os import sys from methods import detect_darwin_sdk_path +from platform_methods import detect_arch def is_active(): @@ -37,6 +38,7 @@ def get_opts(): def get_flags(): return [ + ("arch", detect_arch()), ("use_volk", False), ] @@ -71,6 +73,15 @@ def get_mvk_sdk_path(): def configure(env): + # Validate arch. + supported_arches = ["x86_64", "arm64"] + if env["arch"] not in supported_arches: + print( + 'Unsupported CPU architecture "%s" for macOS. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) + ) + sys.exit() + ## Build type if env["target"] == "release": @@ -96,25 +107,20 @@ def configure(env): env.Prepend(CCFLAGS=["-g3"]) env.Prepend(LINKFLAGS=["-Xlinker", "-no_deduplicate"]) - ## Architecture - - # macOS no longer runs on 32-bit since 10.7 which is unsupported since 2014 - # As such, we only support 64-bit - env["bits"] = "64" - ## Compiler configuration # Save this in environment for use by other modules if "OSXCROSS_ROOT" in os.environ: env["osxcross"] = True + # CPU architecture. if env["arch"] == "arm64": - print("Building for macOS 11.0+, platform arm64.") + print("Building for macOS 11.0+.") env.Append(ASFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) env.Append(CCFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) env.Append(LINKFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) - else: - print("Building for macOS 10.12+, platform x86_64.") + elif env["arch"] == "x86_64": + print("Building for macOS 10.12+.") env.Append(ASFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) @@ -185,9 +191,8 @@ def configure(env): ## Dependencies - if env["builtin_libtheora"]: - if env["arch"] != "arm64": - env["x86_libtheora_opt_gcc"] = True + if env["builtin_libtheora"] and env["arch"] == "x86_64": + env["x86_libtheora_opt_gcc"] = True ## Flags diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 54c479fc81..a08667a259 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -196,6 +196,9 @@ private: static NSCursor *_cursor_from_selector(SEL p_selector, SEL p_fallback = nil); + bool _has_help_menu() const; + NSMenuItem *_menu_add_item(const String &p_menu_root, const String &p_label, Key p_accel, int p_index, int *r_out); + public: NSMenu *get_dock_menu() const; void menu_callback(id p_sender); @@ -224,15 +227,15 @@ public: virtual bool has_feature(Feature p_feature) const override; virtual String get_name() const override; - virtual void global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual void global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1) override; - virtual void global_menu_add_separator(const String &p_menu_root, int p_index = -1) override; + virtual int global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1) override; + virtual int global_menu_add_separator(const String &p_menu_root, int p_index = -1) override; virtual int global_menu_get_item_index_from_text(const String &p_menu_root, const String &p_text) const override; virtual int global_menu_get_item_index_from_tag(const String &p_menu_root, const Variant &p_tag) const override; @@ -250,6 +253,7 @@ public: virtual int global_menu_get_item_state(const String &p_menu_root, int p_idx) const override; virtual int global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const override; virtual Ref<Texture2D> global_menu_get_item_icon(const String &p_menu_root, int p_idx) const override; + virtual int global_menu_get_item_indentation_level(const String &p_menu_root, int p_idx) const override; virtual void global_menu_set_item_checked(const String &p_menu_root, int p_idx, bool p_checked) override; virtual void global_menu_set_item_checkable(const String &p_menu_root, int p_idx, bool p_checkable) override; @@ -264,6 +268,7 @@ public: virtual void global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state) override; virtual void global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states) override; virtual void global_menu_set_item_icon(const String &p_menu_root, int p_idx, const Ref<Texture2D> &p_icon) override; + virtual void global_menu_set_item_indentation_level(const String &p_menu_root, int p_idx, int p_level) override; virtual int global_menu_get_item_count(const String &p_menu_root) const override; @@ -272,7 +277,7 @@ public: virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; - virtual Array tts_get_voices() const override; + virtual TypedArray<Dictionary> tts_get_voices() const override; virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; virtual void tts_pause() override; @@ -372,6 +377,11 @@ public: virtual void window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override; virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override; + virtual bool window_maximize_on_title_dbl_click() const override; + virtual bool window_minimize_on_title_dbl_click() const override; + + virtual Vector2i window_get_safe_title_margins(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual Point2i ime_get_selection() const override; virtual String ime_get_text() const override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index a49485154b..b06ae9f27c 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -63,7 +63,7 @@ const NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) const { const NSMenu *menu = nullptr; - if (p_menu_root == "") { + if (p_menu_root == "" || p_menu_root.to_lower() == "_main") { // Main menu. menu = [NSApp mainMenu]; } else if (p_menu_root.to_lower() == "_dock") { @@ -84,7 +84,7 @@ const NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) cons NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) { NSMenu *menu = nullptr; - if (p_menu_root == "") { + if (p_menu_root == "" || p_menu_root.to_lower() == "_main") { // Main menu. menu = [NSApp mainMenu]; } else if (p_menu_root.to_lower() == "_dock") { @@ -703,6 +703,7 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const { //case FEATURE_KEEP_SCREEN_ON: case FEATURE_SWAP_BUFFERS: case FEATURE_TEXT_TO_SPEECH: + case FEATURE_EXTEND_TO_TITLE: return true; default: { } @@ -714,18 +715,51 @@ String DisplayServerMacOS::get_name() const { return "macOS"; } -void DisplayServerMacOS::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ +bool DisplayServerMacOS::_has_help_menu() const { + if ([NSApp helpMenu]) { + return true; + } else { + NSMenu *menu = [NSApp mainMenu]; + const NSMenuItem *menu_item = [menu itemAtIndex:[menu numberOfItems] - 1]; + if (menu_item) { + String menu_name = String::utf8([[menu_item title] UTF8String]); + if (menu_name == "Help" || menu_name == RTR("Help")) { + return true; + } + } + return false; + } +} +NSMenuItem *DisplayServerMacOS::_menu_add_item(const String &p_menu_root, const String &p_label, Key p_accel, int p_index, int *r_out) { NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + int item_count = ((menu == [NSApp mainMenu]) && _has_help_menu()) ? [menu numberOfItems] - 1 : [menu numberOfItems]; + if ((menu == [NSApp mainMenu]) && (p_label == "Help" || p_label == RTR("Help"))) { + p_index = [menu numberOfItems]; + } else if (p_index < 0) { + p_index = item_count; } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_index++; + } + p_index = CLAMP(p_index, 0, item_count); } + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + *r_out = (menu == [NSApp mainMenu]) ? p_index - 1 : p_index; + return menu_item; + } + return nullptr; +} + +int DisplayServerMacOS::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { + _THREAD_SAFE_METHOD_ + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); + if (menu_item) { GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -735,20 +769,15 @@ void DisplayServerMacOS::global_menu_add_item(const String &p_menu_root, const S [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { +int DisplayServerMacOS::global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); - NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; - } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; - } + int out = -1; + NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); + if (menu_item) { GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -758,20 +787,15 @@ void DisplayServerMacOS::global_menu_add_check_item(const String &p_menu_root, c [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { +int DisplayServerMacOS::global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); - NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; - } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; - } + int out = -1; + NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); + if (menu_item) { GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -790,20 +814,15 @@ void DisplayServerMacOS::global_menu_add_icon_item(const String &p_menu_root, co [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { +int DisplayServerMacOS::global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); - NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; - } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; - } + int out = -1; + NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); + if (menu_item) { GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -822,20 +841,15 @@ void DisplayServerMacOS::global_menu_add_icon_check_item(const String &p_menu_ro [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { +int DisplayServerMacOS::global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); - NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; - } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; - } + int out = -1; + NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); + if (menu_item) { GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -845,20 +859,15 @@ void DisplayServerMacOS::global_menu_add_radio_check_item(const String &p_menu_r [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { +int DisplayServerMacOS::global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); - NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; - } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; - } + int out = -1; + NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); + if (menu_item) { GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -877,20 +886,31 @@ void DisplayServerMacOS::global_menu_add_icon_radio_check_item(const String &p_m [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { +int DisplayServerMacOS::global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback, const Variant &p_tag, Key p_accel, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); + int out = -1; if (menu) { String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + int item_count = ((menu == [NSApp mainMenu]) && _has_help_menu()) ? [menu numberOfItems] - 1 : [menu numberOfItems]; + if ((menu == [NSApp mainMenu]) && (p_label == "Help" || p_label == RTR("Help"))) { + p_index = [menu numberOfItems]; + } else if (p_index < 0) { + p_index = item_count; } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_index++; + } + p_index = CLAMP(p_index, 0, item_count); } + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + out = (menu == [NSApp mainMenu]) ? p_index - 1 : p_index; + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; obj->callback = p_callback; obj->meta = p_tag; @@ -900,44 +920,69 @@ void DisplayServerMacOS::global_menu_add_multistate_item(const String &p_menu_ro [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } + return out; } -void DisplayServerMacOS::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index) { +int DisplayServerMacOS::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); NSMenu *sub_menu = _get_menu_root(p_submenu); + int out = -1; if (menu && sub_menu) { if (sub_menu == menu) { ERR_PRINT("Can't set submenu to self!"); - return; + return -1; } if ([sub_menu supermenu]) { ERR_PRINT("Can't set submenu to menu that is already a submenu of some other menu!"); - return; + return -1; } NSMenuItem *menu_item; - if (p_index != -1) { - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; + int item_count = ((menu == [NSApp mainMenu]) && _has_help_menu()) ? [menu numberOfItems] - 1 : [menu numberOfItems]; + if ((menu == [NSApp mainMenu]) && (p_label == "Help" || p_label == RTR("Help"))) { + p_index = [menu numberOfItems]; + } else if (p_index < 0) { + p_index = item_count; } else { - menu_item = [menu addItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@""]; + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_index++; + } + p_index = CLAMP(p_index, 0, item_count); } + menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; + out = (menu == [NSApp mainMenu]) ? p_index - 1 : p_index; + + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = Callable(); + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = 0; + obj->state = 0; + [menu_item setRepresentedObject:obj]; + [sub_menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; [menu setSubmenu:sub_menu forItem:menu_item]; } + return out; } -void DisplayServerMacOS::global_menu_add_separator(const String &p_menu_root, int p_index) { +int DisplayServerMacOS::global_menu_add_separator(const String &p_menu_root, int p_index) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if (p_index != -1) { - [menu insertItem:[NSMenuItem separatorItem] atIndex:p_index]; + if (menu == [NSApp mainMenu]) { // Do not add separators into main menu. + return -1; + } + if (p_index < 0) { + p_index = [menu numberOfItems]; } else { - [menu addItem:[NSMenuItem separatorItem]]; + p_index = CLAMP(p_index, 0, [menu numberOfItems]); } + [menu insertItem:[NSMenuItem separatorItem] atIndex:p_index]; + return p_index; } + return -1; } int DisplayServerMacOS::global_menu_get_item_index_from_text(const String &p_menu_root, const String &p_text) const { @@ -945,7 +990,11 @@ int DisplayServerMacOS::global_menu_get_item_index_from_text(const String &p_men const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - return [menu indexOfItemWithTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + return [menu indexOfItemWithTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]] - 1; + } else { + return [menu indexOfItemWithTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; + } } return -1; @@ -956,12 +1005,16 @@ int DisplayServerMacOS::global_menu_get_item_index_from_tag(const String &p_menu const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - for (NSInteger i = 0; i < [menu numberOfItems]; i++) { + for (NSInteger i = (menu == [NSApp mainMenu]) ? 1 : 0; i < [menu numberOfItems]; i++) { const NSMenuItem *menu_item = [menu itemAtIndex:i]; if (menu_item) { const GodotMenuItem *obj = [menu_item representedObject]; if (obj && obj->meta == p_tag) { - return i; + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + return i - 1; + } else { + return i; + } } } } @@ -975,6 +1028,11 @@ bool DisplayServerMacOS::global_menu_is_item_checked(const String &p_menu_root, const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, false); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], false); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { return ([menu_item state] == NSControlStateValueOn); @@ -988,6 +1046,11 @@ bool DisplayServerMacOS::global_menu_is_item_checkable(const String &p_menu_root const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, false); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], false); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1004,6 +1067,11 @@ bool DisplayServerMacOS::global_menu_is_item_radio_checkable(const String &p_men const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, false); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], false); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1020,6 +1088,11 @@ Callable DisplayServerMacOS::global_menu_get_item_callback(const String &p_menu_ const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, Callable()); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], Callable()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1036,6 +1109,11 @@ Variant DisplayServerMacOS::global_menu_get_item_tag(const String &p_menu_root, const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, Variant()); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], Variant()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1052,6 +1130,11 @@ String DisplayServerMacOS::global_menu_get_item_text(const String &p_menu_root, const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, String()); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], String()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { return String::utf8([[menu_item title] UTF8String]); @@ -1065,6 +1148,11 @@ String DisplayServerMacOS::global_menu_get_item_submenu(const String &p_menu_roo const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, String()); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], String()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { const NSMenu *sub_menu = [menu_item submenu]; @@ -1085,6 +1173,11 @@ Key DisplayServerMacOS::global_menu_get_item_accelerator(const String &p_menu_ro const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, Key::NONE); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], Key::NONE); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { String ret = String::utf8([[menu_item keyEquivalent] UTF8String]); @@ -1116,6 +1209,11 @@ bool DisplayServerMacOS::global_menu_is_item_disabled(const String &p_menu_root, const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, false); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], false); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { return ![menu_item isEnabled]; @@ -1129,6 +1227,11 @@ String DisplayServerMacOS::global_menu_get_item_tooltip(const String &p_menu_roo const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, String()); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], String()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { return String::utf8([[menu_item toolTip] UTF8String]); @@ -1142,6 +1245,11 @@ int DisplayServerMacOS::global_menu_get_item_state(const String &p_menu_root, in const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], 0); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1158,6 +1266,11 @@ int DisplayServerMacOS::global_menu_get_item_max_states(const String &p_menu_roo const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], 0); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1174,6 +1287,11 @@ Ref<Texture2D> DisplayServerMacOS::global_menu_get_item_icon(const String &p_men const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { + ERR_FAIL_COND_V(p_idx < 0, Ref<Texture2D>()); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], Ref<Texture2D>()); const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; @@ -1187,14 +1305,34 @@ Ref<Texture2D> DisplayServerMacOS::global_menu_get_item_icon(const String &p_men return Ref<Texture2D>(); } +int DisplayServerMacOS::global_menu_get_item_indentation_level(const String &p_menu_root, int p_idx) const { + _THREAD_SAFE_METHOD_ + + const NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + ERR_FAIL_COND_V(p_idx < 0, 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND_V(p_idx >= [menu numberOfItems], 0); + const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + return [menu_item indentationLevel]; + } + } + return 0; +} + void DisplayServerMacOS::global_menu_set_item_checked(const String &p_menu_root, int p_idx, bool p_checked) { _THREAD_SAFE_METHOD_ NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { if (p_checked) { @@ -1211,12 +1349,15 @@ void DisplayServerMacOS::global_menu_set_item_checkable(const String &p_menu_roo NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_COND(!obj); obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_CHECK_BOX : CHECKABLE_TYPE_NONE; } } @@ -1227,12 +1368,15 @@ void DisplayServerMacOS::global_menu_set_item_radio_checkable(const String &p_me NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_COND(!obj); obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_RADIO_BUTTON : CHECKABLE_TYPE_NONE; } } @@ -1243,12 +1387,15 @@ void DisplayServerMacOS::global_menu_set_item_callback(const String &p_menu_root NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_COND(!obj); obj->callback = p_callback; } } @@ -1259,12 +1406,15 @@ void DisplayServerMacOS::global_menu_set_item_tag(const String &p_menu_root, int NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_COND(!obj); obj->meta = p_tag; } } @@ -1275,9 +1425,11 @@ void DisplayServerMacOS::global_menu_set_item_text(const String &p_menu_root, in NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { [menu_item setTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; @@ -1299,9 +1451,11 @@ void DisplayServerMacOS::global_menu_set_item_submenu(const String &p_menu_root, ERR_PRINT("Can't set submenu to menu that is already a submenu of some other menu!"); return; } - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { [menu setSubmenu:sub_menu forItem:menu_item]; @@ -1314,9 +1468,11 @@ void DisplayServerMacOS::global_menu_set_item_accelerator(const String &p_menu_r NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_keycode)]; @@ -1331,9 +1487,11 @@ void DisplayServerMacOS::global_menu_set_item_disabled(const String &p_menu_root NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { [menu_item setEnabled:(!p_disabled)]; @@ -1346,9 +1504,11 @@ void DisplayServerMacOS::global_menu_set_item_tooltip(const String &p_menu_root, NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { [menu_item setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; @@ -1361,15 +1521,16 @@ void DisplayServerMacOS::global_menu_set_item_state(const String &p_menu_root, i NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - obj->state = p_state; - } + ERR_FAIL_COND(!obj); + obj->state = p_state; } } } @@ -1379,15 +1540,16 @@ void DisplayServerMacOS::global_menu_set_item_max_states(const String &p_menu_ro NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - obj->max_states = p_max_states; - } + ERR_FAIL_COND(!obj); + obj->max_states = p_max_states; } } } @@ -1397,12 +1559,15 @@ void DisplayServerMacOS::global_menu_set_item_icon(const String &p_menu_root, in NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not edit Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; if (menu_item) { GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_COND(!obj); if (p_icon.is_valid()) { obj->img = p_icon->get_image(); obj->img = obj->img->duplicate(); @@ -1419,12 +1584,33 @@ void DisplayServerMacOS::global_menu_set_item_icon(const String &p_menu_root, in } } +void DisplayServerMacOS::global_menu_set_item_indentation_level(const String &p_menu_root, int p_idx, int p_level) { + _THREAD_SAFE_METHOD_ + + NSMenu *menu = _get_menu_root(p_menu_root); + if (menu) { + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; + } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); + NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setIndentationLevel:p_level]; + } + } +} + int DisplayServerMacOS::global_menu_get_item_count(const String &p_menu_root) const { _THREAD_SAFE_METHOD_ const NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - return [menu numberOfItems]; + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + return [menu numberOfItems] - 1; + } else { + return [menu numberOfItems]; + } } else { return 0; } @@ -1435,9 +1621,11 @@ void DisplayServerMacOS::global_menu_remove_item(const String &p_menu_root, int NSMenu *menu = _get_menu_root(p_menu_root); if (menu) { - if ((menu == [NSApp mainMenu]) && (p_idx == 0)) { // Do not delete Apple menu. - return; + ERR_FAIL_COND(p_idx < 0); + if (menu == [NSApp mainMenu]) { // Skip Apple menu. + p_idx++; } + ERR_FAIL_COND(p_idx >= [menu numberOfItems]); [menu removeItemAtIndex:p_idx]; } } @@ -1453,6 +1641,9 @@ void DisplayServerMacOS::global_menu_clear(const String &p_menu_root) { NSMenuItem *menu_item = [menu addItemWithTitle:@"" action:nil keyEquivalent:@""]; [menu setSubmenu:apple_menu forItem:menu_item]; } + if (submenu.has(p_menu_root)) { + submenu.erase(p_menu_root); + } } } @@ -1466,7 +1657,7 @@ bool DisplayServerMacOS::tts_is_paused() const { return [tts isPaused]; } -Array DisplayServerMacOS::tts_get_voices() const { +TypedArray<Dictionary> DisplayServerMacOS::tts_get_voices() const { ERR_FAIL_COND_V(!tts, Array()); return [tts getVoices]; } @@ -2348,6 +2539,45 @@ bool DisplayServerMacOS::window_is_maximize_allowed(WindowID p_window) const { return true; } +bool DisplayServerMacOS::window_maximize_on_title_dbl_click() const { + id value = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleActionOnDoubleClick"]; + if ([value isKindOfClass:[NSString class]]) { + return [value isEqualToString:@"Maximize"]; + } + return false; +} + +bool DisplayServerMacOS::window_minimize_on_title_dbl_click() const { + id value = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleActionOnDoubleClick"]; + if ([value isKindOfClass:[NSString class]]) { + return [value isEqualToString:@"Minimize"]; + } + return false; +} + +Vector2i DisplayServerMacOS::window_get_safe_title_margins(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND_V(!windows.has(p_window), Vector2i()); + const WindowData &wd = windows[p_window]; + + float max_x = 0.f; + NSButton *cb = [wd.window_object standardWindowButton:NSWindowCloseButton]; + if (cb) { + max_x = MAX(max_x, [cb frame].origin.x + [cb frame].size.width); + } + NSButton *mb = [wd.window_object standardWindowButton:NSWindowMiniaturizeButton]; + if (mb) { + max_x = MAX(max_x, [mb frame].origin.x + [mb frame].size.width); + } + NSButton *zb = [wd.window_object standardWindowButton:NSWindowZoomButton]; + if (zb) { + max_x = MAX(max_x, [zb frame].origin.x + [zb frame].size.width); + } + + return Vector2i(max_x * screen_get_max_scale(), 0); +} + void DisplayServerMacOS::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -2366,6 +2596,19 @@ void DisplayServerMacOS::window_set_flag(WindowFlags p_flag, bool p_enabled, Win [wd.window_object setStyleMask:[wd.window_object styleMask] | NSWindowStyleMaskResizable]; } } break; + case WINDOW_FLAG_EXTEND_TO_TITLE: { + NSRect rect = [wd.window_object frame]; + if (p_enabled) { + [wd.window_object setTitlebarAppearsTransparent:YES]; + [wd.window_object setTitleVisibility:NSWindowTitleHidden]; + [wd.window_object setStyleMask:[wd.window_object styleMask] | NSWindowStyleMaskFullSizeContentView]; + } else { + [wd.window_object setTitlebarAppearsTransparent:NO]; + [wd.window_object setTitleVisibility:NSWindowTitleVisible]; + [wd.window_object setStyleMask:[wd.window_object styleMask] & ~NSWindowStyleMaskFullSizeContentView]; + } + [wd.window_object setFrame:rect display:YES]; + } break; case WINDOW_FLAG_BORDERLESS: { // OrderOut prevents a lose focus bug with the window. if ([wd.window_object isVisible]) { @@ -2433,6 +2676,9 @@ bool DisplayServerMacOS::window_get_flag(WindowFlags p_flag, WindowID p_window) case WINDOW_FLAG_RESIZE_DISABLED: { return wd.resize_disabled; } break; + case WINDOW_FLAG_EXTEND_TO_TITLE: { + return [wd.window_object styleMask] & NSWindowStyleMaskFullSizeContentView; + } break; case WINDOW_FLAG_BORDERLESS: { return [wd.window_object styleMask] == NSWindowStyleMaskBorderless; } break; @@ -3222,7 +3468,7 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM [apple_menu addItem:[NSMenuItem separatorItem]]; - title = [NSString stringWithFormat:NSLocalizedString(@"Quit %@", nil), nsappname]; + title = [NSString stringWithFormat:NSLocalizedString(@"\t\tQuit %@", nil), nsappname]; [apple_menu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; // Add items to the menu bar. diff --git a/platform/macos/export/export.cpp b/platform/macos/export/export.cpp index ff7457081f..f219616df4 100644 --- a/platform/macos/export/export.cpp +++ b/platform/macos/export/export.cpp @@ -33,8 +33,12 @@ #include "export_plugin.h" void register_macos_exporter() { - EDITOR_DEF("export/macos/force_builtin_codesign", false); - EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::BOOL, "export/macos/force_builtin_codesign", PROPERTY_HINT_NONE)); + EDITOR_DEF("export/macos/rcodesign", ""); +#ifdef WINDOWS_ENABLED + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/macos/rcodesign", PROPERTY_HINT_GLOBAL_FILE, "*.exe")); +#else + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/macos/rcodesign", PROPERTY_HINT_GLOBAL_FILE)); +#endif Ref<EditorExportPlatformMacOS> platform; platform.instantiate(); diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index bcc2636c07..4b453add5a 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -51,11 +51,51 @@ void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset r_features->push_back(p_preset->get("binary_format/architecture")); } -bool EditorExportPlatformMacOS::get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const { - // These options are not supported by built-in codesign, used on non macOS host. - if (!OS::get_singleton()->has_feature("macos")) { - if (p_option == "codesign/identity" || p_option == "codesign/timestamp" || p_option == "codesign/hardened_runtime" || p_option == "codesign/custom_options" || p_option.begins_with("notarization/")) { - return false; +bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const { + // Hide irrelevant code signing options. + if (p_preset) { + int codesign_tool = p_preset->get("codesign/codesign"); + switch (codesign_tool) { + case 1: { // built-in ad-hoc + if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options") { + return false; + } + } break; + case 2: { // "rcodesign" + if (p_option == "codesign/identity") { + return false; + } + } break; +#ifdef MACOS_ENABLED + case 3: { // "codesign" + if (p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password") { + return false; + } + } break; +#endif + default: { // disabled + if (p_option == "codesign/identity" || p_option == "codesign/certificate_file" || p_option == "codesign/certificate_password" || p_option == "codesign/custom_options" || p_option.begins_with("codesign/entitlements")) { + return false; + } + } break; + } + + // Hide irrelevant notarization options. + int notary_tool = p_preset->get("notarization/notarization"); + switch (notary_tool) { + case 1: { // "rcodesign" + if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/apple_team_id") { + return false; + } + } break; + case 2: { // "altool" + // All options are visible. + } break; + default: { // disabled + if (p_option == "notarization/apple_id_name" || p_option == "notarization/apple_id_password" || p_option == "notarization/apple_team_id" || p_option == "notarization/api_uuid" || p_option == "notarization/api_key") { + return false; + } + } break; } } @@ -83,40 +123,22 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/copyright"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "application/copyright_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "display/high_res"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), true)); +#ifdef MACOS_ENABLED + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),PyOxidizer rcodesign,Xcode codesign"), 3, true)); +#else + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/codesign", PROPERTY_HINT_ENUM, "Disabled,Built-in (ad-hoc only),PyOxidizer rcodesign"), 1, true)); +#endif + // "codesign" only options: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_PLACEHOLDER_TEXT, "Type: Name (ID)"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/replace_existing_signature"), true)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/hardened_runtime"), true)); + // "rcodesign" only options: + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_file", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/certificate_password", PROPERTY_HINT_PASSWORD), "")); + // "codesign" and "rcodesign" only options: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/custom_file", PROPERTY_HINT_GLOBAL_FILE, "*.plist"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_jit_code_execution"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_unsigned_executable_memory"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/allow_dyld_environment_variables"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/disable_library_validation"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/audio_input"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/camera"), false)); @@ -126,7 +148,6 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/photos_library"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/apple_events"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/debugging"), false)); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/enabled"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_server"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/entitlements/app_sandbox/network_client"), false)); @@ -137,13 +158,43 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_music", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array())); - r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); - r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "notarization/enable"), false)); +#ifdef MACOS_ENABLED + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,PyOxidizer rcodesign,Xcode altool"), 0, true)); +#else + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "notarization/notarization", PROPERTY_HINT_ENUM, "Disabled,PyOxidizer rcodesign"), 0, true)); +#endif + // "altool" only options: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_name", PROPERTY_HINT_PLACEHOLDER_TEXT, "Apple ID email"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PLACEHOLDER_TEXT, "Enable two-factor authentication and provide app-specific password"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_id_password", PROPERTY_HINT_PASSWORD, "Enable two-factor authentication and provide app-specific password"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/apple_team_id", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide team ID if your Apple ID belongs to multiple teams"), "")); + // "altool" and "rcodesign" only options: + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_uuid", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect issuer ID"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "notarization/api_key", PROPERTY_HINT_PLACEHOLDER_TEXT, "App Store Connect API key ID"), "")); + + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/microphone_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the microphone"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/microphone_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/camera_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the camera"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/camera_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/location_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the location information"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/location_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/address_book_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the address book"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/address_book_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/calendar_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the calendar"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/calendar_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/photos_library_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use the photo library"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/photos_library_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/desktop_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Desktop folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/desktop_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/documents_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Documents folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/documents_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/downloads_folder_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use Downloads folder"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/downloads_folder_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/network_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use network volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/network_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "privacy/removable_volumes_usage_description", PROPERTY_HINT_PLACEHOLDER_TEXT, "Provide a message if you need to use removable volumes"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "privacy/removable_volumes_usage_description_localized", PROPERTY_HINT_LOCALIZABLE_STRING), Dictionary())); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/s3tc"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "texture_format/etc"), false)); @@ -415,156 +466,284 @@ void EditorExportPlatformMacOS::_fix_plist(const Ref<EditorExportPreset> &p_pres */ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + int notary_tool = p_preset->get("notarization/notarization"); + switch (notary_tool) { + case 1: { // "rcodesign" + print_verbose("using rcodesign notarization..."); + + String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String(); + if (rcodesign.is_empty()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); + return Error::FAILED; + } + + List<String> args; + + args.push_back("notary-submit"); + + if (p_preset->get("notarization/api_uuid") == "") { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect issuer ID name not specified.")); + return Error::FAILED; + } + if (p_preset->get("notarization/api_key") == "") { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); + return Error::FAILED; + } + + args.push_back("--api-issuer"); + args.push_back(p_preset->get("notarization/api_uuid")); + + args.push_back("--api-key"); + args.push_back(p_preset->get("notarization/api_key")); + + args.push_back(p_path); + + String str; + int exitcode = 0; + + Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start rcodesign executable.")); + return err; + } + + int rq_offset = str.find("created submission ID:"); + if (exitcode != 0 || rq_offset == -1) { + print_line("rcodesign (" + p_path + "):\n" + str); + add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details.")); + return Error::FAILED; + } else { + print_verbose("rcodesign (" + p_path + "):\n" + str); + int next_nl = str.find("\n", rq_offset); + String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 14, -1) : str.substr(rq_offset + 14, next_nl - rq_offset - 14); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour.")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign notary-log --api-issuer <api uuid> --api-key <api key> <request uuid>\""); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"rcodesign staple <app path>\""); + } + } break; #ifdef MACOS_ENABLED - List<String> args; + case 2: { // "altool" + print_verbose("using altool notarization..."); - args.push_back("altool"); - args.push_back("--notarize-app"); + if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Xcode command line tools are not installed.")); + return Error::FAILED; + } - args.push_back("--primary-bundle-id"); - args.push_back(p_preset->get("application/bundle_identifier")); + List<String> args; - args.push_back("--username"); - args.push_back(p_preset->get("notarization/apple_id_name")); + args.push_back("altool"); + args.push_back("--notarize-app"); - args.push_back("--password"); - args.push_back(p_preset->get("notarization/apple_id_password")); + args.push_back("--primary-bundle-id"); + args.push_back(p_preset->get("application/bundle_identifier")); - args.push_back("--type"); - args.push_back("osx"); + if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Neither Apple ID name nor App Store Connect issuer ID name not specified.")); + return Error::FAILED; + } + if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.")); + return Error::FAILED; + } - if (p_preset->get("notarization/apple_team_id")) { - args.push_back("--asc-provider"); - args.push_back(p_preset->get("notarization/apple_team_id")); - } + if (p_preset->get("notarization/apple_id_name") != "") { + if (p_preset->get("notarization/apple_id_password") == "") { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("Apple ID password not specified.")); + return Error::FAILED; + } + args.push_back("--username"); + args.push_back(p_preset->get("notarization/apple_id_name")); - args.push_back("--file"); - args.push_back(p_path); + args.push_back("--password"); + args.push_back(p_preset->get("notarization/apple_id_password")); + } else { + if (p_preset->get("notarization/api_key") == "") { + add_message(EXPORT_MESSAGE_ERROR, TTR("Notarization"), TTR("App Store Connect API key ID not specified.")); + return Error::FAILED; + } + args.push_back("--apiIssuer"); + args.push_back(p_preset->get("notarization/api_uuid")); - String str; - Error err = OS::get_singleton()->execute("xcrun", args, &str, nullptr, true); - if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable.")); - return err; - } + args.push_back("--apiKey"); + args.push_back(p_preset->get("notarization/api_key")); + } - print_verbose("altool (" + p_path + "):\n" + str); - int rq_offset = str.find("RequestUUID"); - if (rq_offset == -1) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed.")); - return FAILED; - } else { - int next_nl = str.find("\n", rq_offset); - String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 14, -1) : str.substr(rq_offset + 14, next_nl - rq_offset - 14); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); - add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\""); - } + args.push_back("--type"); + args.push_back("osx"); -#endif + if (p_preset->get("notarization/apple_team_id")) { + args.push_back("--asc-provider"); + args.push_back(p_preset->get("notarization/apple_team_id")); + } + + args.push_back("--file"); + args.push_back(p_path); + + String str; + int exitcode = 0; + Error err = OS::get_singleton()->execute("xcrun", args, &str, &exitcode, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Could not start xcrun executable.")); + return err; + } + int rq_offset = str.find("RequestUUID"); + if (exitcode != 0 || rq_offset == -1) { + print_line("xcrun altool (" + p_path + "):\n" + str); + add_message(EXPORT_MESSAGE_WARNING, TTR("Notarization"), TTR("Notarization failed, see editor log for details.")); + return Error::FAILED; + } else { + print_verbose("xcrun altool (" + p_path + "):\n" + str); + int next_nl = str.find("\n", rq_offset); + String request_uuid = (next_nl == -1) ? str.substr(rq_offset + 14, -1) : str.substr(rq_offset + 14, next_nl - rq_offset - 14); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), vformat(TTR("Notarization request UUID: \"%s\""), request_uuid)); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("The notarization process generally takes less than an hour. When the process is completed, you'll receive an email.")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("You can check progress manually by opening a Terminal and running the following command:")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun altool --notarization-history 0 -u <your email> -p <app-specific pwd>\""); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t" + TTR("Run the following command to staple the notarization ticket to the exported application (optional):")); + add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), "\t\t\"xcrun stapler staple <app path>\""); + } + } break; +#endif + default: { + }; + } return OK; } Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) { - bool force_builtin_codesign = EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign"); - bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); - - if ((!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) || force_builtin_codesign) { - print_verbose("using built-in codesign..."); + int codesign_tool = p_preset->get("codesign/codesign"); + switch (codesign_tool) { + case 1: { // built-in ad-hoc + print_verbose("using built-in codesign..."); #ifdef MODULE_REGEX_ENABLED - -#ifdef MACOS_ENABLED - if (p_preset->get("codesign/timestamp") && p_warn) { - add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Timestamping is not compatible with ad-hoc signature, and was disabled!")); - } - if (p_preset->get("codesign/hardened_runtime") && p_warn) { - add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!")); - } -#endif - - String error_msg; - Error err = CodeSign::codesign(false, p_preset->get("codesign/replace_existing_signature"), p_path, p_ent_path, error_msg); - if (err != OK) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg)); - return FAILED; - } + String error_msg; + Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg)); + return Error::FAILED; + } #else - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module.")); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module.")); #endif - return OK; - } else { - print_verbose("using external codesign..."); - List<String> args; - if (p_preset->get("codesign/timestamp")) { - if (ad_hoc) { - if (p_warn) { - add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Timestamping is not compatible with ad-hoc signature, and was disabled!")); - } - } else { - args.push_back("--timestamp"); + } break; + case 2: { // "rcodesign" + print_verbose("using rcodesign codesign..."); + + String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String(); + if (rcodesign.is_empty()) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); + return Error::FAILED; } - } - if (p_preset->get("codesign/hardened_runtime")) { - if (ad_hoc) { - if (p_warn) { - add_message(EXPORT_MESSAGE_INFO, TTR("Code Signing"), TTR("Hardened Runtime is not compatible with ad-hoc signature, and was disabled!")); - } + + List<String> args; + args.push_back("sign"); + + if (p_path.get_extension() != "dmg") { + args.push_back("--entitlements-xml-path"); + args.push_back(p_ent_path); + } + + String certificate_file = p_preset->get("codesign/certificate_file"); + String certificate_pass = p_preset->get("codesign/certificate_password"); + if (!certificate_file.is_empty() && !certificate_file.is_empty()) { + args.push_back("--p12-file"); + args.push_back(certificate_file); + args.push_back("--p12-password"); + args.push_back(certificate_pass); + } + + args.push_back("-v"); /* provide some more feedback */ + + args.push_back(p_path); + + String str; + int exitcode = 0; + + Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable.")); + return err; + } + + if (exitcode != 0) { + print_line("rcodesign (" + p_path + "):\n" + str); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); + return Error::FAILED; } else { + print_verbose("rcodesign (" + p_path + "):\n" + str); + } + } break; +#ifdef MACOS_ENABLED + case 3: { // "codesign" + print_verbose("using xcode codesign..."); + + if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed.")); + return Error::FAILED; + } + + bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); + + List<String> args; + if (!ad_hoc) { + args.push_back("--timestamp"); args.push_back("--options"); args.push_back("runtime"); } - } - if (p_path.get_extension() != "dmg") { - args.push_back("--entitlements"); - args.push_back(p_ent_path); - } - - PackedStringArray user_args = p_preset->get("codesign/custom_options"); - for (int i = 0; i < user_args.size(); i++) { - String user_arg = user_args[i].strip_edges(); - if (!user_arg.is_empty()) { - args.push_back(user_arg); + if (p_path.get_extension() != "dmg") { + args.push_back("--entitlements"); + args.push_back(p_ent_path); } - } - args.push_back("-s"); - if (ad_hoc) { - args.push_back("-"); - } else { - args.push_back(p_preset->get("codesign/identity")); - } + PackedStringArray user_args = p_preset->get("codesign/custom_options"); + for (int i = 0; i < user_args.size(); i++) { + String user_arg = user_args[i].strip_edges(); + if (!user_arg.is_empty()) { + args.push_back(user_arg); + } + } - args.push_back("-v"); /* provide some more feedback */ + args.push_back("-s"); + if (ad_hoc) { + args.push_back("-"); + } else { + args.push_back(p_preset->get("codesign/identity")); + } - if (p_preset->get("codesign/replace_existing_signature")) { + args.push_back("-v"); /* provide some more feedback */ args.push_back("-f"); - } - args.push_back(p_path); + args.push_back(p_path); - String str; - Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true); - if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed.")); - return err; - } + String str; + int exitcode = 0; - print_verbose("codesign (" + p_path + "):\n" + str); - if (str.find("no identity found") != -1) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("No identity found.")); - return FAILED; - } - if ((str.find("unrecognized blob type") != -1) || (str.find("cannot read entitlement data") != -1)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Invalid entitlements file.")); - return FAILED; - } - return OK; + Error err = OS::get_singleton()->execute("codesign", args, &str, nullptr, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed.")); + return err; + } + + if (exitcode != 0) { + print_line("codesign (" + p_path + "):\n" + str); + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); + return Error::FAILED; + } else { + print_verbose("codesign (" + p_path + "):\n" + str); + } + } break; +#endif + default: { + }; } + + return OK; } Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, @@ -816,7 +995,9 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p err = ERR_CANT_CREATE; } - DirAccess::remove_file_or_error(scr_path); + if (FileAccess::exists(scr_path)) { + DirAccess::remove_file_or_error(scr_path); + } if (DirAccess::exists(tmp_app_path_name)) { String old_dir = tmp_app_dir->get_current_dir(); if (tmp_app_dir->change_dir(tmp_app_path_name) == OK) { @@ -919,54 +1100,58 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p f->store_line("NSHumanReadableCopyright = \"" + p_preset->get("application/copyright").operator String() + "\";"); } + HashSet<String> languages; for (const String &E : translations) { Ref<Translation> tr = ResourceLoader::load(E); - if (tr.is_valid()) { - String lang = tr->get_locale(); - String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj"; - tmp_app_dir->make_dir_recursive(fname); - Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); - f->store_line("/* Localized versions of Info.plist keys */"); - f->store_line(""); - if (appnames.has(lang)) { - f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); - } - if (microphone_usage_descriptions.has(lang)) { - f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); - } - if (camera_usage_descriptions.has(lang)) { - f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); - } - if (location_usage_descriptions.has(lang)) { - f->store_line("NSLocationUsageDescription = \"" + location_usage_descriptions[lang].operator String() + "\";"); - } - if (address_book_usage_descriptions.has(lang)) { - f->store_line("NSContactsUsageDescription = \"" + address_book_usage_descriptions[lang].operator String() + "\";"); - } - if (calendar_usage_descriptions.has(lang)) { - f->store_line("NSCalendarsUsageDescription = \"" + calendar_usage_descriptions[lang].operator String() + "\";"); - } - if (photos_library_usage_descriptions.has(lang)) { - f->store_line("NSPhotoLibraryUsageDescription = \"" + photos_library_usage_descriptions[lang].operator String() + "\";"); - } - if (desktop_folder_usage_descriptions.has(lang)) { - f->store_line("NSDesktopFolderUsageDescription = \"" + desktop_folder_usage_descriptions[lang].operator String() + "\";"); - } - if (documents_folder_usage_descriptions.has(lang)) { - f->store_line("NSDocumentsFolderUsageDescription = \"" + documents_folder_usage_descriptions[lang].operator String() + "\";"); - } - if (downloads_folder_usage_descriptions.has(lang)) { - f->store_line("NSDownloadsFolderUsageDescription = \"" + downloads_folder_usage_descriptions[lang].operator String() + "\";"); - } - if (network_volumes_usage_descriptions.has(lang)) { - f->store_line("NSNetworkVolumesUsageDescription = \"" + network_volumes_usage_descriptions[lang].operator String() + "\";"); - } - if (removable_volumes_usage_descriptions.has(lang)) { - f->store_line("NSRemovableVolumesUsageDescription = \"" + removable_volumes_usage_descriptions[lang].operator String() + "\";"); - } - if (copyrights.has(lang)) { - f->store_line("NSHumanReadableCopyright = \"" + copyrights[lang].operator String() + "\";"); - } + if (tr.is_valid() && tr->get_locale() != "en") { + languages.insert(tr->get_locale()); + } + } + + for (const String &lang : languages) { + String fname = tmp_app_path_name + "/Contents/Resources/" + lang + ".lproj"; + tmp_app_dir->make_dir_recursive(fname); + Ref<FileAccess> f = FileAccess::open(fname + "/InfoPlist.strings", FileAccess::WRITE); + f->store_line("/* Localized versions of Info.plist keys */"); + f->store_line(""); + if (appnames.has(lang)) { + f->store_line("CFBundleDisplayName = \"" + appnames[lang].operator String() + "\";"); + } + if (microphone_usage_descriptions.has(lang)) { + f->store_line("NSMicrophoneUsageDescription = \"" + microphone_usage_descriptions[lang].operator String() + "\";"); + } + if (camera_usage_descriptions.has(lang)) { + f->store_line("NSCameraUsageDescription = \"" + camera_usage_descriptions[lang].operator String() + "\";"); + } + if (location_usage_descriptions.has(lang)) { + f->store_line("NSLocationUsageDescription = \"" + location_usage_descriptions[lang].operator String() + "\";"); + } + if (address_book_usage_descriptions.has(lang)) { + f->store_line("NSContactsUsageDescription = \"" + address_book_usage_descriptions[lang].operator String() + "\";"); + } + if (calendar_usage_descriptions.has(lang)) { + f->store_line("NSCalendarsUsageDescription = \"" + calendar_usage_descriptions[lang].operator String() + "\";"); + } + if (photos_library_usage_descriptions.has(lang)) { + f->store_line("NSPhotoLibraryUsageDescription = \"" + photos_library_usage_descriptions[lang].operator String() + "\";"); + } + if (desktop_folder_usage_descriptions.has(lang)) { + f->store_line("NSDesktopFolderUsageDescription = \"" + desktop_folder_usage_descriptions[lang].operator String() + "\";"); + } + if (documents_folder_usage_descriptions.has(lang)) { + f->store_line("NSDocumentsFolderUsageDescription = \"" + documents_folder_usage_descriptions[lang].operator String() + "\";"); + } + if (downloads_folder_usage_descriptions.has(lang)) { + f->store_line("NSDownloadsFolderUsageDescription = \"" + downloads_folder_usage_descriptions[lang].operator String() + "\";"); + } + if (network_volumes_usage_descriptions.has(lang)) { + f->store_line("NSNetworkVolumesUsageDescription = \"" + network_volumes_usage_descriptions[lang].operator String() + "\";"); + } + if (removable_volumes_usage_descriptions.has(lang)) { + f->store_line("NSRemovableVolumesUsageDescription = \"" + removable_volumes_usage_descriptions[lang].operator String() + "\";"); + } + if (copyrights.has(lang)) { + f->store_line("NSHumanReadableCopyright = \"" + copyrights[lang].operator String() + "\";"); } } } @@ -1144,7 +1329,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p err = save_pack(p_preset, p_debug, pack_path, &shared_objects); // See if we can code sign our new package. - bool sign_enabled = p_preset->get("codesign/enable"); + bool sign_enabled = (p_preset->get("codesign/codesign").operator int() > 0); String ent_path = p_preset->get("codesign/entitlements/custom_file"); String hlp_ent_path = EditorPaths::get_singleton()->get_cache_dir().plus_file(pkg_name + "_helper.entitlements"); @@ -1310,14 +1495,25 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } } - bool ad_hoc = true; - if (err == OK) { + bool ad_hoc = false; + int codesign_tool = p_preset->get("codesign/codesign"); + switch (codesign_tool) { + case 1: { // built-in ad-hoc + ad_hoc = true; + } break; + case 2: { // "rcodesign" + ad_hoc = p_preset->get("codesign/certificate_file").operator String().is_empty() || p_preset->get("codesign/certificate_password").operator String().is_empty(); + } break; #ifdef MACOS_ENABLED - String sign_identity = p_preset->get("codesign/identity"); -#else - String sign_identity = "-"; + case 3: { // "codesign" + ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); + } break; #endif - ad_hoc = (sign_identity == "" || sign_identity == "-"); + default: { + }; + } + + if (err == OK) { bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation"); if ((!dylibs_found.is_empty() || !shared_objects.is_empty()) && sign_enabled && ad_hoc && !lib_validation) { add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Ad-hoc signed applications require the 'Disable Library Validation' entitlement to load dynamic libraries.")); @@ -1400,8 +1596,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } } -#ifdef MACOS_ENABLED - bool noto_enabled = p_preset->get("notarization/enable"); + bool noto_enabled = (p_preset->get("notarization/notarization").operator int() > 0); if (err == OK && noto_enabled) { if (export_format == "app") { add_message(EXPORT_MESSAGE_INFO, TTR("Notarization"), TTR("Notarization requires the app to be archived first, select the DMG or ZIP export format instead.")); @@ -1412,10 +1607,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p err = _notarize(p_preset, p_path); } } -#endif // Clean up temporary entitlements files. - DirAccess::remove_file_or_error(hlp_ent_path); + if (FileAccess::exists(hlp_ent_path)) { + DirAccess::remove_file_or_error(hlp_ent_path); + } // Clean up temporary .app dir and generated entitlements. if ((String)(p_preset->get("codesign/entitlements/custom_file")) == "") { @@ -1550,7 +1746,7 @@ void EditorExportPlatformMacOS::_zip_folder_recursive(zipFile &p_zip, const Stri da->list_dir_end(); } -bool EditorExportPlatformMacOS::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformMacOS::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { String err; bool valid = false; @@ -1580,6 +1776,17 @@ bool EditorExportPlatformMacOS::can_export(const Ref<EditorExportPreset> &p_pres valid = dvalid || rvalid; r_missing_templates = !valid; + if (!err.is_empty()) { + r_error = err; + } + + return valid; +} + +bool EditorExportPlatformMacOS::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { + String err; + bool valid = true; + String identifier = p_preset->get("application/bundle_identifier"); String pn_err; if (!is_package_name_valid(identifier, &pn_err)) { @@ -1587,65 +1794,98 @@ bool EditorExportPlatformMacOS::can_export(const Ref<EditorExportPreset> &p_pres valid = false; } - bool sign_enabled = p_preset->get("codesign/enable"); - + bool ad_hoc = false; + int codesign_tool = p_preset->get("codesign/codesign"); + switch (codesign_tool) { + case 1: { // built-in ad-hoc + ad_hoc = true; + } break; + case 2: { // "rcodesign" + ad_hoc = p_preset->get("codesign/certificate_file").operator String().is_empty() || p_preset->get("codesign/certificate_password").operator String().is_empty(); + } break; #ifdef MACOS_ENABLED - bool noto_enabled = p_preset->get("notarization/enable"); - bool ad_hoc = ((p_preset->get("codesign/identity") == "") || (p_preset->get("codesign/identity") == "-")); - - if (!ad_hoc && (bool)EditorSettings::get_singleton()->get("export/macos/force_builtin_codesign")) { - err += TTR("Warning: Built-in \"codesign\" is selected in the Editor Settings. Code signing is limited to ad-hoc signature only.") + "\n"; - } - if (!ad_hoc && !FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) { - err += TTR("Warning: Xcode command line tools are not installed, using built-in \"codesign\". Code signing is limited to ad-hoc signature only.") + "\n"; + case 3: { // "codesign" + ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); + } break; +#endif + default: { + }; } + int notary_tool = p_preset->get("notarization/notarization"); - if (noto_enabled) { + if (notary_tool > 0) { if (ad_hoc) { err += TTR("Notarization: Notarization with an ad-hoc signature is not supported.") + "\n"; valid = false; } - if (!sign_enabled) { + if (codesign_tool == 0) { err += TTR("Notarization: Code signing is required for notarization.") + "\n"; valid = false; } - if (!(bool)p_preset->get("codesign/hardened_runtime")) { - err += TTR("Notarization: Hardened runtime is required for notarization.") + "\n"; - valid = false; - } - if (!(bool)p_preset->get("codesign/timestamp")) { - err += TTR("Notarization: Timestamping is required for notarization.") + "\n"; - valid = false; - } - if (p_preset->get("notarization/apple_id_name") == "") { - err += TTR("Notarization: Apple ID name not specified.") + "\n"; - valid = false; - } - if (p_preset->get("notarization/apple_id_password") == "") { - err += TTR("Notarization: Apple ID password not specified.") + "\n"; - valid = false; + if (notary_tool == 2) { + if (!FileAccess::exists("/usr/bin/xcrun") && !FileAccess::exists("/bin/xcrun")) { + err += TTR("Notarization: Xcode command line tools are not installed.") + "\n"; + valid = false; + } + if (p_preset->get("notarization/apple_id_name") == "" && p_preset->get("notarization/api_uuid") == "") { + err += TTR("Notarization: Neither Apple ID name nor App Store Connect issuer ID name not specified.") + "\n"; + valid = false; + } else if (p_preset->get("notarization/apple_id_name") != "" && p_preset->get("notarization/api_uuid") != "") { + err += TTR("Notarization: Both Apple ID name and App Store Connect issuer ID name are specified, only one should be set at the same time.") + "\n"; + valid = false; + } else { + if (p_preset->get("notarization/apple_id_name") != "") { + if (p_preset->get("notarization/apple_id_password") == "") { + err += TTR("Notarization: Apple ID password not specified.") + "\n"; + } + valid = false; + } + if (p_preset->get("notarization/api_uuid") != "") { + if (p_preset->get("notarization/api_key") == "") { + err += TTR("Notarization: App Store Connect API key ID not specified.") + "\n"; + valid = false; + } + } + } + } else if (notary_tool == 1) { + if (p_preset->get("notarization/api_uuid") == "") { + err += TTR("Notarization: App Store Connect issuer ID name not specified.") + "\n"; + valid = false; + } + if (p_preset->get("notarization/api_key") == "") { + err += TTR("Notarization: App Store Connect API key ID not specified.") + "\n"; + valid = false; + } + + String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String(); + if (rcodesign.is_empty()) { + err += TTR("Notarization: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n"; + valid = false; + } } } else { err += TTR("Warning: Notarization is disabled. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n"; - if (!sign_enabled) { + if (codesign_tool == 0) { err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; - } else { - if ((bool)p_preset->get("codesign/hardened_runtime") && ad_hoc) { - err += TTR("Hardened Runtime is not compatible with ad-hoc signature, and will be disabled!") + "\n"; - } - if ((bool)p_preset->get("codesign/timestamp") && ad_hoc) { - err += TTR("Timestamping is not compatible with ad-hoc signature, and will be disabled!") + "\n"; - } } } -#else - err += TTR("Warning: Notarization is not supported from this OS. The exported project will be blocked by Gatekeeper if it's downloaded from an unknown source.") + "\n"; - if (!sign_enabled) { - err += TTR("Code signing is disabled. The exported project will not run on Macs with enabled Gatekeeper and Apple Silicon powered Macs.") + "\n"; - } -#endif - if (sign_enabled) { + if (codesign_tool > 0) { + if (ad_hoc) { + err += TTR("Code signing: Using ad-hoc signature. The exported project will be blocked by Gatekeeper") + "\n"; + } + if (codesign_tool == 3) { + if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) { + err += TTR("Code signing: Xcode command line tools are not installed.") + "\n"; + valid = false; + } + } else if (codesign_tool == 2) { + String rcodesign = EditorSettings::get_singleton()->get("export/macos/rcodesign").operator String(); + if (rcodesign.is_empty()) { + err += TTR("Code signing: rcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).") + "\n"; + valid = false; + } + } if ((bool)p_preset->get("codesign/entitlements/audio_input") && ((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { err += TTR("Privacy: Microphone access is enabled, but usage description is not specified.") + "\n"; valid = false; diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index 21bc380d55..87790129d3 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -101,7 +101,7 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { protected: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; virtual void get_export_options(List<ExportOption> *r_options) override; - virtual bool get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const override; public: virtual String get_name() const override { return "macOS"; } @@ -119,7 +119,8 @@ public: } virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; virtual void get_platform_features(List<String> *r_features) const override { r_features->push_back("pc"); diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h index 2c12897f10..e0b9f41632 100644 --- a/platform/macos/godot_menu_item.h +++ b/platform/macos/godot_menu_item.h @@ -46,7 +46,6 @@ enum GlobalMenuCheckType { @public Callable callback; Variant meta; - int id; GlobalMenuCheckType checkable_type; int max_states; int state; diff --git a/platform/uwp/README.md b/platform/uwp/README.md new file mode 100644 index 0000000000..575f90e3c7 --- /dev/null +++ b/platform/uwp/README.md @@ -0,0 +1,20 @@ +# UWP platform port + +> **Warning** +> +> The UWP platform port is not currently in a working state for the `master` +> branch, and may be dropped in the future. + +This folder contains the C++ code for the Universal Windows Platform (UWP) +platform port. **This is not to be confused with the "standard" Win32 port**, +which is available in [`platform/windows`](/platform/windows). + +See also [`misc/dist/uwp_template`](/misc/dist/uwp_template) folder for the UWP +project template used for packaging the UWP export templates. + +## Documentation + +- [Compiling for Universal Windows Platform](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_uwp.html) + - Instructions on building this platform port from source. +- [Exporting for Universal Windows Platform](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_uwp.html) + - Instructions on using the compiled export templates to export a project. diff --git a/platform/uwp/detect.py b/platform/uwp/detect.py index 9c91378b22..2c5746cb06 100644 --- a/platform/uwp/detect.py +++ b/platform/uwp/detect.py @@ -1,6 +1,7 @@ import methods import os import sys +from platform_methods import detect_arch def is_active(): @@ -31,6 +32,7 @@ def get_opts(): def get_flags(): return [ + ("arch", detect_arch()), ("tools", False), ("xaudio2", True), ("builtin_pcre2_with_jit", False), @@ -38,19 +40,17 @@ def get_flags(): def configure(env): - env.msvc = True - - if env["bits"] != "default": - print("Error: bits argument is disabled for MSVC") + # Validate arch. + supported_arches = ["x86_32", "x86_64", "arm32"] + if env["arch"] not in supported_arches: print( - """ - Bits argument is not supported for MSVC compilation. Architecture depends on the Native/Cross Compile Tools Prompt/Developer Console - (or Visual Studio settings) that is being used to run SCons. As a consequence, bits argument is disabled. Run scons again without bits - argument (example: scons p=uwp) and SCons will attempt to detect what MSVC compiler will be executed and inform you. - """ + 'Unsupported CPU architecture "%s" for UWP. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) ) sys.exit() + env.msvc = True + ## Build type if env["target"] == "release": @@ -101,11 +101,10 @@ def configure(env): arch = "" if str(os.getenv("Platform")).lower() == "arm": - - print("Compiled program architecture will be an ARM executable. (forcing bits=32).") + print("Compiled program architecture will be an ARM executable (forcing arch=arm32).") arch = "arm" - env["bits"] = "32" + env["arch"] = "arm32" env.Append(LINKFLAGS=["/MACHINE:ARM"]) env.Append(LIBPATH=[vc_base_path + "lib/store/arm"]) @@ -117,20 +116,20 @@ def configure(env): compiler_version_str = methods.detect_visual_c_compiler_version(env["ENV"]) if compiler_version_str == "amd64" or compiler_version_str == "x86_amd64": - env["bits"] = "64" - print("Compiled program architecture will be a x64 executable (forcing bits=64).") + env["arch"] = "x86_64" + print("Compiled program architecture will be a x64 executable (forcing arch=x86_64).") elif compiler_version_str == "x86" or compiler_version_str == "amd64_x86": - env["bits"] = "32" - print("Compiled program architecture will be a x86 executable. (forcing bits=32).") + env["arch"] = "x86_32" + print("Compiled program architecture will be a x86 executable (forcing arch=x86_32).") else: print( - "Failed to detect MSVC compiler architecture version... Defaulting to 32-bit executable settings" - " (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture" + "Failed to detect MSVC compiler architecture version... Defaulting to x86 32-bit executable settings" + " (forcing arch=x86_32). Compilation attempt will continue, but SCons can not detect for what architecture" " this build is compiled for. You should check your settings/compilation setup." ) - env["bits"] = "32" + env["arch"] = "x86_32" - if env["bits"] == "32": + if env["arch"] == "x86_32": arch = "x86" angle_build_cmd += "Win32" diff --git a/platform/uwp/export/export_plugin.cpp b/platform/uwp/export/export_plugin.cpp index 070c46242f..4e4afb9704 100644 --- a/platform/uwp/export/export_plugin.cpp +++ b/platform/uwp/export/export_plugin.cpp @@ -121,7 +121,7 @@ void EditorExportPlatformUWP::get_export_options(List<ExportOption> *r_options) } } -bool EditorExportPlatformUWP::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformUWP::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { #ifndef DEV_ENABLED // We don't provide export templates for the UWP platform currently as it // has not been ported for Godot 4.0. This is skipped in DEV_ENABLED so that @@ -163,7 +163,26 @@ bool EditorExportPlatformUWP::can_export(const Ref<EditorExportPreset> &p_preset valid = dvalid || rvalid; r_missing_templates = !valid; - // Validate the rest of the configuration. + if (!err.is_empty()) { + r_error = err; + } + + return valid; +} + +bool EditorExportPlatformUWP::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { +#ifndef DEV_ENABLED + // We don't provide export templates for the UWP platform currently as it + // has not been ported for Godot 4.0. This is skipped in DEV_ENABLED so that + // contributors can still test the pipeline if/when we can build it again. + r_error = "The UWP platform is currently not supported in Godot 4.0.\n"; + return false; +#endif + + String err; + bool valid = true; + + // Validate the project configuration. if (!_valid_resource_name(p_preset->get("package/short_name"))) { valid = false; diff --git a/platform/uwp/export/export_plugin.h b/platform/uwp/export/export_plugin.h index 4a3c5db377..71d0479543 100644 --- a/platform/uwp/export/export_plugin.h +++ b/platform/uwp/export/export_plugin.h @@ -429,7 +429,8 @@ public: virtual void get_export_options(List<ExportOption> *r_options) override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; diff --git a/platform/windows/README.md b/platform/windows/README.md new file mode 100644 index 0000000000..17ce6f216b --- /dev/null +++ b/platform/windows/README.md @@ -0,0 +1,15 @@ +# Windows platform port + +This folder contains the C++ and JavaScript code for the Windows platform port. + +See also [`misc/dist/windows`](/misc/dist/windows) folder for additional files +used by this platform. + +## Documentation + +- [Compiling for Windows](https://docs.godotengine.org/en/latest/development/compiling/compiling_for_windows.html) + - Instructions on building this platform port from source. +- [Exporting for Windows](https://docs.godotengine.org/en/latest/tutorials/export/exporting_for_windows.html) + - Instructions on using the compiled export templates to export a project. +- [Changing application icon for Windows](https://docs.godotengine.org/en/stable/tutorials/export/changing_application_icon_for_windows.html) + - Instructions on using a custom icon for the exported project executable. diff --git a/platform/windows/detect.py b/platform/windows/detect.py index dd2df1f004..6dd6892757 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -1,5 +1,6 @@ import methods import os +from platform_methods import detect_arch # To match other platforms STACK_SIZE = 8388608 @@ -46,6 +47,7 @@ def can_build(): def get_opts(): from SCons.Variables import BoolVariable, EnumVariable + # TODO: These shouldn't be hard-coded for x86. mingw32 = "" mingw64 = "" if os.name == "posix": @@ -77,11 +79,14 @@ def get_opts(): def get_flags(): - return [] + return [ + ("arch", detect_arch()), + ] def build_res_file(target, source, env): - if env["bits"] == "32": + # TODO: This shouldn't be hard-coded for x86. + if env["arch"] == "x86_32": cmdbase = env["mingw_prefix_32"] else: cmdbase = env["mingw_prefix_64"] @@ -100,21 +105,27 @@ def build_res_file(target, source, env): def setup_msvc_manual(env): + # FIXME: This is super hacky, and probably obsolete. + # Won't work with detect_arch() used for `arch` by default. + """Set up env to use MSVC manually, using VCINSTALLDIR""" - if env["bits"] != "default": + if env["arch"] != "auto": print( """ - Bits argument is not supported for MSVC compilation. Architecture depends on the Native/Cross Compile Tools Prompt/Developer Console - (or Visual Studio settings) that is being used to run SCons. As a consequence, bits argument is disabled. Run scons again without bits - argument (example: scons p=windows) and SCons will attempt to detect what MSVC compiler will be executed and inform you. + Arch argument is not supported for MSVC manual configuration (VCINSTALLDIR configured). + Architecture depends on the Native/Cross Compile Tools Prompt/Developer Console (or Visual Studio settings) that is being used to run SCons. + As a consequence, the arch argument is disabled. Run scons again without arch argument (example: scons p=windows) + and SCons will attempt to detect what MSVC compiler will be executed and inform you. """ ) - raise SCons.Errors.UserError("Bits argument should not be used when using VCINSTALLDIR") + raise SCons.Errors.UserError("Arch argument should not be used when using VCINSTALLDIR") - # Force bits arg + # Force ARCH arg # (Actually msys2 mingw can support 64-bit, we could detect that) - env["bits"] = "32" - env["x86_libtheora_opt_vc"] = True + # TODO: This is wrong, but not sure what to do about it. + # We want to determine the arch and bitness in the SConstruct only. + # We can check if it's correct in here, but if it's not, it should + # just fail with an error message instead of trying to force it. # find compiler manually compiler_version_str = methods.detect_visual_c_compiler_version(env["ENV"]) @@ -122,17 +133,19 @@ def setup_msvc_manual(env): # If building for 64bit architecture, disable assembly optimisations for 32 bit builds (theora as of writing)... vc compiler for 64bit can not compile _asm if compiler_version_str == "amd64" or compiler_version_str == "x86_amd64": - env["bits"] = "64" + env["arch"] = "x86_64" env["x86_libtheora_opt_vc"] = False - print("Compiled program architecture will be a 64 bit executable (forcing bits=64).") + print("Compiled program architecture will be a 64 bit executable (forcing arch=x86_64).") elif compiler_version_str == "x86" or compiler_version_str == "amd64_x86": - print("Compiled program architecture will be a 32 bit executable. (forcing bits=32).") + env["arch"] = "x86_32" + env["x86_libtheora_opt_vc"] = True + print("Compiled program architecture will be a 32 bit executable (forcing arch=x86_32).") else: print( - "Failed to manually detect MSVC compiler architecture version... Defaulting to 32bit executable settings" - " (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture this" - " build is compiled for. You should check your settings/compilation setup, or avoid setting VCINSTALLDIR." + "Failed to manually detect MSVC compiler architecture version.\n" + "You should check your settings/compilation setup, or avoid setting VCINSTALLDIR." ) + sys.exit() def setup_msvc_auto(env): @@ -141,6 +154,18 @@ def setup_msvc_auto(env): # If MSVC_VERSION is set by SCons, we know MSVC is installed. # But we may want a different version or target arch. + # Valid architectures for MSVC's TARGET_ARCH: + # ['amd64', 'emt64', 'i386', 'i486', 'i586', 'i686', 'ia64', 'itanium', 'x86', 'x86_64', 'arm', 'arm64', 'aarch64'] + # Our x86_64 and arm64 are the same, and we need to map the 32-bit + # architectures to other names since MSVC isn't as explicit. + # The rest we don't need to worry about because they are + # aliases or aren't supported by Godot (itanium & ia64). + msvc_arch_aliases = {"x86_32": "x86", "arm32": "arm"} + if env["arch"] in msvc_arch_aliases.keys(): + env["TARGET_ARCH"] = msvc_arch_aliases[env["arch"]] + else: + env["TARGET_ARCH"] = env["arch"] + # The env may have already been set up with default MSVC tools, so # reset a few things so we can set it up with the tools we want. # (Ideally we'd decide on the tool config before configuring any @@ -149,21 +174,14 @@ def setup_msvc_auto(env): env["MSVC_SETUP_RUN"] = False # Need to set this to re-run the tool env["MSVS_VERSION"] = None env["MSVC_VERSION"] = None - env["TARGET_ARCH"] = None - if env["bits"] != "default": - env["TARGET_ARCH"] = {"32": "x86", "64": "x86_64"}[env["bits"]] + if "msvc_version" in env: env["MSVC_VERSION"] = env["msvc_version"] env.Tool("msvc") env.Tool("mssdk") # we want the MS SDK # Note: actual compiler version can be found in env['MSVC_VERSION'], e.g. "14.1" for VS2015 - # Get actual target arch into bits (it may be "default" at this point): - if env["TARGET_ARCH"] in ("amd64", "x86_64"): - env["bits"] = "64" - else: - env["bits"] = "32" - print("Found MSVC version %s, arch %s, bits=%s" % (env["MSVC_VERSION"], env["TARGET_ARCH"], env["bits"])) - if env["TARGET_ARCH"] in ("amd64", "x86_64"): + print("Found MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["TARGET_ARCH"])) + if env["arch"] == "x86_32": env["x86_libtheora_opt_vc"] = False @@ -244,7 +262,7 @@ def configure_msvc(env, manual_msvc_config): ] ) env.AppendUnique(CPPDEFINES=["NOMINMAX"]) # disable bogus min/max WinDef.h macros - if env["bits"] == "64": + if env["arch"] == "x86_64": env.AppendUnique(CPPDEFINES=["_WIN64"]) ## Libs @@ -328,10 +346,10 @@ def configure_mingw(env): env.Append(CCFLAGS=["-msse2"]) if env["optimize"] == "speed": # optimize for speed (default) - if env["bits"] == "64": - env.Append(CCFLAGS=["-O3"]) - else: + if env["arch"] == "x86_32": env.Append(CCFLAGS=["-O2"]) + else: + env.Append(CCFLAGS=["-O3"]) else: # optimize for size env.Prepend(CCFLAGS=["-Os"]) @@ -365,15 +383,12 @@ def configure_mingw(env): if os.name != "nt": env["PROGSUFFIX"] = env["PROGSUFFIX"] + ".exe" # for linux cross-compilation - if env["bits"] == "default": - if os.name == "nt": - env["bits"] = "64" if "PROGRAMFILES(X86)" in os.environ else "32" - else: # default to 64-bit on Linux - env["bits"] = "64" - mingw_prefix = "" - if env["bits"] == "32": + # TODO: This doesn't seem to be working, or maybe I just have + # MinGW set up incorrectly. It always gives me x86_64 builds, + # even though arch == "x86_32" and the file name has x86_32 in it. + if env["arch"] == "x86_32": if env["use_static_cpp"]: env.Append(LINKFLAGS=["-static"]) env.Append(LINKFLAGS=["-static-libgcc"]) @@ -460,11 +475,18 @@ def configure_mingw(env): def configure(env): + # Validate arch. + supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] + if env["arch"] not in supported_arches: + print( + 'Unsupported CPU architecture "%s" for Windows. Supported architectures are: %s.' + % (env["arch"], ", ".join(supported_arches)) + ) + sys.exit() + # At this point the env has been set up with basic tools/compilers. env.Prepend(CPPPATH=["#platform/windows"]) - print("Configuring for Windows: target=%s, bits=%s" % (env["target"], env["bits"])) - if os.name == "nt": env["ENV"] = os.environ # this makes build less repeatable, but simplifies some things env["ENV"]["TMP"] = os.environ["TMP"] diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 8c8dbef8a4..7eb61b3038 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -144,8 +144,8 @@ bool DisplayServerWindows::tts_is_paused() const { return tts->is_paused(); } -Array DisplayServerWindows::tts_get_voices() const { - ERR_FAIL_COND_V(!tts, Array()); +TypedArray<Dictionary> DisplayServerWindows::tts_get_voices() const { + ERR_FAIL_COND_V(!tts, TypedArray<Dictionary>()); return tts->get_voices(); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index db9b589304..556ce9ff5d 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -478,7 +478,7 @@ public: virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; - virtual Array tts_get_voices() const override; + virtual TypedArray<Dictionary> tts_get_voices() const override; virtual void tts_speak(const String &p_text, const String &p_voice, int p_volume = 50, float p_pitch = 1.f, float p_rate = 1.f, int p_utterance_id = 0, bool p_interrupt = false) override; virtual void tts_pause() override; diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index febef5ad12..cb0bd44cfb 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -113,7 +113,7 @@ List<String> EditorExportPlatformWindows::get_binary_extensions(const Ref<Editor return list; } -bool EditorExportPlatformWindows::get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const { +bool EditorExportPlatformWindows::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const { // This option is not supported by "osslsigncode", used on non-Windows host. if (!OS::get_singleton()->has_feature("windows") && p_option == "codesign/identity_type") { return false; @@ -128,7 +128,7 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/enable"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/identity_type", PROPERTY_HINT_ENUM, "Select automatically,Use PKCS12 file (specify *.PFX/*.P12 file),Use certificate store (specify SHA1 hash)"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/identity", PROPERTY_HINT_GLOBAL_FILE, "*.pfx,*.p12"), "")); - r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/password"), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/password", PROPERTY_HINT_PASSWORD), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "codesign/timestamp"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/timestamp_server_url"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/digest_algorithm", PROPERTY_HINT_ENUM, "SHA1,SHA256"), 1)); @@ -231,7 +231,7 @@ Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset String str; Error err = OS::get_singleton()->execute(rcedit_path, args, &str, nullptr, true); if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), TTR("Could not start rcedit executable. Configure rcedit path in the Editor Settings (Export > Windows > Rcedit), or disable \"Application > Modify Resources\" in the export preset.")); + add_message(EXPORT_MESSAGE_WARNING, TTR("Resources Modification"), TTR("Could not start rcedit executable. Configure rcedit path in the Editor Settings (Export > Windows > rcedit), or disable \"Application > Modify Resources\" in the export preset.")); return err; } print_line("rcedit (" + p_path + "): " + str); @@ -379,7 +379,11 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p String str; Error err = OS::get_singleton()->execute(signtool_path, args, &str, nullptr, true); if (err != OK || (str.find("not found") != -1) || (str.find("not recognized") != -1)) { - add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start signtool executable. Configure signtool path in the Editor Settings (Export > Windows > Signtool), or disable \"Codesign\" in the export preset.")); +#ifndef WINDOWS_ENABLED + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start signtool executable. Configure signtool path in the Editor Settings (Export > Windows > signtool), or disable \"Codesign\" in the export preset.")); +#else + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start osslsigncode executable. Configure signtool path in the Editor Settings (Export > Windows > osslsigncode), or disable \"Codesign\" in the export preset.")); +#endif return err; } @@ -412,15 +416,26 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p return OK; } -bool EditorExportPlatformWindows::can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { +bool EditorExportPlatformWindows::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const { String err = ""; - bool valid = EditorExportPlatformPC::can_export(p_preset, err, r_missing_templates); + bool valid = EditorExportPlatformPC::has_valid_export_configuration(p_preset, err, r_missing_templates); String rcedit_path = EditorSettings::get_singleton()->get("export/windows/rcedit"); if (p_preset->get("application/modify_resources") && rcedit_path.is_empty()) { - err += TTR("The rcedit tool must be configured in the Editor Settings (Export > Windows > Rcedit) to change the icon or app information data.") + "\n"; + err += TTR("The rcedit tool must be configured in the Editor Settings (Export > Windows > rcedit) to change the icon or app information data.") + "\n"; + } + + if (!err.is_empty()) { + r_error = err; } + return valid; +} + +bool EditorExportPlatformWindows::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { + String err = ""; + bool valid = true; + String icon_path = ProjectSettings::get_singleton()->globalize_path(p_preset->get("application/icon")); if (!icon_path.is_empty() && !FileAccess::exists(icon_path)) { err += TTR("Invalid icon path:") + " " + icon_path + "\n"; diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index b9e59829a0..f85331c898 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -48,8 +48,9 @@ public: virtual Error sign_shared_object(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual void get_export_options(List<ExportOption> *r_options) override; - virtual bool get_export_option_visibility(const String &p_option, const HashMap<StringName, Variant> &p_options) const override; - virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const override; + virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; + virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option, const HashMap<StringName, Variant> &p_options) const override; virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; }; diff --git a/platform/windows/platform_windows_builders.py b/platform/windows/platform_windows_builders.py index 22e33b51b4..33ca2e8ffa 100644 --- a/platform/windows/platform_windows_builders.py +++ b/platform/windows/platform_windows_builders.py @@ -9,7 +9,7 @@ from platform_methods import subprocess_main def make_debug_mingw(target, source, env): mingw_prefix = "" - if env["bits"] == "32": + if env["arch"] == "x86_32": mingw_prefix = env["mingw_prefix_32"] else: mingw_prefix = env["mingw_prefix_64"] |