diff options
Diffstat (limited to 'platform')
125 files changed, 3754 insertions, 1109 deletions
diff --git a/platform/SCsub b/platform/SCsub index e432cebd48..ca282e3e68 100644 --- a/platform/SCsub +++ b/platform/SCsub @@ -1,5 +1,7 @@ #!/usr/bin/env python +import methods + Import("env") env.platform_sources = [] @@ -18,12 +20,7 @@ reg_apis_inc += "\n" reg_apis += "}\n\n" unreg_apis += "}\n" -# NOTE: It is safe to generate this file here, since this is still execute serially -with open("register_platform_apis.gen.cpp", "w", encoding="utf-8", newline="\n") as f: - f.write(reg_apis_inc) - f.write(reg_apis) - f.write(unreg_apis) - +methods.write_file_if_needed("register_platform_apis.gen.cpp", reg_apis_inc + reg_apis + unreg_apis) env.add_source_files(env.platform_sources, "register_platform_apis.gen.cpp") lib = env.add_library("platform", env.platform_sources) diff --git a/platform/android/SCsub b/platform/android/SCsub index 31bc7c25b0..4d76ffb180 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -1,6 +1,8 @@ #!/usr/bin/env python +import sys import subprocess +from methods import print_warning Import("env") @@ -52,7 +54,7 @@ elif env["arch"] == "x86_32": elif env["arch"] == "x86_64": lib_arch_dir = "x86_64" else: - print("WARN: Architecture not suitable for embedding into APK; keeping .so at \\bin") + print_warning("Architecture not suitable for embedding into APK; keeping .so at \\bin") if lib_arch_dir != "": if env.dev_build: @@ -81,10 +83,21 @@ if lib_arch_dir != "": env_android.Command(out_dir + "/libc++_shared.so", stl_lib_path, Copy("$TARGET", "$SOURCE")) def generate_apk(target, source, env): + gradle_process = [] + + if sys.platform.startswith("win"): + gradle_process = [ + "cmd", + "/c", + "gradlew.bat", + ] + else: + gradle_process = ["./gradlew"] + if env["target"] != "editor" and env["dev_build"]: subprocess.run( - [ - "./gradlew", + gradle_process + + [ "generateDevTemplate", "--quiet", ], @@ -93,8 +106,8 @@ if lib_arch_dir != "": else: # Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task. subprocess.run( - [ - "./gradlew", + gradle_process + + [ "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates", "--quiet", ], diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index b1481ebf7b..e21a331ab9 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -209,8 +209,6 @@ class JavaClassWrapper : public Object { #ifdef ANDROID_ENABLED RBMap<String, Ref<JavaClass>> class_cache; friend class JavaClass; - jclass activityClass; - jmethodID findClass; jmethodID getDeclaredMethods; jmethodID getFields; jmethodID getParameterTypes; @@ -229,7 +227,6 @@ class JavaClassWrapper : public Object { jmethodID Long_longValue; jmethodID Float_floatValue; jmethodID Double_doubleValue; - jobject classLoader; bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig); #endif diff --git a/platform/android/api/jni_singleton.h b/platform/android/api/jni_singleton.h index a2d1c08168..5b30c392e7 100644 --- a/platform/android/api/jni_singleton.h +++ b/platform/android/api/jni_singleton.h @@ -241,6 +241,17 @@ public: instance = nullptr; #endif } + + ~JNISingleton() { +#ifdef ANDROID_ENABLED + if (instance) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(instance); + } +#endif + } }; #endif // JNI_SINGLETON_H diff --git a/platform/android/detect.py b/platform/android/detect.py index fea8ec3287..cbd6144182 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -2,7 +2,7 @@ import os import sys import platform import subprocess - +from methods import print_warning, print_error from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -76,7 +76,6 @@ def get_flags(): # Check if Android NDK version is installed # If not, install it. def install_ndk_if_needed(env: "SConsEnvironment"): - print("Checking for Android NDK...") sdk_root = env["ANDROID_HOME"] if not os.path.exists(get_android_ndk_root(env)): extension = ".bat" if os.name == "nt" else "" @@ -87,13 +86,11 @@ def install_ndk_if_needed(env: "SConsEnvironment"): ndk_download_args = "ndk;" + get_ndk_version() subprocess.check_call([sdkmanager, ndk_download_args]) else: - print("Cannot find " + sdkmanager) - print( - "Please ensure ANDROID_HOME is correct and cmdline-tools are installed, or install NDK version " - + get_ndk_version() - + " manually." + print_error( + f'Cannot find "{sdkmanager}". Please ensure ANDROID_HOME is correct and cmdline-tools' + f'are installed, or install NDK version "{get_ndk_version()}" manually.' ) - sys.exit() + sys.exit(255) env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env) @@ -101,15 +98,15 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) if get_min_sdk_version(env["ndk_platform"]) < get_min_target_api(): - print( - "WARNING: minimum supported Android target api is %d. Forcing target api %d." + print_warning( + "Minimum supported Android target api is %d. Forcing target api %d." % (get_min_target_api(), get_min_target_api()) ) env["ndk_platform"] = "android-" + str(get_min_target_api()) diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 972a7dbe6a..ab90527bfa 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -321,6 +321,14 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) { _current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z"); } +void DirAccessJAndroid::terminate() { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(cls); + env->DeleteGlobalRef(dir_access_handler); +} + DirAccessJAndroid::DirAccessJAndroid() { } diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 9aaa78f38c..68578b0fa9 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -89,6 +89,7 @@ public: virtual uint64_t get_space_left() override; static void setup(jobject p_dir_access_handler); + static void terminate(); DirAccessJAndroid(); ~DirAccessJAndroid(); diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 90759810b1..9869756be1 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -71,6 +71,8 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { case FEATURE_MOUSE: //case FEATURE_MOUSE_WARP: //case FEATURE_NATIVE_DIALOG: + //case FEATURE_NATIVE_DIALOG_INPUT: + //case FEATURE_NATIVE_DIALOG_FILE: //case FEATURE_NATIVE_ICON: //case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_CLIPBOARD: @@ -324,7 +326,7 @@ void DisplayServerAndroid::window_set_drop_files_callback(const Callable &p_call } void DisplayServerAndroid::_window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred) const { - if (!p_callable.is_null()) { + if (p_callable.is_valid()) { if (p_deferred) { p_callable.call_deferred(p_arg); } else { diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml index 7fce5359ae..020e432155 100644 --- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -15,11 +15,11 @@ Array of random bytes that the licensing Policy uses to create an [url=https://developer.android.com/google/play/licensing/adding-licensing#impl-Obfuscator]Obfuscator[/url]. </member> <member name="apk_expansion/enable" type="bool" setter="" getter=""> - If [code]true[/code], project resources are stored in the separate APK expansion file, instead APK. - [b]Note:[/b] APK expansion should be enabled to use PCK encryption. + If [code]true[/code], project resources are stored in the separate APK expansion file, instead of the APK. + [b]Note:[/b] APK expansion should be enabled to use PCK encryption. See [url=https://developer.android.com/google/play/expansion-files]APK Expansion Files[/url] </member> <member name="apk_expansion/public_key" type="String" setter="" getter=""> - Base64 encoded RSA public key for your publisher account, available from the profile page on the "Play Console". + Base64 encoded RSA public key for your publisher account, available from the profile page on the "Google Play Console". </member> <member name="architectures/arm64-v8a" type="bool" setter="" getter=""> If [code]true[/code], [code]arm64[/code] binaries are included into exported project. @@ -34,7 +34,7 @@ If [code]true[/code], [code]x86_64[/code] binaries are included into exported project. </member> <member name="command_line/extra_args" type="String" setter="" getter=""> - A list of additional command line arguments, exported project will receive when started. + A list of additional command line arguments, separated by space, which the exported project will receive when started. </member> <member name="custom_template/debug" type="String" setter="" getter=""> Path to an APK file to use as a custom export template for debug exports. If left empty, default template is used. @@ -52,16 +52,16 @@ [b]Note:[/b] Although your binary may be smaller, your application may load slower because the native libraries are not loaded directly from the binary at runtime. </member> <member name="gradle_build/export_format" type="int" setter="" getter=""> - Export format for Gradle build. + Application export format (*.apk or *.aab). </member> <member name="gradle_build/gradle_build_directory" type="String" setter="" getter=""> Path to the Gradle build directory. If left empty, then [code]res://android[/code] will be used. </member> <member name="gradle_build/min_sdk" type="String" setter="" getter=""> - Minimal Android SDK version for Gradle build. + Minimum Android API level required for the application to run (used during Gradle build). See [url=https://developer.android.com/guide/topics/manifest/uses-sdk-element#uses]android:minSdkVersion[/url]. </member> <member name="gradle_build/target_sdk" type="String" setter="" getter=""> - Target Android SDK version for Gradle build. + The Android API level on which the application is designed to run (used during Gradle build). See [url=https://developer.android.com/guide/topics/manifest/uses-sdk-element#uses]android:targetSdkVersion[/url]. </member> <member name="gradle_build/use_gradle_build" type="bool" setter="" getter=""> If [code]true[/code], Gradle build is used instead of pre-built APK. @@ -97,25 +97,25 @@ Can be overridden with the environment variable [code]GODOT_ANDROID_KEYSTORE_RELEASE_USER[/code]. </member> <member name="launcher_icons/adaptive_background_432x432" type="String" setter="" getter=""> - Background layer of the application adaptive icon file. + Background layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url]. </member> <member name="launcher_icons/adaptive_foreground_432x432" type="String" setter="" getter=""> - Foreground layer of the application adaptive icon file. + Foreground layer of the application adaptive icon file. See [url=https://developer.android.com/develop/ui/views/launch/icon_design_adaptive#design-adaptive-icons]Design adaptive icons[/url]. </member> <member name="launcher_icons/main_192x192" type="String" setter="" getter=""> Application icon file. If left empty, it will fallback to [member ProjectSettings.application/config/icon]. </member> <member name="package/app_category" type="int" setter="" getter=""> - Application category for the Play Store. + Application category for the Google Play Store. Only define this if your application fits one of the categories well. See [url=https://developer.android.com/guide/topics/manifest/application-element#appCategory]android:appCategory[/url]. </member> <member name="package/exclude_from_recents" type="bool" setter="" getter=""> - If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications. + If [code]true[/code], task initiated by main activity will be excluded from the list of recently used applications. See [url=https://developer.android.com/guide/topics/manifest/activity-element#exclude]android:excludeFromRecents[/url]. </member> <member name="package/name" type="String" setter="" getter=""> Name of the application. </member> <member name="package/retain_data_on_uninstall" type="bool" setter="" getter=""> - If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. + If [code]true[/code], when the user uninstalls an app, a prompt to keep the app's data will be shown. See [url=https://developer.android.com/guide/topics/manifest/application-element#fragileuserdata]android:hasFragileUserData[/url]. </member> <member name="package/show_as_launcher_app" type="bool" setter="" getter=""> If [code]true[/code], the user will be able to set this app as the system launcher in Android preferences. @@ -378,7 +378,10 @@ Allows applications to perform I/O operations over NFC. See [url=https://developer.android.com/reference/android/Manifest.permission#NFC]NFC[/url]. </member> <member name="permissions/persistent_activity" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> - Allow an application to make its activities persistent. + Allows an application to make its activities persistent. + </member> + <member name="permissions/post_notifications" type="bool" setter="" getter=""> + Allows an application to post notifications. Added in API level 33. See [url=https://developer.android.com/develop/ui/views/notifications/notification-permission]Notification runtime permission[/url]. </member> <member name="permissions/process_outgoing_calls" type="bool" setter="" getter="" deprecated="Deprecated in API level 29."> Allows an application to see the number being dialed during an outgoing call with the option to redirect the call to a different number or abort the call altogether. See [url=https://developer.android.com/reference/android/Manifest.permission#PROCESS_OUTGOING_CALLS]PROCESS_OUTGOING_CALLS[/url]. @@ -595,6 +598,7 @@ Application version visible to the user. Falls back to [member ProjectSettings.application/config/version] if left empty. </member> <member name="xr_features/xr_mode" type="int" setter="" getter=""> + The extended reality (XR) mode for this application. </member> </members> </class> diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 138714634f..6a6d7149ff 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -33,6 +33,7 @@ #include "export_plugin.h" #include "core/os/os.h" +#include "editor/editor_paths.h" #include "editor/editor_settings.h" #include "editor/export/editor_export.h" @@ -46,10 +47,10 @@ void register_android_exporter() { EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME")); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/android_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); - EDITOR_DEF("export/android/debug_keystore", ""); + EDITOR_DEF("export/android/debug_keystore", EditorPaths::get_singleton()->get_debug_keystore_path()); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore", PROPERTY_HINT_GLOBAL_FILE, "*.keystore,*.jks")); - EDITOR_DEF("export/android/debug_keystore_user", "androiddebugkey"); - EDITOR_DEF("export/android/debug_keystore_pass", "android"); + EDITOR_DEF("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER); + EDITOR_DEF("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/debug_keystore_pass", PROPERTY_HINT_PASSWORD)); EDITOR_DEF("export/android/force_system_user", false); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 894c13cc0b..3b1a534daf 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -141,6 +141,7 @@ static const char *android_perms[] = { "MOUNT_UNMOUNT_FILESYSTEMS", "NFC", "PERSISTENT_ACTIVITY", + "POST_NOTIFICATIONS", "PROCESS_OUTGOING_CALLS", "READ_CALENDAR", "READ_CALL_LOG", @@ -830,14 +831,82 @@ bool EditorExportPlatformAndroid::_uses_vulkan() { void EditorExportPlatformAndroid::_notification(int p_what) { #ifndef ANDROID_ENABLED - if (p_what == NOTIFICATION_POSTINITIALIZE) { - if (EditorExport::get_singleton()) { - EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status)); - } + switch (p_what) { + case NOTIFICATION_POSTINITIALIZE: { + if (EditorExport::get_singleton()) { + EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformAndroid::_update_preset_status)); + } + } break; + + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + if (EditorSettings::get_singleton()->check_changed_settings_in_group("export/android")) { + _create_editor_debug_keystore_if_needed(); + } + } break; } #endif } +void EditorExportPlatformAndroid::_create_editor_debug_keystore_if_needed() { + // Check if we have a valid keytool path. + String keytool_path = get_keytool_path(); + if (!FileAccess::exists(keytool_path)) { + return; + } + + // Check if the current editor debug keystore exists. + String editor_debug_keystore = EDITOR_GET("export/android/debug_keystore"); + if (FileAccess::exists(editor_debug_keystore)) { + return; + } + + // Generate the debug keystore. + String keystore_path = EditorPaths::get_singleton()->get_debug_keystore_path(); + String keystores_dir = keystore_path.get_base_dir(); + if (!DirAccess::exists(keystores_dir)) { + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + Error err = dir_access->make_dir_recursive(keystores_dir); + if (err != OK) { + WARN_PRINT(TTR("Error creating keystores directory:") + "\n" + keystores_dir); + return; + } + } + + if (!FileAccess::exists(keystore_path)) { + String output; + List<String> args; + args.push_back("-genkey"); + args.push_back("-keystore"); + args.push_back(keystore_path); + args.push_back("-storepass"); + args.push_back("android"); + args.push_back("-alias"); + args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_USER); + args.push_back("-keypass"); + args.push_back(DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD); + args.push_back("-keyalg"); + args.push_back("RSA"); + args.push_back("-keysize"); + args.push_back("2048"); + args.push_back("-validity"); + args.push_back("10000"); + args.push_back("-dname"); + args.push_back("cn=Godot, ou=Godot Engine, o=Stichting Godot, c=NL"); + Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true); + print_verbose(output); + if (error != OK) { + WARN_PRINT("Error: Unable to create debug keystore"); + return; + } + } + + // Update the editor settings. + EditorSettings::get_singleton()->set("export/android/debug_keystore", keystore_path); + EditorSettings::get_singleton()->set("export/android/debug_keystore_user", DEFAULT_ANDROID_KEYSTORE_DEBUG_USER); + EditorSettings::get_singleton()->set("export/android/debug_keystore_pass", DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD); + print_verbose("Updated editor debug keystore to " + keystore_path); +} + void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) { const char **aperms = android_perms; while (*aperms) { @@ -1392,6 +1461,14 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p p_manifest = ret; } +String EditorExportPlatformAndroid::_get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug) { + String keystore_preference = p_debug ? "keystore/debug" : "keystore/release"; + String keystore_env_variable = p_debug ? ENV_ANDROID_KEYSTORE_DEBUG_PATH : ENV_ANDROID_KEYSTORE_RELEASE_PATH; + String keystore_path = p_preset->get_or_env(keystore_preference, keystore_env_variable); + + return ProjectSettings::get_singleton()->globalize_path(keystore_path).simplify_path(); +} + String EditorExportPlatformAndroid::_parse_string(const uint8_t *p_bytes, bool p_utf8) { uint32_t offset = 0; uint32_t len = 0; @@ -1920,7 +1997,15 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); if (p_option == "graphics/opengl_debug" || p_option == "command_line/extra_args" || - p_option == "permissions/custom_permissions") { + p_option == "permissions/custom_permissions" || + p_option == "gradle_build/compress_native_libraries" || + p_option == "package/retain_data_on_uninstall" || + p_option == "package/exclude_from_recents" || + p_option == "package/show_in_app_library" || + p_option == "package/show_as_launcher_app" || + p_option == "apk_expansion/enable" || + p_option == "apk_expansion/SALT" || + p_option == "apk_expansion/public_key") { return advanced_options_enabled; } if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") { @@ -1930,6 +2015,12 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor // The APK templates are ignored if Gradle build is enabled. return advanced_options_enabled && !bool(p_preset->get("gradle_build/use_gradle_build")); } + + // Hide .NET embedding option (always enabled). + if (p_option == "dotnet/embed_build_outputs") { + return false; + } + return true; } @@ -2187,6 +2278,15 @@ String EditorExportPlatformAndroid::get_java_path() { return java_sdk_path.path_join("bin/java" + exe_ext); } +String EditorExportPlatformAndroid::get_keytool_path() { + String exe_ext; + if (OS::get_singleton()->get_name() == "Windows") { + exe_ext = ".exe"; + } + String java_sdk_path = EDITOR_GET("export/android/java_sdk_path"); + return java_sdk_path.path_join("bin/keytool" + exe_ext); +} + String EditorExportPlatformAndroid::get_adb_path() { String exe_ext; if (OS::get_singleton()->get_name() == "Windows") { @@ -2332,10 +2432,10 @@ static bool has_valid_keystore_credentials(String &r_error_str, const String &p_ } bool EditorExportPlatformAndroid::has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error) { - String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + String dk = _get_keystore_path(p_preset, true); String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); - String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String rk = _get_keystore_path(p_preset, false); String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); @@ -2434,7 +2534,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito // Validate the rest of the export configuration. - String dk = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + String dk = _get_keystore_path(p_preset, true); String dk_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); String dk_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); @@ -2452,7 +2552,7 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito } } - String rk = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String rk = _get_keystore_path(p_preset, false); String rk_user = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); String rk_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); @@ -2643,7 +2743,7 @@ String EditorExportPlatformAndroid::get_apk_expansion_fullpath(const Ref<EditorE Error EditorExportPlatformAndroid::save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path) { String fullpath = get_apk_expansion_fullpath(p_preset, p_path); - Error err = save_pack(false, p_preset, p_debug, fullpath); + Error err = save_pack(p_preset, p_debug, fullpath); return err; } @@ -2709,7 +2809,7 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &export_path, EditorProgress &ep) { int export_format = int(p_preset->get("gradle_build/export_format")); String export_label = export_format == EXPORT_FORMAT_AAB ? "AAB" : "APK"; - String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String release_keystore = _get_keystore_path(p_preset, false); String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); String target_sdk_version = p_preset->get("gradle_build/target_sdk"); @@ -2731,7 +2831,7 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre String password; String user; if (p_debug) { - keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + keystore = _get_keystore_path(p_preset, true); password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); @@ -3087,16 +3187,16 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP user_data.libs_directory = gradle_build_directory.path_join("libs"); user_data.debug = p_debug; if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { - err = export_project_files(true, p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so); + err = export_project_files(p_preset, p_debug, ignore_apk_file, &user_data, copy_gradle_so); } else { - err = export_project_files(true, p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); + err = export_project_files(p_preset, p_debug, rename_and_store_file_in_gradle_project, &user_data, copy_gradle_so); } if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), TTR("Could not export project files to gradle project.")); return err; } if (user_data.libs.size() > 0) { - Ref<FileAccess> fa = FileAccess::open(GDEXTENSION_LIBS_PATH, FileAccess::WRITE); + Ref<FileAccess> fa = FileAccess::open(gdextension_libs_path, FileAccess::WRITE); fa->store_string(JSON::stringify(user_data.libs, "\t")); } } else { @@ -3216,7 +3316,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP if (should_sign) { if (p_debug) { - String debug_keystore = p_preset->get_or_env("keystore/debug", ENV_ANDROID_KEYSTORE_DEBUG_PATH); + String debug_keystore = _get_keystore_path(p_preset, true); String debug_password = p_preset->get_or_env("keystore/debug_password", ENV_ANDROID_KEYSTORE_DEBUG_PASS); String debug_user = p_preset->get_or_env("keystore/debug_user", ENV_ANDROID_KEYSTORE_DEBUG_USER); @@ -3238,7 +3338,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP cmdline.push_back("-Pdebug_keystore_password=" + debug_password); // argument to specify the debug keystore password. } else { // Pass the release keystore info as well - String release_keystore = p_preset->get_or_env("keystore/release", ENV_ANDROID_KEYSTORE_RELEASE_PATH); + String release_keystore = _get_keystore_path(p_preset, false); String release_username = p_preset->get_or_env("keystore/release_user", ENV_ANDROID_KEYSTORE_RELEASE_USER); String release_password = p_preset->get_or_env("keystore/release_password", ENV_ANDROID_KEYSTORE_RELEASE_PASS); if (release_keystore.is_relative_path()) { @@ -3474,7 +3574,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; - err = export_project_files(true, p_preset, p_debug, ignore_apk_file, &ed, save_apk_so); + err = export_project_files(p_preset, p_debug, ignore_apk_file, &ed, save_apk_so); } else { if (apk_expansion) { err = save_apk_expansion_file(p_preset, p_debug, p_path); @@ -3486,7 +3586,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; - err = export_project_files(true, p_preset, p_debug, save_apk_file, &ed, save_apk_so); + err = export_project_files(p_preset, p_debug, save_apk_file, &ed, save_apk_so); } } @@ -3632,6 +3732,7 @@ EditorExportPlatformAndroid::EditorExportPlatformAndroid() { android_plugins_changed.set(); #endif // DISABLE_DEPRECATED #ifndef ANDROID_ENABLED + _create_editor_debug_keystore_if_needed(); _update_preset_status(); check_for_changes_thread.start(_check_for_changes_poll_thread, this); #endif diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index b968302449..679afdc50f 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -60,6 +60,9 @@ const String ENV_ANDROID_KEYSTORE_RELEASE_PATH = "GODOT_ANDROID_KEYSTORE_RELEASE const String ENV_ANDROID_KEYSTORE_RELEASE_USER = "GODOT_ANDROID_KEYSTORE_RELEASE_USER"; const String ENV_ANDROID_KEYSTORE_RELEASE_PASS = "GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD"; +const String DEFAULT_ANDROID_KEYSTORE_DEBUG_USER = "androiddebugkey"; +const String DEFAULT_ANDROID_KEYSTORE_DEBUG_PASSWORD = "android"; + struct LauncherIcon { const char *export_path; int dimensions = 0; @@ -166,6 +169,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { void _fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet); + static String _get_keystore_path(const Ref<EditorExportPreset> &p_preset, bool p_debug); + static String _parse_string(const uint8_t *p_bytes, bool p_utf8); void _fix_resources(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &r_manifest); @@ -186,6 +191,8 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { const Ref<Image> &foreground, const Ref<Image> &background); + static void _create_editor_debug_keystore_if_needed(); + static Vector<ABI> get_enabled_abis(const Ref<EditorExportPreset> &p_preset); static bool _uses_vulkan(); @@ -234,6 +241,8 @@ public: static String get_java_path(); + static String get_keytool_path(); + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual bool has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const override; static bool has_valid_username_and_password(const Ref<EditorExportPreset> &p_preset, String &r_error); diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index f56eda4694..ae336d6f9d 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -31,8 +31,12 @@ #include "file_access_android.h" #include "core/string/print_string.h" +#include "thread_jandroid.h" + +#include <android/asset_manager_jni.h> AAssetManager *FileAccessAndroid::asset_manager = nullptr; +jobject FileAccessAndroid::j_asset_manager = nullptr; String FileAccessAndroid::get_path() const { return path_src; @@ -257,3 +261,16 @@ void FileAccessAndroid::close() { FileAccessAndroid::~FileAccessAndroid() { _close(); } + +void FileAccessAndroid::setup(jobject p_asset_manager) { + JNIEnv *env = get_jni_env(); + j_asset_manager = env->NewGlobalRef(p_asset_manager); + asset_manager = AAssetManager_fromJava(env, j_asset_manager); +} + +void FileAccessAndroid::terminate() { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(j_asset_manager); +} diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index ec613b6687..e79daeafb3 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -35,9 +35,13 @@ #include <android/asset_manager.h> #include <android/log.h> +#include <jni.h> #include <stdio.h> class FileAccessAndroid : public FileAccess { + static AAssetManager *asset_manager; + static jobject j_asset_manager; + mutable AAsset *asset = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; @@ -48,8 +52,6 @@ class FileAccessAndroid : public FileAccess { void _close(); public: - static AAssetManager *asset_manager; - virtual Error open_internal(const String &p_path, int p_mode_flags) override; // open a file virtual bool is_open() const override; // true when file is open @@ -65,6 +67,7 @@ public: virtual bool eof_reached() const override; // reading passed EOF + virtual Error resize(int64_t p_length) override { return ERR_UNAVAILABLE; } virtual uint8_t get_8() const override; // get a byte virtual uint16_t get_16() const override; virtual uint32_t get_32() const override; @@ -92,6 +95,10 @@ public: virtual void close() override; + static void setup(jobject p_asset_manager); + + static void terminate(); + ~FileAccessAndroid(); }; diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index 46d9728632..f28d469d07 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -53,6 +53,7 @@ jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr; jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_resize = nullptr; String FileAccessFilesystemJAndroid::get_path() const { return path_src; @@ -82,7 +83,7 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo default: return ERR_FILE_CANT_OPEN; - case -1: + case -2: return ERR_FILE_NOT_FOUND; } } @@ -324,6 +325,30 @@ Error FileAccessFilesystemJAndroid::get_error() const { return OK; } +Error FileAccessFilesystemJAndroid::resize(int64_t p_length) { + if (_file_resize) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, FAILED); + ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use."); + int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length); + switch (res) { + case 0: + return OK; + case -4: + return ERR_INVALID_PARAMETER; + case -3: + return ERR_FILE_CANT_OPEN; + case -2: + return ERR_FILE_NOT_FOUND; + case -1: + default: + return FAILED; + } + } else { + return ERR_UNAVAILABLE; + } +} + void FileAccessFilesystemJAndroid::flush() { if (_file_flush) { JNIEnv *env = get_jni_env(); @@ -383,6 +408,15 @@ void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) { _file_flush = env->GetMethodID(cls, "fileFlush", "(I)V"); _file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z"); _file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J"); + _file_resize = env->GetMethodID(cls, "fileResize", "(IJ)I"); +} + +void FileAccessFilesystemJAndroid::terminate() { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(cls); + env->DeleteGlobalRef(file_access_handler); } void FileAccessFilesystemJAndroid::close() { diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h index f33aa64ebe..6a8fc524b7 100644 --- a/platform/android/file_access_filesystem_jandroid.h +++ b/platform/android/file_access_filesystem_jandroid.h @@ -52,6 +52,7 @@ class FileAccessFilesystemJAndroid : public FileAccess { static jmethodID _file_close; static jmethodID _file_exists; static jmethodID _file_last_modified; + static jmethodID _file_resize; int id; String absolute_path; @@ -76,6 +77,7 @@ public: virtual bool eof_reached() const override; ///< reading passed EOF + virtual Error resize(int64_t p_length) override; virtual uint8_t get_8() const override; ///< get a byte virtual uint16_t get_16() const override; virtual uint32_t get_32() const override; @@ -95,6 +97,7 @@ public: virtual bool file_exists(const String &p_path) override; ///< return true if a file exists static void setup(jobject p_file_access_handler); + static void terminate(); virtual uint64_t _get_modified_time(const String &p_file) override; virtual BitField<FileAccess::UnixPermissionFlags> _get_unix_permissions(const String &p_file) override { return 0; } diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 7797f4bc9d..b83ef1471c 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -205,36 +205,66 @@ android { } task copyAndRenameDebugApk(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/apk/debug/android_debug.apk" into getExportPath() rename "android_debug.apk", getExportFilename() } task copyAndRenameDevApk(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/apk/dev/android_dev.apk" into getExportPath() rename "android_dev.apk", getExportFilename() } task copyAndRenameReleaseApk(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/apk/release/android_release.apk" into getExportPath() rename "android_release.apk", getExportFilename() } task copyAndRenameDebugAab(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/bundle/debug/build-debug.aab" into getExportPath() rename "build-debug.aab", getExportFilename() } task copyAndRenameDevAab(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/bundle/dev/build-dev.aab" into getExportPath() rename "build-dev.aab", getExportFilename() } task copyAndRenameReleaseAab(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/bundle/release/build-release.aab" into getExportPath() rename "build-release.aab", getExportFilename() diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index f2c4a5d1b6..d27e75b07a 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -194,17 +194,17 @@ final String VALUE_SEPARATOR_REGEX = "\\|" // get the list of ABIs the project should be exported to ext.getExportEnabledABIs = { -> - String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""; + String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : "" if (enabledABIs == null || enabledABIs.isEmpty()) { enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|" } - Set<String> exportAbiFilter = []; + Set<String> exportAbiFilter = [] for (String abi_name : enabledABIs.split(VALUE_SEPARATOR_REGEX)) { if (!abi_name.trim().isEmpty()){ - exportAbiFilter.add(abi_name); + exportAbiFilter.add(abi_name) } } - return exportAbiFilter; + return exportAbiFilter } ext.getExportPath = { diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 0f7ffeecae..c5ef086152 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -20,7 +20,7 @@ ext { String versionStatus = System.getenv("GODOT_VERSION_STATUS") if (versionStatus != null && !versionStatus.isEmpty()) { try { - buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")); + buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")) } catch (NumberFormatException ignored) { buildNumber = 0 } diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index caf64bc933..c9a62d24b7 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -127,7 +127,7 @@ open class GodotEditor : GodotActivity() { */ protected open fun checkForProjectPermissionsToEnable() { // Check for RECORD_AUDIO permission - val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")); + val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")) if (audioInputEnabled) { PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) } diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index ed967b9660..81ab598b90 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -11,6 +11,8 @@ apply from: "../scripts/publish-module.gradle" dependencies { implementation "androidx.fragment:fragment:$versions.fragmentVersion" + + testImplementation "junit:junit:4.13.2" } def pathToRootDir = "../../../../" @@ -74,6 +76,7 @@ android { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] + test.java.srcDirs = ['srcTest/java'] res.srcDirs = ['res'] aidl.srcDirs = ['aidl'] assets.srcDirs = ['assets'] @@ -118,7 +121,7 @@ android { case "dev": default: sconsTarget += "_debug" - break; + break } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index e2e77e7796..fbdf07e6c2 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -56,6 +56,7 @@ import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.plugin.GodotPluginRegistry import org.godotengine.godot.tts.GodotTTS +import org.godotengine.godot.utils.CommandLineFileParser import org.godotengine.godot.utils.GodotNetUtils import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.PermissionsUtil.requestPermission @@ -68,7 +69,7 @@ import org.godotengine.godot.xr.XRMode import java.io.File import java.io.FileInputStream import java.io.InputStream -import java.nio.charset.StandardCharsets +import java.lang.Exception import java.security.MessageDigest import java.util.* @@ -84,6 +85,9 @@ class Godot(private val context: Context) : SensorEventListener { private val TAG = Godot::class.java.simpleName } + private val windowManager: WindowManager by lazy { + requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager + } private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } @@ -120,6 +124,7 @@ class Godot(private val context: Context) : SensorEventListener { val directoryAccessHandler = DirectoryAccessHandler(context) val fileAccessHandler = FileAccessHandler(context) val netUtils = GodotNetUtils(context) + private val commandLineFileParser = CommandLineFileParser() /** * Tracks whether [onCreate] was completed successfully. @@ -150,7 +155,7 @@ class Godot(private val context: Context) : SensorEventListener { private var useApkExpansion = false private var useImmersive = false private var useDebugOpengl = false - private var darkMode = false; + private var darkMode = false private var containerLayout: FrameLayout? = null var renderView: GodotRenderView? = null @@ -290,7 +295,7 @@ class Godot(private val context: Context) : SensorEventListener { initializationStarted = false throw e } finally { - endBenchmarkMeasure("Startup", "Godot::onCreate"); + endBenchmarkMeasure("Startup", "Godot::onCreate") } } @@ -396,16 +401,19 @@ class Godot(private val context: Context) : SensorEventListener { } if (host == primaryHost) { - renderView!!.startRenderer() + renderView?.startRenderer() } - val view: View = renderView!!.view - containerLayout?.addView( - view, + + renderView?.let { + containerLayout?.addView( + it.view, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) - ) + ) + } + editText.setView(renderView) io?.setEdit(editText) @@ -448,20 +456,23 @@ class Godot(private val context: Context) : SensorEventListener { }) } else { // Infer the virtual keyboard height using visible area. - view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + renderView?.view?.viewTreeObserver?.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { // Don't allocate a new Rect every time the callback is called. val visibleSize = Rect() override fun onGlobalLayout() { - val surfaceView = renderView!!.view - surfaceView.getWindowVisibleDisplayFrame(visibleSize) - val keyboardHeight = surfaceView.height - visibleSize.bottom - GodotLib.setVirtualKeyboardHeight(keyboardHeight) + renderView?.let { + val surfaceView = it.view + + surfaceView.getWindowVisibleDisplayFrame(visibleSize) + val keyboardHeight = surfaceView.height - visibleSize.bottom + GodotLib.setVirtualKeyboardHeight(keyboardHeight) + } } }) } if (host == primaryHost) { - renderView!!.queueOnRenderThread { + renderView?.queueOnRenderThread { for (plugin in pluginRegistry.allPlugins) { plugin.onRegisterPluginWithGodotNative() } @@ -495,7 +506,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityStarted() + renderView?.onActivityStarted() } fun onResume(host: GodotHost) { @@ -503,7 +514,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityResumed() + renderView?.onActivityResumed() if (mAccelerometer != null) { mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) } @@ -535,7 +546,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityPaused() + renderView?.onActivityPaused() mSensorManager.unregisterListener(this) for (plugin in pluginRegistry.allPlugins) { plugin.onMainPause() @@ -547,7 +558,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityStopped() + renderView?.onActivityStopped() } fun onDestroy(primaryHost: GodotHost) { @@ -569,7 +580,7 @@ class Godot(private val context: Context) : SensorEventListener { * Configuration change callback */ fun onConfigurationChanged(newConfig: Configuration) { - var newDarkMode = newConfig.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + val newDarkMode = newConfig.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES if (darkMode != newDarkMode) { darkMode = newDarkMode GodotLib.onNightModeChanged() @@ -613,7 +624,7 @@ class Godot(private val context: Context) : SensorEventListener { // These properties are defined after Godot setup completion, so we retrieve them here. val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")); + val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")) runOnUiThread { renderView?.inputHandler?.apply { @@ -686,9 +697,7 @@ class Godot(private val context: Context) : SensorEventListener { * This must be called after the render thread has started. */ fun runOnRenderThread(action: Runnable) { - if (renderView != null) { - renderView!!.queueOnRenderThread(action) - } + renderView?.queueOnRenderThread(action) } /** @@ -765,7 +774,7 @@ class Godot(private val context: Context) : SensorEventListener { return mClipboard.hasPrimaryClip() } - fun getClipboard(): String? { + fun getClipboard(): String { val clipData = mClipboard.primaryClip ?: return "" val text = clipData.getItemAt(0).text ?: return "" return text.toString() @@ -782,15 +791,14 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun forceQuit(instanceId: Int): Boolean { - if (primaryHost == null) { - return false - } - return if (instanceId == 0) { - primaryHost!!.onGodotForceQuit(this) - true - } else { - primaryHost!!.onGodotForceQuit(instanceId) - } + primaryHost?.let { + if (instanceId == 0) { + it.onGodotForceQuit(this) + return true + } else { + return it.onGodotForceQuit(instanceId) + } + } ?: return false } fun onBackPressed(host: GodotHost) { @@ -804,20 +812,17 @@ class Godot(private val context: Context) : SensorEventListener { shouldQuit = false } } - if (shouldQuit && renderView != null) { - renderView!!.queueOnRenderThread { GodotLib.back() } + if (shouldQuit) { + renderView?.queueOnRenderThread { GodotLib.back() } } } private fun getRotatedValues(values: FloatArray?): FloatArray? { if (values == null || values.size != 3) { - return values + return null } - val display = - (requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay - val displayRotation = display.rotation val rotatedValues = FloatArray(3) - when (displayRotation) { + when (windowManager.defaultDisplay.rotation) { Surface.ROTATION_0 -> { rotatedValues[0] = values[0] rotatedValues[1] = values[1] @@ -846,37 +851,36 @@ class Godot(private val context: Context) : SensorEventListener { if (renderView == null) { return } + + val rotatedValues = getRotatedValues(event.values) + when (event.sensor.type) { Sensor.TYPE_ACCELEROMETER -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.accelerometer( - -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.accelerometer(-it[0], -it[1], -it[2]) + } } } Sensor.TYPE_GRAVITY -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.gravity( - -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.gravity(-it[0], -it[1], -it[2]) + } } } Sensor.TYPE_MAGNETIC_FIELD -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.magnetometer( - -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.magnetometer(-it[0], -it[1], -it[2]) + } } } Sensor.TYPE_GYROSCOPE -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.gyroscope( - rotatedValues!![0], rotatedValues[1], rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.gyroscope(it[0], it[1], it[2]) + } } } } @@ -890,16 +894,25 @@ class Godot(private val context: Context) : SensorEventListener { */ @SuppressLint("MissingPermission") @Keep - private fun vibrate(durationMs: Int) { + private fun vibrate(durationMs: Int, amplitude: Int) { if (durationMs > 0 && requestPermission("VIBRATE")) { val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibratorService.vibrate( - VibrationEffect.createOneShot( - durationMs.toLong(), - VibrationEffect.DEFAULT_AMPLITUDE + if (amplitude <= -1) { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + VibrationEffect.DEFAULT_AMPLITUDE + ) ) - ) + } else { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + amplitude + ) + ) + } } else { // deprecated in API 26 vibratorService.vibrate(durationMs.toLong()) @@ -908,47 +921,18 @@ class Godot(private val context: Context) : SensorEventListener { } private fun getCommandLine(): MutableList<String> { - val original: MutableList<String> = parseCommandLine() + val commandLine = try { + commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_")) + } catch (ignored: Exception) { + mutableListOf() + } + val hostCommandLine = primaryHost?.commandLine if (!hostCommandLine.isNullOrEmpty()) { - original.addAll(hostCommandLine) + commandLine.addAll(hostCommandLine) } - return original - } - private fun parseCommandLine(): MutableList<String> { - val inputStream: InputStream - return try { - inputStream = requireActivity().assets.open("_cl_") - val len = ByteArray(4) - var r = inputStream.read(len) - if (r < 4) { - return mutableListOf() - } - val argc = - (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) - val cmdline = ArrayList<String>(argc) - for (i in 0 until argc) { - r = inputStream.read(len) - if (r < 4) { - return mutableListOf() - } - val strlen = - (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) - if (strlen > 65535) { - return mutableListOf() - } - val arg = ByteArray(strlen) - r = inputStream.read(arg) - if (r == strlen) { - cmdline.add(String(arg, StandardCharsets.UTF_8)) - } - } - cmdline - } catch (e: Exception) { - // The _cl_ file can be missing with no adverse effect - mutableListOf() - } + return commandLine } /** @@ -1039,7 +1023,7 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun initInputDevices() { - renderView!!.initInputDevices() + renderView?.initInputDevices() } @Keep diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index e01c5481d5..7b8fad8952 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -83,8 +83,9 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { override fun onDestroy() { Log.v(TAG, "Destroying Godot app...") super.onDestroy() - if (godotFragment != null) { - terminateGodotInstance(godotFragment!!.godot) + + godotFragment?.let { + terminateGodotInstance(it.godot) } } @@ -93,22 +94,26 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } private fun terminateGodotInstance(instance: Godot) { - if (godotFragment != null && instance === godotFragment!!.godot) { - Log.v(TAG, "Force quitting Godot instance") - ProcessPhoenix.forceQuit(this) + godotFragment?.let { + if (instance === it.godot) { + Log.v(TAG, "Force quitting Godot instance") + ProcessPhoenix.forceQuit(this) + } } } override fun onGodotRestartRequested(instance: Godot) { runOnUiThread { - if (godotFragment != null && instance === godotFragment!!.godot) { - // It's very hard to properly de-initialize Godot on Android to restart the game - // from scratch. Therefore, we need to kill the whole app process and relaunch it. - // - // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including - // releasing and reloading native libs or resetting their state somehow and clearing static data). - Log.v(TAG, "Restarting Godot instance...") - ProcessPhoenix.triggerRebirth(this) + godotFragment?.let { + if (instance === it.godot) { + // It's very hard to properly de-initialize Godot on Android to restart the game + // from scratch. Therefore, we need to kill the whole app process and relaunch it. + // + // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including + // releasing and reloading native libs or resetting their state somehow and clearing static data). + Log.v(TAG, "Restarting Godot instance...") + ProcessPhoenix.triggerRebirth(this) + } } } } 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 0f447f0b05..11cf7b3566 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 @@ -36,7 +36,9 @@ import android.util.Log import org.godotengine.godot.io.StorageScope import java.io.IOException import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException import java.nio.channels.FileChannel +import java.nio.channels.NonWritableChannelException import kotlin.math.max /** @@ -135,6 +137,21 @@ internal abstract class DataAccess(private val filePath: String) { seek(positionFromBeginning) } + fun resize(length: Long): Int { + return try { + fileChannel.truncate(length) + FileErrors.OK.nativeValue + } catch (e: NonWritableChannelException) { + FileErrors.FILE_CANT_OPEN.nativeValue + } catch (e: ClosedChannelException) { + FileErrors.FILE_CANT_OPEN.nativeValue + } catch (e: IllegalArgumentException) { + FileErrors.INVALID_PARAMETER.nativeValue + } catch (e: IOException) { + FileErrors.FAILED.nativeValue + } + } + fun position(): Long { return try { fileChannel.position() 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 984bf607d0..1d773467e8 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 @@ -45,7 +45,6 @@ class FileAccessHandler(val context: Context) { companion object { private val TAG = FileAccessHandler::class.java.simpleName - private const val FILE_NOT_FOUND_ERROR_ID = -1 internal const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 @@ -56,7 +55,9 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.fileExists(storageScope, context, path!!) + path?.let { + DataAccess.fileExists(storageScope, context, it) + } ?: false } catch (e: SecurityException) { false } @@ -69,20 +70,22 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.removeFile(storageScope, context, path!!) + path?.let { + DataAccess.removeFile(storageScope, context, it) + } ?: false } catch (e: Exception) { false } } - internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean { + internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String, to: String): Boolean { val storageScope = storageScopeIdentifier.identifyStorageScope(from) if (storageScope == StorageScope.UNKNOWN) { return false } return try { - DataAccess.renameFile(storageScope, context, from!!, to!!) + DataAccess.renameFile(storageScope, context, from, to) } catch (e: Exception) { false } @@ -106,16 +109,18 @@ class FileAccessHandler(val context: Context) { return INVALID_FILE_ID } - try { - val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID + return try { + path?.let { + val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID - files.put(++lastFileId, dataAccess) - return lastFileId + files.put(++lastFileId, dataAccess) + lastFileId + } ?: INVALID_FILE_ID } catch (e: FileNotFoundException) { - return FILE_NOT_FOUND_ERROR_ID + FileErrors.FILE_NOT_FOUND.nativeValue } catch (e: Exception) { Log.w(TAG, "Error while opening $path", e) - return INVALID_FILE_ID + INVALID_FILE_ID } } @@ -176,12 +181,22 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.fileLastModified(storageScope, context, filepath!!) + filepath?.let { + DataAccess.fileLastModified(storageScope, context, it) + } ?: 0L } catch (e: SecurityException) { 0L } } + fun fileResize(fileId: Int, length: Long): Int { + if (!hasFileId(fileId)) { + return FileErrors.FAILED.nativeValue + } + + return files[fileId].resize(length) + } + fun fileGetPosition(fileId: Int): Long { if (!hasFileId(fileId)) { return 0L diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt new file mode 100644 index 0000000000..2df0195de7 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt @@ -0,0 +1,53 @@ +/**************************************************************************/ +/* FileErrors.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.io.file + +/** + * Set of errors that may occur when performing data access. + */ +internal enum class FileErrors(val nativeValue: Int) { + OK(0), + FAILED(-1), + FILE_NOT_FOUND(-2), + FILE_CANT_OPEN(-3), + INVALID_PARAMETER(-4); + + companion object { + fun fromNativeError(error: Int): FileErrors? { + for (fileError in entries) { + if (fileError.nativeValue == error) { + return fileError + } + } + return null + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt new file mode 100644 index 0000000000..ce5c5b6714 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt @@ -0,0 +1,83 @@ +/**************************************************************************/ +/* CommandLineFileParser.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.utils + +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.util.ArrayList + +/** + * A class that parses the content of file storing command line params. Usually, this file is saved + * in `assets/_cl_` on exporting an apk + * + * Returns a mutable list of command lines + */ +internal class CommandLineFileParser { + fun parseCommandLine(inputStream: InputStream): MutableList<String> { + return try { + val headerBytes = ByteArray(4) + var argBytes = inputStream.read(headerBytes) + if (argBytes < 4) { + return mutableListOf() + } + val argc = decodeHeaderIntValue(headerBytes) + + val cmdline = ArrayList<String>(argc) + for (i in 0 until argc) { + argBytes = inputStream.read(headerBytes) + if (argBytes < 4) { + return mutableListOf() + } + val strlen = decodeHeaderIntValue(headerBytes) + + if (strlen > 65535) { + return mutableListOf() + } + + val arg = ByteArray(strlen) + argBytes = inputStream.read(arg) + if (argBytes == strlen) { + cmdline.add(String(arg, StandardCharsets.UTF_8)) + } + } + cmdline + } catch (e: Exception) { + // The _cl_ file can be missing with no adverse effect + mutableListOf() + } + } + + private fun decodeHeaderIntValue(headerBytes: ByteArray): Int = + (headerBytes[3].toInt() and 0xFF) shl 24 or + ((headerBytes[2].toInt() and 0xFF) shl 16) or + ((headerBytes[1].toInt() and 0xFF) shl 8) or + (headerBytes[0].toInt() and 0xFF) +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index 9a6b6d5037..9df890e6bd 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -293,15 +293,15 @@ public final class PermissionsUtil { /** * Returns the permissions defined in the AndroidManifest.xml file. * @param context the caller context for this method. - * @return manifest permissions list + * @return mutable copy of manifest permissions list * @throws PackageManager.NameNotFoundException the exception is thrown when a given package, application, or component name cannot be found. */ - public static List<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { + public static ArrayList<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); if (packageInfo.requestedPermissions == null) - return Collections.emptyList(); - return Arrays.asList(packageInfo.requestedPermissions); + return new ArrayList<String>(); + return new ArrayList<>(Arrays.asList(packageInfo.requestedPermissions)); } /** diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt index 4aba0c370d..8c0065b31e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt @@ -142,7 +142,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk fun onSurfaceChanged(width: Int, height: Int) { lock.withLock { hasSurface = true - surfaceChanged = true; + surfaceChanged = true this.width = width this.height = height @@ -179,7 +179,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk // blocking the thread lifecycle by holding onto the lock. if (eventQueue.isNotEmpty()) { event = eventQueue.removeAt(0) - break; + break } if (readyToDraw) { @@ -199,7 +199,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk } // Break out of the loop so drawing can occur without holding onto the lock. - break; + break } else if (rendererResumed) { // If we aren't ready to draw but are resumed, that means we either lost a surface // or the app was paused. diff --git a/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt new file mode 100644 index 0000000000..8b0466848a --- /dev/null +++ b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt @@ -0,0 +1,104 @@ +/**************************************************************************/ +/* CommandLineFileParserTest.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.utils + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.ByteArrayInputStream +import java.io.InputStream + +// Godot saves command line params in the `assets/_cl_` file on exporting an apk. By default, +// without any other commands specified in `command_line/extra_args` in Export window, the content +// of that _cl_ file consists of only the `--xr_mode_regular` and `--use_immersive` flags. +// The `CL_` prefix here refers to that file +private val CL_DEFAULT_NO_EXTRA_ARGS = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_ONE_EXTRA_ARG = byteArrayOf(3, 0, 0, 0, 15, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_TWO_EXTRA_ARGS = byteArrayOf(4, 0, 0, 0, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 49, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 50, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_EMPTY = byteArrayOf() +private val CL_HEADER_TOO_SHORT = byteArrayOf(0, 0, 0) +private val CL_INCOMPLETE_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0) +private val CL_LENGTH_TOO_LONG_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG = byteArrayOf(2, 0, 0, 0, 10, 0, 0, 0, 45, 45, 120, 114) +private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) + +@RunWith(Parameterized::class) +class CommandLineFileParserTest( + private val inputStreamArg: InputStream, + private val expectedResult: List<String>, +) { + + private val commandLineFileParser = CommandLineFileParser() + + companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = listOf( + arrayOf(ByteArrayInputStream(CL_EMPTY), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_HEADER_TOO_SHORT), listOf<String>()), + + arrayOf(ByteArrayInputStream(CL_DEFAULT_NO_EXTRA_ARGS), listOf( + "--xr_mode_regular", + "--use_immersive", + )), + + arrayOf(ByteArrayInputStream(CL_ONE_EXTRA_ARG), listOf( + "--unit_test_arg", + "--xr_mode_regular", + "--use_immersive", + )), + + arrayOf(ByteArrayInputStream(CL_TWO_EXTRA_ARGS), listOf( + "--unit_test_arg1", + "--unit_test_arg2", + "--xr_mode_regular", + "--use_immersive", + )), + + arrayOf(ByteArrayInputStream(CL_INCOMPLETE_FIRST_ARG), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_LENGTH_TOO_LONG_IN_FIRST_ARG), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG), listOf<String>()), + ) + } + + @Test + fun `Given inputStream, When parsing command line, Then a correct list is returned`() { + // given + val inputStream = inputStreamArg + + // when + val result = commandLineFileParser.parseCommandLine(inputStream) + + // then + assert(result == expectedResult) { "Expected: $expectedResult Actual: $result" } + } +} diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index d6455cbf1c..a309a6ab74 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -1157,50 +1157,54 @@ JavaClassWrapper::JavaClassWrapper(jobject p_activity) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); - jclass activity = env->FindClass("android/app/Activity"); - jmethodID getClassLoader = env->GetMethodID(activity, "getClassLoader", "()Ljava/lang/ClassLoader;"); - classLoader = env->CallObjectMethod(p_activity, getClassLoader); - classLoader = (jclass)env->NewGlobalRef(classLoader); - jclass classLoaderClass = env->FindClass("java/lang/ClassLoader"); - findClass = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); - jclass bclass = env->FindClass("java/lang/Class"); getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;"); getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;"); Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/reflect/Method"); getParameterTypes = env->GetMethodID(bclass, "getParameterTypes", "()[Ljava/lang/Class;"); getReturnType = env->GetMethodID(bclass, "getReturnType", "()Ljava/lang/Class;"); getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/reflect/Field"); Field_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); Field_getModifiers = env->GetMethodID(bclass, "getModifiers", "()I"); Field_get = env->GetMethodID(bclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Boolean"); Boolean_booleanValue = env->GetMethodID(bclass, "booleanValue", "()Z"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Byte"); Byte_byteValue = env->GetMethodID(bclass, "byteValue", "()B"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Character"); Character_characterValue = env->GetMethodID(bclass, "charValue", "()C"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Short"); Short_shortValue = env->GetMethodID(bclass, "shortValue", "()S"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Integer"); Integer_integerValue = env->GetMethodID(bclass, "intValue", "()I"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Long"); Long_longValue = env->GetMethodID(bclass, "longValue", "()J"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Float"); Float_floatValue = env->GetMethodID(bclass, "floatValue", "()F"); + env->DeleteLocalRef(bclass); bclass = env->FindClass("java/lang/Double"); Double_doubleValue = env->GetMethodID(bclass, "doubleValue", "()D"); + env->DeleteLocalRef(bclass); } diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 10716a5c79..49913b9c30 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -70,7 +70,11 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc } GodotIOJavaWrapper::~GodotIOJavaWrapper() { - // nothing to do here for now + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(cls); + env->DeleteGlobalRef(godot_io_instance); } jobject GodotIOJavaWrapper::get_instance() { @@ -82,7 +86,9 @@ Error GodotIOJavaWrapper::open_uri(const String &p_uri) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, ERR_UNAVAILABLE); jstring jStr = env->NewStringUTF(p_uri.utf8().get_data()); - return env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK; + Error result = env->CallIntMethod(godot_io_instance, _open_URI, jStr) ? ERR_CANT_OPEN : OK; + env->DeleteLocalRef(jStr); + return result; } else { return ERR_UNAVAILABLE; } @@ -220,6 +226,7 @@ void GodotIOJavaWrapper::show_vk(const String &p_existing, int p_type, int p_max ERR_FAIL_NULL(env); jstring jStr = env->NewStringUTF(p_existing.utf8().get_data()); env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_type, p_max_input_length, p_cursor_start, p_cursor_end); + env->DeleteLocalRef(jStr); } } diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 85d5cf2796..6cab7e74fd 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -95,6 +95,13 @@ static void _terminate(JNIEnv *env, bool p_restart = false) { if (godot_io_java) { delete godot_io_java; } + + TTS_Android::terminate(); + FileAccessAndroid::terminate(); + DirAccessJAndroid::terminate(); + FileAccessFilesystemJAndroid::terminate(); + NetSocketAndroid::terminate(); + if (godot_java) { if (!restart_on_cleanup) { if (p_restart) { @@ -125,10 +132,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv init_thread_jandroid(jvm, env); - jobject amgr = env->NewGlobalRef(p_asset_manager); - - FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr); - + FileAccessAndroid::setup(p_asset_manager); DirAccessJAndroid::setup(p_directory_access_handler); FileAccessFilesystemJAndroid::setup(p_file_access_handler); NetSocketAndroid::setup(p_net_utils); @@ -250,7 +254,7 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, } if (step.get() == 1) { - if (!Main::start()) { + if (Main::start() != EXIT_SUCCESS) { return true; // should exit instead and print the error } diff --git a/platform/android/java_godot_view_wrapper.cpp b/platform/android/java_godot_view_wrapper.cpp index a95f762e01..04424c1179 100644 --- a/platform/android/java_godot_view_wrapper.cpp +++ b/platform/android/java_godot_view_wrapper.cpp @@ -95,6 +95,7 @@ void GodotJavaViewWrapper::configure_pointer_icon(int pointer_type, const String jstring jImagePath = env->NewStringUTF(image_path.utf8().get_data()); env->CallVoidMethod(_godot_view, _configure_pointer_icon, pointer_type, jImagePath, p_hotspot.x, p_hotspot.y); + env->DeleteLocalRef(jImagePath); } } diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 3c950bb1b1..0e766e7d56 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -72,7 +72,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); _get_ca_certificates = p_env->GetMethodID(godot_class, "getCACertificates", "()Ljava/lang/String;"); _init_input_devices = p_env->GetMethodID(godot_class, "initInputDevices", "()V"); - _vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V"); + _vibrate = p_env->GetMethodID(godot_class, "vibrate", "(II)V"); _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); @@ -172,6 +172,8 @@ void GodotJavaWrapper::alert(const String &p_message, const String &p_title) { jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data()); jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data()); env->CallVoidMethod(godot_instance, _alert, jStrMessage, jStrTitle); + env->DeleteLocalRef(jStrMessage); + env->DeleteLocalRef(jStrTitle); } } @@ -231,6 +233,7 @@ void GodotJavaWrapper::set_clipboard(const String &p_text) { ERR_FAIL_NULL(env); jstring jStr = env->NewStringUTF(p_text.utf8().get_data()); env->CallVoidMethod(godot_instance, _set_clipboard, jStr); + env->DeleteLocalRef(jStr); } } @@ -253,7 +256,9 @@ bool GodotJavaWrapper::request_permission(const String &p_name) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, false); jstring jStrName = env->NewStringUTF(p_name.utf8().get_data()); - return env->CallBooleanMethod(godot_instance, _request_permission, jStrName); + bool result = env->CallBooleanMethod(godot_instance, _request_permission, jStrName); + env->DeleteLocalRef(jStrName); + return result; } else { return false; } @@ -326,11 +331,18 @@ void GodotJavaWrapper::init_input_devices() { } } -void GodotJavaWrapper::vibrate(int p_duration_ms) { +void GodotJavaWrapper::vibrate(int p_duration_ms, float p_amplitude) { if (_vibrate) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); - env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms); + + int j_amplitude = -1.0; + + if (p_amplitude != -1.0) { + j_amplitude = CLAMP(int(p_amplitude * 255), 1, 255); + } + + env->CallVoidMethod(godot_instance, _vibrate, p_duration_ms, j_amplitude); } } @@ -340,7 +352,9 @@ int GodotJavaWrapper::create_new_godot_instance(const List<String> &args) { ERR_FAIL_NULL_V(env, 0); jobjectArray jargs = env->NewObjectArray(args.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")); for (int i = 0; i < args.size(); i++) { - env->SetObjectArrayElement(jargs, i, env->NewStringUTF(args[i].utf8().get_data())); + jstring j_arg = env->NewStringUTF(args[i].utf8().get_data()); + env->SetObjectArrayElement(jargs, i, j_arg); + env->DeleteLocalRef(j_arg); } return env->CallIntMethod(godot_instance, _create_new_godot_instance, jargs); } else { @@ -355,6 +369,8 @@ void GodotJavaWrapper::begin_benchmark_measure(const String &p_context, const St jstring j_context = env->NewStringUTF(p_context.utf8().get_data()); jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); env->CallVoidMethod(godot_instance, _begin_benchmark_measure, j_context, j_label); + env->DeleteLocalRef(j_context); + env->DeleteLocalRef(j_label); } } @@ -365,6 +381,8 @@ void GodotJavaWrapper::end_benchmark_measure(const String &p_context, const Stri jstring j_context = env->NewStringUTF(p_context.utf8().get_data()); jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); env->CallVoidMethod(godot_instance, _end_benchmark_measure, j_context, j_label); + env->DeleteLocalRef(j_context); + env->DeleteLocalRef(j_label); } } @@ -374,6 +392,7 @@ void GodotJavaWrapper::dump_benchmark(const String &benchmark_file) { ERR_FAIL_NULL(env); jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data()); env->CallVoidMethod(godot_instance, _dump_benchmark, j_benchmark_file); + env->DeleteLocalRef(j_benchmark_file); } } @@ -383,7 +402,9 @@ bool GodotJavaWrapper::has_feature(const String &p_feature) const { ERR_FAIL_NULL_V(env, false); jstring j_feature = env->NewStringUTF(p_feature.utf8().get_data()); - return env->CallBooleanMethod(godot_instance, _has_feature, j_feature); + bool result = env->CallBooleanMethod(godot_instance, _has_feature, j_feature); + env->DeleteLocalRef(j_feature); + return result; } else { return false; } diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 93998021a9..e86391d4e3 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -102,7 +102,7 @@ public: Vector<String> get_granted_permissions() const; String get_ca_certificates() const; void init_input_devices(); - void vibrate(int p_duration_ms); + void vibrate(int p_duration_ms, float p_amplitude = -1.0); String get_input_fallback_mapping(); int create_new_godot_instance(const List<String> &args); void begin_benchmark_measure(const String &p_context, const String &p_label); diff --git a/platform/android/net_socket_android.cpp b/platform/android/net_socket_android.cpp index a2befdc9be..8f0ee51fac 100644 --- a/platform/android/net_socket_android.cpp +++ b/platform/android/net_socket_android.cpp @@ -49,6 +49,14 @@ void NetSocketAndroid::setup(jobject p_net_utils) { _multicast_lock_release = env->GetMethodID(cls, "multicastLockRelease", "()V"); } +void NetSocketAndroid::terminate() { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(cls); + env->DeleteGlobalRef(net_utils); +} + void NetSocketAndroid::multicast_lock_acquire() { if (_multicast_lock_acquire) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/net_socket_android.h b/platform/android/net_socket_android.h index e5f46d3236..26cb2d4e3d 100644 --- a/platform/android/net_socket_android.h +++ b/platform/android/net_socket_android.h @@ -63,6 +63,7 @@ protected: public: static void make_default(); static void setup(jobject p_net_utils); + static void terminate(); virtual void close(); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 82e7fdb320..c60125c34e 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -162,7 +162,39 @@ Vector<String> OS_Android::get_granted_permissions() const { return godot_java->get_granted_permissions(); } -Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { +bool OS_Android::copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path) { + if (!FileAccess::exists(p_library_path)) { + return false; + } + + Ref<DirAccess> da_ref = DirAccess::create_for_path(p_library_path); + if (!da_ref.is_valid()) { + return false; + } + + String copy_path = p_target_dir.path_join(p_library_path.get_file()); + bool copy_exists = FileAccess::exists(copy_path); + if (copy_exists) { + print_verbose("Deleting existing library copy " + copy_path); + if (da_ref->remove(copy_path) != OK) { + print_verbose("Unable to delete " + copy_path); + } + } + + print_verbose("Copying " + p_library_path + " to " + p_target_dir); + Error create_dir_result = da_ref->make_dir_recursive(p_target_dir); + if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) { + copy_exists = da_ref->copy(p_library_path, copy_path) == OK; + } + + if (copy_exists && r_copy_path != nullptr) { + *r_copy_path = copy_path; + } + + return copy_exists; +} + +Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) { String path = p_path; bool so_file_exists = true; if (!FileAccess::exists(path)) { @@ -172,24 +204,32 @@ Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_ha p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); if (!p_library_handle && so_file_exists) { - // The library may be on the sdcard and thus inaccessible. Try to copy it to the internal - // directory. - uint64_t so_modified_time = FileAccess::get_modified_time(p_path); - String dynamic_library_path = get_dynamic_libraries_path().path_join(String::num_uint64(so_modified_time)); - String internal_path = dynamic_library_path.path_join(p_path.get_file()); - - bool internal_so_file_exists = FileAccess::exists(internal_path); - if (!internal_so_file_exists) { - Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path); - if (da_ref.is_valid()) { - Error create_dir_result = da_ref->make_dir_recursive(dynamic_library_path); - if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) { - internal_so_file_exists = da_ref->copy(path, internal_path) == OK; + // The library (and its dependencies) may be on the sdcard and thus inaccessible. + // Try to copy to the internal directory for access. + const String dynamic_library_path = get_dynamic_libraries_path(); + + if (p_data != nullptr && p_data->library_dependencies != nullptr && !p_data->library_dependencies->is_empty()) { + // Copy the library dependencies + print_verbose("Copying library dependencies.."); + for (const String &library_dependency_path : *p_data->library_dependencies) { + String internal_library_dependency_path; + if (!copy_dynamic_library(library_dependency_path, dynamic_library_path.path_join(library_dependency_path.get_base_dir()), &internal_library_dependency_path)) { + ERR_PRINT(vformat("Unable to copy library dependency %s", library_dependency_path)); + } else { + void *lib_dependency_handle = dlopen(internal_library_dependency_path.utf8().get_data(), RTLD_NOW); + if (!lib_dependency_handle) { + ERR_PRINT(vformat("Can't open dynamic library dependency: %s. Error: %s.", internal_library_dependency_path, dlerror())); + } } } } + String internal_path; + print_verbose("Copying library " + p_path); + const bool internal_so_file_exists = copy_dynamic_library(p_path, dynamic_library_path.path_join(p_path.get_base_dir()), &internal_path); + if (internal_so_file_exists) { + print_verbose("Opening library " + internal_path); p_library_handle = dlopen(internal_path.utf8().get_data(), RTLD_NOW); if (p_library_handle) { path = internal_path; @@ -199,8 +239,8 @@ Error OS_Android::open_dynamic_library(const String &p_path, void *&p_library_ha ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror())); - if (r_resolved_path != nullptr) { - *r_resolved_path = path; + if (p_data != nullptr && p_data->r_resolved_path != nullptr) { + *p_data->r_resolved_path = path; } return OK; @@ -706,8 +746,8 @@ ANativeWindow *OS_Android::get_native_window() const { #endif } -void OS_Android::vibrate_handheld(int p_duration_ms) { - godot_java->vibrate(p_duration_ms); +void OS_Android::vibrate_handheld(int p_duration_ms, float p_amplitude) { + godot_java->vibrate(p_duration_ms, p_amplitude); } String OS_Android::get_config_path() const { @@ -736,6 +776,10 @@ void OS_Android::benchmark_dump() { } bool OS_Android::_check_internal_feature_support(const String &p_feature) { + if (p_feature == "macos" || p_feature == "web_ios" || p_feature == "web_macos" || p_feature == "windows") { + return false; + } + if (p_feature == "system_fonts") { return true; } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 31ee7389df..b150ef4f61 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -113,7 +113,7 @@ public: virtual void alert(const String &p_alert, const String &p_title) override; - virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; + virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override; virtual String get_name() const override; virtual String get_distribution_name() const override; @@ -153,7 +153,7 @@ public: virtual Error move_to_trash(const String &p_path) override; - void vibrate_handheld(int p_duration_ms) override; + void vibrate_handheld(int p_duration_ms, float p_amplitude = -1.0) override; virtual String get_config_path() const override; @@ -178,6 +178,8 @@ public: private: // Location where we relocate external dynamic libraries to make them accessible. String get_dynamic_libraries_path() const; + // Copy a dynamic library to the given location to make it accessible for loading. + bool copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path = nullptr); }; #endif // OS_ANDROID_H diff --git a/platform/android/tts_android.cpp b/platform/android/tts_android.cpp index 93517d8045..be85e47972 100644 --- a/platform/android/tts_android.cpp +++ b/platform/android/tts_android.cpp @@ -77,6 +77,14 @@ void TTS_Android::setup(jobject p_tts) { } } +void TTS_Android::terminate() { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + env->DeleteGlobalRef(cls); + env->DeleteGlobalRef(tts); +} + void TTS_Android::_java_utterance_callback(int p_event, int p_id, int p_pos) { ERR_FAIL_COND_MSG(!initialized, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); if (ids.has(p_id)) { @@ -170,6 +178,8 @@ void TTS_Android::speak(const String &p_text, const String &p_voice, int p_volum jstring jStrT = env->NewStringUTF(p_text.utf8().get_data()); jstring jStrV = env->NewStringUTF(p_voice.utf8().get_data()); env->CallVoidMethod(tts, _speak, jStrT, jStrV, CLAMP(p_volume, 0, 100), CLAMP(p_pitch, 0.f, 2.f), CLAMP(p_rate, 0.1f, 10.f), p_utterance_id, p_interrupt); + env->DeleteLocalRef(jStrT); + env->DeleteLocalRef(jStrV); } } diff --git a/platform/android/tts_android.h b/platform/android/tts_android.h index 39efef6ed1..4cc7c12846 100644 --- a/platform/android/tts_android.h +++ b/platform/android/tts_android.h @@ -57,6 +57,7 @@ class TTS_Android { public: static void setup(jobject p_tts); + static void terminate(); static void _java_utterance_callback(int p_event, int p_id, int p_pos); static bool is_speaking(); diff --git a/platform/ios/app_delegate.mm b/platform/ios/app_delegate.mm index 5a0c57be93..37d2696434 100644 --- a/platform/ios/app_delegate.mm +++ b/platform/ios/app_delegate.mm @@ -116,6 +116,8 @@ static ViewController *mainViewController = nil; } else if (sessionCategorySetting == SESSION_CATEGORY_PLAY_AND_RECORD) { category = AVAudioSessionCategoryPlayAndRecord; options |= AVAudioSessionCategoryOptionDefaultToSpeaker; + options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP; + options |= AVAudioSessionCategoryOptionAllowAirPlay; } else if (sessionCategorySetting == SESSION_CATEGORY_PLAYBACK) { category = AVAudioSessionCategoryPlayback; } else if (sessionCategorySetting == SESSION_CATEGORY_RECORD) { diff --git a/platform/ios/detect.py b/platform/ios/detect.py index cd303295ad..e3bac4ec5c 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -1,6 +1,6 @@ import os import sys -from methods import detect_darwin_sdk_path +from methods import print_error, detect_darwin_sdk_path from typing import TYPE_CHECKING @@ -60,11 +60,11 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_64", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) ## LTO @@ -118,7 +118,7 @@ def configure(env: "SConsEnvironment"): if env["arch"] == "x86_64": if not env["ios_simulator"]: - print("ERROR: Building for iOS with 'arch=x86_64' requires 'ios_simulator=yes'.") + print_error("Building for iOS with 'arch=x86_64' requires 'ios_simulator=yes'.") sys.exit(255) env["ENV"]["MACOSX_DEPLOYMENT_TARGET"] = "10.9" @@ -140,7 +140,6 @@ def configure(env: "SConsEnvironment"): ) ) env.Append(ASFLAGS=["-arch", "arm64"]) - env.Append(CPPDEFINES=["NEED_LONG_INT"]) # Temp fix for ABS/MAX/MIN macros in iOS SDK blocking compilation env.Append(CCFLAGS=["-Wno-ambiguous-macro"]) diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index 3f9211c572..c6015a058c 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -129,10 +129,10 @@ public: // MARK: Motion - void update_gravity(float p_x, float p_y, float p_z); - void update_accelerometer(float p_x, float p_y, float p_z); - void update_magnetometer(float p_x, float p_y, float p_z); - void update_gyroscope(float p_x, float p_y, float p_z); + void update_gravity(const Vector3 &p_gravity); + void update_accelerometer(const Vector3 &p_accelerometer); + void update_magnetometer(const Vector3 &p_magnetometer); + void update_gyroscope(const Vector3 &p_gyroscope); // MARK: - diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index e1c3dcd372..62bc55dce8 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -218,7 +218,7 @@ void DisplayServerIOS::send_window_event(DisplayServer::WindowEvent p_event) con } void DisplayServerIOS::_window_callback(const Callable &p_callable, const Variant &p_arg) const { - if (!p_callable.is_null()) { + if (p_callable.is_valid()) { p_callable.call(p_arg); } } @@ -289,26 +289,20 @@ void DisplayServerIOS::key(Key p_key, char32_t p_char, Key p_unshifted, Key p_ph // MARK: Motion -void DisplayServerIOS::update_gravity(float p_x, float p_y, float p_z) { - Input::get_singleton()->set_gravity(Vector3(p_x, p_y, p_z)); +void DisplayServerIOS::update_gravity(const Vector3 &p_gravity) { + Input::get_singleton()->set_gravity(p_gravity); } -void DisplayServerIOS::update_accelerometer(float p_x, float p_y, float p_z) { - // Found out the Z should not be negated! Pass as is! - Vector3 v_accelerometer = Vector3( - p_x / kDisplayServerIOSAcceleration, - p_y / kDisplayServerIOSAcceleration, - p_z / kDisplayServerIOSAcceleration); - - Input::get_singleton()->set_accelerometer(v_accelerometer); +void DisplayServerIOS::update_accelerometer(const Vector3 &p_accelerometer) { + Input::get_singleton()->set_accelerometer(p_accelerometer / kDisplayServerIOSAcceleration); } -void DisplayServerIOS::update_magnetometer(float p_x, float p_y, float p_z) { - Input::get_singleton()->set_magnetometer(Vector3(p_x, p_y, p_z)); +void DisplayServerIOS::update_magnetometer(const Vector3 &p_magnetometer) { + Input::get_singleton()->set_magnetometer(p_magnetometer); } -void DisplayServerIOS::update_gyroscope(float p_x, float p_y, float p_z) { - Input::get_singleton()->set_gyroscope(Vector3(p_x, p_y, p_z)); +void DisplayServerIOS::update_gyroscope(const Vector3 &p_gyroscope) { + Input::get_singleton()->set_gyroscope(p_gyroscope); } // MARK: - @@ -328,6 +322,8 @@ bool DisplayServerIOS::has_feature(Feature p_feature) const { // case FEATURE_MOUSE: // case FEATURE_MOUSE_WARP: // case FEATURE_NATIVE_DIALOG: + // case FEATURE_NATIVE_DIALOG_INPUT: + // case FEATURE_NATIVE_DIALOG_FILE: // case FEATURE_NATIVE_ICON: // case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_CLIPBOARD: diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml index 35ef6d6a78..20c1647843 100644 --- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml +++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml @@ -12,7 +12,7 @@ <members> <member name="application/additional_plist_content" type="String" setter="" getter=""> Additional data added to the root [code]<dict>[/code] section of the [url=https://developer.apple.com/documentation/bundleresources/information_property_list]Info.plist[/url] file. The value should be an XML section with pairs of key-value elements, e.g.: - [codeblock] + [codeblock lang=text] <key>key_name</key> <string>value</string> [/codeblock] @@ -29,6 +29,9 @@ <member name="application/code_sign_identity_release" type="String" setter="" getter=""> The "Full Name", "Common Name" or SHA-1 hash of the signing identity used for release export. </member> + <member name="application/delete_old_export_files_unconditionally" type="bool" setter="" getter=""> + If [code]true[/code], existing "project name" and "project name.xcodeproj" in the export destination directory will be unconditionally deleted during export. + </member> <member name="application/export_method_debug" type="int" setter="" getter=""> Application distribution target (debug export). </member> @@ -123,12 +126,441 @@ <member name="icons/spotlight_80x80" type="String" setter="" getter=""> Spotlight icon file on iPad and iPhone (2x DPI). If left empty, it will fallback to [member ProjectSettings.application/config/icon]. See [url=https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons]App icons[/url]. </member> + <member name="privacy/active_keyboard_access_reasons" type="int" setter="" getter=""> + The reasons your app use active keyboard API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> <member name="privacy/camera_usage_description" type="String" setter="" getter=""> A message displayed when requesting access to the device's camera (in English). </member> <member name="privacy/camera_usage_description_localized" type="Dictionary" setter="" getter=""> A message displayed when requesting access to the device's camera (localized). </member> + <member name="privacy/collected_data/advertising_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects advertising data. + </member> + <member name="privacy/collected_data/advertising_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects advertising data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/advertising_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links advertising data to the user's identity. + </member> + <member name="privacy/collected_data/advertising_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses advertising data for tracking. + </member> + <member name="privacy/collected_data/audio_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects audio data data. + </member> + <member name="privacy/collected_data/audio_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects audio data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/audio_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links audio data data to the user's identity. + </member> + <member name="privacy/collected_data/audio_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses audio data data for tracking. + </member> + <member name="privacy/collected_data/browsing_history/collected" type="bool" setter="" getter=""> + Indicates whether your app collects browsing history. + </member> + <member name="privacy/collected_data/browsing_history/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects browsing history. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/browsing_history/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links browsing history to the user's identity. + </member> + <member name="privacy/collected_data/browsing_history/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses browsing history for tracking. + </member> + <member name="privacy/collected_data/coarse_location/collected" type="bool" setter="" getter=""> + Indicates whether your app collects coarse location data. + </member> + <member name="privacy/collected_data/coarse_location/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects coarse location data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/coarse_location/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links coarse location data to the user's identity. + </member> + <member name="privacy/collected_data/coarse_location/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses coarse location data for tracking. + </member> + <member name="privacy/collected_data/contacts/collected" type="bool" setter="" getter=""> + Indicates whether your app collects contacts. + </member> + <member name="privacy/collected_data/contacts/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects contacts. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/contacts/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links contacts to the user's identity. + </member> + <member name="privacy/collected_data/contacts/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses contacts for tracking. + </member> + <member name="privacy/collected_data/crash_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects crash data. + </member> + <member name="privacy/collected_data/crash_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects crash data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/crash_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links crash data to the user's identity. + </member> + <member name="privacy/collected_data/crash_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses crash data for tracking. + </member> + <member name="privacy/collected_data/credit_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects credit information. + </member> + <member name="privacy/collected_data/credit_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects credit information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/credit_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links credit information to the user's identity. + </member> + <member name="privacy/collected_data/credit_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses credit information for tracking. + </member> + <member name="privacy/collected_data/customer_support/collected" type="bool" setter="" getter=""> + Indicates whether your app collects customer support data. + </member> + <member name="privacy/collected_data/customer_support/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects customer support data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/customer_support/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links customer support data to the user's identity. + </member> + <member name="privacy/collected_data/customer_support/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses customer support data for tracking. + </member> + <member name="privacy/collected_data/device_id/collected" type="bool" setter="" getter=""> + Indicates whether your app collects device IDs. + </member> + <member name="privacy/collected_data/device_id/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects device IDs. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/device_id/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links device IDs to the user's identity. + </member> + <member name="privacy/collected_data/device_id/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses device IDs for tracking. + </member> + <member name="privacy/collected_data/email_address/collected" type="bool" setter="" getter=""> + Indicates whether your app collects email address. + </member> + <member name="privacy/collected_data/email_address/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects email address. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/email_address/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links email address to the user's identity. + </member> + <member name="privacy/collected_data/email_address/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses email address for tracking. + </member> + <member name="privacy/collected_data/emails_or_text_messages/collected" type="bool" setter="" getter=""> + Indicates whether your app collects emails or text messages. + </member> + <member name="privacy/collected_data/emails_or_text_messages/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects emails or text messages. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/emails_or_text_messages/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links emails or text messages to the user's identity. + </member> + <member name="privacy/collected_data/emails_or_text_messages/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses emails or text messages for tracking. + </member> + <member name="privacy/collected_data/environment_scanning/collected" type="bool" setter="" getter=""> + Indicates whether your app collects environment scanning data. + </member> + <member name="privacy/collected_data/environment_scanning/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects environment scanning data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/environment_scanning/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links environment scanning data to the user's identity. + </member> + <member name="privacy/collected_data/environment_scanning/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses environment scanning data for tracking. + </member> + <member name="privacy/collected_data/fitness/collected" type="bool" setter="" getter=""> + Indicates whether your app collects fitness and exercise data. + </member> + <member name="privacy/collected_data/fitness/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects fitness and exercise data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/fitness/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links fitness and exercise data to the user's identity. + </member> + <member name="privacy/collected_data/fitness/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses fitness and exercise data for tracking. + </member> + <member name="privacy/collected_data/gameplay_content/collected" type="bool" setter="" getter=""> + Indicates whether your app collects gameplay content. + </member> + <member name="privacy/collected_data/gameplay_content/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects gameplay content. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/gameplay_content/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links gameplay content to the user's identity. + </member> + <member name="privacy/collected_data/gameplay_content/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses gameplay content for tracking. + </member> + <member name="privacy/collected_data/hands/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user's hand structure and hand movements. + </member> + <member name="privacy/collected_data/hands/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user's hand structure and hand movements. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/hands/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user's hand structure and hand movements to the user's identity. + </member> + <member name="privacy/collected_data/hands/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user's hand structure and hand movements for tracking. + </member> + <member name="privacy/collected_data/head/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user's head movement. + </member> + <member name="privacy/collected_data/head/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user's head movement. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/head/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user's head movement to the user's identity. + </member> + <member name="privacy/collected_data/head/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user's head movement for tracking. + </member> + <member name="privacy/collected_data/health/collected" type="bool" setter="" getter=""> + Indicates whether your app collects health and medical data. + </member> + <member name="privacy/collected_data/health/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects health and medical data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/health/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links health and medical data to the user's identity. + </member> + <member name="privacy/collected_data/health/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses health and medical data for tracking. + </member> + <member name="privacy/collected_data/name/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user's name. + </member> + <member name="privacy/collected_data/name/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user's name. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/name/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user's name to the user's identity. + </member> + <member name="privacy/collected_data/name/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user's name for tracking. + </member> + <member name="privacy/collected_data/other_contact_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other contact information. + </member> + <member name="privacy/collected_data/other_contact_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other contact information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_contact_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other contact information to the user's identity. + </member> + <member name="privacy/collected_data/other_contact_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other contact information for tracking. + </member> + <member name="privacy/collected_data/other_data_types/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other data. + </member> + <member name="privacy/collected_data/other_data_types/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_data_types/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other data to the user's identity. + </member> + <member name="privacy/collected_data/other_data_types/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other data for tracking. + </member> + <member name="privacy/collected_data/other_diagnostic_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other diagnostic data. + </member> + <member name="privacy/collected_data/other_diagnostic_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other diagnostic data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_diagnostic_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other diagnostic data to the user's identity. + </member> + <member name="privacy/collected_data/other_diagnostic_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other diagnostic data for tracking. + </member> + <member name="privacy/collected_data/other_financial_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other financial information. + </member> + <member name="privacy/collected_data/other_financial_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other financial information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_financial_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other financial information to the user's identity. + </member> + <member name="privacy/collected_data/other_financial_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other financial information for tracking. + </member> + <member name="privacy/collected_data/other_usage_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other usage data. + </member> + <member name="privacy/collected_data/other_usage_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other usage data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_usage_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other usage data to the user's identity. + </member> + <member name="privacy/collected_data/other_usage_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other usage data for tracking. + </member> + <member name="privacy/collected_data/other_user_content/collected" type="bool" setter="" getter=""> + Indicates whether your app collects any other user generated content. + </member> + <member name="privacy/collected_data/other_user_content/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects any other user generated content. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/other_user_content/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links any other user generated content to the user's identity. + </member> + <member name="privacy/collected_data/other_user_content/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses any other user generated content for tracking. + </member> + <member name="privacy/collected_data/payment_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects payment information. + </member> + <member name="privacy/collected_data/payment_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects payment information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/payment_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links payment information to the user's identity. + </member> + <member name="privacy/collected_data/payment_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses payment information for tracking. + </member> + <member name="privacy/collected_data/performance_data/collected" type="bool" setter="" getter=""> + Indicates whether your app collects performance data. + </member> + <member name="privacy/collected_data/performance_data/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects performance data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/performance_data/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links performance data to the user's identity. + </member> + <member name="privacy/collected_data/performance_data/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses performance data for tracking. + </member> + <member name="privacy/collected_data/phone_number/collected" type="bool" setter="" getter=""> + Indicates whether your app collects phone number. + </member> + <member name="privacy/collected_data/phone_number/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects phone number. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/phone_number/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links phone number to the user's identity. + </member> + <member name="privacy/collected_data/phone_number/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses phone number for tracking. + </member> + <member name="privacy/collected_data/photos_or_videos/collected" type="bool" setter="" getter=""> + Indicates whether your app collects photos or videos. + </member> + <member name="privacy/collected_data/photos_or_videos/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects photos or videos. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/photos_or_videos/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links photos or videos to the user's identity. + </member> + <member name="privacy/collected_data/photos_or_videos/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses photos or videos for tracking. + </member> + <member name="privacy/collected_data/physical_address/collected" type="bool" setter="" getter=""> + Indicates whether your app collects physical address. + </member> + <member name="privacy/collected_data/physical_address/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects physical address. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/physical_address/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links physical address to the user's identity. + </member> + <member name="privacy/collected_data/physical_address/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses physical address for tracking. + </member> + <member name="privacy/collected_data/precise_location/collected" type="bool" setter="" getter=""> + Indicates whether your app collects precise location data. + </member> + <member name="privacy/collected_data/precise_location/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects precise location data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/precise_location/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links precise location data to the user's identity. + </member> + <member name="privacy/collected_data/precise_location/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses precise location data for tracking. + </member> + <member name="privacy/collected_data/product_interaction/collected" type="bool" setter="" getter=""> + Indicates whether your app collects product interaction data. + </member> + <member name="privacy/collected_data/product_interaction/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects product interaction data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/product_interaction/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links product interaction data to the user's identity. + </member> + <member name="privacy/collected_data/product_interaction/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses product interaction data for tracking. + </member> + <member name="privacy/collected_data/purchase_history/collected" type="bool" setter="" getter=""> + Indicates whether your app collects purchase history. + </member> + <member name="privacy/collected_data/purchase_history/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects purchase history. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/purchase_history/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links purchase history to the user's identity. + </member> + <member name="privacy/collected_data/purchase_history/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses purchase history for tracking. + </member> + <member name="privacy/collected_data/search_hhistory/collected" type="bool" setter="" getter=""> + Indicates whether your app collects search history. + </member> + <member name="privacy/collected_data/search_hhistory/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects search history. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/search_hhistory/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links search history to the user's identity. + </member> + <member name="privacy/collected_data/search_hhistory/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses search history for tracking. + </member> + <member name="privacy/collected_data/sensitive_info/collected" type="bool" setter="" getter=""> + Indicates whether your app collects sensitive user information. + </member> + <member name="privacy/collected_data/sensitive_info/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects sensitive user information. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/sensitive_info/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links sensitive user information to the user's identity. + </member> + <member name="privacy/collected_data/sensitive_info/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses sensitive user information for tracking. + </member> + <member name="privacy/collected_data/user_id/collected" type="bool" setter="" getter=""> + Indicates whether your app collects user IDs. + </member> + <member name="privacy/collected_data/user_id/collection_purposes" type="int" setter="" getter=""> + The reasons your app collects user IDs. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. + </member> + <member name="privacy/collected_data/user_id/linked_to_user" type="bool" setter="" getter=""> + Indicates whether your app links user IDs to the user's identity. + </member> + <member name="privacy/collected_data/user_id/used_for_tracking" type="bool" setter="" getter=""> + Indicates whether your app uses user IDs for tracking. + </member> + <member name="privacy/disk_space_access_reasons" type="int" setter="" getter=""> + The reasons your app use free disk space API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> + <member name="privacy/file_timestamp_access_reasons" type="int" setter="" getter=""> + The reasons your app use file timestamp/metadata API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> <member name="privacy/microphone_usage_description" type="String" setter="" getter=""> A message displayed when requesting access to the device's microphone (in English). </member> @@ -141,6 +573,18 @@ <member name="privacy/photolibrary_usage_description_localized" type="Dictionary" setter="" getter=""> A message displayed when requesting access to the user's photo library (localized). </member> + <member name="privacy/system_boot_time_access_reasons" type="int" setter="" getter=""> + The reasons your app use system boot time / absolute time API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> + <member name="privacy/tracking_domains" type="PackedStringArray" setter="" getter=""> + The list of internet domains your app connects to that engage in tracking. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files]Privacy manifest files[/url]. + </member> + <member name="privacy/tracking_enabled" type="bool" setter="" getter=""> + Indicates whether your app uses data for tracking. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files]Privacy manifest files[/url]. + </member> + <member name="privacy/user_defaults_access_reasons" type="int" setter="" getter=""> + The reasons your app use user defaults API. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api]Describing use of required reason API[/url]. + </member> <member name="storyboard/custom_bg_color" type="Color" setter="" getter=""> A custom background color of the storyboard launch screen. </member> diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 82b4f6f904..c35c72d093 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -106,6 +106,94 @@ static const IconInfo icon_infos[] = { { PNAME("icons/notification_60x60"), "iphone", "Icon-60.png", "60", "3x", "20x20", false } }; +struct APIAccessInfo { + String prop_name; + String type_name; + Vector<String> prop_flag_value; + Vector<String> prop_flag_name; + int default_value; +}; + +static const APIAccessInfo api_info[] = { + { "file_timestamp", + "NSPrivacyAccessedAPICategoryFileTimestamp", + { "DDA9.1", "C617.1", "3B52.1" }, + { "Display to user on-device:", "Inside app or group container", "Files provided to app by user" }, + 3 }, + { "system_boot_time", + "NSPrivacyAccessedAPICategorySystemBootTime", + { "35F9.1", "8FFB.1", "3D61.1" }, + { "Measure time on-device", "Calculate absolute event timestamps", "User-initiated bug report" }, + 1 }, + { "disk_space", + "NSPrivacyAccessedAPICategoryDiskSpace", + { "E174.1", "85F4.1", "7D9E.1", "B728.1" }, + { "Write or delete file on-device", "Display to user on-device", "User-initiated bug report", "Health research app" }, + 3 }, + { "active_keyboard", + "NSPrivacyAccessedAPICategoryActiveKeyboards", + { "3EC4.1", "54BD.1" }, + { "Custom keyboard app on-device", "Customize UI on-device:2" }, + 0 }, + { "user_defaults", + "NSPrivacyAccessedAPICategoryUserDefaults", + { "1C8F.1", "AC6B.1", "CA92.1" }, + { "Access info from same App Group", "Access managed app configuration", "Access info from same app" }, + 0 } +}; + +struct DataCollectionInfo { + String prop_name; + String type_name; +}; + +static const DataCollectionInfo data_collect_type_info[] = { + { "name", "NSPrivacyCollectedDataTypeName" }, + { "email_address", "NSPrivacyCollectedDataTypeEmailAddress" }, + { "phone_number", "NSPrivacyCollectedDataTypePhoneNumber" }, + { "physical_address", "NSPrivacyCollectedDataTypePhysicalAddress" }, + { "other_contact_info", "NSPrivacyCollectedDataTypeOtherUserContactInfo" }, + { "health", "NSPrivacyCollectedDataTypeHealth" }, + { "fitness", "NSPrivacyCollectedDataTypeFitness" }, + { "payment_info", "NSPrivacyCollectedDataTypePaymentInfo" }, + { "credit_info", "NSPrivacyCollectedDataTypeCreditInfo" }, + { "other_financial_info", "NSPrivacyCollectedDataTypeOtherFinancialInfo" }, + { "precise_location", "NSPrivacyCollectedDataTypePreciseLocation" }, + { "coarse_location", "NSPrivacyCollectedDataTypeCoarseLocation" }, + { "sensitive_info", "NSPrivacyCollectedDataTypeSensitiveInfo" }, + { "contacts", "NSPrivacyCollectedDataTypeContacts" }, + { "emails_or_text_messages", "NSPrivacyCollectedDataTypeEmailsOrTextMessages" }, + { "photos_or_videos", "NSPrivacyCollectedDataTypePhotosorVideos" }, + { "audio_data", "NSPrivacyCollectedDataTypeAudioData" }, + { "gameplay_content", "NSPrivacyCollectedDataTypeGameplayContent" }, + { "customer_support", "NSPrivacyCollectedDataTypeCustomerSupport" }, + { "other_user_content", "NSPrivacyCollectedDataTypeOtherUserContent" }, + { "browsing_history", "NSPrivacyCollectedDataTypeBrowsingHistory" }, + { "search_hhistory", "NSPrivacyCollectedDataTypeSearchHistory" }, + { "user_id", "NSPrivacyCollectedDataTypeUserID" }, + { "device_id", "NSPrivacyCollectedDataTypeDeviceID" }, + { "purchase_history", "NSPrivacyCollectedDataTypePurchaseHistory" }, + { "product_interaction", "NSPrivacyCollectedDataTypeProductInteraction" }, + { "advertising_data", "NSPrivacyCollectedDataTypeAdvertisingData" }, + { "other_usage_data", "NSPrivacyCollectedDataTypeOtherUsageData" }, + { "crash_data", "NSPrivacyCollectedDataTypeCrashData" }, + { "performance_data", "NSPrivacyCollectedDataTypePerformanceData" }, + { "other_diagnostic_data", "NSPrivacyCollectedDataTypeOtherDiagnosticData" }, + { "environment_scanning", "NSPrivacyCollectedDataTypeEnvironmentScanning" }, + { "hands", "NSPrivacyCollectedDataTypeHands" }, + { "head", "NSPrivacyCollectedDataTypeHead" }, + { "other_data_types", "NSPrivacyCollectedDataTypeOtherDataTypes" }, +}; + +static const DataCollectionInfo data_collect_purpose_info[] = { + { "Analytics", "NSPrivacyCollectedDataTypePurposeAnalytics" }, + { "App Functionality", "NSPrivacyCollectedDataTypePurposeAppFunctionality" }, + { "Developer Advertising", "NSPrivacyCollectedDataTypePurposeDeveloperAdvertising" }, + { "Third-party Advertising", "NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising" }, + { "Product Personalization", "NSPrivacyCollectedDataTypePurposeProductPersonalization" }, + { "Other", "NSPrivacyCollectedDataTypePurposeOther" }, +}; + String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { if (p_preset) { if (p_name == "application/app_store_team_id") { @@ -119,6 +207,21 @@ String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPres if (!is_package_name_valid(identifier, &pn_err)) { return TTR("Invalid Identifier:") + " " + pn_err; } + } else if (p_name == "privacy/file_timestamp_access_reasons") { + int access = p_preset->get("privacy/file_timestamp_access_reasons"); + if (access == 0) { + return TTR("At least one file timestamp access reason should be selected."); + } + } else if (p_name == "privacy/disk_space_access_reasons") { + int access = p_preset->get("privacy/disk_space_access_reasons"); + if (access == 0) { + return TTR("At least one disk space access reason should be selected."); + } + } else if (p_name == "privacy/system_boot_time_access_reasons") { + int access = p_preset->get("privacy/system_boot_time_access_reasons"); + if (access == 0) { + return TTR("At least one system boot time access reason should be selected."); + } } } return String(); @@ -135,6 +238,20 @@ void EditorExportPlatformIOS::_notification(int p_what) { } bool EditorExportPlatformIOS::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + // Hide unsupported .NET embedding option. + if (p_option == "dotnet/embed_build_outputs") { + return false; + } + + if (p_preset == nullptr) { + return true; + } + + bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); + if (p_option.begins_with("privacy")) { + return advanced_options_enabled; + } + return true; } @@ -170,6 +287,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/export_project_only"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "application/delete_old_export_files_unconditionally"), false)); Vector<PluginConfigIOS> found_plugins = get_plugins(); for (int i = 0; i < found_plugins.size(); i++) { @@ -215,6 +333,37 @@ 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())); + for (uint64_t i = 0; i < sizeof(api_info) / sizeof(api_info[0]); ++i) { + String prop_name = vformat("privacy/%s_access_reasons", api_info[i].prop_name); + String hint; + for (int j = 0; j < api_info[i].prop_flag_value.size(); j++) { + if (j != 0) { + hint += ","; + } + hint += vformat("%s - %s:%d", api_info[i].prop_flag_value[j], api_info[i].prop_flag_name[j], (1 << j)); + } + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, prop_name, PROPERTY_HINT_FLAGS, hint), api_info[i].default_value)); + } + + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "privacy/tracking_enabled"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "privacy/tracking_domains"), Vector<String>())); + + { + String hint; + for (uint64_t i = 0; i < sizeof(data_collect_purpose_info) / sizeof(data_collect_purpose_info[0]); ++i) { + if (i != 0) { + hint += ","; + } + hint += vformat("%s:%d", data_collect_purpose_info[i].prop_name, (1 << i)); + } + for (uint64_t i = 0; i < sizeof(data_collect_type_info) / sizeof(data_collect_type_info[0]); ++i) { + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/collected", data_collect_type_info[i].prop_name)), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[i].prop_name)), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[i].prop_name)), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[i].prop_name), PROPERTY_HINT_FLAGS, hint), 0)); + } + } + HashSet<String> used_names; for (uint64_t i = 0; i < sizeof(icon_infos) / sizeof(icon_infos[0]); ++i) { if (!used_names.has(icon_infos[i].preset_key)) { @@ -517,6 +666,87 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ } else if (lines[i].find("$swift_runtime_build_phase") != -1) { String value = !p_config.use_swift_runtime ? "" : "90B4C2B62680C7E90039117A /* dummy.swift */,"; strnew += lines[i].replace("$swift_runtime_build_phase", value) + "\n"; + } else if (lines[i].find("$priv_collection") != -1) { + bool section_opened = false; + for (uint64_t j = 0; j < sizeof(data_collect_type_info) / sizeof(data_collect_type_info[0]); ++j) { + bool data_collected = p_preset->get(vformat("privacy/collected_data/%s/collected", data_collect_type_info[j].prop_name)); + bool linked = p_preset->get(vformat("privacy/collected_data/%s/linked_to_user", data_collect_type_info[j].prop_name)); + bool tracking = p_preset->get(vformat("privacy/collected_data/%s/used_for_tracking", data_collect_type_info[j].prop_name)); + int purposes = p_preset->get(vformat("privacy/collected_data/%s/collection_purposes", data_collect_type_info[j].prop_name)); + if (data_collected) { + if (!section_opened) { + section_opened = true; + strnew += "\t<key>NSPrivacyCollectedDataTypes</key>\n"; + strnew += "\t<array>\n"; + } + strnew += "\t\t<dict>\n"; + strnew += "\t\t\t<key>NSPrivacyCollectedDataType</key>\n"; + strnew += vformat("\t\t\t<string>%s</string>\n", data_collect_type_info[j].type_name); + strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypeLinked</key>\n"; + if (linked) { + strnew += "\t\t\t\t<true/>\n"; + } else { + strnew += "\t\t\t\t<false/>\n"; + } + strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypeTracking</key>\n"; + if (tracking) { + strnew += "\t\t\t\t<true/>\n"; + } else { + strnew += "\t\t\t\t<false/>\n"; + } + if (purposes != 0) { + strnew += "\t\t\t\t<key>NSPrivacyCollectedDataTypePurposes</key>\n"; + strnew += "\t\t\t\t<array>\n"; + for (uint64_t k = 0; k < sizeof(data_collect_purpose_info) / sizeof(data_collect_purpose_info[0]); ++k) { + if (purposes & (1 << k)) { + strnew += vformat("\t\t\t\t\t<string>%s</string>\n", data_collect_purpose_info[k].type_name); + } + } + strnew += "\t\t\t\t</array>\n"; + } + strnew += "\t\t\t</dict>\n"; + } + } + if (section_opened) { + strnew += "\t</array>\n"; + } + } else if (lines[i].find("$priv_tracking") != -1) { + bool tracking = p_preset->get("privacy/tracking_enabled"); + strnew += "\t<key>NSPrivacyTracking</key>\n"; + if (tracking) { + strnew += "\t<true/>\n"; + } else { + strnew += "\t<false/>\n"; + } + Vector<String> tracking_domains = p_preset->get("privacy/tracking_domains"); + if (!tracking_domains.is_empty()) { + strnew += "\t<key>NSPrivacyTrackingDomains</key>\n"; + strnew += "\t<array>\n"; + for (const String &E : tracking_domains) { + strnew += "\t\t<string>" + E + "</string>\n"; + } + strnew += "\t</array>\n"; + } + } else if (lines[i].find("$priv_api_types") != -1) { + strnew += "\t<array>\n"; + for (uint64_t j = 0; j < sizeof(api_info) / sizeof(api_info[0]); ++j) { + int api_access = p_preset->get(vformat("privacy/%s_access_reasons", api_info[j].prop_name)); + if (api_access != 0) { + strnew += "\t\t<dict>\n"; + strnew += "\t\t\t<key>NSPrivacyAccessedAPITypeReasons</key>\n"; + strnew += "\t\t\t<array>\n"; + for (int k = 0; k < api_info[j].prop_flag_value.size(); k++) { + if (api_access & (1 << k)) { + strnew += vformat("\t\t\t\t<string>%s</string>\n", api_info[j].prop_flag_value[k]); + } + } + strnew += "\t\t\t</array>\n"; + strnew += "\t\t\t<key>NSPrivacyAccessedAPIType</key>\n"; + strnew += vformat("\t\t\t<string>%s</string>\n", api_info[j].type_name); + strnew += "\t\t</dict>\n"; + } + } + strnew += "\t</array>\n"; } else { strnew += lines[i] + "\n"; } @@ -1316,7 +1546,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const Ref<EditorExportP if (asset.begins_with("res://")) { Error err = _copy_asset(p_preset, p_out_dir, asset, nullptr, p_is_framework, p_should_embed, r_exported_assets); ERR_FAIL_COND_V(err != OK, err); - } else if (ProjectSettings::get_singleton()->localize_path(asset).begins_with("res://")) { + } else if (asset.is_absolute_path() && ProjectSettings::get_singleton()->localize_path(asset).begins_with("res://")) { Error err = _copy_asset(p_preset, p_out_dir, ProjectSettings::get_singleton()->localize_path(asset), nullptr, p_is_framework, p_should_embed, r_exported_assets); ERR_FAIL_COND_V(err != OK, err); } else { @@ -1627,19 +1857,72 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres } { + bool delete_old = p_preset->get("application/delete_old_export_files_unconditionally"); Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (da.is_valid()) { String current_dir = da->get_current_dir(); - // remove leftovers from last export so they don't interfere - // in case some files are no longer needed + // Remove leftovers from last export so they don't interfere in case some files are no longer needed. if (da->change_dir(binary_dir + ".xcodeproj") == OK) { - da->erase_contents_recursive(); + // Check directory content before deleting. + int expected_files = 0; + int total_files = 0; + if (!delete_old) { + da->list_dir_begin(); + for (String n = da->get_next(); !n.is_empty(); n = da->get_next()) { + if (!n.begins_with(".")) { // Ignore ".", ".." and hidden files. + if (da->current_is_dir()) { + if (n == "xcshareddata" || n == "project.xcworkspace") { + expected_files++; + } + } else { + if (n == "project.pbxproj") { + expected_files++; + } + } + total_files++; + } + } + da->list_dir_end(); + } + if ((total_files == 0) || (expected_files >= Math::floor(total_files * 0.8))) { + da->erase_contents_recursive(); + } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s.xcodeproj\", delete it manually or select another destination."), binary_dir)); + return ERR_CANT_CREATE; + } } + da->change_dir(current_dir); + if (da->change_dir(binary_dir) == OK) { - da->erase_contents_recursive(); + // Check directory content before deleting. + int expected_files = 0; + int total_files = 0; + if (!delete_old) { + da->list_dir_begin(); + for (String n = da->get_next(); !n.is_empty(); n = da->get_next()) { + if (!n.begins_with(".")) { // Ignore ".", ".." and hidden files. + if (da->current_is_dir()) { + if (n == "dylibs" || n == "Images.xcassets" || n.ends_with(".lproj") || n == "godot-publish-dotnet" || n.ends_with(".xcframework") || n.ends_with(".framework")) { + expected_files++; + } + } else { + if (n == binary_name + "-Info.plist" || n == binary_name + ".entitlements" || n == "Launch Screen.storyboard" || n == "export_options.plist" || n.begins_with("dummy.") || n.ends_with(".gdip")) { + expected_files++; + } + } + total_files++; + } + } + da->list_dir_end(); + } + if ((total_files == 0) || (expected_files >= Math::floor(total_files * 0.8))) { + da->erase_contents_recursive(); + } else { + add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Unexpected files found in the export destination directory \"%s\", delete it manually or select another destination."), binary_dir)); + return ERR_CANT_CREATE; + } } - da->change_dir(current_dir); if (!da->dir_exists(binary_dir)) { @@ -1657,7 +1940,7 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres } String pack_path = binary_dir + ".pck"; Vector<SharedObject> libraries; - Error err = save_pack(true, p_preset, p_debug, pack_path, &libraries); + Error err = save_pack(p_preset, p_debug, pack_path, &libraries); if (err) { // Message is supplied by the subroutine method. return err; @@ -1689,6 +1972,7 @@ Error EditorExportPlatformIOS::_export_project_helper(const Ref<EditorExportPres files_to_parse.insert("godot_ios.xcodeproj/xcshareddata/xcschemes/godot_ios.xcscheme"); files_to_parse.insert("godot_ios/godot_ios.entitlements"); files_to_parse.insert("godot_ios/Launch Screen.storyboard"); + files_to_parse.insert("PrivacyInfo.xcprivacy"); IOSConfigData config_data = { pkg_name, diff --git a/platform/ios/godot_ios.mm b/platform/ios/godot_ios.mm index 5e66c8b47b..9d35d43344 100644 --- a/platform/ios/godot_ios.mm +++ b/platform/ios/godot_ios.mm @@ -102,15 +102,16 @@ int ios_main(int argc, char **argv) { Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); - if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; - } else if (err != OK) { - return 255; + if (err != OK) { + if (err == ERR_HELP) { // Returned by --help and --version, so success. + return EXIT_SUCCESS; + } + return EXIT_FAILURE; } os->initialize_modules(); - return 0; + return os->get_exit_code(); } void ios_finish() { diff --git a/platform/ios/godot_view.mm b/platform/ios/godot_view.mm index 4b87863fc5..1dddc9306e 100644 --- a/platform/ios/godot_view.mm +++ b/platform/ios/godot_view.mm @@ -451,28 +451,28 @@ static const float earth_gravity = 9.80665; switch (interfaceOrientation) { case UIInterfaceOrientationLandscapeLeft: { - DisplayServerIOS::get_singleton()->update_gravity(-gravity.y, gravity.x, gravity.z); - DisplayServerIOS::get_singleton()->update_accelerometer(-(acceleration.y + gravity.y), (acceleration.x + gravity.x), acceleration.z + gravity.z); - DisplayServerIOS::get_singleton()->update_magnetometer(-magnetic.y, magnetic.x, magnetic.z); - DisplayServerIOS::get_singleton()->update_gyroscope(-rotation.y, rotation.x, rotation.z); + DisplayServerIOS::get_singleton()->update_gravity(Vector3(gravity.x, gravity.y, gravity.z).rotated(Vector3(0, 0, 1), -Math_PI * 0.5)); + DisplayServerIOS::get_singleton()->update_accelerometer(Vector3(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z).rotated(Vector3(0, 0, 1), -Math_PI * 0.5)); + DisplayServerIOS::get_singleton()->update_magnetometer(Vector3(magnetic.x, magnetic.y, magnetic.z).rotated(Vector3(0, 0, 1), -Math_PI * 0.5)); + DisplayServerIOS::get_singleton()->update_gyroscope(Vector3(rotation.x, rotation.y, rotation.z).rotated(Vector3(0, 0, 1), -Math_PI * 0.5)); } break; case UIInterfaceOrientationLandscapeRight: { - DisplayServerIOS::get_singleton()->update_gravity(gravity.y, -gravity.x, gravity.z); - DisplayServerIOS::get_singleton()->update_accelerometer((acceleration.y + gravity.y), -(acceleration.x + gravity.x), acceleration.z + gravity.z); - DisplayServerIOS::get_singleton()->update_magnetometer(magnetic.y, -magnetic.x, magnetic.z); - DisplayServerIOS::get_singleton()->update_gyroscope(rotation.y, -rotation.x, rotation.z); + DisplayServerIOS::get_singleton()->update_gravity(Vector3(gravity.x, gravity.y, gravity.z).rotated(Vector3(0, 0, 1), Math_PI * 0.5)); + DisplayServerIOS::get_singleton()->update_accelerometer(Vector3(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z).rotated(Vector3(0, 0, 1), Math_PI * 0.5)); + DisplayServerIOS::get_singleton()->update_magnetometer(Vector3(magnetic.x, magnetic.y, magnetic.z).rotated(Vector3(0, 0, 1), Math_PI * 0.5)); + DisplayServerIOS::get_singleton()->update_gyroscope(Vector3(rotation.x, rotation.y, rotation.z).rotated(Vector3(0, 0, 1), Math_PI * 0.5)); } break; case UIInterfaceOrientationPortraitUpsideDown: { - DisplayServerIOS::get_singleton()->update_gravity(-gravity.x, gravity.y, gravity.z); - DisplayServerIOS::get_singleton()->update_accelerometer(-(acceleration.x + gravity.x), (acceleration.y + gravity.y), acceleration.z + gravity.z); - DisplayServerIOS::get_singleton()->update_magnetometer(-magnetic.x, magnetic.y, magnetic.z); - DisplayServerIOS::get_singleton()->update_gyroscope(-rotation.x, rotation.y, rotation.z); + DisplayServerIOS::get_singleton()->update_gravity(Vector3(gravity.x, gravity.y, gravity.z).rotated(Vector3(0, 0, 1), Math_PI)); + DisplayServerIOS::get_singleton()->update_accelerometer(Vector3(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z).rotated(Vector3(0, 0, 1), Math_PI)); + DisplayServerIOS::get_singleton()->update_magnetometer(Vector3(magnetic.x, magnetic.y, magnetic.z).rotated(Vector3(0, 0, 1), Math_PI)); + DisplayServerIOS::get_singleton()->update_gyroscope(Vector3(rotation.x, rotation.y, rotation.z).rotated(Vector3(0, 0, 1), Math_PI)); } break; default: { // assume portrait - DisplayServerIOS::get_singleton()->update_gravity(gravity.x, gravity.y, gravity.z); - DisplayServerIOS::get_singleton()->update_accelerometer(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z); - DisplayServerIOS::get_singleton()->update_magnetometer(magnetic.x, magnetic.y, magnetic.z); - DisplayServerIOS::get_singleton()->update_gyroscope(rotation.x, rotation.y, rotation.z); + DisplayServerIOS::get_singleton()->update_gravity(Vector3(gravity.x, gravity.y, gravity.z)); + DisplayServerIOS::get_singleton()->update_accelerometer(Vector3(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z)); + DisplayServerIOS::get_singleton()->update_magnetometer(Vector3(magnetic.x, magnetic.y, magnetic.z)); + DisplayServerIOS::get_singleton()->update_gyroscope(Vector3(rotation.x, rotation.y, rotation.z)); } break; } } diff --git a/platform/ios/ios.h b/platform/ios/ios.h index d488cde257..cb5be64cee 100644 --- a/platform/ios/ios.h +++ b/platform/ios/ios.h @@ -51,7 +51,7 @@ public: static void alert(const char *p_alert, const char *p_title); bool supports_haptic_engine(); - void vibrate_haptic_engine(float p_duration_seconds); + void vibrate_haptic_engine(float p_duration_seconds, float p_amplitude); String get_model() const; String get_rate_url(int p_app_id) const; diff --git a/platform/ios/ios.mm b/platform/ios/ios.mm index 0a2e1fd5cd..6943de5ac8 100644 --- a/platform/ios/ios.mm +++ b/platform/ios/ios.mm @@ -69,21 +69,41 @@ CHHapticEngine *iOS::get_haptic_engine_instance() API_AVAILABLE(ios(13)) { return haptic_engine; } -void iOS::vibrate_haptic_engine(float p_duration_seconds) API_AVAILABLE(ios(13)) { +void iOS::vibrate_haptic_engine(float p_duration_seconds, float p_amplitude) API_AVAILABLE(ios(13)) { if (@available(iOS 13, *)) { // We need the @available check every time to make the compiler happy... if (supports_haptic_engine()) { CHHapticEngine *cur_haptic_engine = get_haptic_engine_instance(); if (cur_haptic_engine) { - NSDictionary *hapticDict = @{ - CHHapticPatternKeyPattern : @[ - @{CHHapticPatternKeyEvent : @{ - CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, - CHHapticPatternKeyTime : @(CHHapticTimeImmediate), - CHHapticPatternKeyEventDuration : @(p_duration_seconds) - }, - }, - ], - }; + NSDictionary *hapticDict; + if (p_amplitude < 0) { + hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : @(p_duration_seconds), + }, + }, + ], + }; + } else { + hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : @(p_duration_seconds), + CHHapticPatternKeyEventParameters : @[ + @{ + CHHapticPatternKeyParameterID : @("HapticIntensity"), + CHHapticPatternKeyParameterValue : @(p_amplitude) + }, + ], + }, + }, + ], + }; + } NSError *error; CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; diff --git a/platform/ios/os_ios.h b/platform/ios/os_ios.h index 10ecd08a89..b7c5a73065 100644 --- a/platform/ios/os_ios.h +++ b/platform/ios/os_ios.h @@ -103,7 +103,7 @@ public: virtual Vector<String> get_system_font_path_for_text(const String &p_font_name, const String &p_text, const String &p_locale = String(), const String &p_script = String(), int p_weight = 400, int p_stretch = 100, bool p_italic = false) const override; virtual String get_system_font_path(const String &p_font_name, int p_weight = 400, int p_stretch = 100, bool p_italic = false) const override; - virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; + virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String &p_name, void *&p_symbol_handle, bool p_optional = false) override; @@ -123,7 +123,7 @@ public: virtual String get_unique_id() const override; virtual String get_processor_name() const override; - virtual void vibrate_handheld(int p_duration_ms = 500) override; + virtual void vibrate_handheld(int p_duration_ms = 500, float p_amplitude = -1.0) override; virtual bool _check_internal_feature_support(const String &p_feature) override; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index ac13a4d457..35b87ea647 100644 --- a/platform/ios/os_ios.mm +++ b/platform/ios/os_ios.mm @@ -149,10 +149,6 @@ void OS_IOS::deinitialize_modules() { void OS_IOS::set_main_loop(MainLoop *p_main_loop) { main_loop = p_main_loop; - - if (main_loop) { - main_loop->initialize(); - } } MainLoop *OS_IOS::get_main_loop() const { @@ -181,7 +177,9 @@ bool OS_IOS::iterate() { } void OS_IOS::start() { - Main::start(); + if (Main::start() == EXIT_SUCCESS) { + main_loop->initialize(); + } if (joypad_ios) { joypad_ios->start_processing(); @@ -219,13 +217,13 @@ _FORCE_INLINE_ String OS_IOS::get_framework_executable(const String &p_path) { return p_path; } -Error OS_IOS::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { +Error OS_IOS::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) { if (p_path.length() == 0) { // Static xcframework. p_library_handle = RTLD_SELF; - if (r_resolved_path != nullptr) { - *r_resolved_path = p_path; + if (p_data != nullptr && p_data->r_resolved_path != nullptr) { + *p_data->r_resolved_path = p_path; } return OK; @@ -258,8 +256,8 @@ Error OS_IOS::open_dynamic_library(const String &p_path, void *&p_library_handle p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror())); - if (r_resolved_path != nullptr) { - *r_resolved_path = path; + if (p_data != nullptr && p_data->r_resolved_path != nullptr) { + *p_data->r_resolved_path = path; } return OK; @@ -573,9 +571,13 @@ String OS_IOS::get_system_font_path(const String &p_font_name, int p_weight, int return ret; } -void OS_IOS::vibrate_handheld(int p_duration_ms) { +void OS_IOS::vibrate_handheld(int p_duration_ms, float p_amplitude) { if (ios->supports_haptic_engine()) { - ios->vibrate_haptic_engine((float)p_duration_ms / 1000.f); + if (p_amplitude > 0.0) { + p_amplitude = CLAMP(p_amplitude, 0.0, 1.0); + } + + ios->vibrate_haptic_engine((float)p_duration_ms / 1000.f, p_amplitude); } else { // iOS <13 does not support duration for vibration AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 27dec73b65..afc9d25a80 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -1,7 +1,7 @@ import os import platform import sys -from methods import get_compiler_version, using_gcc +from methods import print_warning, print_error, get_compiler_version, using_gcc from platform_methods import detect_arch from typing import TYPE_CHECKING @@ -20,7 +20,7 @@ def can_build(): pkgconf_error = os.system("pkg-config --version > /dev/null") if pkgconf_error: - print("Error: pkg-config not found. Aborting.") + print_error("pkg-config not found. Aborting.") return False return True @@ -75,7 +75,7 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Linux / *BSD. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) @@ -128,7 +128,9 @@ def configure(env: "SConsEnvironment"): found_wrapper = True break if not found_wrapper: - print("Couldn't locate mold installation path. Make sure it's installed in /usr or /usr/local.") + print_error( + "Couldn't locate mold installation path. Make sure it's installed in /usr or /usr/local." + ) sys.exit(255) else: env.Append(LINKFLAGS=["-fuse-ld=mold"]) @@ -185,7 +187,7 @@ def configure(env: "SConsEnvironment"): if env["lto"] != "none": if env["lto"] == "thin": if not env["use_llvm"]: - print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + print_error("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") sys.exit(255) env.Append(CCFLAGS=["-flto=thin"]) env.Append(LINKFLAGS=["-flto=thin"]) @@ -209,7 +211,7 @@ def configure(env: "SConsEnvironment"): if env["wayland"]: if os.system("wayland-scanner -v 2>/dev/null") != 0: - print("wayland-scanner not found. Disabling Wayland support.") + print_warning("wayland-scanner not found. Disabling Wayland support.") env["wayland"] = False if env["touch"]: @@ -227,7 +229,7 @@ def configure(env: "SConsEnvironment"): env["builtin_harfbuzz"], ] if (not all(ft_linked_deps)) and any(ft_linked_deps): # All or nothing. - print( + print_error( "These libraries should be either all builtin, or all system provided:\n" "freetype, libpng, zlib, graphite, harfbuzz.\n" "Please specify `builtin_<name>=no` for all of them, or none." @@ -318,7 +320,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config fontconfig --cflags --libs") env.Append(CPPDEFINES=["FONTCONFIG_ENABLED"]) else: - print("Warning: fontconfig development libraries not found. Disabling the system fonts support.") + print_warning("fontconfig development libraries not found. Disabling the system fonts support.") env["fontconfig"] = False else: env.Append(CPPDEFINES=["FONTCONFIG_ENABLED"]) @@ -329,7 +331,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config alsa --cflags --libs") env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) else: - print("Warning: ALSA development libraries not found. Disabling the ALSA audio driver.") + print_warning("ALSA development libraries not found. Disabling the ALSA audio driver.") env["alsa"] = False else: env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) @@ -340,7 +342,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config libpulse --cflags --libs") env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED"]) else: - print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") + print_warning("PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") env["pulseaudio"] = False else: env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED", "_REENTRANT"]) @@ -351,7 +353,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config dbus-1 --cflags --libs") env.Append(CPPDEFINES=["DBUS_ENABLED"]) else: - print("Warning: D-Bus development libraries not found. Disabling screensaver prevention.") + print_warning("D-Bus development libraries not found. Disabling screensaver prevention.") env["dbus"] = False else: env.Append(CPPDEFINES=["DBUS_ENABLED"]) @@ -362,7 +364,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config speech-dispatcher --cflags --libs") env.Append(CPPDEFINES=["SPEECHD_ENABLED"]) else: - print("Warning: speech-dispatcher development libraries not found. Disabling text to speech support.") + print_warning("speech-dispatcher development libraries not found. Disabling text to speech support.") env["speechd"] = False else: env.Append(CPPDEFINES=["SPEECHD_ENABLED"]) @@ -373,11 +375,11 @@ def configure(env: "SConsEnvironment"): env.Append(CPPDEFINES=["XKB_ENABLED"]) else: if env["wayland"]: - print("Error: libxkbcommon development libraries required by Wayland not found. Aborting.") + print_error("libxkbcommon development libraries required by Wayland not found. Aborting.") sys.exit(255) else: - print( - "Warning: libxkbcommon development libraries not found. Disabling dead key composition and key label support." + print_warning( + "libxkbcommon development libraries not found. Disabling dead key composition and key label support." ) else: env.Append(CPPDEFINES=["XKB_ENABLED"]) @@ -390,7 +392,7 @@ def configure(env: "SConsEnvironment"): env.ParseConfig("pkg-config libudev --cflags --libs") env.Append(CPPDEFINES=["UDEV_ENABLED"]) else: - print("Warning: libudev development libraries not found. Disabling controller hotplugging support.") + print_warning("libudev development libraries not found. Disabling controller hotplugging support.") env["udev"] = False else: env.Append(CPPDEFINES=["UDEV_ENABLED"]) @@ -416,31 +418,31 @@ def configure(env: "SConsEnvironment"): if env["x11"]: if not env["use_sowrap"]: if os.system("pkg-config --exists x11"): - print("Error: X11 libraries not found. Aborting.") + print_error("X11 libraries not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config x11 --cflags --libs") if os.system("pkg-config --exists xcursor"): - print("Error: Xcursor library not found. Aborting.") + print_error("Xcursor library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xcursor --cflags --libs") if os.system("pkg-config --exists xinerama"): - print("Error: Xinerama library not found. Aborting.") + print_error("Xinerama library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xinerama --cflags --libs") if os.system("pkg-config --exists xext"): - print("Error: Xext library not found. Aborting.") + print_error("Xext library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xext --cflags --libs") if os.system("pkg-config --exists xrandr"): - print("Error: XrandR library not found. Aborting.") + print_error("XrandR library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xrandr --cflags --libs") if os.system("pkg-config --exists xrender"): - print("Error: XRender library not found. Aborting.") + print_error("XRender library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xrender --cflags --libs") if os.system("pkg-config --exists xi"): - print("Error: Xi library not found. Aborting.") + print_error("Xi library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config xi --cflags --libs") env.Append(CPPDEFINES=["X11_ENABLED"]) @@ -448,20 +450,20 @@ def configure(env: "SConsEnvironment"): if env["wayland"]: if not env["use_sowrap"]: if os.system("pkg-config --exists libdecor-0"): - print("Warning: libdecor development libraries not found. Disabling client-side decorations.") + print_warning("libdecor development libraries not found. Disabling client-side decorations.") env["libdecor"] = False else: env.ParseConfig("pkg-config libdecor-0 --cflags --libs") if os.system("pkg-config --exists wayland-client"): - print("Error: Wayland client library not found. Aborting.") + print_error("Wayland client library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config wayland-client --cflags --libs") if os.system("pkg-config --exists wayland-cursor"): - print("Error: Wayland cursor library not found. Aborting.") + print_error("Wayland cursor library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config wayland-cursor --cflags --libs") if os.system("pkg-config --exists wayland-egl"): - print("Error: Wayland EGL library not found. Aborting.") + print_error("Wayland EGL library not found. Aborting.") sys.exit(255) env.ParseConfig("pkg-config wayland-egl --cflags --libs") diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 773b124c6a..936adddda3 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -146,12 +146,19 @@ List<String> EditorExportPlatformLinuxBSD::get_binary_extensions(const Ref<Edito } bool EditorExportPlatformLinuxBSD::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { - if (p_preset) { - // Hide SSH options. - bool ssh = p_preset->get("ssh_remote_deploy/enabled"); - if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) { - return false; - } + if (p_preset == nullptr) { + return true; + } + + bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); + + // Hide SSH options. + bool ssh = p_preset->get("ssh_remote_deploy/enabled"); + if (!ssh && p_option != "ssh_remote_deploy/enabled" && p_option.begins_with("ssh_remote_deploy/")) { + return false; + } + if (p_option == "dotnet/embed_build_outputs") { + return advanced_options_enabled; } return true; } diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index cdebed58b2..e65404a531 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -367,6 +367,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo } ERR_FAIL_INDEX_V(int(p_mode), DisplayServer::FILE_DIALOG_MODE_SAVE_MAX, FAILED); + ERR_FAIL_NULL_V(monitor_connection, FAILED); Vector<String> filter_names; Vector<String> filter_exts; @@ -406,24 +407,16 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo Error rng_err = rng.get_random_bytes(uuid, 64); ERR_FAIL_COND_V_MSG(rng_err, rng_err, "Failed to generate unique token."); - fd.connection = dbus_bus_get(DBUS_BUS_SESSION, &err); - if (dbus_error_is_set(&err)) { - ERR_PRINT(vformat("Failed to open DBus connection: %s", err.message)); - dbus_error_free(&err); - unsupported = true; - return FAILED; - } - - String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(fd.connection)); + String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(monitor_connection)); String token = String::hex_encode_buffer(uuid, 64); String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").replace(":", ""), token); - fd.path = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name); - dbus_bus_add_match(fd.connection, fd.path.utf8().get_data(), &err); + fd.path = path; + fd.filter = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name); + dbus_bus_add_match(monitor_connection, fd.filter.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to add DBus match: %s", err.message)); dbus_error_free(&err); - dbus_connection_unref(fd.connection); return FAILED; } @@ -460,14 +453,13 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo dbus_message_iter_close_container(&iter, &arr_iter); } - DBusMessage *reply = dbus_connection_send_with_reply_and_block(fd.connection, message, DBUS_TIMEOUT_INFINITE, &err); + DBusMessage *reply = dbus_connection_send_with_reply_and_block(monitor_connection, message, DBUS_TIMEOUT_INFINITE, &err); dbus_message_unref(message); if (!reply || dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to send DBus message: %s", err.message)); dbus_error_free(&err); - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); - dbus_connection_unref(fd.connection); + dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err); return FAILED; } @@ -479,19 +471,17 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo const char *new_path = nullptr; dbus_message_iter_get_basic(&iter, &new_path); if (String::utf8(new_path) != path) { - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); + dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to remove DBus match: %s", err.message)); dbus_error_free(&err); - dbus_connection_unref(fd.connection); return FAILED; } - fd.path = String::utf8(new_path); - dbus_bus_add_match(fd.connection, fd.path.utf8().get_data(), &err); + fd.filter = String::utf8(new_path); + dbus_bus_add_match(monitor_connection, fd.filter.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to add DBus match: %s", err.message)); dbus_error_free(&err); - dbus_connection_unref(fd.connection); return FAILED; } } @@ -506,24 +496,30 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo return OK; } -void FreeDesktopPortalDesktop::_file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &p_index, const Variant &p_options, bool p_opt_in_cb) { - if (p_opt_in_cb) { - Variant ret; - Callable::CallError ce; - const Variant *args[4] = { &p_status, &p_list, &p_index, &p_options }; - - p_callable.callp(args, 4, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 4, ce))); - } - } else { - Variant ret; - Callable::CallError ce; - const Variant *args[3] = { &p_status, &p_list, &p_index }; +void FreeDesktopPortalDesktop::process_file_dialog_callbacks() { + MutexLock lock(file_dialog_mutex); + while (!pending_cbs.is_empty()) { + FileDialogCallback cb = pending_cbs.front()->get(); + pending_cbs.pop_front(); + + if (cb.opt_in_cb) { + Variant ret; + Callable::CallError ce; + const Variant *args[4] = { &cb.status, &cb.files, &cb.index, &cb.options }; + + cb.callback.callp(args, 4, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 4, ce))); + } + } else { + Variant ret; + Callable::CallError ce; + const Variant *args[3] = { &cb.status, &cb.files, &cb.index }; - p_callable.callp(args, 3, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callable, args, 3, ce))); + cb.callback.callp(args, 3, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute file dialog callback: %s.", Variant::get_callable_error_text(cb.callback, args, 3, ce))); + } } } } @@ -532,57 +528,9 @@ void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) { FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud; while (!portal->monitor_thread_abort.is_set()) { - { - MutexLock lock(portal->file_dialog_mutex); - for (int i = portal->file_dialogs.size() - 1; i >= 0; i--) { - bool remove = false; - { - FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i]; - if (fd.connection) { - while (true) { - DBusMessage *msg = dbus_connection_pop_message(fd.connection); - if (!msg) { - break; - } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { - DBusMessageIter iter; - if (dbus_message_iter_init(msg, &iter)) { - bool cancel = false; - Vector<String> uris; - Dictionary options; - int index = 0; - file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index, options); - - if (fd.callback.is_valid()) { - callable_mp(portal, &FreeDesktopPortalDesktop::_file_dialog_callback).call_deferred(fd.callback, !cancel, uris, index, options, fd.opt_in_cb); - } - if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) { - callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus); - } - } - dbus_message_unref(msg); - - DBusError err; - dbus_error_init(&err); - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); - dbus_error_free(&err); - dbus_connection_unref(fd.connection); - remove = true; - break; - } - dbus_message_unref(msg); - } - dbus_connection_read_write(fd.connection, 0); - } - } - if (remove) { - portal->file_dialogs.remove_at(i); - } - } - } - - if (portal->theme_connection) { + if (portal->monitor_connection) { while (true) { - DBusMessage *msg = dbus_connection_pop_message(portal->theme_connection); + DBusMessage *msg = dbus_connection_pop_message(portal->monitor_connection); if (!msg) { break; } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Settings", "SettingChanged")) { @@ -599,12 +547,48 @@ void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) { callable_mp(portal, &FreeDesktopPortalDesktop::_system_theme_changed_callback).call_deferred(); } } - dbus_message_unref(msg); - break; + } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { + String path = String::utf8(dbus_message_get_path(msg)); + MutexLock lock(portal->file_dialog_mutex); + for (int i = 0; i < portal->file_dialogs.size(); i++) { + FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i]; + if (fd.path == path) { + DBusMessageIter iter; + if (dbus_message_iter_init(msg, &iter)) { + bool cancel = false; + Vector<String> uris; + Dictionary options; + int index = 0; + file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index, options); + + if (fd.callback.is_valid()) { + FileDialogCallback cb; + cb.callback = fd.callback; + cb.status = !cancel; + cb.files = uris; + cb.index = index; + cb.options = options; + cb.opt_in_cb = fd.opt_in_cb; + portal->pending_cbs.push_back(cb); + } + if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) { + callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus); + } + } + + DBusError err; + dbus_error_init(&err); + dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err); + dbus_error_free(&err); + + portal->file_dialogs.remove_at(i); + break; + } + } } dbus_message_unref(msg); } - dbus_connection_read_write(portal->theme_connection, 0); + dbus_connection_read_write(portal->monitor_connection, 0); } usleep(50000); @@ -647,18 +631,18 @@ FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() { DBusError err; dbus_error_init(&err); - theme_connection = dbus_bus_get(DBUS_BUS_SESSION, &err); + monitor_connection = dbus_bus_get(DBUS_BUS_SESSION, &err); if (dbus_error_is_set(&err)) { dbus_error_free(&err); } else { theme_path = "type='signal',sender='org.freedesktop.portal.Desktop',interface='org.freedesktop.portal.Settings',member='SettingChanged'"; - dbus_bus_add_match(theme_connection, theme_path.utf8().get_data(), &err); + dbus_bus_add_match(monitor_connection, theme_path.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { dbus_error_free(&err); - dbus_connection_unref(theme_connection); - theme_connection = nullptr; + dbus_connection_unref(monitor_connection); + monitor_connection = nullptr; } - dbus_connection_read_write(theme_connection, 0); + dbus_connection_read_write(monitor_connection, 0); } if (!unsupported) { @@ -673,21 +657,17 @@ FreeDesktopPortalDesktop::~FreeDesktopPortalDesktop() { monitor_thread.wait_to_finish(); } - for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) { - if (fd.connection) { - DBusError err; + if (monitor_connection) { + DBusError err; + for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) { dbus_error_init(&err); - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); + dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err); dbus_error_free(&err); - dbus_connection_unref(fd.connection); } - } - if (theme_connection) { - DBusError err; dbus_error_init(&err); - dbus_bus_remove_match(theme_connection, theme_path.utf8().get_data(), &err); + dbus_bus_remove_match(monitor_connection, theme_path.utf8().get_data(), &err); dbus_error_free(&err); - dbus_connection_unref(theme_connection); + dbus_connection_unref(monitor_connection); } } diff --git a/platform/linuxbsd/freedesktop_portal_desktop.h b/platform/linuxbsd/freedesktop_portal_desktop.h index 75afe02a26..96c38de2c2 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/platform/linuxbsd/freedesktop_portal_desktop.h @@ -56,23 +56,31 @@ private: static void append_dbus_dict_bool(DBusMessageIter *p_iter, const String &p_key, bool p_value); static bool file_chooser_parse_response(DBusMessageIter *p_iter, const Vector<String> &p_names, bool &r_cancel, Vector<String> &r_urls, int &r_index, Dictionary &r_options); - void _file_dialog_callback(const Callable &p_callable, const Variant &p_status, const Variant &p_list, const Variant &p_index, const Variant &p_options, bool p_opt_in_cb); - struct FileDialogData { Vector<String> filter_names; - DBusConnection *connection = nullptr; DisplayServer::WindowID prev_focus = DisplayServer::INVALID_WINDOW_ID; Callable callback; + String filter; String path; bool opt_in_cb = false; }; + struct FileDialogCallback { + Callable callback; + Variant status; + Variant files; + Variant index; + Variant options; + bool opt_in_cb = false; + }; + List<FileDialogCallback> pending_cbs; + Mutex file_dialog_mutex; Vector<FileDialogData> file_dialogs; Thread monitor_thread; SafeFlag monitor_thread_abort; + DBusConnection *monitor_connection = nullptr; - DBusConnection *theme_connection = nullptr; String theme_path; Callable system_theme_changed; void _system_theme_changed_callback(); @@ -86,6 +94,7 @@ public: bool is_supported() { return !unsupported; } Error file_dialog_show(DisplayServer::WindowID p_window_id, const String &p_xid, const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, DisplayServer::FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb); + void process_file_dialog_callbacks(); // Retrieve the system's preferred color scheme. // 0: No preference or unknown. diff --git a/platform/linuxbsd/godot_linuxbsd.cpp b/platform/linuxbsd/godot_linuxbsd.cpp index a2b6fbeb25..b0880c86b8 100644 --- a/platform/linuxbsd/godot_linuxbsd.cpp +++ b/platform/linuxbsd/godot_linuxbsd.cpp @@ -72,18 +72,19 @@ int main(int argc, char *argv[]) { char *ret = getcwd(cwd, PATH_MAX); Error err = Main::setup(argv[0], argc - 1, &argv[1]); + if (err != OK) { free(cwd); - if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; + return EXIT_SUCCESS; } - return 255; + return EXIT_FAILURE; } - if (Main::start()) { - os.set_exit_code(EXIT_SUCCESS); - os.run(); // it is actually the OS that decides how to run + if (Main::start() == EXIT_SUCCESS) { + os.run(); + } else { + os.set_exit_code(EXIT_FAILURE); } Main::cleanup(); diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index f29275c910..68b4cd7f5a 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -62,6 +62,10 @@ #include <mntent.h> #endif +#if defined(__FreeBSD__) +#include <sys/sysctl.h> +#endif + void OS_LinuxBSD::alert(const String &p_alert, const String &p_title) { const char *message_programs[] = { "zenity", "kdialog", "Xdialog", "xmessage" }; @@ -145,17 +149,36 @@ void OS_LinuxBSD::initialize_joypads() { String OS_LinuxBSD::get_unique_id() const { static String machine_id; if (machine_id.is_empty()) { +#if defined(__FreeBSD__) + const int mib[2] = { CTL_KERN, KERN_HOSTUUID }; + char buf[4096]; + memset(buf, 0, sizeof(buf)); + size_t len = sizeof(buf) - 1; + if (sysctl(mib, 2, buf, &len, 0x0, 0) != -1) { + machine_id = String::utf8(buf).replace("-", ""); + } +#else Ref<FileAccess> f = FileAccess::open("/etc/machine-id", FileAccess::READ); if (f.is_valid()) { while (machine_id.is_empty() && !f->eof_reached()) { machine_id = f->get_line().strip_edges(); } } +#endif } return machine_id; } String OS_LinuxBSD::get_processor_name() const { +#if defined(__FreeBSD__) + const int mib[2] = { CTL_HW, HW_MODEL }; + char buf[4096]; + memset(buf, 0, sizeof(buf)); + size_t len = sizeof(buf) - 1; + if (sysctl(mib, 2, buf, &len, 0x0, 0) != -1) { + return String::utf8(buf); + } +#else Ref<FileAccess> f = FileAccess::open("/proc/cpuinfo", FileAccess::READ); ERR_FAIL_COND_V_MSG(f.is_null(), "", String("Couldn't open `/proc/cpuinfo` to get the CPU model name. Returning an empty string.")); @@ -165,8 +188,9 @@ String OS_LinuxBSD::get_processor_name() const { return line.split(":")[1].strip_edges(); } } +#endif - ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name from `/proc/cpuinfo`. Returning an empty string.")); + ERR_FAIL_V_MSG("", String("Couldn't get the CPU model. Returning an empty string.")); } bool OS_LinuxBSD::is_sandboxed() const { diff --git a/platform/linuxbsd/wayland/SCsub b/platform/linuxbsd/wayland/SCsub index cab45b7672..add5bdb504 100644 --- a/platform/linuxbsd/wayland/SCsub +++ b/platform/linuxbsd/wayland/SCsub @@ -199,6 +199,7 @@ if env["vulkan"]: if env["opengl3"]: source_files.append("egl_manager_wayland.cpp") + source_files.append("egl_manager_wayland_gles.cpp") objects = [] diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 80b6029c9d..1c3cae0435 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -46,6 +46,8 @@ #ifdef GLES3_ENABLED #include "detect_prime_egl.h" #include "drivers/gles3/rasterizer_gles3.h" +#include "wayland/egl_manager_wayland.h" +#include "wayland/egl_manager_wayland_gles.h" #endif String DisplayServerWayland::_get_app_id_from_context(Context p_context) { @@ -210,8 +212,10 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const { return true; } break; + //case FEATURE_NATIVE_DIALOG: + //case FEATURE_NATIVE_DIALOG_INPUT: #ifdef DBUS_ENABLED - case FEATURE_NATIVE_DIALOG: { + case FEATURE_NATIVE_DIALOG_FILE: { return true; } break; #endif @@ -1159,6 +1163,12 @@ void DisplayServerWayland::process_events() { } } +#ifdef DBUS_ENABLED + if (portal_desktop) { + portal_desktop->process_file_dialog_callbacks(); + } +#endif + wayland_thread.mutex.unlock(); Input::get_singleton()->flush_buffered_events(); @@ -1172,14 +1182,6 @@ void DisplayServerWayland::release_rendering_thread() { #endif } -void DisplayServerWayland::make_rendering_thread() { -#ifdef GLES3_ENABLED - if (egl_manager) { - egl_manager->make_current(); - } -#endif -} - void DisplayServerWayland::swap_buffers() { #ifdef GLES3_ENABLED if (egl_manager) { @@ -1208,6 +1210,7 @@ Vector<String> DisplayServerWayland::get_rendering_drivers_func() { #ifdef GLES3_ENABLED drivers.push_back("opengl3"); + drivers.push_back("opengl3_es"); #endif return drivers; @@ -1258,14 +1261,14 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win #ifdef RD_ENABLED #ifdef VULKAN_ENABLED - if (p_rendering_driver == "vulkan") { + if (rendering_driver == "vulkan") { rendering_context = memnew(RenderingContextDriverVulkanWayland); } #endif if (rendering_context) { if (rendering_context->initialize() != OK) { - ERR_PRINT(vformat("Could not initialize %s", p_rendering_driver)); + ERR_PRINT(vformat("Could not initialize %s", rendering_driver)); memdelete(rendering_context); rendering_context = nullptr; r_error = ERR_CANT_CREATE; @@ -1275,7 +1278,14 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win #endif #ifdef GLES3_ENABLED - if (p_rendering_driver == "opengl3") { + if (rendering_driver == "opengl3" || rendering_driver == "opengl3_es") { +#ifdef SOWRAP_ENABLED + if (initialize_wayland_egl(dylibloader_verbose) != 0) { + WARN_PRINT("Can't load the Wayland EGL library."); + return; + } +#endif // SOWRAP_ENABLED + if (getenv("DRI_PRIME") == nullptr) { int prime_idx = -1; @@ -1318,23 +1328,38 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win } } - egl_manager = memnew(EGLManagerWayland); + if (rendering_driver == "opengl3") { + egl_manager = memnew(EGLManagerWayland); -#ifdef SOWRAP_ENABLED - if (initialize_wayland_egl(dylibloader_verbose) != 0) { - WARN_PRINT("Can't load the Wayland EGL library."); - return; - } -#endif // SOWRAP_ENABLED + if (egl_manager->initialize() != OK || egl_manager->open_display(wayland_thread.get_wl_display()) != OK) { + memdelete(egl_manager); + egl_manager = nullptr; - if (egl_manager->initialize() != OK) { - memdelete(egl_manager); - egl_manager = nullptr; - r_error = ERR_CANT_CREATE; - ERR_FAIL_MSG("Could not initialize GLES3."); + bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_gles"); + if (fallback) { + WARN_PRINT("Your video card drivers seem not to support the required OpenGL version, switching to OpenGLES."); + rendering_driver = "opengl3_es"; + } else { + r_error = ERR_UNAVAILABLE; + ERR_FAIL_MSG("Could not initialize OpenGL."); + } + } else { + RasterizerGLES3::make_current(true); + } } - RasterizerGLES3::make_current(true); + if (rendering_driver == "opengl3_es") { + egl_manager = memnew(EGLManagerWaylandGLES); + + if (egl_manager->initialize() != OK) { + memdelete(egl_manager); + egl_manager = nullptr; + r_error = ERR_CANT_CREATE; + ERR_FAIL_MSG("Could not initialize GLES3."); + } + + RasterizerGLES3::make_current(false); + } } #endif // GLES3_ENABLED diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index b7d7bee005..1bad358462 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -45,7 +45,7 @@ #endif //RD_ENABLED #ifdef GLES3_ENABLED -#include "wayland/egl_manager_wayland.h" +#include "drivers/egl/egl_manager.h" #endif #if defined(SPEECHD_ENABLED) @@ -126,7 +126,7 @@ class DisplayServerWayland : public DisplayServer { #endif #ifdef GLES3_ENABLED - EGLManagerWayland *egl_manager = nullptr; + EGLManager *egl_manager = nullptr; #endif #ifdef SPEECHD_ENABLED @@ -276,7 +276,6 @@ public: virtual void process_events() override; virtual void release_rendering_thread() override; - virtual void make_rendering_thread() override; virtual void swap_buffers() override; virtual void set_context(Context p_context) override; diff --git a/platform/linuxbsd/wayland/egl_manager_wayland_gles.cpp b/platform/linuxbsd/wayland/egl_manager_wayland_gles.cpp new file mode 100644 index 0000000000..9431b18f05 --- /dev/null +++ b/platform/linuxbsd/wayland/egl_manager_wayland_gles.cpp @@ -0,0 +1,64 @@ +/**************************************************************************/ +/* egl_manager_wayland_gles.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "egl_manager_wayland_gles.h" + +#ifdef WAYLAND_ENABLED +#ifdef EGL_ENABLED +#ifdef GLES3_ENABLED + +const char *EGLManagerWaylandGLES::_get_platform_extension_name() const { + return "EGL_KHR_platform_wayland"; +} + +EGLenum EGLManagerWaylandGLES::_get_platform_extension_enum() const { + return EGL_PLATFORM_WAYLAND_KHR; +} + +EGLenum EGLManagerWaylandGLES::_get_platform_api_enum() const { + return EGL_OPENGL_ES_API; +} + +Vector<EGLAttrib> EGLManagerWaylandGLES::_get_platform_display_attributes() const { + return Vector<EGLAttrib>(); +} + +Vector<EGLint> EGLManagerWaylandGLES::_get_platform_context_attribs() const { + Vector<EGLint> ret; + ret.push_back(EGL_CONTEXT_MAJOR_VERSION); + ret.push_back(3); + ret.push_back(EGL_NONE); + + return ret; +} + +#endif // GLES3_ENABLED +#endif // EGL_ENABLED +#endif // WAYLAND_ENABLED diff --git a/platform/linuxbsd/wayland/egl_manager_wayland_gles.h b/platform/linuxbsd/wayland/egl_manager_wayland_gles.h new file mode 100644 index 0000000000..f526f18277 --- /dev/null +++ b/platform/linuxbsd/wayland/egl_manager_wayland_gles.h @@ -0,0 +1,53 @@ +/**************************************************************************/ +/* egl_manager_wayland_gles.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EGL_MANAGER_WAYLAND_GLES_H +#define EGL_MANAGER_WAYLAND_GLES_H + +#ifdef WAYLAND_ENABLED +#ifdef EGL_ENABLED +#ifdef GLES3_ENABLED + +#include "drivers/egl/egl_manager.h" + +class EGLManagerWaylandGLES : public EGLManager { +public: + virtual const char *_get_platform_extension_name() const override; + virtual EGLenum _get_platform_extension_enum() const override; + virtual EGLenum _get_platform_api_enum() const override; + virtual Vector<EGLAttrib> _get_platform_display_attributes() const override; + virtual Vector<EGLint> _get_platform_context_attribs() const override; +}; + +#endif // GLES3_ENABLED +#endif // EGL_ENABLED +#endif // WAYLAND_ENABLED + +#endif // EGL_MANAGER_WAYLAND_GLES_H diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 5040f5dd45..1701aa650d 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -371,28 +371,22 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, zxdg_exporter_v1_interface.name) == 0) { - registry->wl_exporter = (struct zxdg_exporter_v1 *)wl_registry_bind(wl_registry, name, &zxdg_exporter_v1_interface, 1); - registry->wl_exporter_name = name; + registry->xdg_exporter = (struct zxdg_exporter_v1 *)wl_registry_bind(wl_registry, name, &zxdg_exporter_v1_interface, 1); + registry->xdg_exporter_name = name; return; } if (strcmp(interface, wl_compositor_interface.name) == 0) { - registry->wl_compositor = (struct wl_compositor *)wl_registry_bind(wl_registry, name, &wl_compositor_interface, 4); + registry->wl_compositor = (struct wl_compositor *)wl_registry_bind(wl_registry, name, &wl_compositor_interface, CLAMP((int)version, 1, 6)); registry->wl_compositor_name = name; return; } - if (strcmp(interface, wl_subcompositor_interface.name) == 0) { - registry->wl_subcompositor = (struct wl_subcompositor *)wl_registry_bind(wl_registry, name, &wl_subcompositor_interface, 1); - registry->wl_subcompositor_name = name; - return; - } - if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { - registry->wl_data_device_manager = (struct wl_data_device_manager *)wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, 3); + registry->wl_data_device_manager = (struct wl_data_device_manager *)wl_registry_bind(wl_registry, name, &wl_data_device_manager_interface, CLAMP((int)version, 1, 3)); registry->wl_data_device_manager_name = name; - // This global creates some seats data. Let's do that for the ones already available. + // This global creates some seat data. Let's do that for the ones already available. for (struct wl_seat *wl_seat : registry->wl_seats) { SeatState *ss = wl_seat_get_seat_state(wl_seat); ERR_FAIL_NULL(ss); @@ -406,7 +400,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, wl_output_interface.name) == 0) { - struct wl_output *wl_output = (struct wl_output *)wl_registry_bind(wl_registry, name, &wl_output_interface, 2); + struct wl_output *wl_output = (struct wl_output *)wl_registry_bind(wl_registry, name, &wl_output_interface, CLAMP((int)version, 1, 4)); wl_proxy_tag_godot((struct wl_proxy *)wl_output); registry->wl_outputs.push_back(wl_output); @@ -421,7 +415,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, wl_seat_interface.name) == 0) { - struct wl_seat *wl_seat = (struct wl_seat *)wl_registry_bind(wl_registry, name, &wl_seat_interface, 5); + struct wl_seat *wl_seat = (struct wl_seat *)wl_registry_bind(wl_registry, name, &wl_seat_interface, CLAMP((int)version, 1, 9)); wl_proxy_tag_godot((struct wl_proxy *)wl_seat); SeatState *ss = memnew(SeatState); @@ -466,7 +460,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re } if (strcmp(interface, xdg_wm_base_interface.name) == 0) { - registry->xdg_wm_base = (struct xdg_wm_base *)wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, MAX(2, MIN(6, (int)version))); + registry->xdg_wm_base = (struct xdg_wm_base *)wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, CLAMP((int)version, 1, 6)); registry->xdg_wm_base_name = name; xdg_wm_base_add_listener(registry->xdg_wm_base, &xdg_wm_base_listener, nullptr); @@ -502,7 +496,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re if (strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name) == 0) { registry->wp_primary_selection_device_manager = (struct zwp_primary_selection_device_manager_v1 *)wl_registry_bind(wl_registry, name, &zwp_primary_selection_device_manager_v1_interface, 1); - // This global creates some seats data. Let's do that for the ones already available. + // This global creates some seat data. Let's do that for the ones already available. for (struct wl_seat *wl_seat : registry->wl_seats) { SeatState *ss = wl_seat_get_seat_state(wl_seat); ERR_FAIL_NULL(ss); @@ -570,13 +564,13 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } - if (name == registry->wl_exporter_name) { - if (registry->wl_exporter) { - zxdg_exporter_v1_destroy(registry->wl_exporter); - registry->wl_exporter = nullptr; + if (name == registry->xdg_exporter_name) { + if (registry->xdg_exporter) { + zxdg_exporter_v1_destroy(registry->xdg_exporter); + registry->xdg_exporter = nullptr; } - registry->wl_exporter_name = 0; + registry->xdg_exporter_name = 0; return; } @@ -592,17 +586,6 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } - if (name == registry->wl_subcompositor_name) { - if (registry->wl_subcompositor) { - wl_subcompositor_destroy(registry->wl_subcompositor); - registry->wl_subcompositor = nullptr; - } - - registry->wl_subcompositor_name = 0; - - return; - } - if (name == registry->wl_data_device_manager_name) { if (registry->wl_data_device_manager) { wl_data_device_manager_destroy(registry->wl_data_device_manager); @@ -1000,6 +983,12 @@ void WaylandThread::_wl_output_on_geometry(void *data, struct wl_output *wl_outp ss->pending_data.make.parse_utf8(make); ss->pending_data.model.parse_utf8(model); + + // `wl_output::done` is a version 2 addition. We'll directly update the data + // for compatibility. + if (wl_output_get_version(wl_output) == 1) { + ss->data = ss->pending_data; + } } void WaylandThread::_wl_output_on_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { @@ -1010,8 +999,17 @@ void WaylandThread::_wl_output_on_mode(void *data, struct wl_output *wl_output, ss->pending_data.size.height = height; ss->pending_data.refresh_rate = refresh ? refresh / 1000.0f : -1; + + // `wl_output::done` is a version 2 addition. We'll directly update the data + // for compatibility. + if (wl_output_get_version(wl_output) == 1) { + ss->data = ss->pending_data; + } } +// NOTE: The following `wl_output` events are only for version 2 onwards, so we +// can assume that they're "atomic" (i.e. rely on the `wl_output::done` event). + void WaylandThread::_wl_output_on_done(void *data, struct wl_output *wl_output) { ScreenState *ss = (ScreenState *)data; ERR_FAIL_NULL(ss); @@ -1523,7 +1521,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point wayland_thread->push_message(msg); } - if (pd.discrete_scroll_vector - old_pd.discrete_scroll_vector != Vector2i()) { + if (pd.discrete_scroll_vector_120 - old_pd.discrete_scroll_vector_120 != Vector2i()) { // This is a discrete scroll (eg. from a scroll wheel), so we'll just emit // scroll wheel buttons. if (pd.scroll_vector.y != 0) { @@ -1563,7 +1561,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point } if (old_pd.pressed_button_mask != pd.pressed_button_mask) { - BitField<MouseButtonMask> pressed_mask_delta = BitField<MouseButtonMask>((uint32_t)old_pd.pressed_button_mask ^ (uint32_t)pd.pressed_button_mask); + BitField<MouseButtonMask> pressed_mask_delta = old_pd.pressed_button_mask ^ pd.pressed_button_mask; const MouseButton buttons_to_test[] = { MouseButton::LEFT, @@ -1596,13 +1594,13 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point if (test_button == MouseButton::WHEEL_UP || test_button == MouseButton::WHEEL_DOWN) { // If this is a discrete scroll, specify how many "clicks" it did for this // pointer frame. - mb->set_factor(abs(pd.discrete_scroll_vector.y)); + mb->set_factor(Math::abs(pd.discrete_scroll_vector_120.y / (float)120)); } if (test_button == MouseButton::WHEEL_RIGHT || test_button == MouseButton::WHEEL_LEFT) { // If this is a discrete scroll, specify how many "clicks" it did for this // pointer frame. - mb->set_factor(abs(pd.discrete_scroll_vector.x)); + mb->set_factor(fabs(pd.discrete_scroll_vector_120.x / (float)120)); } mb->set_button_mask(pd.pressed_button_mask); @@ -1661,7 +1659,7 @@ void WaylandThread::_wl_pointer_on_frame(void *data, struct wl_pointer *wl_point // Reset the scroll vectors as we already handled them. pd.scroll_vector = Vector2(); - pd.discrete_scroll_vector = Vector2(); + pd.discrete_scroll_vector_120 = Vector2i(); // Update the data all getters read. Wayland's specification requires us to do // this, since all pointer actions are sent in individual events. @@ -1683,6 +1681,9 @@ void WaylandThread::_wl_pointer_on_axis_source(void *data, struct wl_pointer *wl void WaylandThread::_wl_pointer_on_axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { } +// NOTE: This event is deprecated since version 8 and superseded by +// `wl_pointer::axis_value120`. This thus converts the data to its +// fraction-of-120 format. void WaylandThread::_wl_pointer_on_axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -1694,17 +1695,37 @@ void WaylandThread::_wl_pointer_on_axis_discrete(void *data, struct wl_pointer * PointerData &pd = ss->pointer_data_buffer; + // NOTE: We can allow ourselves to not accumulate this data (and thus just + // assign it) as the spec guarantees only one event per axis type. + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { - pd.discrete_scroll_vector.y = discrete; + pd.discrete_scroll_vector_120.y = discrete * 120; } if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { - pd.discrete_scroll_vector.x = discrete; + pd.discrete_scroll_vector_120.x = discrete * 120; } } -// TODO: Add support to this event. +// Supersedes `wl_pointer::axis_discrete` Since version 8. void WaylandThread::_wl_pointer_on_axis_value120(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t value120) { + SeatState *ss = (SeatState *)data; + ERR_FAIL_NULL(ss); + + if (!ss->pointed_surface) { + // We're probably on a decoration or some other third-party thing. + return; + } + + PointerData &pd = ss->pointer_data_buffer; + + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { + pd.discrete_scroll_vector_120.y += value120; + } + + if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) { + pd.discrete_scroll_vector_120.x += value120; + } } // TODO: Add support to this event. @@ -1999,7 +2020,7 @@ void WaylandThread::_wp_relative_pointer_on_relative_motion(void *data, struct z pd.relative_motion_time = uptime_lo; } -void WaylandThread::_wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers) { +void WaylandThread::_wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2009,7 +2030,7 @@ void WaylandThread::_wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_po } } -void WaylandThread::_wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) { +void WaylandThread::_wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2068,7 +2089,7 @@ void WaylandThread::_wp_pointer_gesture_pinch_on_update(void *data, struct zwp_p } } -void WaylandThread::_wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled) { +void WaylandThread::_wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2093,7 +2114,7 @@ void WaylandThread::_wp_primary_selection_device_on_selection(void *data, struct ss->wp_primary_selection_offer = id; } -void WaylandThread::_wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime_type) { +void WaylandThread::_wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *wp_primary_selection_offer_v1, const char *mime_type) { OfferState *os = (OfferState *)data; ERR_FAIL_NULL(os); @@ -2147,10 +2168,10 @@ void WaylandThread::_wp_primary_selection_source_on_cancelled(void *data, struct } } -void WaylandThread::_wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_v2 *id) { +void WaylandThread::_wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_v2 *id) { } -void WaylandThread::_wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id) { +void WaylandThread::_wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); @@ -2163,31 +2184,31 @@ void WaylandThread::_wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_ ss->tablet_tools.push_back(id); } -void WaylandThread::_wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id) { +void WaylandThread::_wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id) { } -void WaylandThread::_wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t tool_type) { - TabletToolState *state = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t tool_type) { + TabletToolState *state = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (state && tool_type == ZWP_TABLET_TOOL_V2_TYPE_ERASER) { state->is_eraser = true; } } -void WaylandThread::_wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) { +void WaylandThread::_wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) { } -void WaylandThread::_wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo) { +void WaylandThread::_wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo) { } -void WaylandThread::_wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t capability) { +void WaylandThread::_wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t capability) { } -void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { +void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { } -void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2199,7 +2220,7 @@ void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_too return; } - List<struct zwp_tablet_tool_v2 *>::Element *E = ss->tablet_tools.find(zwp_tablet_tool_v2); + List<struct zwp_tablet_tool_v2 *>::Element *E = ss->tablet_tools.find(wp_tablet_tool_v2); if (E && E->get()) { struct zwp_tablet_tool_v2 *tool = E->get(); @@ -2213,8 +2234,8 @@ void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_too } } -void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2241,8 +2262,8 @@ void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_table DEBUG_LOG_WAYLAND_THREAD("Tablet tool entered window."); } -void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2268,8 +2289,8 @@ void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tabl DEBUG_LOG_WAYLAND_THREAD("Tablet tool left window."); } -void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2286,8 +2307,8 @@ void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v td.button_time = OS::get_singleton()->get_ticks_msec(); } -void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2302,8 +2323,8 @@ void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 td.button_time = OS::get_singleton()->get_ticks_msec(); } -void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2323,8 +2344,8 @@ void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool td.motion_time = OS::get_singleton()->get_ticks_msec(); } -void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t pressure) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t pressure) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2333,12 +2354,12 @@ void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_to ts->data_pending.pressure = pressure; } -void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t distance) { +void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t distance) { // Unsupported } -void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2350,20 +2371,20 @@ void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v td.tilt.y = wl_fixed_to_double(tilt_y); } -void WaylandThread::_wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees) { +void WaylandThread::_wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees) { // Unsupported. } -void WaylandThread::_wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, int32_t position) { +void WaylandThread::_wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, int32_t position) { // Unsupported. } -void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks) { +void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks) { // TODO } -void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2398,8 +2419,8 @@ void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool } } -void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t time) { - TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); +void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t time) { + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; @@ -2440,7 +2461,7 @@ void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_ // According to the tablet proto spec, tilt is expressed in degrees relative // to the Z axis of the tablet, so it shouldn't go over 90 degrees either way, // I think. We'll clamp it just in case. - td.tilt = td.tilt.clamp(Vector2(-90, -90), Vector2(90, 90)); + td.tilt = td.tilt.clampf(-90, 90); mm->set_tilt(td.tilt / 90); @@ -2454,7 +2475,7 @@ void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_ mm->set_relative_screen_position(mm->get_relative()); Vector2i pos_delta = td.position - old_td.position; - uint32_t time_delta = td.motion_time - td.motion_time; + uint32_t time_delta = td.motion_time - old_td.motion_time; mm->set_velocity((Vector2)pos_delta / time_delta); Ref<InputEventMessage> inputev_msg; @@ -3071,8 +3092,8 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid // "loop". wl_surface_commit(ws.wl_surface); - if (registry.wl_exporter) { - ws.xdg_exported = zxdg_exporter_v1_export(registry.wl_exporter, ws.wl_surface); + if (registry.xdg_exporter) { + ws.xdg_exported = zxdg_exporter_v1_export(registry.xdg_exporter, ws.wl_surface); zxdg_exported_v1_add_listener(ws.xdg_exported, &xdg_exported_listener, &ws); } @@ -3529,9 +3550,6 @@ Error WaylandThread::init() { ERR_FAIL_NULL_V_MSG(registry.wl_shm, ERR_UNAVAILABLE, "Can't obtain the Wayland shared memory global."); ERR_FAIL_NULL_V_MSG(registry.wl_compositor, ERR_UNAVAILABLE, "Can't obtain the Wayland compositor global."); - ERR_FAIL_NULL_V_MSG(registry.wl_subcompositor, ERR_UNAVAILABLE, "Can't obtain the Wayland subcompositor global."); - ERR_FAIL_NULL_V_MSG(registry.wl_data_device_manager, ERR_UNAVAILABLE, "Can't obtain the Wayland data device manager global."); - ERR_FAIL_NULL_V_MSG(registry.wp_pointer_constraints, ERR_UNAVAILABLE, "Can't obtain the Wayland pointer constraints global."); ERR_FAIL_NULL_V_MSG(registry.xdg_wm_base, ERR_UNAVAILABLE, "Can't obtain the Wayland XDG shell global."); if (!registry.xdg_decoration_manager) { @@ -3660,7 +3678,10 @@ void WaylandThread::cursor_shape_set_custom_image(DisplayServer::CursorShape p_c munmap(cursor.buffer_data, cursor.buffer_data_size); } - cursor.buffer_data = (uint32_t *)mmap(nullptr, data_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + // NOTE: From `wl_keyboard`s of version 7 or later, the spec requires the mmap + // operation to be done with MAP_PRIVATE, as "MAP_SHARED may fail". We'll do it + // regardless of global version. + cursor.buffer_data = (uint32_t *)mmap(nullptr, data_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); if (cursor.wl_buffer) { // Clean up the old Wayland buffer. @@ -4179,18 +4200,14 @@ void WaylandThread::destroy() { xdg_wm_base_destroy(registry.xdg_wm_base); } - if (registry.wl_exporter) { - zxdg_exporter_v1_destroy(registry.wl_exporter); + if (registry.xdg_exporter) { + zxdg_exporter_v1_destroy(registry.xdg_exporter); } if (registry.wl_shm) { wl_shm_destroy(registry.wl_shm); } - if (registry.wl_subcompositor) { - wl_subcompositor_destroy(registry.wl_subcompositor); - } - if (registry.wl_compositor) { wl_compositor_destroy(registry.wl_compositor); } diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index d49f0c9d34..d35a5b7139 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -133,8 +133,8 @@ public: struct xdg_wm_base *xdg_wm_base = nullptr; uint32_t xdg_wm_base_name = 0; - struct zxdg_exporter_v1 *wl_exporter = nullptr; - uint32_t wl_exporter_name = 0; + struct zxdg_exporter_v1 *xdg_exporter = nullptr; + uint32_t xdg_exporter_name = 0; // wayland-protocols globals. @@ -300,8 +300,8 @@ public: // The amount "scrolled" in pixels, in each direction. Vector2 scroll_vector; - // The amount of scroll "clicks" in each direction. - Vector2i discrete_scroll_vector; + // The amount of scroll "clicks" in each direction, in fractions of 120. + Vector2i discrete_scroll_vector_120; uint32_t pinch_scale = 1; }; @@ -579,41 +579,41 @@ private: static void _wp_relative_pointer_on_relative_motion(void *data, struct zwp_relative_pointer_v1 *wp_relative_pointer_v1, uint32_t uptime_hi, uint32_t uptime_lo, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel); - static void _wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers); - static void _wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation); - static void _wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zwp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled); + static void _wp_pointer_gesture_pinch_on_begin(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, struct wl_surface *surface, uint32_t fingers); + static void _wp_pointer_gesture_pinch_on_update(void *data, struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch_v1, uint32_t time, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t scale, wl_fixed_t rotation); + static void _wp_pointer_gesture_pinch_on_end(void *data, struct zwp_pointer_gesture_pinch_v1 *zp_pointer_gesture_pinch_v1, uint32_t serial, uint32_t time, int32_t cancelled); static void _wp_primary_selection_device_on_data_offer(void *data, struct zwp_primary_selection_device_v1 *wp_primary_selection_device_v1, struct zwp_primary_selection_offer_v1 *offer); static void _wp_primary_selection_device_on_selection(void *data, struct zwp_primary_selection_device_v1 *wp_primary_selection_device_v1, struct zwp_primary_selection_offer_v1 *id); - static void _wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime_type); + static void _wp_primary_selection_offer_on_offer(void *data, struct zwp_primary_selection_offer_v1 *wp_primary_selection_offer_v1, const char *mime_type); static void _wp_primary_selection_source_on_send(void *data, struct zwp_primary_selection_source_v1 *wp_primary_selection_source_v1, const char *mime_type, int32_t fd); static void _wp_primary_selection_source_on_cancelled(void *data, struct zwp_primary_selection_source_v1 *wp_primary_selection_source_v1); - static void _wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_v2 *id); - static void _wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id); - static void _wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *zwp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id); - - static void _wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t tool_type); - static void _wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo); - static void _wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo); - static void _wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t capability); - static void _wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface); - static void _wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial); - static void _wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2); - static void _wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y); - static void _wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t pressure); - static void _wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t distance); - static void _wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y); - static void _wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees); - static void _wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, int32_t position); - static void _wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks); - static void _wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state); - static void _wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t time); + static void _wp_tablet_seat_on_tablet_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_v2 *id); + static void _wp_tablet_seat_on_tool_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_tool_v2 *id); + static void _wp_tablet_seat_on_pad_added(void *data, struct zwp_tablet_seat_v2 *wp_tablet_seat_v2, struct zwp_tablet_pad_v2 *id); + + static void _wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t tool_type); + static void _wp_tablet_tool_on_hardware_serial(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_serial_hi, uint32_t hardware_serial_lo); + static void _wp_tablet_tool_on_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t hardware_id_hi, uint32_t hardware_id_lo); + static void _wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t capability); + static void _wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface); + static void _wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial); + static void _wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2); + static void _wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y); + static void _wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t pressure); + static void _wp_tablet_tool_on_distance(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t distance); + static void _wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y); + static void _wp_tablet_tool_on_rotation(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees); + static void _wp_tablet_tool_on_slider(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, int32_t position); + static void _wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t degrees, int32_t clicks); + static void _wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state); + static void _wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t time); static void _xdg_toplevel_decoration_on_configure(void *data, struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration, uint32_t mode); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 34aa15abbb..b76cbc126f 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -128,8 +128,10 @@ bool DisplayServerX11::has_feature(Feature p_feature) const { //case FEATURE_HIDPI: case FEATURE_ICON: #ifdef DBUS_ENABLED - case FEATURE_NATIVE_DIALOG: + case FEATURE_NATIVE_DIALOG_FILE: #endif + //case FEATURE_NATIVE_DIALOG: + //case FEATURE_NATIVE_DIALOG_INPUT: //case FEATURE_NATIVE_ICON: case FEATURE_SWAP_BUFFERS: #ifdef DBUS_ENABLED @@ -1995,8 +1997,7 @@ void DisplayServerX11::window_set_current_screen(int p_screen, WindowID p_window Size2i wsize = window_get_size(p_window); wpos += srect.position; if (srect != Rect2i()) { - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - wsize.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - wsize.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - wsize / 3); } window_set_position(wpos, p_window); } @@ -2224,8 +2225,7 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) { ERR_FAIL_COND(!windows.has(p_window)); Size2i size = p_size; - size.x = MAX(1, size.x); - size.y = MAX(1, size.y); + size = size.maxi(1); WindowData &wd = windows[p_window]; @@ -4268,7 +4268,7 @@ bool DisplayServerX11::_window_focus_check() { } void DisplayServerX11::process_events() { - _THREAD_SAFE_METHOD_ + _THREAD_SAFE_LOCK_ #ifdef DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED static int frame = 0; @@ -4992,7 +4992,7 @@ void DisplayServerX11::process_events() { files.write[i] = files[i].replace("file://", "").uri_decode(); } - if (!windows[window_id].drop_files_callback.is_null()) { + if (windows[window_id].drop_files_callback.is_valid()) { windows[window_id].drop_files_callback.call(files); } @@ -5097,6 +5097,14 @@ void DisplayServerX11::process_events() { */ } +#ifdef DBUS_ENABLED + if (portal_desktop) { + portal_desktop->process_file_dialog_callbacks(); + } +#endif + + _THREAD_SAFE_UNLOCK_ + Input::get_singleton()->flush_buffered_events(); } @@ -5111,17 +5119,6 @@ void DisplayServerX11::release_rendering_thread() { #endif } -void DisplayServerX11::make_rendering_thread() { -#if defined(GLES3_ENABLED) - if (gl_manager) { - gl_manager->make_current(); - } - if (gl_manager_egl) { - gl_manager_egl->make_current(); - } -#endif -} - void DisplayServerX11::swap_buffers() { #if defined(GLES3_ENABLED) if (gl_manager) { @@ -5212,7 +5209,7 @@ void DisplayServerX11::set_icon(const Ref<Image> &p_icon) { if (g_set_icon_error) { g_set_icon_error = false; - WARN_PRINT("Icon too large, attempting to resize icon."); + WARN_PRINT(vformat("Icon too large (%dx%d), attempting to downscale icon.", w, h)); int new_width, new_height; if (w > h) { @@ -5451,8 +5448,7 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, V } else { Rect2i srect = screen_get_usable_rect(rq_screen); Point2i wpos = p_rect.position; - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - p_rect.size / 3); win_rect.position = wpos; } diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index 715a8e48e6..8a7062857c 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -526,7 +526,6 @@ public: virtual void process_events() override; virtual void release_rendering_thread() override; - virtual void make_rendering_thread() override; virtual void swap_buffers() override; virtual void set_context(Context p_context) override; diff --git a/platform/linuxbsd/x11/gl_manager_x11.cpp b/platform/linuxbsd/x11/gl_manager_x11.cpp index 602dd784e0..febb7ae584 100644 --- a/platform/linuxbsd/x11/gl_manager_x11.cpp +++ b/platform/linuxbsd/x11/gl_manager_x11.cpp @@ -311,20 +311,6 @@ void GLManager_X11::window_make_current(DisplayServer::WindowID p_window_id) { _internal_set_current_window(&win); } -void GLManager_X11::make_current() { - if (!_current_window) { - return; - } - if (!_current_window->in_use) { - WARN_PRINT("current window not in use!"); - return; - } - const GLDisplay &disp = get_current_display(); - if (!glXMakeCurrent(_x_windisp.x11_display, _x_windisp.x11_window, disp.context->glx_context)) { - ERR_PRINT("glXMakeCurrent failed"); - } -} - void GLManager_X11::swap_buffers() { if (!_current_window) { return; diff --git a/platform/linuxbsd/x11/gl_manager_x11.h b/platform/linuxbsd/x11/gl_manager_x11.h index 235c7d22f9..06e147e39f 100644 --- a/platform/linuxbsd/x11/gl_manager_x11.h +++ b/platform/linuxbsd/x11/gl_manager_x11.h @@ -117,7 +117,6 @@ public: void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height); void release_current(); - void make_current(); void swap_buffers(); void window_make_current(DisplayServer::WindowID p_window_id); diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 3c8b1ebee1..a5ef29e34f 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -1,6 +1,6 @@ import os import sys -from methods import detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang +from methods import print_error, detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang from platform_methods import detect_arch, detect_mvk from typing import TYPE_CHECKING @@ -64,11 +64,11 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_64", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for macOS. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) ## Build type @@ -254,7 +254,7 @@ def configure(env: "SConsEnvironment"): if mvk_path != "": env.Append(LINKFLAGS=["-L" + mvk_path]) else: - print( + print_error( "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path." ) sys.exit(255) diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 09073a8030..db76b7d78a 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -192,7 +192,7 @@ private: HashMap<WindowID, WindowData> windows; struct IndicatorData { - id view; + id delegate; id item; }; @@ -241,6 +241,8 @@ public: NSImage *_convert_to_nsimg(Ref<Image> &p_image) const; Point2i _get_screens_origin() const; + void set_menu_delegate(NSMenu *p_menu); + void send_event(NSEvent *p_event); void send_window_event(const WindowData &p_wd, WindowEvent p_event); void release_pressed_events(); @@ -424,16 +426,17 @@ public: virtual void force_process_and_drop_events() override; virtual void release_rendering_thread() override; - virtual void make_rendering_thread() override; virtual void swap_buffers() override; virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override; virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override; + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) override; virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override; + virtual Rect2 status_indicator_get_rect(IndicatorID p_id) const override; virtual void delete_status_indicator(IndicatorID p_id) override; static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Error &r_error); diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index c52ed00b3d..0041848c78 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -83,8 +83,7 @@ DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mod Rect2i srect = screen_get_usable_rect(rq_screen); Point2i wpos = p_rect.position; if (srect != Rect2i()) { - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - p_rect.size / 3); } // macOS native y-coordinate relative to _get_screens_origin() is negative, // Godot passes a positive value. @@ -291,6 +290,10 @@ void DisplayServerMacOS::_update_displays_arrangement() { displays_arrangement_dirty = false; } +void DisplayServerMacOS::set_menu_delegate(NSMenu *p_menu) { + [p_menu setDelegate:menu_delegate]; +} + Point2i DisplayServerMacOS::_get_screens_origin() const { // Returns the native top-left screen coordinate of the smallest rectangle // that encompasses all screens. Needed in get_screen_position(), @@ -355,7 +358,6 @@ void DisplayServerMacOS::_dispatch_input_events(const Ref<InputEvent> &p_event) } void DisplayServerMacOS::_dispatch_input_event(const Ref<InputEvent> &p_event) { - _THREAD_SAFE_METHOD_ if (!in_dispatch_input_event) { in_dispatch_input_event = true; @@ -756,6 +758,8 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const { case FEATURE_CURSOR_SHAPE: case FEATURE_CUSTOM_CURSOR_SHAPE: case FEATURE_NATIVE_DIALOG: + case FEATURE_NATIVE_DIALOG_INPUT: + case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_IME: case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_HIDPI: @@ -934,7 +938,7 @@ Error DisplayServerMacOS::dialog_show(String p_title, String p_description, Vect button_pressed = int64_t(2 + (ret - NSAlertThirdButtonReturn)); } - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { Variant ret; Callable::CallError ce; const Variant *args[1] = { &button_pressed }; @@ -1014,7 +1018,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, String url; url.parse_utf8([[[panel URL] path] UTF8String]); files.push_back(url); - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = true; Variant v_files = files; @@ -1043,7 +1047,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, } } } else { - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); @@ -1130,7 +1134,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, url.parse_utf8([[[urls objectAtIndex:i] path] UTF8String]); files.push_back(url); } - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = true; Variant v_files = files; @@ -1159,7 +1163,7 @@ Error DisplayServerMacOS::_file_dialog_with_options_show(const String &p_title, } } } else { - if (!callback.is_null()) { + if (callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); @@ -1218,7 +1222,7 @@ Error DisplayServerMacOS::dialog_input_text(String p_title, String p_description String ret; ret.parse_utf8([[input stringValue] UTF8String]); - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { Variant v_result = ret; Variant ret; Callable::CallError ce; @@ -1877,8 +1881,7 @@ void DisplayServerMacOS::window_set_current_screen(int p_screen, WindowID p_wind Size2i wsize = window_get_size(p_window); wpos += srect.position; - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - wsize.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - wsize.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - wsize / 3); window_set_position(wpos, p_window); if (was_fullscreen) { @@ -1991,7 +1994,7 @@ void DisplayServerMacOS::window_set_position(const Point2i &p_position, WindowID ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd = windows[p_window]; - if (NSEqualRects([wd.window_object frame], [[wd.window_object screen] visibleFrame])) { + if (wd.fullscreen) { return; } @@ -2080,12 +2083,21 @@ Size2i DisplayServerMacOS::window_get_max_size(WindowID p_window) const { } void DisplayServerMacOS::update_presentation_mode() { + bool has_fs_windows = false; for (const KeyValue<WindowID, WindowData> &wd : windows) { - if (wd.value.fullscreen && wd.value.exclusive_fullscreen) { - return; + if (wd.value.fullscreen) { + if (wd.value.exclusive_fullscreen) { + return; + } else { + has_fs_windows = true; + } } } - [NSApp setPresentationOptions:NSApplicationPresentationDefault]; + if (has_fs_windows) { + [NSApp setPresentationOptions:NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock | NSApplicationPresentationFullScreen]; + } else { + [NSApp setPresentationOptions:NSApplicationPresentationDefault]; + } } void DisplayServerMacOS::window_set_min_size(const Size2i p_size, WindowID p_window) { @@ -2309,8 +2321,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs WindowData &wd = windows[p_window]; float scale = screen_get_max_scale(); wd.wb_offset = p_offset / scale; - wd.wb_offset.x = MAX(wd.wb_offset.x, 12); - wd.wb_offset.y = MAX(wd.wb_offset.y, 12); + wd.wb_offset = wd.wb_offset.maxi(12); if (wd.window_button_view) { [wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)]; } @@ -2974,7 +2985,7 @@ Key DisplayServerMacOS::keyboard_get_label_from_physical(Key p_keycode) const { } void DisplayServerMacOS::process_events() { - _THREAD_SAFE_METHOD_ + _THREAD_SAFE_LOCK_ while (true) { NSEvent *event = [NSApp @@ -3007,7 +3018,9 @@ void DisplayServerMacOS::process_events() { if (!drop_events) { _process_key_events(); + _THREAD_SAFE_UNLOCK_ Input::get_singleton()->flush_buffered_events(); + _THREAD_SAFE_LOCK_ } for (KeyValue<WindowID, WindowData> &E : windows) { @@ -3033,6 +3046,8 @@ void DisplayServerMacOS::process_events() { } } } + + _THREAD_SAFE_UNLOCK_ } void DisplayServerMacOS::force_process_and_drop_events() { @@ -3044,9 +3059,14 @@ void DisplayServerMacOS::force_process_and_drop_events() { } void DisplayServerMacOS::release_rendering_thread() { -} - -void DisplayServerMacOS::make_rendering_thread() { +#if defined(GLES3_ENABLED) + if (gl_manager_angle) { + gl_manager_angle->release_current(); + } + if (gl_manager_legacy) { + gl_manager_legacy->release_current(); + } +#endif } void DisplayServerMacOS::swap_buffers() { @@ -3131,10 +3151,11 @@ void DisplayServerMacOS::set_icon(const Ref<Image> &p_icon) { } } -DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { NSImage *nsimg = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon->duplicate(); + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3172,13 +3193,18 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref IndicatorData idat; - idat.item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; - idat.view = [[GodotStatusItemView alloc] init]; + NSStatusItem *item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; + idat.item = item; + idat.delegate = [[GodotStatusItemDelegate alloc] init]; + [idat.delegate setCallback:p_callback]; - [idat.view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; - [idat.view setImage:nsimg]; - [idat.view setCallback:p_callback]; - [idat.item setView:idat.view]; + item.button.image = nsimg; + item.button.imagePosition = NSImageOnly; + item.button.imageScaling = NSImageScaleProportionallyUpOrDown; + item.button.target = idat.delegate; + item.button.action = @selector(click:); + [item.button sendActionOn:(NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown)]; + item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()]; IndicatorID iid = indicator_id_counter++; indicators[iid] = idat; @@ -3186,12 +3212,13 @@ DisplayServer::IndicatorID DisplayServerMacOS::create_status_indicator(const Ref return iid; } -void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!indicators.has(p_id)); NSImage *nsimg = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon->duplicate(); + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); img->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] @@ -3227,19 +3254,57 @@ void DisplayServerMacOS::status_indicator_set_icon(IndicatorID p_id, const Ref<I } } - [indicators[p_id].view setImage:nsimg]; + NSStatusItem *item = indicators[p_id].item; + item.button.image = nsimg; } void DisplayServerMacOS::status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) { ERR_FAIL_COND(!indicators.has(p_id)); - [indicators[p_id].view setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; + NSStatusItem *item = indicators[p_id].item; + item.button.toolTip = [NSString stringWithUTF8String:p_tooltip.utf8().get_data()]; +} + +void DisplayServerMacOS::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + ERR_FAIL_COND(!indicators.has(p_id)); + + NSStatusItem *item = indicators[p_id].item; + if (p_menu_rid.is_valid() && native_menu->has_menu(p_menu_rid)) { + NSMenu *menu = native_menu->get_native_menu_handle(p_menu_rid); + item.menu = menu; + } else { + item.menu = nullptr; + } } void DisplayServerMacOS::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { ERR_FAIL_COND(!indicators.has(p_id)); - [indicators[p_id].view setCallback:p_callback]; + [indicators[p_id].delegate setCallback:p_callback]; +} + +Rect2 DisplayServerMacOS::status_indicator_get_rect(IndicatorID p_id) const { + ERR_FAIL_COND_V(!indicators.has(p_id), Rect2()); + + NSStatusItem *item = indicators[p_id].item; + NSView *v = item.button; + const NSRect contentRect = [v frame]; + const NSRect nsrect = [v.window convertRectToScreen:contentRect]; + Rect2 rect; + + // Return the position of the top-left corner, for macOS the y starts at the bottom. + const float scale = screen_get_max_scale(); + rect.size.x = nsrect.size.width; + rect.size.y = nsrect.size.height; + rect.size *= scale; + rect.position.x = nsrect.origin.x; + rect.position.y = (nsrect.origin.y + nsrect.size.height); + rect.position *= scale; + rect.position -= _get_screens_origin(); + // macOS native y-coordinate relative to _get_screens_origin() is negative, + // Godot expects a positive value. + rect.position.y *= -1; + return rect; } void DisplayServerMacOS::delete_status_indicator(IndicatorID p_id) { diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml index 506b0dffb8..7355042a48 100644 --- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml +++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml @@ -12,7 +12,7 @@ <members> <member name="application/additional_plist_content" type="String" setter="" getter=""> Additional data added to the root [code]<dict>[/code] section of the [url=https://developer.apple.com/documentation/bundleresources/information_property_list]Info.plist[/url] file. The value should be an XML section with pairs of key-value elements, e.g.: - [codeblock] + [codeblock lang=text] <key>key_name</key> <string>value</string> [/codeblock] diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 05ae4a74c9..5f52d33318 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -333,6 +333,12 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP return false; } } + + // Hide unsupported .NET embedding option. + if (p_option == "dotnet/embed_build_outputs") { + return false; + } + return true; } @@ -903,7 +909,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres return OK; } -Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn) { +Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) { int codesign_tool = p_preset->get("codesign/codesign"); switch (codesign_tool) { case 1: { // built-in ad-hoc @@ -947,6 +953,12 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre args.push_back("--code-signature-flags"); args.push_back("runtime"); + if (p_set_id) { + String app_id = p_preset->get("application/bundle_identifier"); + args.push_back("--binary-identifier"); + args.push_back(app_id); + } + args.push_back("-v"); /* provide some more feedback */ args.push_back(p_path); @@ -1006,6 +1018,12 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre args.push_back(p_preset->get("codesign/identity")); } + if (p_set_id) { + String app_id = p_preset->get("application/bundle_identifier"); + args.push_back("-i"); + args.push_back(app_id); + } + args.push_back("-v"); /* provide some more feedback */ args.push_back("-f"); @@ -1037,7 +1055,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre } Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, - const String &p_ent_path, bool p_should_error_on_non_code) { + const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code) { static Vector<String> extensions_to_sign; if (extensions_to_sign.is_empty()) { @@ -1064,7 +1082,8 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres } if (extensions_to_sign.find(current_file.get_extension()) > -1) { - Error code_sign_error{ _code_sign(p_preset, current_file_path, p_ent_path, false) }; + int ftype = MachO::get_filetype(current_file_path); + Error code_sign_error{ _code_sign(p_preset, current_file_path, (ftype == 2 || ftype == 5) ? p_helper_ent_path : p_ent_path, false, (ftype == 2 || ftype == 5)) }; if (code_sign_error != OK) { return code_sign_error; } @@ -1073,7 +1092,7 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres FileAccess::set_unix_permissions(current_file_path, 0755); } } else if (dir_access->current_is_dir()) { - Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_should_error_on_non_code) }; + Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code) }; if (code_sign_error != OK) { return code_sign_error; } @@ -1091,6 +1110,7 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, + const String &p_helper_ent_path, bool p_should_error_on_non_code_sign) { static Vector<String> extensions_to_sign; @@ -1180,10 +1200,11 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access if (err == OK && p_sign_enabled) { if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) { // If it is a directory, find and sign all dynamic libraries. - err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_should_error_on_non_code_sign); + err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign); } else { if (extensions_to_sign.find(p_in_app_path.get_extension()) > -1) { - err = _code_sign(p_preset, p_in_app_path, p_ent_path, false); + int ftype = MachO::get_filetype(p_in_app_path); + err = _code_sign(p_preset, p_in_app_path, (ftype == 2 || ftype == 5) ? p_helper_ent_path : p_ent_path, false, (ftype == 2 || ftype == 5)); } if (dir_access->file_exists(p_in_app_path) && is_executable(p_in_app_path)) { // chmod with 0755 if the file is executable. @@ -1197,13 +1218,13 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, - const String &p_ent_path) { + const String &p_ent_path, const String &p_helper_ent_path) { Error error{ OK }; const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() }; for (int i = 0; i < macos_plugins.size(); ++i) { String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) }; String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() }; - error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, false); + error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false); if (error != OK) { break; } @@ -1768,7 +1789,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String pack_path = tmp_app_path_name + "/Contents/Resources/" + pkg_name + ".pck"; Vector<SharedObject> shared_objects; - err = save_pack(true, p_preset, p_debug, pack_path, &shared_objects); + err = save_pack(p_preset, p_debug, pack_path, &shared_objects); bool lib_validation = p_preset->get("codesign/entitlements/disable_library_validation"); if (!shared_objects.is_empty() && sign_enabled && ad_hoc && !lib_validation) { @@ -1780,8 +1801,9 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries.")); } + bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled"); String ent_path = p_preset->get("codesign/entitlements/custom_file"); - String hlp_ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + "_helper.entitlements"); + String hlp_ent_path = sandbox ? EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + "_helper.entitlements") : ent_path; if (sign_enabled && (ent_path.is_empty())) { ent_path = EditorPaths::get_singleton()->get_cache_dir().path_join(pkg_name + ".entitlements"); @@ -1933,7 +1955,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p err = ERR_CANT_CREATE; } - if ((err == OK) && helpers.size() > 0) { + if ((err == OK) && sandbox && (helpers.size() > 0 || shared_objects.size() > 0)) { ent_f = FileAccess::open(hlp_ent_path, FileAccess::WRITE); if (ent_f.is_valid()) { ent_f->store_line("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); @@ -1959,7 +1981,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String hlp_path = helpers[i]; err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file()); if (err == OK && sign_enabled) { - err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false); + err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true); } FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); } @@ -1971,11 +1993,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); if (shared_objects[i].target.is_empty()) { String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(); - err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, true); + err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true); } else { String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target); tmp_app_dir->make_dir_recursive(path_in_app); - err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, false); + err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false); } if (err != OK) { break; @@ -1984,7 +2006,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() }; for (int i = 0; i < export_plugins.size(); ++i) { - err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path); + err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path); if (err != OK) { break; } @@ -2004,7 +2026,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (ep.step(TTR("Code signing bundle"), 2)) { return ERR_SKIP; } - err = _code_sign(p_preset, tmp_app_path_name, ent_path); + err = _code_sign(p_preset, tmp_app_path_name, ent_path, true, false); } String noto_path = p_path; @@ -2022,7 +2044,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (ep.step(TTR("Code signing DMG"), 3)) { return ERR_SKIP; } - err = _code_sign(p_preset, p_path, ent_path, false); + err = _code_sign(p_preset, p_path, ent_path, false, false); } } else if (export_format == "pkg") { // Create a Installer. diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index 0764b63e8c..2d615abede 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -89,14 +89,14 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { void _make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data); Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path); - Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true); - Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_should_error_on_non_code = true); + Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false); + Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true); Error _copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, - bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, + bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code_sign); Error _export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, - const String &p_ent_path); + const String &p_ent_path, const String &p_helper_ent_path); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); Error _create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name); Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); diff --git a/platform/macos/export/macho.cpp b/platform/macos/export/macho.cpp index c7556c1964..a829774a88 100644 --- a/platform/macos/export/macho.cpp +++ b/platform/macos/export/macho.cpp @@ -105,6 +105,26 @@ bool MachO::is_macho(const String &p_path) { return (magic == 0xcefaedfe || magic == 0xfeedface || magic == 0xcffaedfe || magic == 0xfeedfacf); } +uint32_t MachO::get_filetype(const String &p_path) { + Ref<FileAccess> fa = FileAccess::open(p_path, FileAccess::READ); + ERR_FAIL_COND_V_MSG(fa.is_null(), 0, vformat("MachO: Can't open file: \"%s\".", p_path)); + uint32_t magic = fa->get_32(); + MachHeader mach_header; + + // Read MachO header. + if (magic == 0xcefaedfe || magic == 0xfeedface) { + // Thin 32-bit binary. + fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader)); + } else if (magic == 0xcffaedfe || magic == 0xfeedfacf) { + // Thin 64-bit binary. + fa->get_buffer((uint8_t *)&mach_header, sizeof(MachHeader)); + fa->get_32(); // Skip extra reserved field. + } else { + ERR_FAIL_V_MSG(0, vformat("MachO: File is not a valid MachO binary: \"%s\".", p_path)); + } + return mach_header.filetype; +} + bool MachO::open_file(const String &p_path) { fa = FileAccess::open(p_path, FileAccess::READ_WRITE); ERR_FAIL_COND_V_MSG(fa.is_null(), false, vformat("MachO: Can't open file: \"%s\".", p_path)); diff --git a/platform/macos/export/macho.h b/platform/macos/export/macho.h index 37975f0820..a84de7de60 100644 --- a/platform/macos/export/macho.h +++ b/platform/macos/export/macho.h @@ -181,6 +181,7 @@ class MachO : public RefCounted { public: static bool is_macho(const String &p_path); + static uint32_t get_filetype(const String &p_path); bool open_file(const String &p_path); diff --git a/platform/macos/gl_manager_macos_legacy.h b/platform/macos/gl_manager_macos_legacy.h index bafe825efb..af9be8f5ba 100644 --- a/platform/macos/gl_manager_macos_legacy.h +++ b/platform/macos/gl_manager_macos_legacy.h @@ -73,7 +73,6 @@ public: void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height); void release_current(); - void make_current(); void swap_buffers(); void window_make_current(DisplayServer::WindowID p_window_id); diff --git a/platform/macos/gl_manager_macos_legacy.mm b/platform/macos/gl_manager_macos_legacy.mm index 701de6df78..6ce3831d9c 100644 --- a/platform/macos/gl_manager_macos_legacy.mm +++ b/platform/macos/gl_manager_macos_legacy.mm @@ -117,6 +117,7 @@ void GLManagerLegacy_MacOS::release_current() { } [NSOpenGLContext clearCurrentContext]; + current_window = DisplayServer::INVALID_WINDOW_ID; } void GLManagerLegacy_MacOS::window_make_current(DisplayServer::WindowID p_window_id) { @@ -133,18 +134,6 @@ void GLManagerLegacy_MacOS::window_make_current(DisplayServer::WindowID p_window current_window = p_window_id; } -void GLManagerLegacy_MacOS::make_current() { - if (current_window == DisplayServer::INVALID_WINDOW_ID) { - return; - } - if (!windows.has(current_window)) { - return; - } - - GLWindow &win = windows[current_window]; - [win.context makeCurrentContext]; -} - void GLManagerLegacy_MacOS::swap_buffers() { GLWindow &win = windows[current_window]; [win.context flushBuffer]; diff --git a/platform/macos/godot_content_view.mm b/platform/macos/godot_content_view.mm index 93bba84783..68a7288ad4 100644 --- a/platform/macos/godot_content_view.mm +++ b/platform/macos/godot_content_view.mm @@ -313,7 +313,7 @@ } DisplayServerMacOS::WindowData &wd = ds->get_window(window_id); - if (!wd.drop_files_callback.is_null()) { + if (wd.drop_files_callback.is_valid()) { Vector<String> files; NSPasteboard *pboard = [sender draggingPasteboard]; diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm index 3959fb686c..942c351ac0 100644 --- a/platform/macos/godot_main_macos.mm +++ b/platform/macos/godot_main_macos.mm @@ -69,18 +69,21 @@ int main(int argc, char **argv) { err = Main::setup(argv[0], argc - first_arg, &argv[first_arg]); } - if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; - } else if (err != OK) { - return 255; + if (err != OK) { + if (err == ERR_HELP) { // Returned by --help and --version, so success. + return EXIT_SUCCESS; + } + return EXIT_FAILURE; } - bool ok; + int ret; @autoreleasepool { - ok = Main::start(); + ret = Main::start(); } - if (ok) { - os.run(); // It is actually the OS that decides how to run. + if (ret == EXIT_SUCCESS) { + os.run(); + } else { + os.set_exit_code(EXIT_FAILURE); } @autoreleasepool { diff --git a/platform/macos/godot_open_save_delegate.mm b/platform/macos/godot_open_save_delegate.mm index 6b55b70629..6ffd939545 100644 --- a/platform/macos/godot_open_save_delegate.mm +++ b/platform/macos/godot_open_save_delegate.mm @@ -177,14 +177,14 @@ if ([new_allowed_types count] > 0) { NSMutableArray *type_filters = [new_allowed_types objectAtIndex:0]; if (type_filters && [type_filters count] == 1 && [[type_filters objectAtIndex:0] isEqualToString:@"*"]) { - [p_panel setAllowedFileTypes:@[]]; + [p_panel setAllowedFileTypes:nil]; [p_panel setAllowsOtherFileTypes:true]; } else { [p_panel setAllowsOtherFileTypes:false]; [p_panel setAllowedFileTypes:type_filters]; } } else { - [p_panel setAllowedFileTypes:@[]]; + [p_panel setAllowedFileTypes:nil]; [p_panel setAllowsOtherFileTypes:true]; } } @@ -248,7 +248,7 @@ if (allowed_types && index < [allowed_types count]) { NSMutableArray *type_filters = [allowed_types objectAtIndex:index]; if (type_filters && [type_filters count] == 1 && [[type_filters objectAtIndex:0] isEqualToString:@"*"]) { - [dialog setAllowedFileTypes:@[]]; + [dialog setAllowedFileTypes:nil]; [dialog setAllowsOtherFileTypes:true]; } else { [dialog setAllowsOtherFileTypes:false]; @@ -256,7 +256,7 @@ } cur_index = index; } else { - [dialog setAllowedFileTypes:@[]]; + [dialog setAllowedFileTypes:nil]; [dialog setAllowsOtherFileTypes:true]; cur_index = -1; } diff --git a/platform/macos/godot_status_item.h b/platform/macos/godot_status_item.h index 1827baa9bd..5bc790956e 100644 --- a/platform/macos/godot_status_item.h +++ b/platform/macos/godot_status_item.h @@ -37,13 +37,12 @@ #import <AppKit/AppKit.h> #import <Foundation/Foundation.h> -@interface GodotStatusItemView : NSView { - NSImage *image; +@interface GodotStatusItemDelegate : NSObject { Callable cb; } -- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index; -- (void)setImage:(NSImage *)image; +- (IBAction)click:(id)sender; + - (void)setCallback:(const Callable &)callback; @end diff --git a/platform/macos/godot_status_item.mm b/platform/macos/godot_status_item.mm index 71ed0a0f71..0990a16b2b 100644 --- a/platform/macos/godot_status_item.mm +++ b/platform/macos/godot_status_item.mm @@ -32,30 +32,32 @@ #include "display_server_macos.h" -@implementation GodotStatusItemView +@implementation GodotStatusItemDelegate - (id)init { self = [super init]; - image = nullptr; return self; } -- (void)setImage:(NSImage *)newImage { - image = newImage; - [self setNeedsDisplayInRect:self.frame]; -} - -- (void)setCallback:(const Callable &)callback { - cb = callback; -} - -- (void)drawRect:(NSRect)rect { - if (image) { - [image drawInRect:rect]; +- (IBAction)click:(id)sender { + NSEvent *current_event = [NSApp currentEvent]; + MouseButton index = MouseButton::LEFT; + if (current_event) { + if (current_event.type == NSEventTypeLeftMouseDown) { + index = MouseButton::LEFT; + } else if (current_event.type == NSEventTypeRightMouseDown) { + index = MouseButton::RIGHT; + } else if (current_event.type == NSEventTypeOtherMouseDown) { + if ((int)[current_event buttonNumber] == 2) { + index = MouseButton::MIDDLE; + } else if ((int)[current_event buttonNumber] == 3) { + index = MouseButton::MB_XBUTTON1; + } else if ((int)[current_event buttonNumber] == 4) { + index = MouseButton::MB_XBUTTON2; + } + } } -} -- (void)processMouseEvent:(NSEvent *)event index:(MouseButton)index { DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (!ds) { return; @@ -71,31 +73,8 @@ } } -- (void)mouseDown:(NSEvent *)event { - [super mouseDown:event]; - if (([event modifierFlags] & NSEventModifierFlagControl)) { - [self processMouseEvent:event index:MouseButton::RIGHT]; - } else { - [self processMouseEvent:event index:MouseButton::LEFT]; - } -} - -- (void)rightMouseDown:(NSEvent *)event { - [super rightMouseDown:event]; - - [self processMouseEvent:event index:MouseButton::RIGHT]; -} - -- (void)otherMouseDown:(NSEvent *)event { - [super otherMouseDown:event]; - - if ((int)[event buttonNumber] == 2) { - [self processMouseEvent:event index:MouseButton::MIDDLE]; - } else if ((int)[event buttonNumber] == 3) { - [self processMouseEvent:event index:MouseButton::MB_XBUTTON1]; - } else if ((int)[event buttonNumber] == 4) { - [self processMouseEvent:event index:MouseButton::MB_XBUTTON2]; - } +- (void)setCallback:(const Callable &)callback { + cb = callback; } @end diff --git a/platform/macos/godot_window_delegate.mm b/platform/macos/godot_window_delegate.mm index 2d83b46007..7749debfd6 100644 --- a/platform/macos/godot_window_delegate.mm +++ b/platform/macos/godot_window_delegate.mm @@ -268,7 +268,7 @@ ds->window_resize(window_id, wd.size.width, wd.size.height); - if (!wd.rect_changed_callback.is_null()) { + if (wd.rect_changed_callback.is_valid()) { wd.rect_changed_callback.call(Rect2i(ds->window_get_position(window_id), ds->window_get_size(window_id))); } } @@ -291,7 +291,7 @@ DisplayServerMacOS::WindowData &wd = ds->get_window(window_id); ds->release_pressed_events(); - if (!wd.rect_changed_callback.is_null()) { + if (wd.rect_changed_callback.is_valid()) { wd.rect_changed_callback.call(Rect2i(ds->window_get_position(window_id), ds->window_get_size(window_id))); } } diff --git a/platform/macos/native_menu_macos.h b/platform/macos/native_menu_macos.h index c00a510fd5..b5dbb8b9b0 100644 --- a/platform/macos/native_menu_macos.h +++ b/platform/macos/native_menu_macos.h @@ -33,7 +33,7 @@ #include "core/templates/hash_map.h" #include "core/templates/rid_owner.h" -#include "servers/native_menu.h" +#include "servers/display/native_menu.h" #import <AppKit/AppKit.h> #import <ApplicationServices/ApplicationServices.h> @@ -85,6 +85,8 @@ public: virtual bool has_menu(const RID &p_rid) const override; virtual void free_menu(const RID &p_rid) override; + NSMenu *get_native_menu_handle(const RID &p_rid); + virtual Size2 get_size(const RID &p_rid) const override; virtual void popup(const RID &p_rid, const Vector2i &p_position) override; diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm index 250b64dc04..1cf13a2d69 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -223,6 +223,11 @@ RID NativeMenuMacOS::get_system_menu(SystemMenus p_menu_id) const { RID NativeMenuMacOS::create_menu() { MenuData *md = memnew(MenuData); md->menu = [[NSMenu alloc] initWithTitle:@""]; + [md->menu setAutoenablesItems:NO]; + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds) { + ds->set_menu_delegate(md->menu); + } RID rid = menus.make_rid(md); menu_lookup[md->menu] = rid; return rid; @@ -243,6 +248,13 @@ void NativeMenuMacOS::free_menu(const RID &p_rid) { } } +NSMenu *NativeMenuMacOS::get_native_menu_handle(const RID &p_rid) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, nullptr); + + return md->menu; +} + Size2 NativeMenuMacOS::get_size(const RID &p_rid) const { const MenuData *md = menus.get_or_null(p_rid); ERR_FAIL_NULL_V(md, Size2()); diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index 8088c3431c..912a682a6b 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -85,7 +85,7 @@ public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; - virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; + virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override; virtual MainLoop *get_main_loop() const override; diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index d9ad8f937a..9f0bea5951 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -217,7 +217,7 @@ _FORCE_INLINE_ String OS_MacOS::get_framework_executable(const String &p_path) { return p_path; } -Error OS_MacOS::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { +Error OS_MacOS::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) { String path = get_framework_executable(p_path); if (!FileAccess::exists(path)) { @@ -235,8 +235,8 @@ Error OS_MacOS::open_dynamic_library(const String &p_path, void *&p_library_hand p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror())); - if (r_resolved_path != nullptr) { - *r_resolved_path = path; + if (p_data != nullptr && p_data->r_resolved_path != nullptr) { + *p_data->r_resolved_path = path; } return OK; diff --git a/platform/web/SCsub b/platform/web/SCsub index 3e0cc9ac4a..bc5893ab3a 100644 --- a/platform/web/SCsub +++ b/platform/web/SCsub @@ -1,5 +1,7 @@ #!/usr/bin/env python +from methods import print_error + Import("env") # The HTTP server "targets". Run with "scons p=web serve", or "scons p=web run" @@ -11,7 +13,7 @@ if "serve" in COMMAND_LINE_TARGETS or "run" in COMMAND_LINE_TARGETS: try: port = int(port) except Exception: - print("GODOT_WEB_TEST_PORT must be a valid integer") + print_error("GODOT_WEB_TEST_PORT must be a valid integer") sys.exit(255) serve(env.Dir(env.GetTemplateZipPath()).abspath, port, "run" in COMMAND_LINE_TARGETS) sys.exit(0) diff --git a/platform/web/api/web_tools_editor_plugin.h b/platform/web/api/web_tools_editor_plugin.h index ac0d5e20ec..2902f60f24 100644 --- a/platform/web/api/web_tools_editor_plugin.h +++ b/platform/web/api/web_tools_editor_plugin.h @@ -34,7 +34,7 @@ #if defined(TOOLS_ENABLED) && defined(WEB_ENABLED) #include "core/io/zip_io.h" -#include "editor/editor_plugin.h" +#include "editor/plugins/editor_plugin.h" class WebToolsEditorPlugin : public EditorPlugin { GDCLASS(WebToolsEditorPlugin, EditorPlugin); diff --git a/platform/web/detect.py b/platform/web/detect.py index e692c79a20..ccd884b225 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -10,7 +10,7 @@ from emscripten_helpers import ( create_template_zip, get_template_zip_path, ) -from methods import get_compiler_version +from methods import print_warning, print_error, get_compiler_version from SCons.Util import WhereIs from typing import TYPE_CHECKING @@ -85,16 +85,16 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["wasm32"] if env["arch"] not in supported_arches: - print( - 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' + print_error( + 'Unsupported CPU architecture "%s" for Web. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) try: env["initial_memory"] = int(env["initial_memory"]) except Exception: - print("Initial memory must be a valid integer") + print_error("Initial memory must be a valid integer") sys.exit(255) ## Build type @@ -109,7 +109,7 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-s", "ASSERTIONS=1"]) if env.editor_build and env["initial_memory"] < 64: - print('Note: Forcing "initial_memory=64" as it is required for the web editor.') + print("Note: Forcing `initial_memory=64` as it is required for the web editor.") env["initial_memory"] = 64 env.Append(LINKFLAGS=["-s", "INITIAL_MEMORY=%sMB" % env["initial_memory"]]) @@ -227,7 +227,7 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"]) env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"]) elif env["proxy_to_pthread"]: - print('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.') + print_warning('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.') env["proxy_to_pthread"] = False if env["lto"] != "none": @@ -240,11 +240,11 @@ def configure(env: "SConsEnvironment"): if env["dlink_enabled"]: if env["proxy_to_pthread"]: - print("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.") + print_warning("GDExtension support requires proxy_to_pthread=no, disabling proxy to pthread.") env["proxy_to_pthread"] = False if cc_semver < (3, 1, 14): - print("GDExtension support requires emscripten >= 3.1.14, detected: %s.%s.%s" % cc_semver) + print_error("GDExtension support requires emscripten >= 3.1.14, detected: %s.%s.%s" % cc_semver) sys.exit(255) env.Append(CCFLAGS=["-s", "SIDE_MODULE=2"]) diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index 281f312000..a51c161b9c 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -58,7 +58,7 @@ DisplayServerWeb *DisplayServerWeb::get_singleton() { // Window (canvas) bool DisplayServerWeb::check_size_force_redraw() { bool size_changed = godot_js_display_size_update() != 0; - if (size_changed && !rect_changed_callback.is_null()) { + if (size_changed && rect_changed_callback.is_valid()) { Size2i window_size = window_get_size(); Variant size = Rect2i(Point2i(), window_size); // TODO use window_get_position if implemented. rect_changed_callback.call(size); @@ -109,7 +109,7 @@ void DisplayServerWeb::_drop_files_js_callback(const Vector<String> &p_files) { if (!ds) { ERR_FAIL_MSG("Unable to drop files because the DisplayServer is not active"); } - if (ds->drop_files_callback.is_null()) { + if (!ds->drop_files_callback.is_valid()) { return; } ds->drop_files_callback.call(p_files); @@ -129,7 +129,7 @@ void DisplayServerWeb::request_quit_callback() { void DisplayServerWeb::_request_quit_callback() { DisplayServerWeb *ds = get_singleton(); - if (ds && !ds->window_event_callback.is_null()) { + if (ds && ds->window_event_callback.is_valid()) { Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); ds->window_event_callback.call(event); } @@ -722,7 +722,7 @@ void DisplayServerWeb::vk_input_text_callback(const char *p_text, int p_cursor) void DisplayServerWeb::_vk_input_text_callback(const String &p_text, int p_cursor) { DisplayServerWeb *ds = DisplayServerWeb::get_singleton(); - if (!ds || ds->input_text_callback.is_null()) { + if (!ds || !ds->input_text_callback.is_valid()) { return; } // Call input_text @@ -972,7 +972,7 @@ void DisplayServerWeb::_send_window_event_callback(int p_notification) { if (godot_js_is_ime_focused() && (p_notification == DisplayServer::WINDOW_EVENT_FOCUS_IN || p_notification == DisplayServer::WINDOW_EVENT_FOCUS_OUT)) { return; } - if (!ds->window_event_callback.is_null()) { + if (ds->window_event_callback.is_valid()) { Variant event = int(p_notification); ds->window_event_callback.call(event); } @@ -1128,6 +1128,8 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const { return true; //case FEATURE_MOUSE_WARP: //case FEATURE_NATIVE_DIALOG: + //case FEATURE_NATIVE_DIALOG_INPUT: + //case FEATURE_NATIVE_DIALOG_FILE: //case FEATURE_NATIVE_ICON: //case FEATURE_WINDOW_TRANSPARENCY: //case FEATURE_KEEP_SCREEN_ON: diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index ec7f599507..d42303ad25 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -170,6 +170,7 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name"); replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include; replaces["$GODOT_CONFIG"] = str_config; + replaces["$GODOT_SPLASH"] = p_name + ".png"; if (p_preset->get("variant/thread_support")) { replaces["$GODOT_THREADS_ENABLED"] = "true"; @@ -479,7 +480,7 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p // Export pck and shared objects Vector<SharedObject> shared_objects; String pck_path = base_path + ".pck"; - Error error = save_pack(true, p_preset, p_debug, pck_path, &shared_objects); + Error error = save_pack(p_preset, p_debug, pck_path, &shared_objects); if (error != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Could not write file: \"%s\"."), pck_path)); return error; @@ -584,32 +585,176 @@ bool EditorExportPlatformWeb::poll_export() { } } - int prev = menu_options; - menu_options = preset.is_valid(); + HTTPServerState prev_server_state = server_state; + server_state = HTTP_SERVER_STATE_OFF; if (server->is_listening()) { - if (menu_options == 0) { + if (preset.is_null()) { server->stop(); } else { - menu_options += 1; + server_state = HTTP_SERVER_STATE_ON; } } - return menu_options != prev; + + return server_state != prev_server_state; } Ref<ImageTexture> EditorExportPlatformWeb::get_option_icon(int p_index) const { - return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index); + Ref<ImageTexture> play_icon = EditorExportPlatform::get_option_icon(p_index); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_index) { + case 0: + case 1: + return play_icon; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_index) { + case 0: + return play_icon; + case 1: + return restart_icon; + case 2: + return stop_icon; + } + } break; + } + + ERR_FAIL_V_MSG(nullptr, vformat(R"(EditorExportPlatformWeb option icon index "%s" is invalid.)", p_index)); } int EditorExportPlatformWeb::get_options_count() const { - return menu_options; + if (server_state == HTTP_SERVER_STATE_ON) { + return 3; + } + return 2; +} + +String EditorExportPlatformWeb::get_option_label(int p_index) const { + String run_in_browser = TTR("Run in Browser"); + String start_http_server = TTR("Start HTTP Server"); + String reexport_project = TTR("Re-export Project"); + String stop_http_server = TTR("Stop HTTP Server"); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return start_http_server; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return reexport_project; + case 2: + return stop_http_server; + } + } break; + } + + ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option label index "%s" is invalid.)", p_index)); +} + +String EditorExportPlatformWeb::get_option_tooltip(int p_index) const { + String run_in_browser = TTR("Run exported HTML in the system's default browser."); + String start_http_server = TTR("Start the HTTP server."); + String reexport_project = TTR("Export project again to account for updates."); + String stop_http_server = TTR("Stop the HTTP server."); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return start_http_server; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_index) { + case 0: + return run_in_browser; + case 1: + return reexport_project; + case 2: + return stop_http_server; + } + } break; + } + + ERR_FAIL_V_MSG("", vformat(R"(EditorExportPlatformWeb option tooltip index "%s" is invalid.)", p_index)); } Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { - if (p_option == 1) { - server->stop(); - return OK; + const uint16_t bind_port = EDITOR_GET("export/web/http_port"); + // Resolve host if needed. + const String bind_host = EDITOR_GET("export/web/http_host"); + const bool use_tls = EDITOR_GET("export/web/use_tls"); + + switch (server_state) { + case HTTP_SERVER_STATE_OFF: { + switch (p_option) { + // Run in Browser. + case 0: { + Error err = _export_project(p_preset, p_debug_flags); + if (err != OK) { + return err; + } + err = _start_server(bind_host, bind_port, use_tls); + if (err != OK) { + return err; + } + return _launch_browser(bind_host, bind_port, use_tls); + } break; + + // Start HTTP Server. + case 1: { + Error err = _export_project(p_preset, p_debug_flags); + if (err != OK) { + return err; + } + return _start_server(bind_host, bind_port, use_tls); + } break; + } + } break; + + case HTTP_SERVER_STATE_ON: { + switch (p_option) { + // Run in Browser. + case 0: { + Error err = _export_project(p_preset, p_debug_flags); + if (err != OK) { + return err; + } + return _launch_browser(bind_host, bind_port, use_tls); + } break; + + // Re-export Project. + case 1: { + return _export_project(p_preset, p_debug_flags); + } break; + + // Stop HTTP Server. + case 2: { + return _stop_server(); + } break; + } + } break; } + ERR_FAIL_V_MSG(ERR_INVALID_PARAMETER, vformat(R"(Trying to run EditorExportPlatformWeb, but option "%s" isn't known.)", p_option)); +} + +Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) { const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("web"); Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); if (!da->dir_exists(dest)) { @@ -636,35 +781,40 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int DirAccess::remove_file_or_error(basepath + ".wasm"); DirAccess::remove_file_or_error(basepath + ".icon.png"); DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png"); - return err; } + return err; +} - const uint16_t bind_port = EDITOR_GET("export/web/http_port"); - // Resolve host if needed. - const String bind_host = EDITOR_GET("export/web/http_host"); +Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) { + OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_bind_port) + "/tmp_js_export.html")); + // FIXME: Find out how to clean up export files after running the successfully + // exported game. Might not be trivial. + return OK; +} + +Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) { IPAddress bind_ip; - if (bind_host.is_valid_ip_address()) { - bind_ip = bind_host; + if (p_bind_host.is_valid_ip_address()) { + bind_ip = p_bind_host; } else { - bind_ip = IP::get_singleton()->resolve_hostname(bind_host); + bind_ip = IP::get_singleton()->resolve_hostname(p_bind_host); } - ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'."); + ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + p_bind_host + "'. Try using '127.0.0.1'."); - const bool use_tls = EDITOR_GET("export/web/use_tls"); const String tls_key = EDITOR_GET("export/web/tls_key"); const String tls_cert = EDITOR_GET("export/web/tls_certificate"); // Restart server. server->stop(); - err = server->listen(bind_port, bind_ip, use_tls, tls_key, tls_cert); + Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert); if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err)); - return err; } + return err; +} - OS::get_singleton()->shell_open(String((use_tls ? "https://" : "http://") + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html")); - // FIXME: Find out how to clean up export files after running the successfully - // exported game. Might not be trivial. +Error EditorExportPlatformWeb::_stop_server() { + server->stop(); return OK; } @@ -690,8 +840,10 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() { Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme(); if (theme.is_valid()) { stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons)); + restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons)); } else { stop_icon.instantiate(); + restart_icon.instantiate(); } } } diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h index 952d03cdb4..9d3a1a7861 100644 --- a/platform/web/export/export_plugin.h +++ b/platform/web/export/export_plugin.h @@ -46,10 +46,16 @@ class EditorExportPlatformWeb : public EditorExportPlatform { GDCLASS(EditorExportPlatformWeb, EditorExportPlatform); + enum HTTPServerState { + HTTP_SERVER_STATE_OFF, + HTTP_SERVER_STATE_ON, + }; + Ref<ImageTexture> logo; Ref<ImageTexture> run_icon; Ref<ImageTexture> stop_icon; - int menu_options = 0; + Ref<ImageTexture> restart_icon; + HTTPServerState server_state = HTTP_SERVER_STATE_OFF; Ref<EditorHTTPServer> server; @@ -96,6 +102,11 @@ class EditorExportPlatformWeb : public EditorExportPlatform { Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects); Error _write_or_error(const uint8_t *p_content, int p_len, String p_path); + Error _export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags); + Error _launch_browser(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls); + Error _start_server(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls); + Error _stop_server(); + public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override; @@ -112,8 +123,8 @@ public: virtual bool poll_export() override; virtual int get_options_count() const override; - virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); } - virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); } + virtual String get_option_label(int p_index) const override; + virtual String get_option_tooltip(int p_index) const override; virtual Ref<ImageTexture> get_option_icon(int p_index) const override; virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override; virtual Ref<Texture2D> get_run_icon() const override; diff --git a/platform/web/javascript_bridge_singleton.cpp b/platform/web/javascript_bridge_singleton.cpp index d72ad8331b..a2c83d2f2b 100644 --- a/platform/web/javascript_bridge_singleton.cpp +++ b/platform/web/javascript_bridge_singleton.cpp @@ -248,7 +248,7 @@ Variant JavaScriptObjectImpl::callp(const StringName &p_method, const Variant ** void JavaScriptObjectImpl::callback(void *p_ref, int p_args_id, int p_argc) { const JavaScriptObjectImpl *obj = (JavaScriptObjectImpl *)p_ref; - ERR_FAIL_COND_MSG(obj->_callable.is_null(), "JavaScript callback failed."); + ERR_FAIL_COND_MSG(!obj->_callable.is_valid(), "JavaScript callback failed."); Vector<const Variant *> argp; Array arg_arr; diff --git a/platform/web/js/engine/features.js b/platform/web/js/engine/features.js index 81bc82f3c6..263ea6ac88 100644 --- a/platform/web/js/engine/features.js +++ b/platform/web/js/engine/features.js @@ -72,8 +72,7 @@ const Features = { // eslint-disable-line no-unused-vars * * @returns {Array<string>} A list of human-readable missing features. * @function Engine.getMissingFeatures - * @typedef {{ threads: boolean }} SupportedFeatures - * @param {SupportedFeatures} supportedFeatures + * @param {{threads: (boolean|undefined)}} supportedFeatures */ getMissingFeatures: function (supportedFeatures = {}) { const { diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp index 45671ca491..6b6c9ddd63 100644 --- a/platform/web/os_web.cpp +++ b/platform/web/os_web.cpp @@ -105,6 +105,10 @@ Error OS_Web::execute(const String &p_path, const List<String> &p_arguments, Str return create_process(p_path, p_arguments); } +Dictionary OS_Web::execute_with_pipe(const String &p_path, const List<String> &p_arguments) { + ERR_FAIL_V_MSG(Dictionary(), "OS::execute_with_pipe is not available on the Web platform."); +} + Error OS_Web::create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id, bool p_open_console) { Array args; for (const String &E : p_arguments) { @@ -128,6 +132,10 @@ bool OS_Web::is_process_running(const ProcessID &p_pid) const { return false; } +int OS_Web::get_process_exit_code(const ProcessID &p_pid) const { + return -1; +} + int OS_Web::get_processor_count() const { return godot_js_os_hw_concurrency_get(); } @@ -166,7 +174,7 @@ void OS_Web::add_frame_delay(bool p_can_draw) { #endif } -void OS_Web::vibrate_handheld(int p_duration_ms) { +void OS_Web::vibrate_handheld(int p_duration_ms, float p_amplitude) { godot_js_input_vibrate_handheld(p_duration_ms); } @@ -239,13 +247,13 @@ bool OS_Web::is_userfs_persistent() const { return idb_available; } -Error OS_Web::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { +Error OS_Web::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) { String path = p_path.get_file(); p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW); ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror())); - if (r_resolved_path != nullptr) { - *r_resolved_path = path; + if (p_data != nullptr && p_data->r_resolved_path != nullptr) { + *p_data->r_resolved_path = path; } return OK; diff --git a/platform/web/os_web.h b/platform/web/os_web.h index e578c93925..55a5fcc6c6 100644 --- a/platform/web/os_web.h +++ b/platform/web/os_web.h @@ -80,10 +80,12 @@ public: bool main_loop_iterate(); Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override; + Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) override; Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; Error kill(const ProcessID &p_pid) override; int get_process_id() const override; bool is_process_running(const ProcessID &p_pid) const override; + int get_process_exit_code(const ProcessID &p_pid) const override; int get_processor_count() const override; String get_unique_id() const override; int get_default_thread_pool_size() const override { return 1; } @@ -96,7 +98,7 @@ public: // Implemented in web_main.cpp loop callback instead. void add_frame_delay(bool p_can_draw) override; - void vibrate_handheld(int p_duration_ms) override; + void vibrate_handheld(int p_duration_ms, float p_amplitude) override; String get_cache_path() const override; String get_config_path() const override; @@ -107,7 +109,7 @@ public: void alert(const String &p_alert, const String &p_title = "ALERT!") override; - Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; + Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override; void resume_audio(); diff --git a/platform/web/serve.py b/platform/web/serve.py index 6a3efcc463..89dff63ca3 100755 --- a/platform/web/serve.py +++ b/platform/web/serve.py @@ -5,9 +5,20 @@ from pathlib import Path import os import sys import argparse +import contextlib +import socket import subprocess +# See cpython GH-17851 and GH-17864. +class DualStackServer(HTTPServer): + def server_bind(self): + # Suppress exception when protocol is IPv4. + with contextlib.suppress(Exception): + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + return super().server_bind() + + class CORSRequestHandler(SimpleHTTPRequestHandler): def end_headers(self): self.send_header("Cross-Origin-Opener-Policy", "same-origin") @@ -32,7 +43,7 @@ def serve(root, port, run_browser): print("Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this).") shell_open(f"http://127.0.0.1:{port}") - test(CORSRequestHandler, HTTPServer, port=port) + test(CORSRequestHandler, DualStackServer, port=port) if __name__ == "__main__": diff --git a/platform/web/web_main.cpp b/platform/web/web_main.cpp index ad2a801881..04513f6d57 100644 --- a/platform/web/web_main.cpp +++ b/platform/web/web_main.cpp @@ -109,24 +109,22 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { // Proper shutdown in case of setup failure. if (err != OK) { - int exit_code = (int)err; - if (err == ERR_HELP) { - exit_code = 0; // Called with --help. - } - os->set_exit_code(exit_code); // Will only exit after sync. emscripten_set_main_loop(exit_callback, -1, false); godot_js_os_finish_async(cleanup_after_sync); - return exit_code; + if (err == ERR_HELP) { // Returned by --help and --version, so success. + return EXIT_SUCCESS; + } + return EXIT_FAILURE; } - os->set_exit_code(0); main_started = true; // Ease up compatibility. ResourceLoader::set_abort_on_missing_resources(false); - Main::start(); + int ret = Main::start(); + os->set_exit_code(ret); os->get_main_loop()->initialize(); #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_project_manager_hint() && FileAccess::exists("/tmp/preload.zip")) { @@ -140,5 +138,5 @@ extern EMSCRIPTEN_KEEPALIVE int godot_web_main(int argc, char *argv[]) { // We are inside an animation frame, we want to immediately draw on the newly setup canvas. main_loop_callback(); - return 0; + return os->get_exit_code(); } diff --git a/platform/windows/SCsub b/platform/windows/SCsub index cf6416b8da..435c501956 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -10,13 +10,13 @@ sources = [] common_win = [ "godot_windows.cpp", - "crash_handler_windows.cpp", "os_windows.cpp", "display_server_windows.cpp", "key_mapping_windows.cpp", "joypad_windows.cpp", "tts_windows.cpp", "windows_terminal_logger.cpp", + "windows_utils.cpp", "native_menu_windows.cpp", "gl_manager_windows_native.cpp", "gl_manager_windows_angle.cpp", @@ -24,6 +24,11 @@ common_win = [ "rendering_context_driver_vulkan_windows.cpp", ] +if env.msvc: + common_win += ["crash_handler_windows_seh.cpp"] +else: + common_win += ["crash_handler_windows_signal.cpp"] + common_win_wrap = [ "console_wrapper_windows.cpp", ] diff --git a/platform/windows/console_wrapper_windows.cpp b/platform/windows/console_wrapper_windows.cpp index de751580b7..133711a9ea 100644 --- a/platform/windows/console_wrapper_windows.cpp +++ b/platform/windows/console_wrapper_windows.cpp @@ -136,6 +136,10 @@ int main(int argc, char *argv[]) { STARTUPINFOW si; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); WCHAR new_command_line[32767]; _snwprintf_s(new_command_line, 32767, _TRUNCATE, L"%ls %ls", exe_name, PathGetArgsW(GetCommandLineW())); diff --git a/platform/windows/crash_handler_windows.h b/platform/windows/crash_handler_windows.h index 3871210977..a0a0b610d0 100644 --- a/platform/windows/crash_handler_windows.h +++ b/platform/windows/crash_handler_windows.h @@ -35,12 +35,15 @@ #include <windows.h> // Crash handler exception only enabled with MSVC -#if defined(DEBUG_ENABLED) && defined(_MSC_VER) +#if defined(DEBUG_ENABLED) #define CRASH_HANDLER_EXCEPTION 1 +#ifdef _MSC_VER extern DWORD CrashHandlerException(EXCEPTION_POINTERS *ep); #endif +#endif + class CrashHandler { bool disabled; diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows_seh.cpp index 133d36aa0d..2abe285d31 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows_seh.cpp @@ -1,5 +1,5 @@ /**************************************************************************/ -/* crash_handler_windows.cpp */ +/* crash_handler_windows_seh.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ diff --git a/platform/windows/crash_handler_windows_signal.cpp b/platform/windows/crash_handler_windows_signal.cpp new file mode 100644 index 0000000000..e11a60bdc7 --- /dev/null +++ b/platform/windows/crash_handler_windows_signal.cpp @@ -0,0 +1,205 @@ +/**************************************************************************/ +/* crash_handler_windows_signal.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "crash_handler_windows.h" + +#include "core/config/project_settings.h" +#include "core/os/os.h" +#include "core/string/print_string.h" +#include "core/version.h" +#include "main/main.h" + +#ifdef CRASH_HANDLER_EXCEPTION + +#include <cxxabi.h> +#include <signal.h> +#include <algorithm> +#include <iterator> +#include <string> +#include <vector> + +#include <psapi.h> + +#include "thirdparty/libbacktrace/backtrace.h" + +struct CrashHandlerData { + int64_t index = 0; + backtrace_state *state = nullptr; + int64_t offset = 0; +}; + +int symbol_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) { + CrashHandlerData *ch_data = reinterpret_cast<CrashHandlerData *>(data); + if (!function) { + return 0; + } + + char fname[1024]; + snprintf(fname, 1024, "%s", function); + + if (function[0] == '_') { + int status; + char *demangled = abi::__cxa_demangle(function, nullptr, nullptr, &status); + + if (status == 0 && demangled) { + snprintf(fname, 1024, "%s", demangled); + } + + if (demangled) { + free(demangled); + } + } + + print_error(vformat("[%d] %s (%s:%d)", ch_data->index++, String::utf8(fname), String::utf8(filename), lineno)); + return 0; +} + +void error_callback(void *data, const char *msg, int errnum) { + CrashHandlerData *ch_data = reinterpret_cast<CrashHandlerData *>(data); + if (ch_data->index == 0) { + print_error(vformat("Error(%d): %s", errnum, String::utf8(msg))); + } else { + print_error(vformat("[%d] error(%d): %s", ch_data->index++, errnum, String::utf8(msg))); + } +} + +int trace_callback(void *data, uintptr_t pc) { + CrashHandlerData *ch_data = reinterpret_cast<CrashHandlerData *>(data); + backtrace_pcinfo(ch_data->state, pc - ch_data->offset, &symbol_callback, &error_callback, data); + return 0; +} + +int64_t get_image_base(const String &p_path) { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + if (f.is_null()) { + return 0; + } + { + f->seek(0x3c); + uint32_t pe_pos = f->get_32(); + + f->seek(pe_pos); + uint32_t magic = f->get_32(); + if (magic != 0x00004550) { + return 0; + } + } + int64_t opt_header_pos = f->get_position() + 0x14; + f->seek(opt_header_pos); + + uint16_t opt_header_magic = f->get_16(); + if (opt_header_magic == 0x10B) { + f->seek(opt_header_pos + 0x1C); + return f->get_32(); + } else if (opt_header_magic == 0x20B) { + f->seek(opt_header_pos + 0x18); + return f->get_64(); + } else { + return 0; + } +} + +extern void CrashHandlerException(int signal) { + CrashHandlerData data; + + if (OS::get_singleton() == nullptr || OS::get_singleton()->is_disable_crash_handler() || IsDebuggerPresent()) { + return; + } + + String msg; + const ProjectSettings *proj_settings = ProjectSettings::get_singleton(); + if (proj_settings) { + msg = proj_settings->get("debug/settings/crash_handler/message"); + } + + // Tell MainLoop about the crash. This can be handled by users too in Node. + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); + } + + print_error("\n================================================================"); + print_error(vformat("%s: Program crashed with signal %d", __FUNCTION__, signal)); + + // Print the engine version just before, so that people are reminded to include the version in backtrace reports. + if (String(VERSION_HASH).is_empty()) { + print_error(vformat("Engine version: %s", VERSION_FULL_NAME)); + } else { + print_error(vformat("Engine version: %s (%s)", VERSION_FULL_NAME, VERSION_HASH)); + } + print_error(vformat("Dumping the backtrace. %s", msg)); + + String _execpath = OS::get_singleton()->get_executable_path(); + + // Load process and image info to determine ASLR addresses offset. + MODULEINFO mi; + GetModuleInformation(GetCurrentProcess(), GetModuleHandle(NULL), &mi, sizeof(mi)); + int64_t image_mem_base = reinterpret_cast<int64_t>(mi.lpBaseOfDll); + int64_t image_file_base = get_image_base(_execpath); + data.offset = image_mem_base - image_file_base; + + data.state = backtrace_create_state(_execpath.utf8().get_data(), 0, &error_callback, reinterpret_cast<void *>(&data)); + if (data.state != nullptr) { + data.index = 1; + backtrace_simple(data.state, 1, &trace_callback, &error_callback, reinterpret_cast<void *>(&data)); + } + + print_error("-- END OF BACKTRACE --"); + print_error("================================================================"); +} +#endif + +CrashHandler::CrashHandler() { + disabled = false; +} + +CrashHandler::~CrashHandler() { +} + +void CrashHandler::disable() { + if (disabled) { + return; + } + +#if defined(CRASH_HANDLER_EXCEPTION) + signal(SIGSEGV, nullptr); + signal(SIGFPE, nullptr); + signal(SIGILL, nullptr); +#endif + + disabled = true; +} + +void CrashHandler::initialize() { +#if defined(CRASH_HANDLER_EXCEPTION) + signal(SIGSEGV, CrashHandlerException); + signal(SIGFPE, CrashHandlerException); + signal(SIGILL, CrashHandlerException); +#endif +} diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 196beb423f..93eb34001e 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -2,6 +2,7 @@ import methods import os import subprocess import sys +from methods import print_warning, print_error from platform_methods import detect_arch from typing import TYPE_CHECKING @@ -202,9 +203,7 @@ def get_opts(): BoolVariable("use_asan", "Use address sanitizer (ASAN)", False), BoolVariable("debug_crt", "Compile with MSVC's debug CRT (/MDd)", False), BoolVariable("incremental_link", "Use MSVC incremental linking. May increase or decrease build times.", False), - BoolVariable( - "silence_msvc", "Silence MSVC's stdout to decrease output log bloat. May hide error messages.", False - ), + BoolVariable("silence_msvc", "Silence MSVC's cl/link stdout bloat, redirecting any errors to stderr.", True), ("angle_libs", "Path to the ANGLE static libraries", ""), # Direct3D 12 support. ( @@ -295,16 +294,14 @@ def setup_msvc_manual(env: "SConsEnvironment"): env_arch = detect_build_env_arch() if env["arch"] != env_arch: - print( - """ - Arch argument (%s) is not matching Native/Cross Compile Tools Prompt/Developer Console (or Visual Studio settings) that is being used to run SCons (%s). - 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. - """ + print_error( + "Arch argument (%s) is not matching Native/Cross Compile Tools Prompt/Developer Console (or Visual Studio settings) that is being used to run SCons (%s).\n" + "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." % (env["arch"], env_arch) ) - sys.exit(200) + sys.exit(255) - print("Found MSVC, arch %s" % (env_arch)) + print("Using VCVARS-determined MSVC, arch %s" % (env_arch)) def setup_msvc_auto(env: "SConsEnvironment"): @@ -340,7 +337,7 @@ def setup_msvc_auto(env: "SConsEnvironment"): 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 - print("Found MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["arch"])) + print("Using SCons-detected MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["arch"])) def setup_mingw(env: "SConsEnvironment"): @@ -348,32 +345,24 @@ def setup_mingw(env: "SConsEnvironment"): env_arch = detect_build_env_arch() if os.getenv("MSYSTEM") == "MSYS": - print( - """ - Running from base MSYS2 console/environment, use target specific environment instead (e.g., mingw32, mingw64, clang32, clang64). - """ + print_error( + "Running from base MSYS2 console/environment, use target specific environment instead (e.g., mingw32, mingw64, clang32, clang64)." ) - sys.exit(201) + sys.exit(255) if env_arch != "" and env["arch"] != env_arch: - print( - """ - Arch argument (%s) is not matching MSYS2 console/environment that is being used to run SCons (%s). - Run SCons again without arch argument (example: scons p=windows) and SCons will attempt to detect what MSYS2 compiler will be executed and inform you. - """ + print_error( + "Arch argument (%s) is not matching MSYS2 console/environment that is being used to run SCons (%s).\n" + "Run SCons again without arch argument (example: scons p=windows) and SCons will attempt to detect what MSYS2 compiler will be executed and inform you." % (env["arch"], env_arch) ) - sys.exit(202) + sys.exit(255) if not try_cmd("gcc --version", env["mingw_prefix"], env["arch"]) and not try_cmd( "clang --version", env["mingw_prefix"], env["arch"] ): - print( - """ - No valid compilers found, use MINGW_PREFIX environment variable to set MinGW path. - """ - ) - sys.exit(202) + print_error("No valid compilers found, use MINGW_PREFIX environment variable to set MinGW path.") + sys.exit(255) print("Using MinGW, arch %s" % (env["arch"])) @@ -398,16 +387,35 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): env["MAXLINELENGTH"] = 8192 # Windows Vista and beyond, so always applicable. if env["silence_msvc"]: - env.Prepend(CCFLAGS=[">", "NUL"]) - env.Prepend(LINKFLAGS=[">", "NUL"]) + from tempfile import mkstemp - # "> NUL" fails if using a tempfile, circumvent by removing the argument altogether. - old_esc_func = env["TEMPFILEARGESCFUNC"] + old_spawn = env["SPAWN"] - def trim_nul(arg): - return "" if arg in [">", "NUL"] else old_esc_func(arg) + def spawn_capture(sh, escape, cmd, args, env): + # We only care about cl/link, process everything else as normal. + if args[0] not in ["cl", "link"]: + return old_spawn(sh, escape, cmd, args, env) - env["TEMPFILEARGESCFUNC"] = trim_nul + tmp_stdout, tmp_stdout_name = mkstemp() + os.close(tmp_stdout) + args.append(f">{tmp_stdout_name}") + ret = old_spawn(sh, escape, cmd, args, env) + + try: + with open(tmp_stdout_name, "rb") as tmp_stdout: + # First line is always bloat, subsequent lines are always errors. If content + # exists after discarding the first line, safely decode & send to stderr. + tmp_stdout.readline() + content = tmp_stdout.read() + if content: + sys.stderr.write(content.decode(sys.stdout.encoding, "replace")) + os.remove(tmp_stdout_name) + except OSError: + pass + + return ret + + env["SPAWN"] = spawn_capture if env["debug_crt"]: # Always use dynamic runtime, static debug CRT breaks thread_local. @@ -437,10 +445,10 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if os.getenv("WindowsSdkDir") is not None: env.Prepend(CPPPATH=[str(os.getenv("WindowsSdkDir")) + "/Include"]) else: - print("Missing environment variable: WindowsSdkDir") + print_warning("Missing environment variable: WindowsSdkDir") if int(env["target_win_version"], 16) < 0x0601: - print("`target_win_version` should be 0x0601 or higher (Windows 7).") + print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") sys.exit(255) env.AppendUnique( @@ -498,10 +506,10 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["d3d12"]: # Check whether we have d3d12 dependencies installed. if not os.path.exists(env["mesa_libs"]): - print("The Direct3D 12 rendering driver requires dependencies to be installed.") - print(r"You can install them by running `python misc\scripts\install_d3d12_sdk_windows.py`.") - print("See the documentation for more information:") - print( + print_error( + "The Direct3D 12 rendering driver requires dependencies to be installed.\n" + "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" + "See the documentation for more information:\n\t" "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" ) sys.exit(255) @@ -540,13 +548,16 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): LIBS += ["dxgi", "d3d9", "d3d11"] env.Prepend(CPPPATH=["#thirdparty/angle/include"]) + if env["target"] in ["editor", "template_debug"]: + LIBS += ["psapi", "dbghelp"] + env.Append(LINKFLAGS=[p + env["LIBSUFFIX"] for p in LIBS]) if vcvars_msvc_config: if os.getenv("WindowsSdkDir") is not None: env.Append(LIBPATH=[str(os.getenv("WindowsSdkDir")) + "/Lib"]) else: - print("Missing environment variable: WindowsSdkDir") + print_warning("Missing environment variable: WindowsSdkDir") ## LTO @@ -555,7 +566,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["lto"] != "none": if env["lto"] == "thin": - print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + print_error("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") sys.exit(255) env.AppendUnique(CCFLAGS=["/GL"]) env.AppendUnique(ARFLAGS=["/LTCG"]) @@ -673,7 +684,7 @@ def configure_mingw(env: "SConsEnvironment"): ## Compile flags if int(env["target_win_version"], 16) < 0x0601: - print("`target_win_version` should be 0x0601 or higher (Windows 7).") + print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") sys.exit(255) if not env["use_llvm"]: @@ -727,10 +738,10 @@ def configure_mingw(env: "SConsEnvironment"): if env["d3d12"]: # Check whether we have d3d12 dependencies installed. if not os.path.exists(env["mesa_libs"]): - print("The Direct3D 12 rendering driver requires dependencies to be installed.") - print(r"You can install them by running `python misc\scripts\install_d3d12_sdk_windows.py`.") - print("See the documentation for more information:") - print( + print_error( + "The Direct3D 12 rendering driver requires dependencies to be installed.\n" + "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" + "See the documentation for more information:\n\t" "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" ) sys.exit(255) @@ -777,11 +788,11 @@ def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] if env["arch"] not in supported_arches: - print( + print_error( 'Unsupported CPU architecture "%s" for Windows. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) - sys.exit() + sys.exit(255) # At this point the env has been set up with basic tools/compilers. env.Prepend(CPPPATH=["#platform/windows"]) diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 2093f552ce..f101d85d58 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -114,6 +114,8 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_ICON: case FEATURE_NATIVE_ICON: case FEATURE_NATIVE_DIALOG: + case FEATURE_NATIVE_DIALOG_INPUT: + case FEATURE_NATIVE_DIALOG_FILE: case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: case FEATURE_TEXT_TO_SPEECH: @@ -543,7 +545,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title result->Release(); } } - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { if (p_options_in_cb) { Variant v_result = true; Variant v_files = file_names; @@ -572,7 +574,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title } } } else { - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { if (p_options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); @@ -903,8 +905,7 @@ static BOOL CALLBACK _MonitorEnumProcPos(HMONITOR hMonitor, HDC hdcMonitor, LPRE static BOOL CALLBACK _MonitorEnumProcOrigin(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { EnumPosData *data = (EnumPosData *)dwData; - data->pos.x = MIN(data->pos.x, lprcMonitor->left); - data->pos.y = MIN(data->pos.y, lprcMonitor->top); + data->pos = data->pos.min(Point2(lprcMonitor->left, lprcMonitor->top)); return TRUE; } @@ -1637,8 +1638,7 @@ void DisplayServerWindows::window_set_current_screen(int p_screen, WindowID p_wi Size2i wsize = window_get_size(p_window); wpos += srect.position; - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - wsize.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - wsize.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - wsize / 3); window_set_position(wpos, p_window); } } @@ -1895,7 +1895,7 @@ Size2i DisplayServerWindows::window_get_size_with_decorations(WindowID p_window) return Size2(); } -void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex) { +void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex) { // Windows docs for window styles: // https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles @@ -1909,7 +1909,17 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscre if (p_fullscreen || p_borderless) { r_style |= WS_POPUP; // p_borderless was WS_EX_TOOLWINDOW in the past. - if ((p_fullscreen && p_multiwindow_fs) || p_maximized) { + if (p_maximized) { + r_style |= WS_MAXIMIZE; + } + if (!p_fullscreen) { + r_style |= WS_SYSMENU | WS_MINIMIZEBOX; + + if (p_resizable) { + r_style |= WS_MAXIMIZEBOX; + } + } + if ((p_fullscreen && p_multiwindow_fs) || p_maximized_fs) { r_style |= WS_BORDER; // Allows child windows to be displayed on top of full screen. } } else { @@ -1945,7 +1955,7 @@ void DisplayServerWindows::_update_window_style(WindowID p_window, bool p_repain DWORD style = 0; DWORD style_ex = 0; - _get_window_style(p_window == MAIN_WINDOW_ID, wd.fullscreen, wd.multiwindow_fs, wd.borderless, wd.resizable, wd.maximized, wd.no_focus || wd.is_popup, style, style_ex); + _get_window_style(p_window == MAIN_WINDOW_ID, wd.fullscreen, wd.multiwindow_fs, wd.borderless, wd.resizable, wd.maximized, wd.maximized_fs, wd.no_focus || wd.is_popup, style, style_ex); SetWindowLongPtr(wd.hWnd, GWL_STYLE, style); SetWindowLongPtr(wd.hWnd, GWL_EXSTYLE, style_ex); @@ -1988,6 +1998,7 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) wd.pre_fs_valid = true; } + ShowWindow(wd.hWnd, SW_RESTORE); MoveWindow(wd.hWnd, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, TRUE); if (restore_mouse_trails > 1) { @@ -2023,7 +2034,7 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) } if ((p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN) && !wd.fullscreen) { - if (wd.minimized) { + if (wd.minimized || wd.maximized) { ShowWindow(wd.hWnd, SW_RESTORE); } wd.was_maximized = wd.maximized; @@ -2545,7 +2556,7 @@ Error DisplayServerWindows::dialog_show(String p_title, String p_description, Ve int button_pressed; if (task_dialog_indirect && SUCCEEDED(task_dialog_indirect(&config, &button_pressed, nullptr, nullptr))) { - if (!p_callback.is_null()) { + if (p_callback.is_valid()) { Variant button = button_pressed; const Variant *args[1] = { &button }; Variant ret; @@ -2951,7 +2962,7 @@ String DisplayServerWindows::keyboard_get_layout_name(int p_index) const { } void DisplayServerWindows::process_events() { - _THREAD_SAFE_METHOD_ + _THREAD_SAFE_LOCK_ MSG msg; @@ -2966,7 +2977,10 @@ void DisplayServerWindows::process_events() { if (!drop_events) { _process_key_events(); + _THREAD_SAFE_UNLOCK_ Input::get_singleton()->flush_buffered_events(); + } else { + _THREAD_SAFE_UNLOCK_ } } @@ -2979,9 +2993,14 @@ void DisplayServerWindows::force_process_and_drop_events() { } void DisplayServerWindows::release_rendering_thread() { -} - -void DisplayServerWindows::make_rendering_thread() { +#if defined(GLES3_ENABLED) + if (gl_manager_angle) { + gl_manager_angle->release_current(); + } + if (gl_manager_native) { + gl_manager_native->release_current(); + } +#endif } void DisplayServerWindows::swap_buffers() { @@ -3152,14 +3171,12 @@ void DisplayServerWindows::set_icon(const Ref<Image> &p_icon) { } } -DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) { +DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) { HICON hicon = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3222,16 +3239,14 @@ DisplayServer::IndicatorID DisplayServerWindows::create_status_indicator(const R return iid; } -void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) { +void DisplayServerWindows::status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!indicators.has(p_id)); HICON hicon = nullptr; if (p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0) { - Ref<Image> img = p_icon; - if (img != icon) { - img = img->duplicate(); - img->convert(Image::FORMAT_RGBA8); - } + Ref<Image> img = p_icon->get_image(); + img = img->duplicate(); + img->convert(Image::FORMAT_RGBA8); int w = img->get_width(); int h = img->get_height(); @@ -3298,12 +3313,42 @@ void DisplayServerWindows::status_indicator_set_tooltip(IndicatorID p_id, const Shell_NotifyIconW(NIM_MODIFY, &ndat); } +void DisplayServerWindows::status_indicator_set_menu(IndicatorID p_id, const RID &p_menu_rid) { + ERR_FAIL_COND(!indicators.has(p_id)); + + indicators[p_id].menu_rid = p_menu_rid; +} + void DisplayServerWindows::status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) { ERR_FAIL_COND(!indicators.has(p_id)); indicators[p_id].callback = p_callback; } +Rect2 DisplayServerWindows::status_indicator_get_rect(IndicatorID p_id) const { + ERR_FAIL_COND_V(!indicators.has(p_id), Rect2()); + + NOTIFYICONIDENTIFIER nid; + ZeroMemory(&nid, sizeof(NOTIFYICONIDENTIFIER)); + nid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + nid.hWnd = windows[MAIN_WINDOW_ID].hWnd; + nid.uID = p_id; + nid.guidItem = GUID_NULL; + + RECT rect; + if (Shell_NotifyIconGetRect(&nid, &rect) != S_OK) { + return Rect2(); + } + Rect2 ind_rect = Rect2(Point2(rect.left, rect.top) - _get_screens_origin(), Size2(rect.right - rect.left, rect.bottom - rect.top)); + for (int i = 0; i < get_screen_count(); i++) { + Rect2 screen_rect = Rect2(screen_get_position(i), screen_get_size(i)); + if (screen_rect.encloses(ind_rect)) { + return ind_rect; + } + } + return Rect2(); +} + void DisplayServerWindows::delete_status_indicator(IndicatorID p_id) { ERR_FAIL_COND(!indicators.has(p_id)); @@ -3422,7 +3467,6 @@ void DisplayServerWindows::_dispatch_input_events(const Ref<InputEvent> &p_event } void DisplayServerWindows::_dispatch_input_event(const Ref<InputEvent> &p_event) { - _THREAD_SAFE_METHOD_ if (in_dispatch_input_event) { return; } @@ -3709,63 +3753,18 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA return MA_NOACTIVATE; // Do not activate, but process mouse messages. } } break; - case WM_SETFOCUS: { - windows[window_id].window_has_focus = true; - last_focused_window = window_id; - - // Restore mouse mode. - _set_mouse_mode_impl(mouse_mode); - - if (!app_focused) { - if (OS::get_singleton()->get_main_loop()) { - OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN); - } - app_focused = true; - } - } break; - case WM_KILLFOCUS: { - windows[window_id].window_has_focus = false; - last_focused_window = window_id; - - // Release capture unconditionally because it can be set due to dragging, in addition to captured mode. - ReleaseCapture(); - - // Release every touch to avoid sticky points. - for (const KeyValue<int, Vector2> &E : touch_state) { - _touch_event(window_id, false, E.value.x, E.value.y, E.key); + case WM_ACTIVATEAPP: { + bool new_app_focused = (bool)wParam; + if (new_app_focused == app_focused) { + break; } - touch_state.clear(); - - bool self_steal = false; - HWND new_hwnd = (HWND)wParam; - if (IsWindow(new_hwnd)) { - self_steal = true; - } - - if (!self_steal) { - if (OS::get_singleton()->get_main_loop()) { - OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); - } - app_focused = false; + app_focused = new_app_focused; + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(app_focused ? MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN : MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); } } break; - case WM_ACTIVATE: { // Watch for window activate message. - if (!windows[window_id].window_focused) { - _process_activate_event(window_id, wParam, lParam); - } else { - windows[window_id].saved_wparam = wParam; - windows[window_id].saved_lparam = lParam; - - // Run a timer to prevent event catching warning if the focused window is closing. - windows[window_id].focus_timer_id = SetTimer(windows[window_id].hWnd, 2, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); - } - if (wParam != WA_INACTIVE) { - track_mouse_leave_event(hWnd); - - if (!IsIconic(hWnd)) { - SetFocus(hWnd); - } - } + case WM_ACTIVATE: { + _process_activate_event(window_id, wParam, lParam); return 0; // Return to the message loop. } break; case WM_GETMINMAXINFO: { @@ -3782,6 +3781,15 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA min_max_info->ptMaxTrackSize.x = windows[window_id].max_size.x + decor.x; min_max_info->ptMaxTrackSize.y = windows[window_id].max_size.y + decor.y; } + if (windows[window_id].borderless) { + Rect2i screen_rect = screen_get_usable_rect(window_get_current_screen(window_id)); + + // Set the size of (borderless) maximized mode to exclude taskbar (or any other panel) if present. + min_max_info->ptMaxPosition.x = screen_rect.position.x; + min_max_info->ptMaxPosition.y = screen_rect.position.y; + min_max_info->ptMaxSize.x = screen_rect.size.x; + min_max_info->ptMaxSize.y = screen_rect.size.y; + } return 0; } } break; @@ -3833,9 +3841,15 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case SC_MONITORPOWER: // Monitor trying to enter powersave? return 0; // Prevent from happening. case SC_KEYMENU: - if ((lParam >> 16) <= 0) { + Engine *engine = Engine::get_singleton(); + if (((lParam >> 16) <= 0) && !engine->is_project_manager_hint() && !engine->is_editor_hint() && !GLOBAL_GET("application/run/enable_alt_space_menu")) { + return 0; + } + if (!alt_mem || !(GetAsyncKeyState(VK_SPACE) & (1 << 15))) { return 0; } + SendMessage(windows[window_id].hWnd, WM_SYSKEYUP, VK_SPACE, 0); + SendMessage(windows[window_id].hWnd, WM_SYSKEYUP, VK_MENU, 0); } } break; case WM_INDICATOR_CALLBACK_MESSAGE: { @@ -3850,7 +3864,19 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mb = MouseButton::MB_XBUTTON1; } if (indicators.has(iid)) { - if (indicators[iid].callback.is_valid()) { + if (lParam == WM_RBUTTONDOWN && indicators[iid].menu_rid.is_valid() && native_menu->has_menu(indicators[iid].menu_rid)) { + NOTIFYICONIDENTIFIER nid; + ZeroMemory(&nid, sizeof(NOTIFYICONIDENTIFIER)); + nid.cbSize = sizeof(NOTIFYICONIDENTIFIER); + nid.hWnd = windows[MAIN_WINDOW_ID].hWnd; + nid.uID = iid; + nid.guidItem = GUID_NULL; + + RECT rect; + if (Shell_NotifyIconGetRect(&nid, &rect) == S_OK) { + native_menu->popup(indicators[iid].menu_rid, Vector2i((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2)); + } + } else if (indicators[iid].callback.is_valid()) { Variant v_button = mb; Variant v_pos = mouse_get_position(); Variant *v_args[2] = { &v_button, &v_pos }; @@ -3864,9 +3890,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } break; case WM_CLOSE: // Did we receive a close message? { - if (windows[window_id].focus_timer_id != 0U) { - KillTimer(windows[window_id].hWnd, windows[window_id].focus_timer_id); - } _send_window_event(windows[window_id], WINDOW_EVENT_CLOSE_REQUEST); return 0; // Jump back. @@ -3978,7 +4001,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } mm->set_relative_screen_position(mm->get_relative()); - if ((windows[window_id].window_has_focus || windows[window_id].is_popup) && mm->get_relative() != Vector2()) { + if ((windows[window_id].window_focused || windows[window_id].is_popup) && mm->get_relative() != Vector2()) { Input::get_singleton()->parse_input_event(mm); } } @@ -4026,7 +4049,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA windows[window_id].last_pen_inverted = inverted; // Don't calculate relative mouse movement if we don't have focus in CAPTURED mode. - if (!windows[window_id].window_has_focus && mouse_mode == MOUSE_MODE_CAPTURED) { + if (!windows[window_id].window_focused && mouse_mode == MOUSE_MODE_CAPTURED) { break; } @@ -4076,7 +4099,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_relative_screen_position(mm->get_relative()); old_x = mm->get_position().x; old_y = mm->get_position().y; - if (windows[window_id].window_has_focus || window_get_active_popup() == window_id) { + if (windows[window_id].window_focused || window_get_active_popup() == window_id) { Input::get_singleton()->parse_input_event(mm); } } @@ -4162,7 +4185,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } // Don't calculate relative mouse movement if we don't have focus in CAPTURED mode. - if (!windows[window_id].window_has_focus && mouse_mode == MOUSE_MODE_CAPTURED) { + if (!windows[window_id].window_focused && mouse_mode == MOUSE_MODE_CAPTURED) { break; } @@ -4225,7 +4248,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_relative_screen_position(mm->get_relative()); old_x = mm->get_position().x; old_y = mm->get_position().y; - if (windows[window_id].window_has_focus || window_get_active_popup() == window_id) { + if (windows[window_id].window_focused || window_get_active_popup() == window_id) { Input::get_singleton()->parse_input_event(mm); } @@ -4277,7 +4300,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } // Don't calculate relative mouse movement if we don't have focus in CAPTURED mode. - if (!windows[window_id].window_has_focus && mouse_mode == MOUSE_MODE_CAPTURED) { + if (!windows[window_id].window_focused && mouse_mode == MOUSE_MODE_CAPTURED) { break; } @@ -4569,10 +4592,23 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA window.minimized = true; } else if (IsZoomed(hWnd)) { window.maximized = true; + + // If maximized_window_size == screen_size add 1px border to prevent switching to exclusive_fs. + if (!window.maximized_fs && window.borderless && window_rect.position == screen_position && window_rect.size == screen_size) { + // Window (borderless) was just maximized and the covers the entire screen. + window.maximized_fs = true; + _update_window_style(window_id, false); + } } else if (window_rect.position == screen_position && window_rect.size == screen_size) { window.fullscreen = true; } + if (window.maximized_fs && !window.maximized) { + // Window (maximized and covering fullscreen) was just non-maximized. + window.maximized_fs = false; + _update_window_style(window_id, false); + } + if (!window.minimized) { window.width = window_client_rect.size.width; window.height = window_client_rect.size.height; @@ -4593,7 +4629,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } if (rect_changed) { - if (!window.rect_changed_callback.is_null()) { + if (window.rect_changed_callback.is_valid()) { window.rect_changed_callback.call(Rect2i(window.last_pos.x, window.last_pos.y, window.width, window.height)); } @@ -4625,10 +4661,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA if (!Main::is_iterating()) { Main::iteration(); } - } else if (wParam == windows[window_id].focus_timer_id) { - _process_activate_event(window_id, windows[window_id].saved_wparam, windows[window_id].saved_lparam); - KillTimer(windows[window_id].hWnd, wParam); - windows[window_id].focus_timer_id = 0U; } } break; case WM_SYSKEYUP: @@ -4777,7 +4809,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } break; case WM_SETCURSOR: { if (LOWORD(lParam) == HTCLIENT) { - if (windows[window_id].window_has_focus && (mouse_mode == MOUSE_MODE_HIDDEN || mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN)) { + if (windows[window_id].window_focused && (mouse_mode == MOUSE_MODE_HIDDEN || mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN)) { // Hide the cursor. if (hCursor == nullptr) { hCursor = SetCursor(nullptr); @@ -4809,7 +4841,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA files.push_back(file); } - if (files.size() && !windows[window_id].drop_files_callback.is_null()) { + if (files.size() && windows[window_id].drop_files_callback.is_valid()) { windows[window_id].drop_files_callback.call(files); } } break; @@ -4834,20 +4866,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { void DisplayServerWindows::_process_activate_event(WindowID p_window_id, WPARAM wParam, LPARAM lParam) { if (LOWORD(wParam) == WA_ACTIVE || LOWORD(wParam) == WA_CLICKACTIVE) { - _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_IN); - windows[p_window_id].window_focused = true; + last_focused_window = p_window_id; alt_mem = false; control_mem = false; shift_mem = false; gr_mem = false; - - // Restore mouse mode. _set_mouse_mode_impl(mouse_mode); + if (!IsIconic(windows[p_window_id].hWnd)) { + SetFocus(windows[p_window_id].hWnd); + } + windows[p_window_id].window_focused = true; + _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_IN); } else { // WM_INACTIVE. Input::get_singleton()->release_pressed_events(); - _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_OUT); - windows[p_window_id].window_focused = false; + track_mouse_leave_event(windows[p_window_id].hWnd); + // Release capture unconditionally because it can be set due to dragging, in addition to captured mode. + ReleaseCapture(); alt_mem = false; + windows[p_window_id].window_focused = false; + _send_window_event(windows[p_window_id], WINDOW_EVENT_FOCUS_OUT); } if ((tablet_get_current_driver() == "wintab") && wintab_available && windows[p_window_id].wtctx) { @@ -5051,7 +5088,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, DWORD dwExStyle; DWORD dwStyle; - _get_window_style(window_id_counter == MAIN_WINDOW_ID, (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MAXIMIZED, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) | (p_flags & WINDOW_FLAG_POPUP), dwStyle, dwExStyle); + _get_window_style(window_id_counter == MAIN_WINDOW_ID, (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MAXIMIZED, false, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) | (p_flags & WINDOW_FLAG_POPUP), dwStyle, dwExStyle); RECT WindowRect; @@ -5076,8 +5113,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, Rect2i srect = screen_get_usable_rect(rq_screen); Point2i wpos = p_rect.position; if (srect != Rect2i()) { - wpos.x = CLAMP(wpos.x, srect.position.x, srect.position.x + srect.size.width - p_rect.size.width / 3); - wpos.y = CLAMP(wpos.y, srect.position.y, srect.position.y + srect.size.height - p_rect.size.height / 3); + wpos = wpos.clamp(srect.position, srect.position + srect.size - p_rect.size / 3); } WindowRect.left = wpos.x; @@ -5285,6 +5321,12 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, wd.height = p_rect.size.height; } + // Set size of maximized borderless window (by default it covers the entire screen). + if (p_mode == WINDOW_MODE_MAXIMIZED && (p_flags & WINDOW_FLAG_BORDERLESS_BIT)) { + Rect2i srect = screen_get_usable_rect(rq_screen); + SetWindowPos(wd.hWnd, HWND_TOP, srect.position.x, srect.position.y, srect.size.width, srect.size.height, SWP_NOZORDER | SWP_NOACTIVATE); + } + window_id_counter++; } @@ -5497,6 +5539,32 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win GetImmersiveColorFromColorSetEx = (GetImmersiveColorFromColorSetExPtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(95)); GetImmersiveColorTypeFromName = (GetImmersiveColorTypeFromNamePtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(96)); GetImmersiveUserColorSetPreference = (GetImmersiveUserColorSetPreferencePtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(98)); + if (os_ver.dwBuildNumber >= 17763) { + AllowDarkModeForAppPtr AllowDarkModeForApp = nullptr; + SetPreferredAppModePtr SetPreferredAppMode = nullptr; + FlushMenuThemesPtr FlushMenuThemes = nullptr; + if (os_ver.dwBuildNumber < 18362) { + AllowDarkModeForApp = (AllowDarkModeForAppPtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(135)); + } else { + SetPreferredAppMode = (SetPreferredAppModePtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(135)); + FlushMenuThemes = (FlushMenuThemesPtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(136)); + } + RefreshImmersiveColorPolicyStatePtr RefreshImmersiveColorPolicyState = (RefreshImmersiveColorPolicyStatePtr)GetProcAddress(ux_theme_lib, MAKEINTRESOURCEA(104)); + if (ShouldAppsUseDarkMode) { + bool dark_mode = ShouldAppsUseDarkMode(); + if (SetPreferredAppMode) { + SetPreferredAppMode(dark_mode ? APPMODE_ALLOWDARK : APPMODE_DEFAULT); + } else if (AllowDarkModeForApp) { + AllowDarkModeForApp(dark_mode); + } + if (RefreshImmersiveColorPolicyState) { + RefreshImmersiveColorPolicyState(); + } + if (FlushMenuThemes) { + FlushMenuThemes(); + } + } + } ux_theme_available = ShouldAppsUseDarkMode && GetImmersiveColorFromColorSetEx && GetImmersiveColorTypeFromName && GetImmersiveUserColorSetPreference; if (os_ver.dwBuildNumber >= 18363) { diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 910b9baa45..80f6061348 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -154,11 +154,23 @@ typedef UINT(WINAPI *WTInfoPtr)(UINT p_category, UINT p_index, LPVOID p_output); typedef BOOL(WINAPI *WTPacketPtr)(HANDLE p_ctx, UINT p_param, LPVOID p_packets); typedef BOOL(WINAPI *WTEnablePtr)(HANDLE p_ctx, BOOL p_enable); +enum PreferredAppMode { + APPMODE_DEFAULT = 0, + APPMODE_ALLOWDARK = 1, + APPMODE_FORCEDARK = 2, + APPMODE_FORCELIGHT = 3, + APPMODE_MAX = 4 +}; + typedef bool(WINAPI *ShouldAppsUseDarkModePtr)(); typedef DWORD(WINAPI *GetImmersiveColorFromColorSetExPtr)(UINT dwImmersiveColorSet, UINT dwImmersiveColorType, bool bIgnoreHighContrast, UINT dwHighContrastCacheMode); typedef int(WINAPI *GetImmersiveColorTypeFromNamePtr)(const WCHAR *name); typedef int(WINAPI *GetImmersiveUserColorSetPreferencePtr)(bool bForceCheckRegistry, bool bSkipCheckOnFail); typedef HRESULT(WINAPI *RtlGetVersionPtr)(OSVERSIONINFOW *lpVersionInformation); +typedef bool(WINAPI *AllowDarkModeForAppPtr)(bool darkMode); +typedef PreferredAppMode(WINAPI *SetPreferredAppModePtr)(PreferredAppMode appMode); +typedef void(WINAPI *RefreshImmersiveColorPolicyStatePtr)(); +typedef void(WINAPI *FlushMenuThemesPtr)(); // Windows Ink API #ifndef POINTER_STRUCTURES @@ -370,6 +382,7 @@ class DisplayServerWindows : public DisplayServer { bool pre_fs_valid = false; RECT pre_fs_rect; bool maximized = false; + bool maximized_fs = false; bool minimized = false; bool fullscreen = false; bool multiwindow_fs = false; @@ -379,7 +392,6 @@ class DisplayServerWindows : public DisplayServer { bool was_maximized = false; bool always_on_top = false; bool no_focus = false; - bool window_has_focus = false; bool exclusive = false; bool context_created = false; bool mpass = false; @@ -390,7 +402,6 @@ class DisplayServerWindows : public DisplayServer { // Timers. uint32_t move_timer_id = 0U; - uint32_t focus_timer_id = 0U; HANDLE wtctx; LOGCONTEXTW wtlc; @@ -453,6 +464,7 @@ class DisplayServerWindows : public DisplayServer { WNDPROC user_proc = nullptr; struct IndicatorData { + RID menu_rid; Callable callback; }; @@ -460,7 +472,7 @@ class DisplayServerWindows : public DisplayServer { HashMap<IndicatorID, IndicatorData> indicators; void _send_window_event(const WindowData &wd, WindowEvent p_event); - void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); + void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); MouseMode mouse_mode; int restore_mouse_trails = 0; @@ -668,16 +680,17 @@ public: virtual void force_process_and_drop_events() override; virtual void release_rendering_thread() override; - virtual void make_rendering_thread() override; virtual void swap_buffers() override; virtual void set_native_icon(const String &p_filename) override; virtual void set_icon(const Ref<Image> &p_icon) override; - virtual IndicatorID create_status_indicator(const Ref<Image> &p_icon, const String &p_tooltip, const Callable &p_callback) override; - virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Image> &p_icon) override; + virtual IndicatorID create_status_indicator(const Ref<Texture2D> &p_icon, const String &p_tooltip, const Callable &p_callback) override; + virtual void status_indicator_set_icon(IndicatorID p_id, const Ref<Texture2D> &p_icon) override; virtual void status_indicator_set_tooltip(IndicatorID p_id, const String &p_tooltip) override; + virtual void status_indicator_set_menu(IndicatorID p_id, const RID &p_rid) override; virtual void status_indicator_set_callback(IndicatorID p_id, const Callable &p_callback) override; + virtual Rect2 status_indicator_get_rect(IndicatorID p_id) const override; virtual void delete_status_indicator(IndicatorID p_id) override; virtual void set_context(Context p_context) override; diff --git a/platform/windows/doc_classes/EditorExportPlatformWindows.xml b/platform/windows/doc_classes/EditorExportPlatformWindows.xml index 1239a2b32f..06b272c10e 100644 --- a/platform/windows/doc_classes/EditorExportPlatformWindows.xml +++ b/platform/windows/doc_classes/EditorExportPlatformWindows.xml @@ -4,6 +4,7 @@ Exporter for Windows. </brief_description> <description> + The Windows exporter customizes how a Windows build is handled. In the editor's "Export" window, it is created when adding a new "Windows" preset. </description> <tutorials> <link title="Exporting for Windows">$DOCS_URL/tutorials/export/exporting_for_windows.html</link> diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index fe2a930cc8..a3c86611a4 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -367,11 +367,17 @@ String EditorExportPlatformWindows::get_export_option_warning(const EditorExport } bool EditorExportPlatformWindows::get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const { + if (p_preset == nullptr) { + return true; + } + // 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; } + bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); + // Hide codesign. bool codesign = p_preset->get("codesign/enable"); if (!codesign && p_option != "codesign/enable" && p_option.begins_with("codesign/")) { @@ -390,6 +396,9 @@ bool EditorExportPlatformWindows::get_export_option_visibility(const EditorExpor return false; } + if (p_option == "dotnet/embed_build_outputs") { + return advanced_options_enabled; + } return true; } diff --git a/platform/windows/gl_manager_windows_native.cpp b/platform/windows/gl_manager_windows_native.cpp index da837b3f94..80cf818199 100644 --- a/platform/windows/gl_manager_windows_native.cpp +++ b/platform/windows/gl_manager_windows_native.cpp @@ -427,10 +427,6 @@ Error GLManagerNative_Windows::window_create(DisplayServer::WindowID p_window_id return OK; } -void GLManagerNative_Windows::_internal_set_current_window(GLWindow *p_win) { - _current_window = p_win; -} - void GLManagerNative_Windows::window_destroy(DisplayServer::WindowID p_window_id) { GLWindow &win = get_window(p_window_id); if (_current_window == &win) { @@ -447,6 +443,8 @@ void GLManagerNative_Windows::release_current() { if (!gd_wglMakeCurrent(_current_window->hDC, nullptr)) { ERR_PRINT("Could not detach OpenGL context from window marked current: " + format_error_message(GetLastError())); } + + _current_window = nullptr; } void GLManagerNative_Windows::window_make_current(DisplayServer::WindowID p_window_id) { @@ -467,17 +465,7 @@ void GLManagerNative_Windows::window_make_current(DisplayServer::WindowID p_wind ERR_PRINT("Could not switch OpenGL context to other window: " + format_error_message(GetLastError())); } - _internal_set_current_window(&win); -} - -void GLManagerNative_Windows::make_current() { - if (!_current_window) { - return; - } - const GLDisplay &disp = get_current_display(); - if (!gd_wglMakeCurrent(_current_window->hDC, disp.hRC)) { - ERR_PRINT("Could not switch OpenGL context to window marked current: " + format_error_message(GetLastError())); - } + _current_window = &win; } void GLManagerNative_Windows::swap_buffers() { @@ -491,7 +479,6 @@ Error GLManagerNative_Windows::initialize() { void GLManagerNative_Windows::set_use_vsync(DisplayServer::WindowID p_window_id, bool p_use) { GLWindow &win = get_window(p_window_id); - GLWindow *current = _current_window; if (&win != _current_window) { window_make_current(p_window_id); @@ -506,11 +493,6 @@ void GLManagerNative_Windows::set_use_vsync(DisplayServer::WindowID p_window_id, } else { WARN_PRINT("Could not set V-Sync mode. V-Sync is not supported."); } - - if (current != _current_window) { - _current_window = current; - make_current(); - } } bool GLManagerNative_Windows::is_using_vsync(DisplayServer::WindowID p_window_id) const { diff --git a/platform/windows/gl_manager_windows_native.h b/platform/windows/gl_manager_windows_native.h index 829c53b3d2..b4e2a3acdf 100644 --- a/platform/windows/gl_manager_windows_native.h +++ b/platform/windows/gl_manager_windows_native.h @@ -68,9 +68,6 @@ private: PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = nullptr; - // funcs - void _internal_set_current_window(GLWindow *p_win); - GLWindow &get_window(unsigned int id) { return _windows[id]; } const GLWindow &get_window(unsigned int id) const { return _windows[id]; } @@ -91,7 +88,6 @@ public: void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) {} void release_current(); - void make_current(); void swap_buffers(); void window_make_current(DisplayServer::WindowID p_window_id); diff --git a/platform/windows/godot.natvis b/platform/windows/godot.natvis index d049ed9a3d..e0d3cfe13a 100644 --- a/platform/windows/godot.natvis +++ b/platform/windows/godot.natvis @@ -29,7 +29,7 @@ </ArrayItems> </Expand> </Type> - + <Type Name="Dictionary"> <Expand> <Item Name="[size]">_p && _p->variant_map.head_element ? _p->variant_map.num_elements : 0</Item> @@ -38,7 +38,7 @@ <HeadPointer>_p ? _p->variant_map.head_element : nullptr</HeadPointer> <NextPointer>next</NextPointer> <ValueNode Name="[{data.key}]">(*this),view(MapHelper)</ValueNode> - </LinkedListItems> + </LinkedListItems> </Expand> </Type> @@ -186,6 +186,7 @@ <DisplayString Condition="type == Variant::PACKED_VECTOR2_ARRAY">{reinterpret_cast<const Variant::PackedArrayRef<Vector2>*>(_data.packed_array)->array}</DisplayString> <DisplayString Condition="type == Variant::PACKED_VECTOR3_ARRAY">{reinterpret_cast<const Variant::PackedArrayRef<Vector3>*>(_data.packed_array)->array}</DisplayString> <DisplayString Condition="type == Variant::PACKED_COLOR_ARRAY">{reinterpret_cast<const Variant::PackedArrayRef<Color>*>(_data.packed_array)->array}</DisplayString> + <DisplayString Condition="type == Variant::PACKED_VECTOR4_ARRAY">{reinterpret_cast<const Variant::PackedArrayRef<Vector4>*>(_data.packed_array)->array}</DisplayString> <DisplayString Condition="type < 0 || type >= Variant::VARIANT_MAX">[INVALID]</DisplayString> <StringView Condition="type == Variant::STRING && ((String *)(_data._mem))->_cowdata._ptr">((String *)(_data._mem))->_cowdata._ptr,s32</StringView> @@ -214,12 +215,13 @@ <Item Name="[value]" Condition="type == Variant::PACKED_BYTE_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<unsigned char>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_INT32_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<int>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_INT64_ARRAY">*reinterpret_cast<PackedInt64Array *>(&_data.packed_array[1])</Item> - <Item Name="[value]" Condition="type == Variant::PACKED_FLOAT32_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<float>*>(_data.packed_array)->array</Item> + <Item Name="[value]" Condition="type == Variant::PACKED_FLOAT32_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<float>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_FLOAT64_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<double>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_STRING_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<String>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_VECTOR2_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<Vector2>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_VECTOR3_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<Vector3>*>(_data.packed_array)->array</Item> <Item Name="[value]" Condition="type == Variant::PACKED_COLOR_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<Color>*>(_data.packed_array)->array</Item> + <Item Name="[value]" Condition="type == Variant::PACKED_VECTOR4_ARRAY">reinterpret_cast<const Variant::PackedArrayRef<Vector4>*>(_data.packed_array)->array</Item> </Expand> </Type> diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index f86ecd87fb..486c3120fc 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -171,13 +171,15 @@ int widechar_main(int argc, wchar_t **argv) { delete[] argv_utf8; if (err == ERR_HELP) { // Returned by --help and --version, so success. - return 0; + return EXIT_SUCCESS; } - return 255; + return EXIT_FAILURE; } - if (Main::start()) { + if (Main::start() == EXIT_SUCCESS) { os.run(); + } else { + os.set_exit_code(EXIT_FAILURE); } Main::cleanup(); @@ -213,7 +215,7 @@ int main(int argc, char **argv) { // _argc and _argv are ignored // we are going to use the WideChar version of them instead -#ifdef CRASH_HANDLER_EXCEPTION +#if defined(CRASH_HANDLER_EXCEPTION) && defined(_MSC_VER) __try { return _main(); } __except (CrashHandlerException(GetExceptionInformation())) { diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp index 13d1cc2a67..40a08f87df 100644 --- a/platform/windows/native_menu_windows.cpp +++ b/platform/windows/native_menu_windows.cpp @@ -141,7 +141,7 @@ RID NativeMenuWindows::create_menu() { ZeroMemory(&menu_info, sizeof(menu_info)); menu_info.cbSize = sizeof(menu_info); menu_info.fMask = MIM_STYLE; - menu_info.dwStyle = MNS_NOTIFYBYPOS | MNS_MODELESS; + menu_info.dwStyle = MNS_NOTIFYBYPOS; SetMenuInfo(md->menu, &menu_info); RID rid = menus.make_rid(md); @@ -189,7 +189,9 @@ void NativeMenuWindows::popup(const RID &p_rid, const Vector2i &p_position) { if (md->is_rtl) { flags |= TPM_LAYOUTRTL; } + SetForegroundWindow(hwnd); TrackPopupMenuEx(md->menu, flags, p_position.x, p_position.y, hwnd, nullptr); + PostMessage(hwnd, WM_NULL, 0, 0); } void NativeMenuWindows::set_interface_direction(const RID &p_rid, bool p_is_rtl) { @@ -556,9 +558,16 @@ int NativeMenuWindows::find_item_index_with_text(const RID &p_rid, const String ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); item.fMask = MIIM_STRING; + item.dwTypeData = nullptr; if (GetMenuItemInfoW(md->menu, i, true, &item)) { - if (String::utf16((const char16_t *)item.dwTypeData) == p_text) { - return i; + item.cch++; + Char16String str; + str.resize(item.cch); + item.dwTypeData = (LPWSTR)str.ptrw(); + if (GetMenuItemInfoW(md->menu, i, true, &item)) { + if (String::utf16((const char16_t *)str.get_data()) == p_text) { + return i; + } } } } @@ -700,8 +709,15 @@ String NativeMenuWindows::get_item_text(const RID &p_rid, int p_idx) const { ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); item.fMask = MIIM_STRING; + item.dwTypeData = nullptr; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - return String::utf16((const char16_t *)item.dwTypeData); + item.cch++; + Char16String str; + str.resize(item.cch); + item.dwTypeData = (LPWSTR)str.ptrw(); + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + return String::utf16((const char16_t *)str.get_data()); + } } return String(); } diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h index 6eab56d8b6..74fd231903 100644 --- a/platform/windows/native_menu_windows.h +++ b/platform/windows/native_menu_windows.h @@ -33,7 +33,7 @@ #include "core/templates/hash_map.h" #include "core/templates/rid_owner.h" -#include "servers/native_menu.h" +#include "servers/display/native_menu.h" #define WIN32_LEAN_AND_MEAN #include <windows.h> diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 152b9ac10f..157702655e 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -34,6 +34,7 @@ #include "joypad_windows.h" #include "lang_table.h" #include "windows_terminal_logger.h" +#include "windows_utils.h" #include "core/debugger/engine_debugger.h" #include "core/debugger/script_debugger.h" @@ -42,6 +43,7 @@ #include "drivers/unix/net_socket_posix.h" #include "drivers/windows/dir_access_windows.h" #include "drivers/windows/file_access_windows.h" +#include "drivers/windows/file_access_windows_pipe.h" #include "main/main.h" #include "servers/audio_server.h" #include "servers/rendering/rendering_server_default.h" @@ -113,7 +115,24 @@ void RedirectStream(const char *p_file_name, const char *p_mode, FILE *p_cpp_str } void RedirectIOToConsole() { + // Save current handles. + HANDLE h_stdin = GetStdHandle(STD_INPUT_HANDLE); + HANDLE h_stdout = GetStdHandle(STD_OUTPUT_HANDLE); + HANDLE h_stderr = GetStdHandle(STD_ERROR_HANDLE); + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + // Restore redirection (Note: if not redirected it's NULL handles not INVALID_HANDLE_VALUE). + if (h_stdin != 0) { + SetStdHandle(STD_INPUT_HANDLE, h_stdin); + } + if (h_stdout != 0) { + SetStdHandle(STD_OUTPUT_HANDLE, h_stdout); + } + if (h_stderr != 0) { + SetStdHandle(STD_ERROR_HANDLE, h_stderr); + } + + // Update file handles. RedirectStream("CONIN$", "r", stdin, STD_INPUT_HANDLE); RedirectStream("CONOUT$", "w", stdout, STD_OUTPUT_HANDLE); RedirectStream("CONOUT$", "w", stderr, STD_ERROR_HANDLE); @@ -171,13 +190,10 @@ void OS_Windows::initialize() { add_error_handler(&error_handlers); #endif -#ifndef WINDOWS_SUBSYSTEM_CONSOLE - RedirectIOToConsole(); -#endif - FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_RESOURCES); FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_USERDATA); FileAccess::make_default<FileAccessWindows>(FileAccess::ACCESS_FILESYSTEM); + FileAccess::make_default<FileAccessWindowsPipe>(FileAccess::ACCESS_PIPE); DirAccess::make_default<DirAccessWindows>(DirAccess::ACCESS_RESOURCES); DirAccess::make_default<DirAccessWindows>(DirAccess::ACCESS_USERDATA); DirAccess::make_default<DirAccessWindows>(DirAccess::ACCESS_FILESYSTEM); @@ -267,6 +283,10 @@ void OS_Windows::finalize() { } void OS_Windows::finalize_core() { + while (!temp_libraries.is_empty()) { + _remove_temp_library(temp_libraries.last()->key); + } + FileAccessWindows::finalize(); timeEndPeriod(1); @@ -352,7 +372,7 @@ void debug_dynamic_library_check_dependencies(const String &p_root_path, const S } #endif -Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) { +Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data) { String path = p_path.replace("/", "\\"); if (!FileAccess::exists(path)) { @@ -362,6 +382,35 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha ERR_FAIL_COND_V(!FileAccess::exists(path), ERR_FILE_NOT_FOUND); + // Here we want a copy to be loaded. + // This is so the original file isn't locked and can be updated by a compiler. + if (p_data != nullptr && p_data->generate_temp_files) { + // Copy the file to the same directory as the original with a prefix in the name. + // This is so relative path to dependencies are satisfied. + String copy_path = path.get_base_dir().path_join("~" + path.get_file()); + + // If there's a left-over copy (possibly from a crash) then delete it first. + if (FileAccess::exists(copy_path)) { + DirAccess::remove_absolute(copy_path); + } + + Error copy_err = DirAccess::copy_absolute(path, copy_path); + if (copy_err) { + ERR_PRINT("Error copying library: " + path); + return ERR_CANT_CREATE; + } + + FileAccess::set_hidden_attribute(copy_path, true); + + // Save the copied path so it can be deleted later. + path = copy_path; + + Error pdb_err = WindowsUtils::copy_and_rename_pdb(path); + if (pdb_err != OK && pdb_err != ERR_SKIP) { + WARN_PRINT(vformat("Failed to rename the PDB file. The original PDB file for '%s' will be loaded.", path)); + } + } + typedef DLL_DIRECTORY_COOKIE(WINAPI * PAddDllDirectory)(PCWSTR); typedef BOOL(WINAPI * PRemoveDllDirectory)(DLL_DIRECTORY_COOKIE); @@ -371,13 +420,17 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha bool has_dll_directory_api = ((add_dll_directory != nullptr) && (remove_dll_directory != nullptr)); DLL_DIRECTORY_COOKIE cookie = nullptr; - if (p_also_set_library_path && has_dll_directory_api) { + if (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) { cookie = add_dll_directory((LPCWSTR)(path.get_base_dir().utf16().get_data())); } - p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); -#ifdef DEBUG_ENABLED + p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); if (!p_library_handle) { + if (p_data != nullptr && p_data->generate_temp_files) { + DirAccess::remove_absolute(path); + } + +#ifdef DEBUG_ENABLED DWORD err_code = GetLastError(); HashSet<String> checekd_libs; @@ -395,8 +448,10 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha } else { ERR_FAIL_V_MSG(ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, format_error_message(err_code))); } +#endif } -#else + +#ifndef DEBUG_ENABLED ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, format_error_message(GetLastError()))); #endif @@ -404,8 +459,12 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha remove_dll_directory(cookie); } - if (r_resolved_path != nullptr) { - *r_resolved_path = path; + if (p_data != nullptr && p_data->r_resolved_path != nullptr) { + *p_data->r_resolved_path = path; + } + + if (p_data != nullptr && p_data->generate_temp_files) { + temp_libraries[p_library_handle] = path; } return OK; @@ -415,9 +474,22 @@ Error OS_Windows::close_dynamic_library(void *p_library_handle) { if (!FreeLibrary((HMODULE)p_library_handle)) { return FAILED; } + + // Delete temporary copy of library if it exists. + _remove_temp_library(p_library_handle); + return OK; } +void OS_Windows::_remove_temp_library(void *p_library_handle) { + if (temp_libraries.has(p_library_handle)) { + String path = temp_libraries[p_library_handle]; + DirAccess::remove_absolute(path); + WindowsUtils::remove_temp_pdbs(path); + temp_libraries.erase(p_library_handle); + } +} + Error OS_Windows::get_dynamic_library_symbol_handle(void *p_library_handle, const String &p_name, void *&p_symbol_handle, bool p_optional) { p_symbol_handle = (void *)GetProcAddress((HMODULE)p_library_handle, p_name.utf8().get_data()); if (!p_symbol_handle) { @@ -727,6 +799,107 @@ Dictionary OS_Windows::get_memory_info() const { return meminfo; } +Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String> &p_arguments) { +#define CLEAN_PIPES \ + if (pipe_in[0] != 0) { \ + CloseHandle(pipe_in[0]); \ + } \ + if (pipe_in[1] != 0) { \ + CloseHandle(pipe_in[1]); \ + } \ + if (pipe_out[0] != 0) { \ + CloseHandle(pipe_out[0]); \ + } \ + if (pipe_out[1] != 0) { \ + CloseHandle(pipe_out[1]); \ + } \ + if (pipe_err[0] != 0) { \ + CloseHandle(pipe_err[0]); \ + } \ + if (pipe_err[1] != 0) { \ + CloseHandle(pipe_err[1]); \ + } + + Dictionary ret; + + String path = p_path.replace("/", "\\"); + String command = _quote_command_line_argument(path); + for (const String &E : p_arguments) { + command += " " + _quote_command_line_argument(E); + } + + // Create pipes. + HANDLE pipe_in[2] = { 0, 0 }; + HANDLE pipe_out[2] = { 0, 0 }; + HANDLE pipe_err[2] = { 0, 0 }; + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = true; + sa.lpSecurityDescriptor = nullptr; + + ERR_FAIL_COND_V(!CreatePipe(&pipe_in[0], &pipe_in[1], &sa, 0), ret); + if (!SetHandleInformation(pipe_in[1], HANDLE_FLAG_INHERIT, 0)) { + CLEAN_PIPES + ERR_FAIL_V(ret); + } + if (!CreatePipe(&pipe_out[0], &pipe_out[1], &sa, 0)) { + CLEAN_PIPES + ERR_FAIL_V(ret); + } + if (!SetHandleInformation(pipe_out[0], HANDLE_FLAG_INHERIT, 0)) { + CLEAN_PIPES + ERR_FAIL_V(ret); + } + if (!CreatePipe(&pipe_err[0], &pipe_err[1], &sa, 0)) { + CLEAN_PIPES + ERR_FAIL_V(ret); + } + ERR_FAIL_COND_V(!SetHandleInformation(pipe_err[0], HANDLE_FLAG_INHERIT, 0), ret); + + // Create process. + ProcessInfo pi; + ZeroMemory(&pi.si, sizeof(pi.si)); + pi.si.cb = sizeof(pi.si); + ZeroMemory(&pi.pi, sizeof(pi.pi)); + LPSTARTUPINFOW si_w = (LPSTARTUPINFOW)&pi.si; + + pi.si.dwFlags |= STARTF_USESTDHANDLES; + pi.si.hStdInput = pipe_in[0]; + pi.si.hStdOutput = pipe_out[1]; + pi.si.hStdError = pipe_err[1]; + + DWORD creation_flags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW; + + if (!CreateProcessW(nullptr, (LPWSTR)(command.utf16().ptrw()), nullptr, nullptr, true, creation_flags, nullptr, nullptr, si_w, &pi.pi)) { + CLEAN_PIPES + ERR_FAIL_V_MSG(ret, "Could not create child process: " + command); + } + CloseHandle(pipe_in[0]); + CloseHandle(pipe_out[1]); + CloseHandle(pipe_err[1]); + + ProcessID pid = pi.pi.dwProcessId; + process_map_mutex.lock(); + process_map->insert(pid, pi); + process_map_mutex.unlock(); + + Ref<FileAccessWindowsPipe> main_pipe; + main_pipe.instantiate(); + main_pipe->open_existing(pipe_out[0], pipe_in[1]); + + Ref<FileAccessWindowsPipe> err_pipe; + err_pipe.instantiate(); + err_pipe->open_existing(pipe_err[0], 0); + + ret["stdio"] = main_pipe; + ret["stderr"] = err_pipe; + ret["pid"] = pid; + +#undef CLEAN_PIPES + return ret; +} + Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex, bool p_open_console) { String path = p_path.replace("/", "\\"); String command = _quote_command_line_argument(path); @@ -856,13 +1029,16 @@ Error OS_Windows::create_process(const String &p_path, const List<String> &p_arg if (r_child_id) { *r_child_id = pid; } + process_map_mutex.lock(); process_map->insert(pid, pi); + process_map_mutex.unlock(); return OK; } Error OS_Windows::kill(const ProcessID &p_pid) { int ret = 0; + MutexLock lock(process_map_mutex); if (process_map->has(p_pid)) { const PROCESS_INFORMATION pi = (*process_map)[p_pid].pi; process_map->erase(p_pid); @@ -888,24 +1064,58 @@ int OS_Windows::get_process_id() const { } bool OS_Windows::is_process_running(const ProcessID &p_pid) const { + MutexLock lock(process_map_mutex); if (!process_map->has(p_pid)) { return false; } - const PROCESS_INFORMATION &pi = (*process_map)[p_pid].pi; + const ProcessInfo &info = (*process_map)[p_pid]; + if (!info.is_running) { + return false; + } + const PROCESS_INFORMATION &pi = info.pi; DWORD dw_exit_code = 0; if (!GetExitCodeProcess(pi.hProcess, &dw_exit_code)) { return false; } if (dw_exit_code != STILL_ACTIVE) { + info.is_running = false; + info.exit_code = dw_exit_code; return false; } return true; } +int OS_Windows::get_process_exit_code(const ProcessID &p_pid) const { + MutexLock lock(process_map_mutex); + if (!process_map->has(p_pid)) { + return -1; + } + + const ProcessInfo &info = (*process_map)[p_pid]; + if (!info.is_running) { + return info.exit_code; + } + + const PROCESS_INFORMATION &pi = info.pi; + + DWORD dw_exit_code = 0; + if (!GetExitCodeProcess(pi.hProcess, &dw_exit_code)) { + return -1; + } + + if (dw_exit_code == STILL_ACTIVE) { + return -1; + } + + info.is_running = false; + info.exit_code = dw_exit_code; + return dw_exit_code; +} + Error OS_Windows::set_cwd(const String &p_cwd) { if (_wchdir((LPCWSTR)(p_cwd.utf16().get_data())) != 0) { return ERR_CANT_OPEN; @@ -1324,10 +1534,10 @@ void OS_Windows::unset_environment(const String &p_var) const { } String OS_Windows::get_stdin_string() { - WCHAR buff[1024]; + char buff[1024]; DWORD count = 0; - if (ReadConsoleW(GetStdHandle(STD_INPUT_HANDLE), buff, 1024, &count, nullptr)) { - return String::utf16((const char16_t *)buff, count); + if (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buff, 1024, &count, nullptr)) { + return String::utf8((const char *)buff, count); } return String(); @@ -1711,6 +1921,13 @@ String OS_Windows::get_system_ca_certificates() { OS_Windows::OS_Windows(HINSTANCE _hInstance) { hInstance = _hInstance; +#ifndef WINDOWS_SUBSYSTEM_CONSOLE + RedirectIOToConsole(); +#endif + + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); #ifdef WASAPI_ENABLED diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index 351cb32c9b..b6a21ed42d 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -127,6 +127,9 @@ class OS_Windows : public OS { bool dwrite_init = false; bool dwrite2_init = false; + HashMap<void *, String> temp_libraries; + + void _remove_temp_library(void *p_library_handle); String _get_default_fontname(const String &p_font_name) const; DWRITE_FONT_WEIGHT _weight_to_dw(int p_weight) const; DWRITE_FONT_STRETCH _stretch_to_dw(int p_stretch) const; @@ -147,15 +150,18 @@ protected: struct ProcessInfo { STARTUPINFO si; PROCESS_INFORMATION pi; + mutable bool is_running = true; + mutable int exit_code = -1; }; HashMap<ProcessID, ProcessInfo> *process_map = nullptr; + Mutex process_map_mutex; public: virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; virtual Error get_entropy(uint8_t *r_buffer, int p_bytes) override; - virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, bool p_also_set_library_path = false, String *r_resolved_path = nullptr) override; + virtual Error open_dynamic_library(const String &p_path, void *&p_library_handle, GDExtensionData *p_data = nullptr) override; virtual Error close_dynamic_library(void *p_library_handle) override; virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String &p_name, void *&p_symbol_handle, bool p_optional = false) override; @@ -181,10 +187,12 @@ public: virtual Dictionary get_memory_info() const override; virtual Error execute(const String &p_path, const List<String> &p_arguments, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false) override; + virtual Dictionary execute_with_pipe(const String &p_path, const List<String> &p_arguments) override; virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error kill(const ProcessID &p_pid) override; virtual int get_process_id() const override; virtual bool is_process_running(const ProcessID &p_pid) const override; + virtual int get_process_exit_code(const ProcessID &p_pid) const override; virtual bool has_environment(const String &p_var) const override; virtual String get_environment(const String &p_var) const override; diff --git a/platform/windows/rendering_context_driver_vulkan_windows.h b/platform/windows/rendering_context_driver_vulkan_windows.h index 34546c9ea6..1bb70cb0ff 100644 --- a/platform/windows/rendering_context_driver_vulkan_windows.h +++ b/platform/windows/rendering_context_driver_vulkan_windows.h @@ -52,7 +52,7 @@ public: }; RenderingContextDriverVulkanWindows(); - ~RenderingContextDriverVulkanWindows() override final; + ~RenderingContextDriverVulkanWindows() override; }; #endif // VULKAN_ENABLED diff --git a/platform/windows/windows_terminal_logger.cpp b/platform/windows/windows_terminal_logger.cpp index 6881a75596..6fc33afe99 100644 --- a/platform/windows/windows_terminal_logger.cpp +++ b/platform/windows/windows_terminal_logger.cpp @@ -53,26 +53,12 @@ void WindowsTerminalLogger::logv(const char *p_format, va_list p_list, bool p_er } buf[len] = 0; - int wlen = MultiByteToWideChar(CP_UTF8, 0, buf, len, nullptr, 0); - if (wlen < 0) { - return; - } - - wchar_t *wbuf = (wchar_t *)memalloc((len + 1) * sizeof(wchar_t)); - ERR_FAIL_NULL_MSG(wbuf, "Out of memory."); - MultiByteToWideChar(CP_UTF8, 0, buf, len, wbuf, wlen); - wbuf[wlen] = 0; - - if (p_err) { - fwprintf(stderr, L"%ls", wbuf); - } else { - wprintf(L"%ls", wbuf); - } - - memfree(wbuf); + DWORD written = 0; + HANDLE h = p_err ? GetStdHandle(STD_ERROR_HANDLE) : GetStdHandle(STD_OUTPUT_HANDLE); + WriteFile(h, &buf[0], len, &written, nullptr); #ifdef DEBUG_ENABLED - fflush(stdout); + FlushFileBuffers(h); #endif } diff --git a/platform/windows/windows_utils.cpp b/platform/windows/windows_utils.cpp new file mode 100644 index 0000000000..147b8bcbae --- /dev/null +++ b/platform/windows/windows_utils.cpp @@ -0,0 +1,280 @@ +/**************************************************************************/ +/* windows_utils.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "windows_utils.h" + +#ifdef WINDOWS_ENABLED + +#include "core/error/error_macros.h" +#include "core/io/dir_access.h" +#include "core/io/file_access.h" + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#undef FAILED // Overrides Error::FAILED + +// dbghelp is linked only in DEBUG_ENABLED builds. +#ifdef DEBUG_ENABLED +#include <dbghelp.h> +#endif +#include <winnt.h> + +HashMap<String, Vector<String>> WindowsUtils::temp_pdbs; + +Error WindowsUtils::copy_and_rename_pdb(const String &p_dll_path) { +#ifdef DEBUG_ENABLED + // 1000 ought to be enough for anybody, in case the debugger does not unblock previous PDBs. + // Usually no more than 2 will be used. + const int max_pdb_names = 1000; + + struct PDBResourceInfo { + uint32_t address = 0; + String path; + } pdb_info; + + // Open and read the PDB information if available. + { + ULONG dbg_info_size = 0; + DWORD dbg_info_position = 0; + + { + // The custom LoadLibraryExW is used instead of open_dynamic_library + // to avoid loading the original PDB into the debugger. + HMODULE library_ptr = LoadLibraryExW((LPCWSTR)(p_dll_path.utf16().get_data()), NULL, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE); + + ERR_FAIL_NULL_V_MSG(library_ptr, ERR_FILE_CANT_OPEN, vformat("Failed to load library '%s'.", p_dll_path)); + + IMAGE_DEBUG_DIRECTORY *dbg_dir = (IMAGE_DEBUG_DIRECTORY *)ImageDirectoryEntryToDataEx(library_ptr, FALSE, IMAGE_DIRECTORY_ENTRY_DEBUG, &dbg_info_size, NULL); + + bool has_debug = dbg_dir && dbg_dir->Type == IMAGE_DEBUG_TYPE_CODEVIEW; + if (has_debug) { + dbg_info_position = dbg_dir->PointerToRawData; + dbg_info_size = dbg_dir->SizeOfData; + } + + ERR_FAIL_COND_V_MSG(!FreeLibrary((HMODULE)library_ptr), FAILED, vformat("Failed to free library '%s'.", p_dll_path)); + + if (!has_debug) { + // Skip with no debugging symbols. + return ERR_SKIP; + } + } + + struct CV_HEADER { + DWORD Signature; + DWORD Offset; + }; + + const DWORD nb10_magic = '01BN'; + struct CV_INFO_PDB20 { + CV_HEADER CvHeader; // CvHeader.Signature = "NB10" + DWORD Signature; + DWORD Age; + BYTE PdbFileName[1]; + }; + + const DWORD rsds_magic = 'SDSR'; + struct CV_INFO_PDB70 { + DWORD Signature; // "RSDS" + BYTE Guid[16]; + DWORD Age; + BYTE PdbFileName[1]; + }; + + Vector<uint8_t> dll_data; + + { + Error err = OK; + Ref<FileAccess> file = FileAccess::open(p_dll_path, FileAccess::READ, &err); + ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Failed to read library '%s'.", p_dll_path)); + + file->seek(dbg_info_position); + dll_data = file->get_buffer(dbg_info_size); + ERR_FAIL_COND_V_MSG(file->get_error() != OK, file->get_error(), vformat("Failed to read data from library '%s'.", p_dll_path)); + } + + const char *raw_pdb_path = nullptr; + int raw_pdb_offset = 0; + DWORD *pdb_info_signature = (DWORD *)dll_data.ptr(); + + if (*pdb_info_signature == rsds_magic) { + raw_pdb_path = (const char *)(((CV_INFO_PDB70 *)pdb_info_signature)->PdbFileName); + raw_pdb_offset = offsetof(CV_INFO_PDB70, PdbFileName); + } else if (*pdb_info_signature == nb10_magic) { + // Not even sure if this format still exists anywhere... + raw_pdb_path = (const char *)(((CV_INFO_PDB20 *)pdb_info_signature)->PdbFileName); + raw_pdb_offset = offsetof(CV_INFO_PDB20, PdbFileName); + } else { + ERR_FAIL_V_MSG(FAILED, vformat("Unknown PDB format in '%s'.", p_dll_path)); + } + + String utf_path; + Error err = utf_path.parse_utf8(raw_pdb_path); + ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Failed to read PDB path from '%s'.", p_dll_path)); + + pdb_info.path = utf_path; + pdb_info.address = dbg_info_position + raw_pdb_offset; + } + + String dll_base_dir = p_dll_path.get_base_dir(); + String copy_pdb_path = pdb_info.path; + + // Attempting to find the PDB by absolute and relative paths. + if (copy_pdb_path.is_relative_path()) { + copy_pdb_path = dll_base_dir.path_join(copy_pdb_path); + if (!FileAccess::exists(copy_pdb_path)) { + copy_pdb_path = dll_base_dir.path_join(copy_pdb_path.get_file()); + } + } else if (!FileAccess::exists(copy_pdb_path)) { + copy_pdb_path = dll_base_dir.path_join(copy_pdb_path.get_file()); + } + ERR_FAIL_COND_V_MSG(!FileAccess::exists(copy_pdb_path), FAILED, vformat("File '%s' does not exist.", copy_pdb_path)); + + String new_pdb_base_name = p_dll_path.get_file().get_basename() + "_"; + + // Checking the available space for the updated string + // and trying to shorten it if there is not much space. + { + // e.g. 999.pdb + const uint8_t suffix_size = String::num_characters((int64_t)max_pdb_names - 1) + 4; + // e.g. ~lib_ + 1 for the \0 + const uint8_t min_base_size = 5 + 1; + int original_path_size = pdb_info.path.utf8().length(); + CharString utf8_name = new_pdb_base_name.utf8(); + int new_expected_buffer_size = utf8_name.length() + suffix_size; + + // Since we have limited space inside the DLL to patch the path to the PDB, + // it is necessary to limit the size based on the number of bytes occupied by the string. + if (new_expected_buffer_size > original_path_size) { + ERR_FAIL_COND_V_MSG(original_path_size < min_base_size + suffix_size, FAILED, vformat("The original PDB path size in bytes is too small: '%s'. Expected size: %d or more bytes, but available %d.", pdb_info.path, min_base_size + suffix_size, original_path_size)); + + utf8_name.resize(original_path_size - suffix_size + 1); // +1 for the \0 + utf8_name[utf8_name.size() - 1] = '\0'; + new_pdb_base_name.parse_utf8(utf8_name); + new_pdb_base_name[new_pdb_base_name.length() - 1] = '_'; // Restore the last '_' + WARN_PRINT(vformat("The original path size of '%s' in bytes was too small to fit the new name, so it was shortened to '%s%d.pdb'.", pdb_info.path, new_pdb_base_name, max_pdb_names - 1)); + } + } + + // Delete old PDB files. + for (const String &file : DirAccess::get_files_at(dll_base_dir)) { + if (file.begins_with(new_pdb_base_name) && file.ends_with(".pdb")) { + String path = dll_base_dir.path_join(file); + + // Just try to delete without showing any errors. + Error err = DirAccess::remove_absolute(path); + if (err == OK && temp_pdbs[p_dll_path].has(path)) { + temp_pdbs[p_dll_path].erase(path); + } + } + } + + // Try to copy PDB with new name and patch DLL. + Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + for (int i = 0; i < max_pdb_names; i++) { + String new_pdb_name = vformat("%s%d.pdb", new_pdb_base_name, i); + String new_pdb_path = dll_base_dir.path_join(new_pdb_name); + Error err = OK; + + Ref<FileAccess> test_pdb_is_locked = FileAccess::open(new_pdb_path, FileAccess::READ_WRITE, &err); + if (err == ERR_FILE_CANT_OPEN) { + // If the file is blocked, continue searching. + continue; + } else if (err != OK && err != ERR_FILE_NOT_FOUND) { + ERR_FAIL_V_MSG(err, vformat("Failed to open '%s' to check if it is locked.", new_pdb_path)); + } + + err = d->copy(copy_pdb_path, new_pdb_path); + ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Failed to copy PDB from '%s' to '%s'.", copy_pdb_path, new_pdb_path)); + temp_pdbs[p_dll_path].append(new_pdb_path); + + Ref<FileAccess> file = FileAccess::open(p_dll_path, FileAccess::READ_WRITE, &err); + ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Failed to open '%s' to patch the PDB path.", p_dll_path)); + + int original_path_size = pdb_info.path.utf8().length(); + // Double-check file bounds. + ERR_FAIL_INDEX_V_MSG(pdb_info.address + original_path_size, file->get_length(), FAILED, vformat("Failed to write a new PDB path. Probably '%s' has been changed.", p_dll_path)); + + Vector<uint8_t> u8 = new_pdb_name.to_utf8_buffer(); + file->seek(pdb_info.address); + file->store_buffer(u8); + + // Terminate string and fill the remaining part of the original string with the '\0'. + // Can be replaced by file->store_8('\0'); + Vector<uint8_t> padding_buffer; + padding_buffer.resize((int64_t)original_path_size - u8.size()); + padding_buffer.fill('\0'); + file->store_buffer(padding_buffer); + ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Failed to write a new PDB path to '%s'.", p_dll_path)); + + return OK; + } + + ERR_FAIL_V_MSG(FAILED, vformat("Failed to find an unblocked PDB name for '%s' among %d files.", p_dll_path, max_pdb_names)); +#else + WARN_PRINT_ONCE("Renaming PDB files is only available in debug builds. If your libraries use PDB files, then the original ones will be used."); + return ERR_SKIP; +#endif +} + +void WindowsUtils::remove_temp_pdbs(const String &p_dll_path) { +#ifdef DEBUG_ENABLED + if (temp_pdbs.has(p_dll_path)) { + Vector<String> removed; + int failed = 0; + const int failed_limit = 10; + for (const String &pdb : temp_pdbs[p_dll_path]) { + if (FileAccess::exists(pdb)) { + Error err = DirAccess::remove_absolute(pdb); + if (err == OK) { + removed.append(pdb); + } else { + failed++; + if (failed <= failed_limit) { + print_verbose("Failed to remove temp PDB: " + pdb); + } + } + } else { + removed.append(pdb); + } + } + + if (failed > failed_limit) { + print_verbose(vformat("And %d more PDB files could not be removed....", failed - failed_limit)); + } + + for (const String &pdb : removed) { + temp_pdbs[p_dll_path].erase(pdb); + } + } +#endif +} + +#endif // WINDOWS_ENABLED diff --git a/platform/windows/windows_utils.h b/platform/windows/windows_utils.h new file mode 100644 index 0000000000..3f5d75294e --- /dev/null +++ b/platform/windows/windows_utils.h @@ -0,0 +1,49 @@ +/**************************************************************************/ +/* windows_utils.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef WINDOWS_UTILS_H +#define WINDOWS_UTILS_H + +#ifdef WINDOWS_ENABLED + +#include "core/string/ustring.h" +#include "core/templates/hash_map.h" + +class WindowsUtils { + static HashMap<String, Vector<String>> temp_pdbs; + +public: + static Error copy_and_rename_pdb(const String &p_dll_path); + static void remove_temp_pdbs(const String &p_dll_path); +}; + +#endif // WINDOWS_ENABLED + +#endif // WINDOWS_UTILS_H |
