diff options
Diffstat (limited to 'platform')
122 files changed, 6319 insertions, 3158 deletions
diff --git a/platform/SCsub b/platform/SCsub index 5194a19518..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") 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/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/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 01ecbc7164..c6f2f82117 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -58,15 +58,21 @@ DisplayServerAndroid *DisplayServerAndroid::get_singleton() { bool DisplayServerAndroid::has_feature(Feature p_feature) const { switch (p_feature) { +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif case FEATURE_CURSOR_SHAPE: //case FEATURE_CUSTOM_CURSOR_SHAPE: - //case FEATURE_GLOBAL_MENU: //case FEATURE_HIDPI: //case FEATURE_ICON: //case FEATURE_IME: 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: @@ -578,6 +584,8 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis keep_screen_on = GLOBAL_GET("display/window/energy_saving/keep_screen_on"); + native_menu = memnew(NativeMenu); + #if defined(GLES3_ENABLED) if (rendering_driver == "opengl3") { RasterizerGLES3::make_current(false); @@ -641,6 +649,11 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis } DisplayServerAndroid::~DisplayServerAndroid() { + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } + #if defined(RD_ENABLED) if (rendering_device) { memdelete(rendering_device); diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index c95eaddf93..e1914f4d18 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -76,6 +76,7 @@ class DisplayServerAndroid : public DisplayServer { RenderingContextDriver *rendering_context = nullptr; RenderingDevice *rendering_device = nullptr; #endif + NativeMenu *native_menu = nullptr; ObjectID window_attached_instance_id; diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml index 64485afeb0..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. @@ -274,8 +274,7 @@ <member name="permissions/custom_permissions" type="PackedStringArray" setter="" getter=""> Array of custom permission strings. </member> - <member name="permissions/delete_cache_files" type="bool" setter="" getter=""> - Deprecated. + <member name="permissions/delete_cache_files" type="bool" setter="" getter="" deprecated=""> </member> <member name="permissions/delete_packages" type="bool" setter="" getter=""> Allows an application to delete packages. See [url=https://developer.android.com/reference/android/Manifest.permission#DELETE_PACKAGES]DELETE_PACKAGES[/url]. @@ -310,8 +309,7 @@ <member name="permissions/get_package_size" type="bool" setter="" getter=""> Allows an application to find out the space used by any package. See [url=https://developer.android.com/reference/android/Manifest.permission#GET_PACKAGE_SIZE]GET_PACKAGE_SIZE[/url]. </member> - <member name="permissions/get_tasks" type="bool" setter="" getter=""> - Deprecated in API level 21. + <member name="permissions/get_tasks" type="bool" setter="" getter="" deprecated="Deprecated in API level 21."> </member> <member name="permissions/get_top_activity_info" type="bool" setter="" getter=""> Allows an application to retrieve private information about the current top activity. @@ -379,13 +377,14 @@ <member name="permissions/nfc" type="bool" setter="" getter=""> 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=""> - Allow an application to make its activities persistent. - Deprecated in API level 15. + <member name="permissions/persistent_activity" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> + Allows an application to make its activities persistent. </member> - <member name="permissions/process_outgoing_calls" type="bool" setter="" getter=""> + <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]. - Deprecated in API level 29. </member> <member name="permissions/read_calendar" type="bool" setter="" getter=""> Allows an application to read the user's calendar data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CALENDAR]READ_CALENDAR[/url]. @@ -396,9 +395,8 @@ <member name="permissions/read_contacts" type="bool" setter="" getter=""> Allows an application to read the user's contacts data. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_CONTACTS]READ_CONTACTS[/url]. </member> - <member name="permissions/read_external_storage" type="bool" setter="" getter=""> + <member name="permissions/read_external_storage" type="bool" setter="" getter="" deprecated="Deprecated in API level 33."> Allows an application to read from external storage. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE]READ_EXTERNAL_STORAGE[/url]. - Deprecated in API level 33. </member> <member name="permissions/read_frame_buffer" type="bool" setter="" getter=""> Allows an application to take screen shots and more generally get access to the frame buffer data. @@ -406,8 +404,7 @@ <member name="permissions/read_history_bookmarks" type="bool" setter="" getter=""> Allows an application to read (but not write) the user's browsing history and bookmarks. </member> - <member name="permissions/read_input_state" type="bool" setter="" getter=""> - Deprecated in API level 16. + <member name="permissions/read_input_state" type="bool" setter="" getter="" deprecated="Deprecated in API level 16."> </member> <member name="permissions/read_logs" type="bool" setter="" getter=""> Allows an application to read the low-level system log files. See [url=https://developer.android.com/reference/android/Manifest.permission#READ_LOGS]READ_LOGS[/url]. @@ -454,8 +451,7 @@ <member name="permissions/reorder_tasks" type="bool" setter="" getter=""> Allows an application to change the Z-order of tasks. See [url=https://developer.android.com/reference/android/Manifest.permission#REORDER_TASKS]REORDER_TASKS[/url]. </member> - <member name="permissions/restart_packages" type="bool" setter="" getter=""> - Deprecated in API level 15. + <member name="permissions/restart_packages" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> </member> <member name="permissions/send_respond_via_message" type="bool" setter="" getter=""> Allows an application (Phone) to send a request to other applications to handle the respond-via-message action during incoming calls. See [url=https://developer.android.com/reference/android/Manifest.permission#SEND_RESPOND_VIA_MESSAGE]SEND_RESPOND_VIA_MESSAGE[/url]. @@ -484,8 +480,7 @@ <member name="permissions/set_pointer_speed" type="bool" setter="" getter=""> Allows low-level access to setting the pointer speed. </member> - <member name="permissions/set_preferred_applications" type="bool" setter="" getter=""> - Deprecated in API level 15. + <member name="permissions/set_preferred_applications" type="bool" setter="" getter="" deprecated="Deprecated in API level 15."> </member> <member name="permissions/set_process_limit" type="bool" setter="" getter=""> Allows an application to set the maximum number of (not needed) application processes that can be running. See [url=https://developer.android.com/reference/android/Manifest.permission#SET_PROCESS_LIMIT]SET_PROCESS_LIMIT[/url]. @@ -511,8 +506,7 @@ <member name="permissions/subscribed_feeds_read" type="bool" setter="" getter=""> Allows an application to allow access the subscribed feeds ContentProvider. </member> - <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter=""> - Deprecated. + <member name="permissions/subscribed_feeds_write" type="bool" setter="" getter="" deprecated=""> </member> <member name="permissions/system_alert_window" type="bool" setter="" getter=""> Allows an app to create windows using the type WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, shown on top of all other apps. See [url=https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW]SYSTEM_ALERT_WINDOW[/url]. @@ -520,8 +514,7 @@ <member name="permissions/transmit_ir" type="bool" setter="" getter=""> Allows using the device's IR transmitter, if available. See [url=https://developer.android.com/reference/android/Manifest.permission#TRANSMIT_IR]TRANSMIT_IR[/url]. </member> - <member name="permissions/uninstall_shortcut" type="bool" setter="" getter=""> - Deprecated. + <member name="permissions/uninstall_shortcut" type="bool" setter="" getter="" deprecated=""> </member> <member name="permissions/update_device_stats" type="bool" setter="" getter=""> Allows an application to update device statistics. See [url=https://developer.android.com/reference/android/Manifest.permission#UPDATE_DEVICE_STATS]UPDATE_DEVICE_STATS[/url]. @@ -605,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 a485b57a64..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", @@ -379,14 +380,15 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } else if (p.begins_with("ro.build.version.sdk=")) { d.api_level = p.get_slice("=", 1).to_int(); } else if (p.begins_with("ro.product.cpu.abi=")) { - d.description += "CPU: " + p.get_slice("=", 1).strip_edges() + "\n"; + d.architecture = p.get_slice("=", 1).strip_edges(); + d.description += "CPU: " + d.architecture + "\n"; } else if (p.begins_with("ro.product.manufacturer=")) { d.description += "Manufacturer: " + p.get_slice("=", 1).strip_edges() + "\n"; } else if (p.begins_with("ro.board.platform=")) { d.description += "Chipset: " + p.get_slice("=", 1).strip_edges() + "\n"; } else if (p.begins_with("ro.opengles.version=")) { uint32_t opengl = p.get_slice("=", 1).to_int(); - d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl)&0xFF) + "\n"; + d.description += "OpenGL: " + itos(opengl >> 16) + "." + itos((opengl >> 8) & 0xFF) + "." + itos((opengl) & 0xFF) + "\n"; } } @@ -415,7 +417,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } - if (EDITOR_GET("export/android/shutdown_adb_on_exit")) { + if (ea->has_runnable_preset.is_set() && EDITOR_GET("export/android/shutdown_adb_on_exit")) { String adb = get_adb_path(); if (!FileAccess::exists(adb)) { return; //adb not configured @@ -829,13 +831,82 @@ bool EditorExportPlatformAndroid::_uses_vulkan() { void EditorExportPlatformAndroid::_notification(int p_what) { #ifndef ANDROID_ENABLED - if (p_what == NOTIFICATION_POSTINITIALIZE) { - ERR_FAIL_NULL(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) { @@ -1390,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; @@ -1918,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") { @@ -1928,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; } @@ -1991,6 +2084,12 @@ String EditorExportPlatformAndroid::get_option_tooltip(int p_index) const { return s; } +String EditorExportPlatformAndroid::get_device_architecture(int p_index) const { + ERR_FAIL_INDEX_V(p_index, devices.size(), ""); + MutexLock lock(device_lock); + return devices[p_index].architecture; +} + Error EditorExportPlatformAndroid::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) { ERR_FAIL_INDEX_V(p_device, devices.size(), ERR_INVALID_PARAMETER); @@ -2179,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") { @@ -2324,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); @@ -2400,9 +2508,22 @@ bool EditorExportPlatformAndroid::has_valid_export_configuration(const Ref<Edito err += template_err; } } else { + // Validate the custom gradle android source template. + bool android_source_template_valid = false; + const String android_source_template = p_preset->get("gradle_build/android_source_template"); + if (!android_source_template.is_empty()) { + android_source_template_valid = FileAccess::exists(android_source_template); + if (!android_source_template_valid) { + err += TTR("Custom Android source template not found.") + "\n"; + } + } + + // Validate the installed build template. bool installed_android_build_template = FileAccess::exists(ExportTemplateManager::get_android_build_directory(p_preset).path_join("build.gradle")); if (!installed_android_build_template) { - r_missing_templates = !exists_export_template("android_source.zip", &err); + if (!android_source_template_valid) { + r_missing_templates = !exists_export_template("android_source.zip", &err); + } err += TTR("Android build template not installed in the project. Install it from the Project menu.") + "\n"; } else { r_missing_templates = false; @@ -2413,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); @@ -2431,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); @@ -2688,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"); @@ -2710,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); @@ -3075,7 +3196,7 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP 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 { @@ -3195,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); @@ -3217,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()) { @@ -3611,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 e25655c6cc..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; @@ -76,6 +79,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { String name; String description; int api_level = 0; + String architecture; }; struct APKExportData { @@ -165,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); @@ -185,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(); @@ -221,6 +229,8 @@ public: virtual String get_option_tooltip(int p_index) const override; + virtual String get_device_architecture(int p_index) const override; + virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) override; virtual Ref<Texture2D> get_run_icon() const override; @@ -231,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..ce53aeebcb 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]) + } } } } @@ -908,47 +912,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 +1014,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/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index ef97aaeab9..bd8c58ad69 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -1673,7 +1673,24 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mWantRenderNotification = true; mRequestRender = true; mRenderComplete = false; - mFinishDrawingRunnable = finishDrawing; + + // fix lost old callback when continuous call requestRenderAndNotify + // + // If continuous call requestRenderAndNotify before trigger old + // callback, old callback will lose, cause VRI will wait for SV's + // draw to finish forever not calling finishDraw. + // https://android.googlesource.com/platform/frameworks/base/+/044fce0b826f2da3a192aac56785b5089143e693%5E%21/ + //+++++++++++++++++++++++++++++++++++++++++++++++++++ + final Runnable oldCallback = mFinishDrawingRunnable; + mFinishDrawingRunnable = () -> { + if (oldCallback != null) { + oldCallback.run(); + } + if (finishDrawing != null) { + finishDrawing.run(); + } + }; + //---------------------------------------------------- sGLThreadManager.notifyAll(); } 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 737b4ac20b..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 @@ -41,12 +41,16 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -67,12 +71,74 @@ public final class PermissionsUtil { } /** - * Request a dangerous permission. name must be specified in <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/AndroidManifest.xml">this</a> - * @param permissionName the name of the requested permission. + * Request a list of dangerous permissions. The requested permissions must be included in the app's AndroidManifest + * @param permissions list of the permissions to request. + * @param activity the caller activity for this method. + * @return true/false. "true" if permissions are already granted, "false" if a permissions request was dispatched. + */ + public static boolean requestPermissions(Activity activity, List<String> permissions) { + if (activity == null) { + return false; + } + + if (permissions == null || permissions.isEmpty()) { + return true; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Not necessary, asked on install already + return true; + } + + Set<String> requestedPermissions = new HashSet<>(); + for (String permission : permissions) { + try { + if (permission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + Log.d(TAG, "Requesting permission " + permission); + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } catch (Exception ignored) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } + } + } else { + PermissionInfo permissionInfo = getPermissionInfo(activity, permission); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + Log.d(TAG, "Requesting permission " + permission); + requestedPermissions.add(permission); + } + } + } catch (PackageManager.NameNotFoundException e) { + // Skip this permission and continue. + Log.w(TAG, "Unable to identify permission " + permission, e); + } + } + + if (requestedPermissions.isEmpty()) { + // If list is empty, all of dangerous permissions were granted. + return true; + } + + activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); + return true; + } + + /** + * Request a dangerous permission. The requested permission must be included in the app's AndroidManifest + * @param permissionName the name of the permission to request. * @param activity the caller activity for this method. * @return true/false. "true" if permission is already granted, "false" if a permission request was dispatched. */ public static boolean requestPermission(String permissionName, Activity activity) { + if (activity == null || TextUtils.isEmpty(permissionName)) { + return false; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // Not necessary, asked on install already return true; @@ -137,11 +203,15 @@ public final class PermissionsUtil { * @return true/false. "true" if all permissions were already granted, returns "false" if permissions requests were dispatched. */ public static boolean requestManifestPermissions(Activity activity, @Nullable Set<String> excludes) { + if (activity == null) { + return false; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } - String[] manifestPermissions; + List<String> manifestPermissions; try { manifestPermissions = getManifestPermissions(activity); } catch (PackageManager.NameNotFoundException e) { @@ -149,48 +219,17 @@ public final class PermissionsUtil { return false; } - if (manifestPermissions.length == 0) + if (manifestPermissions.isEmpty()) { return true; - - List<String> requestedPermissions = new ArrayList<>(); - for (String manifestPermission : manifestPermissions) { - if (excludes != null && excludes.contains(manifestPermission)) { - continue; - } - try { - if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { - Log.d(TAG, "Requesting permission " + manifestPermission); - try { - Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); - intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); - activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); - } catch (Exception ignored) { - Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); - } - } - } else { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); - int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { - Log.d(TAG, "Requesting permission " + manifestPermission); - requestedPermissions.add(manifestPermission); - } - } - } catch (PackageManager.NameNotFoundException e) { - // Skip this permission and continue. - Log.w(TAG, "Unable to identify permission " + manifestPermission, e); - } } - if (requestedPermissions.isEmpty()) { - // If list is empty, all of dangerous permissions were granted. - return true; + if (excludes != null && !excludes.isEmpty()) { + for (String excludedPermission : excludes) { + manifestPermissions.remove(excludedPermission); + } } - activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); - return false; + return requestPermissions(activity, manifestPermissions); } /** @@ -199,15 +238,16 @@ public final class PermissionsUtil { * @return granted permissions list */ public static String[] getGrantedPermissions(Context context) { - String[] manifestPermissions; + List<String> manifestPermissions; try { manifestPermissions = getManifestPermissions(context); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); return new String[0]; } - if (manifestPermissions.length == 0) - return manifestPermissions; + if (manifestPermissions.isEmpty()) { + return new String[0]; + } List<String> grantedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { @@ -253,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. */ - private static 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 new String[0]; - return 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/src/org/godotengine/godot/xr/regular/RegularContextFactory.java b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java index 01ee41e30b..1f0d8592b3 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java +++ b/platform/android/java/lib/src/org/godotengine/godot/xr/regular/RegularContextFactory.java @@ -66,11 +66,15 @@ public class RegularContextFactory implements GLSurfaceView.EGLContextFactory { GLUtils.checkEglError(TAG, "Before eglCreateContext", egl); EGLContext context; + int[] debug_attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; + int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE }; if (mUseDebugOpengl) { - int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, _EGL_CONTEXT_FLAGS_KHR, _EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR, EGL10.EGL_NONE }; - context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, debug_attrib_list); + if (context == null || context == EGL10.EGL_NO_CONTEXT) { + Log.w(TAG, "creating 'OpenGL Debug' context failed"); + context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + } } else { - int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL10.EGL_NONE }; context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); } GLUtils.checkEglError(TAG, "After eglCreateContext", egl); 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..61be6fc5db 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -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; } @@ -340,7 +345,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 +362,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 +374,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 +385,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 +395,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/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..463a307854 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; @@ -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..7bdbeef77a 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; @@ -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/SCsub b/platform/ios/SCsub index 5a57f3840b..f38914434b 100644 --- a/platform/ios/SCsub +++ b/platform/ios/SCsub @@ -3,7 +3,7 @@ Import("env") import os, json -from platform_methods import run_in_subprocess, architectures, lipo, get_build_version, detect_mvk +from platform_methods import architectures, lipo, get_build_version, detect_mvk import subprocess import shutil diff --git a/platform/ios/app_delegate.mm b/platform/ios/app_delegate.mm index 32ebf7be44..37d2696434 100644 --- a/platform/ios/app_delegate.mm +++ b/platform/ios/app_delegate.mm @@ -105,11 +105,19 @@ static ViewController *mainViewController = nil; // Initialize with default Ambient category. AVAudioSessionCategory category = AVAudioSessionCategoryAmbient; + AVAudioSessionCategoryOptions options = 0; + + if (GLOBAL_GET("audio/general/ios/mix_with_others")) { + options |= AVAudioSessionCategoryOptionMixWithOthers; + } if (sessionCategorySetting == SESSION_CATEGORY_MULTI_ROUTE) { category = AVAudioSessionCategoryMultiRoute; } 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) { @@ -118,11 +126,7 @@ static ViewController *mainViewController = nil; category = AVAudioSessionCategorySoloAmbient; } - if (GLOBAL_GET("audio/general/ios/mix_with_others")) { - [[AVAudioSession sharedInstance] setCategory:category withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; - } else { - [[AVAudioSession sharedInstance] setCategory:category error:nil]; - } + [[AVAudioSession sharedInstance] setCategory:category withOptions:options error:nil]; return YES; } diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 4d6e3ae9ba..0c9b7b3204 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -52,6 +52,7 @@ def get_flags(): ("target", "template_debug"), ("use_volk", False), ("supported", ["mono"]), + ("builtin_pcre2_with_jit", False), ] @@ -139,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 6f66783a47..3f9211c572 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -65,6 +65,7 @@ class DisplayServerIOS : public DisplayServer { RenderingContextDriver *rendering_context = nullptr; RenderingDevice *rendering_device = nullptr; #endif + NativeMenu *native_menu = nullptr; id tts = nullptr; diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index ed69b91fdd..f84fb01ad0 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -61,6 +61,7 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode if (tts_enabled) { tts = [[TTS_IOS alloc] init]; } + native_menu = memnew(NativeMenu); #if defined(RD_ENABLED) rendering_context = nullptr; @@ -134,6 +135,11 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode } DisplayServerIOS::~DisplayServerIOS() { + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } + #if defined(RD_ENABLED) if (rendering_device) { rendering_device->screen_free(MAIN_WINDOW_ID); @@ -309,15 +315,21 @@ void DisplayServerIOS::update_gyroscope(float p_x, float p_y, float p_z) { bool DisplayServerIOS::has_feature(Feature p_feature) const { switch (p_feature) { +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif // case FEATURE_CURSOR_SHAPE: // case FEATURE_CUSTOM_CURSOR_SHAPE: - // case FEATURE_GLOBAL_MENU: // case FEATURE_HIDPI: // case FEATURE_ICON: // case FEATURE_IME: // 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..0c0ded5fea 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] diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index ea2b23cfb9..33389129b7 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -127,12 +127,19 @@ String EditorExportPlatformIOS::get_export_option_warning(const EditorExportPres void EditorExportPlatformIOS::_notification(int p_what) { #ifdef MACOS_ENABLED if (p_what == NOTIFICATION_POSTINITIALIZE) { - EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformIOS::_update_preset_status)); + if (EditorExport::get_singleton()) { + EditorExport::get_singleton()->connect_presets_runnable_updated(callable_mp(this, &EditorExportPlatformIOS::_update_preset_status)); + } } #endif } 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; + } + return true; } @@ -414,6 +421,10 @@ void EditorExportPlatformIOS::_fix_config_file(const Ref<EditorExportPreset> &p_ } else if (lines[i].find("$pbx_launch_screen_build_reference") != -1) { String value = "90DD2D9E24B36E8000717FE1 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 90DD2D9D24B36E8000717FE1 /* Launch Screen.storyboard */; };"; strnew += lines[i].replace("$pbx_launch_screen_build_reference", value) + "\n"; +#ifndef DISABLE_DEPRECATED + } else if (lines[i].find("$pbx_launch_image_usage_setting") != -1) { + strnew += lines[i].replace("$pbx_launch_image_usage_setting", "") + "\n"; +#endif } else if (lines[i].find("$launch_screen_image_mode") != -1) { int image_scale_mode = p_preset->get("storyboard/image_scale_mode"); String value; @@ -1310,7 +1321,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 { 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/os_ios.h b/platform/ios/os_ios.h index 10ecd08a89..c4782a4768 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; diff --git a/platform/ios/os_ios.mm b/platform/ios/os_ios.mm index 23614af366..52d496d641 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; @@ -361,7 +359,7 @@ String OS_IOS::get_unique_id() const { String OS_IOS::get_processor_name() const { char buffer[256]; size_t buffer_len = 256; - if (sysctlbyname("machdep.cpu.brand_string", &buffer, &buffer_len, NULL, 0) == 0) { + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &buffer_len, nullptr, 0) == 0) { return String::utf8(buffer, buffer_len); } ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name. Returning an empty string.")); diff --git a/platform/linuxbsd/SCsub b/platform/linuxbsd/SCsub index a3ce773ac2..0802b528f4 100644 --- a/platform/linuxbsd/SCsub +++ b/platform/linuxbsd/SCsub @@ -2,7 +2,6 @@ Import("env") -from platform_methods import run_in_subprocess import platform_linuxbsd_builders common_linuxbsd = [ @@ -42,4 +41,4 @@ if env["dbus"]: prog = env.add_program("#bin/godot", ["godot_linuxbsd.cpp"] + common_linuxbsd) if env["debug_symbols"] and env["separate_debug_symbols"]: - env.AddPostAction(prog, run_in_subprocess(platform_linuxbsd_builders.make_debug_linuxbsd)) + env.AddPostAction(prog, env.Run(platform_linuxbsd_builders.make_debug_linuxbsd)) diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index fd4bcf92be..446fe5c7a1 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -36,8 +36,8 @@ #include "core/version.h" #include "main/main.h" -#ifdef DEBUG_ENABLED -#define CRASH_HANDLER_ENABLED 1 +#ifndef DEBUG_ENABLED +#undef CRASH_HANDLER_ENABLED #endif #ifdef CRASH_HANDLER_ENABLED diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 4856076436..27dec73b65 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -50,7 +50,7 @@ def get_opts(): BoolVariable("wayland", "Enable Wayland display", True), BoolVariable("libdecor", "Enable libdecor support", True), BoolVariable("touch", "Enable touch events", True), - BoolVariable("execinfo", "Use libexecinfo on systems where glibc is not available", False), + BoolVariable("execinfo", "Use libexecinfo on systems where glibc is not available", None), ] @@ -488,14 +488,20 @@ def configure(env: "SConsEnvironment"): if platform.system() == "Linux": env.Append(LIBS=["dl"]) - if not env["execinfo"] and platform.libc_ver()[0] != "glibc": + if platform.libc_ver()[0] != "glibc": # The default crash handler depends on glibc, so if the host uses # a different libc (BSD libc, musl), fall back to libexecinfo. - print("Note: Using `execinfo=yes` for the crash handler as required on platforms where glibc is missing.") - env["execinfo"] = True + if not "execinfo" in env: + print("Note: Using `execinfo=yes` for the crash handler as required on platforms where glibc is missing.") + env["execinfo"] = True - if env["execinfo"]: - env.Append(LIBS=["execinfo"]) + if env["execinfo"]: + env.Append(LIBS=["execinfo"]) + env.Append(CPPDEFINES=["CRASH_HANDLER_ENABLED"]) + else: + print("Note: Using `execinfo=no` disables the crash handler on platforms where glibc is missing.") + else: + env.Append(CPPDEFINES=["CRASH_HANDLER_ENABLED"]) if platform.system() == "FreeBSD": env.Append(LINKFLAGS=["-lkvm"]) 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..214725832f 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; } } @@ -532,57 +522,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 +541,41 @@ 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()) { + 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); + } + } + + 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 +618,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 +644,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..ba9c352e62 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/platform/linuxbsd/freedesktop_portal_desktop.h @@ -60,9 +60,9 @@ private: 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; }; @@ -71,8 +71,8 @@ private: 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(); 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/platform_linuxbsd_builders.py b/platform/linuxbsd/platform_linuxbsd_builders.py index 58234f3748..46fa1947e8 100644 --- a/platform/linuxbsd/platform_linuxbsd_builders.py +++ b/platform/linuxbsd/platform_linuxbsd_builders.py @@ -1,17 +1,10 @@ -"""Functions used to generate source files during build time +"""Functions used to generate source files during build time""" -All such functions are invoked in a subprocess on Windows to prevent build flakiness. - -""" import os -from platform_methods import subprocess_main def make_debug_linuxbsd(target, source, env): - os.system("objcopy --only-keep-debug {0} {0}.debugsymbols".format(target[0])) - os.system("strip --strip-debug --strip-unneeded {0}".format(target[0])) - os.system("objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(target[0])) - - -if __name__ == "__main__": - subprocess_main(globals()) + dst = str(target[0]) + os.system("objcopy --only-keep-debug {0} {0}.debugsymbols".format(dst)) + os.system("strip --strip-debug --strip-unneeded {0}".format(dst)) + os.system("objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(dst)) diff --git a/platform/linuxbsd/wayland/detect_prime_egl.cpp b/platform/linuxbsd/wayland/detect_prime_egl.cpp index 4bee32ae3a..4c97a80039 100644 --- a/platform/linuxbsd/wayland/detect_prime_egl.cpp +++ b/platform/linuxbsd/wayland/detect_prime_egl.cpp @@ -69,7 +69,7 @@ void DetectPrimeEGL::create_context() { EGLConfig egl_config; EGLContext egl_context = EGL_NO_CONTEXT; - eglInitialize(egl_display, NULL, NULL); + eglInitialize(egl_display, nullptr, nullptr); #if defined(GLAD_ENABLED) if (!gladLoaderLoadEGL(egl_display)) { diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index c957dea32d..a815db1c05 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -192,19 +192,37 @@ void DisplayServerWayland::_show_window() { bool DisplayServerWayland::has_feature(Feature p_feature) const { switch (p_feature) { +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif case FEATURE_MOUSE: + case FEATURE_MOUSE_WARP: case FEATURE_CLIPBOARD: case FEATURE_CURSOR_SHAPE: + case FEATURE_CUSTOM_CURSOR_SHAPE: case FEATURE_WINDOW_TRANSPARENCY: + case FEATURE_HIDPI: case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: - case FEATURE_CLIPBOARD_PRIMARY: + case FEATURE_CLIPBOARD_PRIMARY: { + 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 - case FEATURE_HIDPI: { + +#ifdef SPEECHD_ENABLED + case FEATURE_TEXT_TO_SPEECH: { return true; } break; +#endif default: { return false; @@ -569,9 +587,9 @@ void DisplayServerWayland::screen_set_keep_on(bool p_enable) { bool DisplayServerWayland::screen_is_kept_on() const { #ifdef DBUS_ENABLED return wayland_thread.window_get_idle_inhibition(MAIN_WINDOW_ID) || screensaver_inhibited; -#endif - +#else return wayland_thread.window_get_idle_inhibition(MAIN_WINDOW_ID); +#endif } Vector<DisplayServer::WindowID> DisplayServerWayland::get_window_list() const { @@ -990,36 +1008,9 @@ void DisplayServerWayland::cursor_set_custom_image(const Ref<Resource> &p_cursor wayland_thread.cursor_shape_clear_custom_image(p_shape); } - Ref<Texture2D> texture = p_cursor; - ERR_FAIL_COND(!texture.is_valid()); - Size2i texture_size; - - Ref<AtlasTexture> atlas_texture = texture; - - if (atlas_texture.is_valid()) { - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - - // NOTE: The Wayland protocol says nothing about cursor size limits, yet if - // the texture is larger than 256x256 it won't show at least on sway. - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - ERR_FAIL_COND(texture_size.height == 0 || texture_size.width == 0); - - Ref<Image> image = texture->get_image(); - ERR_FAIL_COND(!image.is_valid()); - - if (image->is_compressed()) { - image = image->duplicate(true); - Error err = image->decompress(); - ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); - } + Rect2 atlas_rect; + Ref<Image> image = _get_cursor_image_from_resource(p_cursor, p_hotspot, atlas_rect); + ERR_FAIL_COND(image.is_null()); CustomCursor &cursor = custom_cursors[p_shape]; @@ -1183,14 +1174,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) { @@ -1258,6 +1241,8 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win // Input. Input::get_singleton()->set_event_dispatch_function(dispatch_input_events); + native_menu = memnew(NativeMenu); + #ifdef SPEECHD_ENABLED // Init TTS tts = memnew(TTS_Linux); @@ -1382,6 +1367,12 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win DisplayServerWayland::~DisplayServerWayland() { // TODO: Multiwindow support. + + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } + if (main_window.visible) { #ifdef VULKAN_ENABLED if (rendering_device) { diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index e42967eb7b..368f1b402b 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -59,8 +59,6 @@ #include "core/config/project_settings.h" #include "core/input/input.h" -#include "scene/resources/atlas_texture.h" -#include "scene/resources/texture.h" #include "servers/display_server.h" #include <limits.h> @@ -134,6 +132,7 @@ class DisplayServerWayland : public DisplayServer { #ifdef SPEECHD_ENABLED TTS_Linux *tts = nullptr; #endif + NativeMenu *native_menu = nullptr; #if DBUS_ENABLED FreeDesktopPortalDesktop *portal_desktop = nullptr; @@ -277,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/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index ebb21722e2..7f9008e952 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -448,14 +448,11 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re zwp_primary_selection_device_v1_add_listener(ss->wp_primary_selection_device, &wp_primary_selection_device_listener, ss); } -#if 0 - // FIXME: Broken. if (!ss->wp_tablet_seat && registry->wp_tablet_manager) { // Tablet. ss->wp_tablet_seat = zwp_tablet_manager_v2_get_tablet_seat(registry->wp_tablet_manager, wl_seat); zwp_tablet_seat_v2_add_listener(ss->wp_tablet_seat, &wp_tablet_seat_listener, ss); } -#endif registry->wl_seats.push_back(wl_seat); @@ -541,13 +538,11 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re return; } -#if 0 - // FIXME: Broken. if (strcmp(interface, zwp_tablet_manager_v2_interface.name) == 0) { registry->wp_tablet_manager = (struct zwp_tablet_manager_v2 *)wl_registry_bind(wl_registry, name, &zwp_tablet_manager_v2_interface, 1); registry->wp_tablet_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); @@ -558,7 +553,6 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re return; } -#endif } void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name) { @@ -820,8 +814,6 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } -#if 0 - // FIXME: Broken. if (name == registry->wp_tablet_manager_name) { if (registry->wp_tablet_manager) { zwp_tablet_manager_v2_destroy(registry->wp_tablet_manager); @@ -835,25 +827,27 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry SeatState *ss = wl_seat_get_seat_state(wl_seat); ERR_FAIL_NULL(ss); - List<struct zwp_tablet_tool_v2 *>::Element *it = ss->tablet_tools.front(); - - while (it) { - zwp_tablet_tool_v2_destroy(it->get()); - ss->tablet_tools.erase(it); + for (struct zwp_tablet_tool_v2 *tool : ss->tablet_tools) { + TabletToolState *state = wp_tablet_tool_get_state(tool); + if (state) { + memdelete(state); + } - it = it->next(); + zwp_tablet_tool_v2_destroy(tool); } + + ss->tablet_tools.clear(); } return; } -#endif { // Iterate through all of the seats to find if any got removed. - List<struct wl_seat *>::Element *it = registry->wl_seats.front(); - while (it) { - struct wl_seat *wl_seat = it->get(); + List<struct wl_seat *>::Element *E = registry->wl_seats.front(); + while (E) { + struct wl_seat *wl_seat = E->get(); + List<struct wl_seat *>::Element *N = E->next(); SeatState *ss = wl_seat_get_seat_state(wl_seat); ERR_FAIL_NULL(ss); @@ -867,28 +861,26 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry wl_data_device_destroy(ss->wl_data_device); } -#if 0 - // FIXME: Broken. if (ss->wp_tablet_seat) { zwp_tablet_seat_v2_destroy(ss->wp_tablet_seat); for (struct zwp_tablet_tool_v2 *tool : ss->tablet_tools) { + TabletToolState *state = wp_tablet_tool_get_state(tool); + if (state) { + memdelete(state); + } + zwp_tablet_tool_v2_destroy(tool); } } - // Let's destroy all tools. - for (struct zwp_tablet_tool_v2 *tool : ss->tablet_tools) { - zwp_tablet_tool_v2_destroy(tool); - } - memdelete(ss); - registry->wl_seats.erase(it); -#endif + + registry->wl_seats.erase(E); return; } - it = it->next(); + E = N; } } @@ -1239,8 +1231,6 @@ void WaylandThread::_wl_seat_on_capabilities(void *data, struct wl_seat *wl_seat // Pointer handling. if (capabilities & WL_SEAT_CAPABILITY_POINTER) { ss->cursor_surface = wl_compositor_create_surface(ss->registry->wl_compositor); - ss->cursor_frame_callback = wl_surface_frame(ss->cursor_surface); - wl_callback_add_listener(ss->cursor_frame_callback, &cursor_frame_callback_listener, ss); wl_surface_commit(ss->cursor_surface); ss->wl_pointer = wl_seat_get_pointer(wl_seat); @@ -1321,11 +1311,9 @@ void WaylandThread::_cursor_frame_callback_on_done(void *data, struct wl_callbac SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); - ss->cursor_time_ms = time_ms; + ss->cursor_frame_callback = nullptr; - ss->cursor_frame_callback = wl_surface_frame(ss->cursor_surface); - wl_callback_add_listener(ss->cursor_frame_callback, &cursor_frame_callback_listener, ss); - wl_surface_commit(ss->cursor_surface); + ss->cursor_time_ms = time_ms; seat_state_update_cursor(ss); } @@ -1575,7 +1563,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, @@ -1736,7 +1724,7 @@ void WaylandThread::_wl_keyboard_on_keymap(void *data, struct wl_keyboard *wl_ke ss->keymap_buffer = nullptr; } - ss->keymap_buffer = (const char *)mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + ss->keymap_buffer = (const char *)mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); ss->keymap_buffer_size = size; xkb_keymap_unref(ss->xkb_keymap); @@ -2160,82 +2148,90 @@ 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) { - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet seat %x on tablet %x added", (size_t)zwp_tablet_seat_v2, (size_t)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) { SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); - ss->tablet_tools.push_back(id); + TabletToolState *state = memnew(TabletToolState); + state->wl_seat = ss->wl_seat; - zwp_tablet_tool_v2_add_listener(id, &wp_tablet_tool_listener, ss); + wl_proxy_tag_godot((struct wl_proxy *)id); + zwp_tablet_tool_v2_add_listener(id, &wp_tablet_tool_listener, state); - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet seat %x on tool %x added", (size_t)zwp_tablet_seat_v2, (size_t)id)); + 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) { - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet seat %x on pad %x added", (size_t)zwp_tablet_seat_v2, (size_t)id)); } void WaylandThread::_wp_tablet_tool_on_type(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t tool_type) { - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on type %d", (size_t)zwp_tablet_tool_v2, tool_type)); + TabletToolState *state = wp_tablet_tool_get_state(zwp_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) { - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on hardware serial %x%x", (size_t)zwp_tablet_tool_v2, hardware_serial_hi, 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) { - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on hardware id wacom hardware id %x%x", (size_t)zwp_tablet_tool_v2, hardware_id_hi, hardware_id_lo)); } void WaylandThread::_wp_tablet_tool_on_capability(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2, uint32_t capability) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); - - if (capability == ZWP_TABLET_TOOL_V2_TYPE_ERASER) { - ss->tablet_tool_data_buffer.is_eraser = true; - } - - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on capability %d", (size_t)zwp_tablet_tool_v2, capability)); } void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on done", (size_t)zwp_tablet_tool_v2)); } void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); - List<struct zwp_tablet_tool_v2 *>::Element *it = ss->tablet_tools.front(); + if (!ts) { + return; + } - while (it) { - struct zwp_tablet_tool_v2 *tool = it->get(); + SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); - if (tool == zwp_tablet_tool_v2) { - zwp_tablet_tool_v2_destroy(tool); - ss->tablet_tools.erase(it); - break; - } + if (!ss) { + return; } - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on removed", (size_t)zwp_tablet_tool_v2)); + List<struct zwp_tablet_tool_v2 *>::Element *E = ss->tablet_tools.find(zwp_tablet_tool_v2); + + if (E && E->get()) { + struct zwp_tablet_tool_v2 *tool = E->get(); + TabletToolState *state = wp_tablet_tool_get_state(tool); + if (state) { + memdelete(state); + } + + zwp_tablet_tool_v2_destroy(tool); + ss->tablet_tools.erase(E); + } } 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } + + SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); + + if (!ss) { + return; + } WaylandThread *wayland_thread = ss->wayland_thread; ERR_FAIL_NULL(wayland_thread); - ss->tablet_tool_data_buffer.in_proximity = true; - - ss->pointer_enter_serial = serial; - ss->pointed_surface = surface; - ss->last_pointed_surface = surface; + ts->data_pending.proximity_serial = serial; + ts->data_pending.proximal_surface = surface; + ts->last_surface = surface; Ref<WindowEventMessage> msg; msg.instantiate(); @@ -2243,21 +2239,25 @@ void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_table wayland_thread->push_message(msg); DEBUG_LOG_WAYLAND_THREAD("Tablet tool entered window."); - - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on proximity in serial %d tablet %x surface %x", (size_t)zwp_tablet_tool_v2, serial, (size_t)tablet, (size_t)surface)); } void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } + + SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); + + if (!ss) { + return; + } WaylandThread *wayland_thread = ss->wayland_thread; ERR_FAIL_NULL(wayland_thread); - ss->pointed_surface = nullptr; - ss->tablet_tool_data_buffer.in_proximity = false; - - DEBUG_LOG_WAYLAND_THREAD("Tablet tool left window."); + ts->data_pending.proximal_surface = nullptr; Ref<WindowEventMessage> msg; msg.instantiate(); @@ -2265,16 +2265,18 @@ void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tabl wayland_thread->push_message(msg); - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on proximity out", (size_t)zwp_tablet_tool_v2)); + 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } - TabletToolData &td = ss->tablet_tool_data_buffer; + TabletToolData &td = ts->data_pending; - td.touching = true; td.pressed_button_mask.set_flag(mouse_button_to_mask(MouseButton::LEFT)); td.last_button_pressed = MouseButton::LEFT; td.double_click_begun = true; @@ -2282,45 +2284,53 @@ void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v // The protocol doesn't cover this, but we can use this funky hack to make // double clicking work. td.button_time = OS::get_singleton()->get_ticks_msec(); - - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on down serial %x", (size_t)zwp_tablet_tool_v2, serial)); } void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *zwp_tablet_tool_v2) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } - TabletToolData &td = ss->tablet_tool_data_buffer; + TabletToolData &td = ts->data_pending; - td.touching = false; td.pressed_button_mask.clear_flag(mouse_button_to_mask(MouseButton::LEFT)); // The protocol doesn't cover this, but we can use this funky hack to make // double clicking work. td.button_time = OS::get_singleton()->get_ticks_msec(); - - DEBUG_LOG_WAYLAND_THREAD(vformat("wp tablet tool %x on up", (size_t)zwp_tablet_tool_v2)); } 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); - WindowState *ws = wl_surface_get_window_state(ss->pointed_surface); + if (!ts) { + return; + } + + WindowState *ws = wl_surface_get_window_state(ts->data_pending.proximal_surface); ERR_FAIL_NULL(ws); - double scale_factor = window_state_get_scale_factor(ws); + TabletToolData &td = ts->data_pending; - TabletToolData &td = ss->tablet_tool_data_buffer; + double scale_factor = window_state_get_scale_factor(ws); + td.position.x = wl_fixed_to_int(x); + td.position.y = wl_fixed_to_int(y); td.position = scale_vector2i(td.position, scale_factor); + + 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } - ss->tablet_tool_data_buffer.pressure = pressure; + 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) { @@ -2328,11 +2338,16 @@ void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_to } 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } - ss->tablet_tool_data_buffer.tilt.x = wl_fixed_to_double(tilt_x); - ss->tablet_tool_data_buffer.tilt.y = wl_fixed_to_double(tilt_y); + TabletToolData &td = ts->data_pending; + + td.tilt.x = wl_fixed_to_double(tilt_x); + 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) { @@ -2348,10 +2363,13 @@ void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_ } 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } - TabletToolData &td = ss->tablet_tool_data_buffer; + TabletToolData &td = ts->data_pending; MouseButton mouse_button = MouseButton::NONE; @@ -2381,14 +2399,23 @@ 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) { - SeatState *ss = (SeatState *)data; - ERR_FAIL_NULL(ss); + TabletToolState *ts = wp_tablet_tool_get_state(zwp_tablet_tool_v2); + + if (!ts) { + return; + } + + SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); + + if (!ss) { + return; + } WaylandThread *wayland_thread = ss->wayland_thread; ERR_FAIL_NULL(wayland_thread); - TabletToolData &old_td = ss->tablet_tool_data; - TabletToolData &td = ss->tablet_tool_data_buffer; + TabletToolData &old_td = ts->data; + TabletToolData &td = ts->data_pending; if (old_td.position != td.position || old_td.tilt != td.tilt || old_td.pressure != td.pressure) { Ref<InputEventMouseMotion> mm; @@ -2411,25 +2438,24 @@ void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_ // straight from the compositor, so we have to normalize them here. // 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, I think. - // TODO: Investigate whether the tilt can go over 90 degrees (it shouldn't). + // 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)); + mm->set_tilt(td.tilt / 90); // The tablet proto spec explicitly says that pressure is defined as a value // between 0 to 65535. mm->set_pressure(td.pressure / (float)65535); - // FIXME: Tool handling is broken. - mm->set_pen_inverted(td.is_eraser); + mm->set_pen_inverted(ts->is_eraser); mm->set_relative(td.position - old_td.position); mm->set_relative_screen_position(mm->get_relative()); - // FIXME: Stop doing this to calculate speed. - // FIXME2: It has been done, port this from the pointer logic once this works again. - Input::get_singleton()->set_mouse_position(td.position); - mm->set_velocity(Input::get_singleton()->get_last_mouse_velocity()); - mm->set_screen_velocity(mm->get_velocity()); + Vector2i pos_delta = td.position - old_td.position; + uint32_t time_delta = td.motion_time - old_td.motion_time; + mm->set_velocity((Vector2)pos_delta / time_delta); Ref<InputEventMessage> inputev_msg; inputev_msg.instantiate(); @@ -2618,6 +2644,15 @@ WaylandThread::SeatState *WaylandThread::wl_seat_get_seat_state(struct wl_seat * return nullptr; } +// Returns the wp_tablet_tool's `TabletToolState`, otherwise `nullptr`. +// NOTE: This will fail if the output isn't tagged as ours. +WaylandThread::TabletToolState *WaylandThread::wp_tablet_tool_get_state(struct zwp_tablet_tool_v2 *p_tool) { + if (p_tool && wl_proxy_is_godot((wl_proxy *)p_tool)) { + return (TabletToolState *)zwp_tablet_tool_v2_get_user_data(p_tool); + } + + return nullptr; +} // Returns the wl_data_offer's `OfferState`, otherwise `nullptr`. // NOTE: This will fail if the output isn't tagged as ours. WaylandThread::OfferState *WaylandThread::wl_data_offer_get_offer_state(struct wl_data_offer *p_offer) { @@ -2819,7 +2854,7 @@ void WaylandThread::seat_state_lock_pointer(SeatState *p_ss) { ERR_FAIL_NULL(locked_surface); - p_ss->wp_locked_pointer = zwp_pointer_constraints_v1_lock_pointer(registry.wp_pointer_constraints, locked_surface, p_ss->wl_pointer, NULL, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + p_ss->wp_locked_pointer = zwp_pointer_constraints_v1_lock_pointer(registry.wp_pointer_constraints, locked_surface, p_ss->wl_pointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); } } @@ -2851,7 +2886,7 @@ void WaylandThread::seat_state_confine_pointer(SeatState *p_ss) { ERR_FAIL_NULL(confined_surface); - p_ss->wp_confined_pointer = zwp_pointer_constraints_v1_confine_pointer(registry.wp_pointer_constraints, confined_surface, p_ss->wl_pointer, NULL, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); + p_ss->wp_confined_pointer = zwp_pointer_constraints_v1_confine_pointer(registry.wp_pointer_constraints, confined_surface, p_ss->wl_pointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT); } } @@ -2880,7 +2915,18 @@ void WaylandThread::seat_state_update_cursor(SeatState *p_ss) { // compositor do it for us (badly). scale = 1; } else if (wl_cursor) { - int frame_idx = wl_cursor_frame(wl_cursor, p_ss->cursor_time_ms); + int frame_idx = 0; + + if (wl_cursor->image_count > 1) { + // The cursor is animated. + frame_idx = wl_cursor_frame(wl_cursor, p_ss->cursor_time_ms); + + if (!p_ss->cursor_frame_callback) { + // Since it's animated, we'll re-update it the next frame. + p_ss->cursor_frame_callback = wl_surface_frame(p_ss->cursor_surface); + wl_callback_add_listener(p_ss->cursor_frame_callback, &cursor_frame_callback_listener, p_ss); + } + } struct wl_cursor_image *wl_cursor_image = wl_cursor->images[frame_idx]; @@ -3614,7 +3660,7 @@ 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(NULL, data_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + cursor.buffer_data = (uint32_t *)mmap(nullptr, data_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (cursor.wl_buffer) { // Clean up the old Wayland buffer. @@ -4070,14 +4116,16 @@ void WaylandThread::destroy() { zwp_confined_pointer_v1_destroy(ss->wp_confined_pointer); } -#if 0 - // FIXME: Broken. if (ss->wp_tablet_seat) { zwp_tablet_seat_v2_destroy(ss->wp_tablet_seat); } -#endif for (struct zwp_tablet_tool_v2 *tool : ss->tablet_tools) { + TabletToolState *state = wp_tablet_tool_get_state(tool); + if (state) { + memdelete(state); + } + zwp_tablet_tool_v2_destroy(tool); } diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index f3e3c3a2ac..d49f0c9d34 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -308,7 +308,7 @@ public: struct TabletToolData { Point2i position; - Vector2i tilt; + Vector2 tilt; uint32_t pressure = 0; BitField<MouseButtonMask> pressed_button_mask; @@ -322,10 +322,20 @@ public: // be used as a mouse...), but we'll hack one in with the current ticks. uint64_t button_time = 0; + uint64_t motion_time = 0; + + uint32_t proximity_serial = 0; + struct wl_surface *proximal_surface = nullptr; + }; + + struct TabletToolState { + struct wl_seat *wl_seat = nullptr; + + struct wl_surface *last_surface = nullptr; bool is_eraser = false; - bool in_proximity = false; - bool touching = false; + TabletToolData data_pending; + TabletToolData data; }; struct OfferState { @@ -425,9 +435,6 @@ public: struct zwp_tablet_seat_v2 *wp_tablet_seat = nullptr; List<struct zwp_tablet_tool_v2 *> tablet_tools; - - TabletToolData tablet_tool_data_buffer; - TabletToolData tablet_tool_data; }; struct CustomCursor { @@ -855,6 +862,7 @@ public: static WindowState *wl_surface_get_window_state(struct wl_surface *p_surface); static ScreenState *wl_output_get_screen_state(struct wl_output *p_output); static SeatState *wl_seat_get_seat_state(struct wl_seat *p_seat); + static TabletToolState *wp_tablet_tool_get_state(struct zwp_tablet_tool_v2 *p_tool); static OfferState *wl_data_offer_get_offer_state(struct wl_data_offer *p_offer); static OfferState *wp_primary_selection_offer_get_offer_state(struct zwp_primary_selection_offer_v1 *p_offer); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 35bfe81827..4c7dfbb107 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -41,7 +41,6 @@ #include "core/string/ustring.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" -#include "scene/resources/atlas_texture.h" #if defined(VULKAN_ENABLED) #include "servers/rendering/renderer_rd/renderer_compositor_rd.h" @@ -110,6 +109,11 @@ static String get_atom_name(Display *p_disp, Atom p_atom) { bool DisplayServerX11::has_feature(Feature p_feature) const { switch (p_feature) { +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif case FEATURE_SUBWINDOWS: #ifdef TOUCH_ENABLED case FEATURE_TOUCHSCREEN: @@ -124,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 @@ -1991,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); } @@ -2220,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.max(Size2i(1, 1)); WindowData &wd = windows[p_window]; @@ -3064,39 +3068,10 @@ void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu cursors_cache.erase(p_shape); } - Ref<Texture2D> texture = p_cursor; - ERR_FAIL_COND(!texture.is_valid()); - Ref<AtlasTexture> atlas_texture = p_cursor; - Size2i texture_size; - Rect2i atlas_rect; - - if (atlas_texture.is_valid()) { - texture = atlas_texture->get_atlas(); - - atlas_rect.size.width = texture->get_width(); - atlas_rect.size.height = texture->get_height(); - atlas_rect.position.x = atlas_texture->get_region().position.x; - atlas_rect.position.y = atlas_texture->get_region().position.y; - - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - - Ref<Image> image = texture->get_image(); - - ERR_FAIL_COND(!image.is_valid()); - if (image->is_compressed()) { - image = image->duplicate(true); - Error err = image->decompress(); - ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); - } + Rect2 atlas_rect; + Ref<Image> image = _get_cursor_image_from_resource(p_cursor, p_hotspot, atlas_rect); + ERR_FAIL_COND(image.is_null()); + Vector2i texture_size = image->get_size(); // Create the cursor structure XcursorImage *cursor_image = XcursorImageCreate(texture_size.width, texture_size.height); @@ -3115,7 +3090,7 @@ void DisplayServerX11::cursor_set_custom_image(const Ref<Resource> &p_cursor, Cu int row_index = floor(index / texture_size.width) + atlas_rect.position.y; int column_index = (index % int(texture_size.width)) + atlas_rect.position.x; - if (atlas_texture.is_valid()) { + if (atlas_rect.has_area()) { column_index = MIN(column_index, atlas_rect.size.width - 1); row_index = MIN(row_index, atlas_rect.size.height - 1); } @@ -4293,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; @@ -4731,19 +4706,6 @@ void DisplayServerX11::process_events() { break; } - const WindowData &wd = windows[window_id]; - - XWindowAttributes xwa; - XSync(x11_display, False); - XGetWindowAttributes(x11_display, wd.x11_window, &xwa); - - // Set focus when menu window is re-used. - // RevertToPointerRoot is used to make sure we don't lose all focus in case - // a subwindow and its parent are both destroyed. - if ((xwa.map_state == IsViewable) && !wd.no_focus && !wd.is_popup && _window_focus_check()) { - _set_input_focus(wd.x11_window, RevertToPointerRoot); - } - _window_changed(&event); } break; @@ -5135,6 +5097,8 @@ void DisplayServerX11::process_events() { */ } + _THREAD_SAFE_UNLOCK_ + Input::get_singleton()->flush_buffered_events(); } @@ -5149,17 +5113,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) { @@ -5250,7 +5203,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) { @@ -5489,8 +5442,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; } @@ -5795,6 +5747,8 @@ static ::XIMStyle _get_best_xim_style(const ::XIMStyle &p_style_a, const ::XIMSt DisplayServerX11::DisplayServerX11(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) { KeyMappingX11::initialize(); + native_menu = memnew(NativeMenu); + #ifdef SOWRAP_ENABLED #ifdef DEBUG_ENABLED int dylibloader_verbose = 1; @@ -6387,6 +6341,11 @@ DisplayServerX11::~DisplayServerX11() { events_thread_done.set(); events_thread.wait_to_finish(); + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } + //destroy all windows for (KeyValue<WindowID, WindowData> &E : windows) { #if defined(RD_ENABLED) diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index a5cbe34d26..8a7062857c 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -156,6 +156,7 @@ class DisplayServerX11 : public DisplayServer { #ifdef SPEECHD_ENABLED TTS_Linux *tts = nullptr; #endif + NativeMenu *native_menu = nullptr; #if defined(DBUS_ENABLED) FreeDesktopPortalDesktop *portal_desktop = nullptr; @@ -525,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/SCsub b/platform/macos/SCsub index 08783ee14a..59ef4ee85c 100644 --- a/platform/macos/SCsub +++ b/platform/macos/SCsub @@ -3,7 +3,7 @@ Import("env") import os, json -from platform_methods import run_in_subprocess, architectures, lipo, get_build_version +from platform_methods import architectures, lipo, get_build_version import platform_macos_builders import subprocess import shutil @@ -36,7 +36,7 @@ def generate_bundle(target, source, env): version = get_build_version(False) short_version = get_build_version(True) with open(Dir("#misc/dist/macos").abspath + "/editor_info_plist.template", "rt") as fin: - with open(app_dir + "/Contents/Info.plist", "wt") as fout: + with open(app_dir + "/Contents/Info.plist", "wt", encoding="utf-8", newline="\n") as fout: for line in fin: line = line.replace("$version", version) line = line.replace("$short_version", short_version) @@ -113,9 +113,10 @@ files = [ "godot_menu_delegate.mm", "godot_menu_item.mm", "godot_open_save_delegate.mm", + "native_menu_macos.mm", "dir_access_macos.mm", "tts_macos.mm", - "joypad_macos.cpp", + "joypad_macos.mm", "rendering_context_driver_vulkan_macos.mm", "gl_manager_macos_angle.mm", "gl_manager_macos_legacy.mm", @@ -124,7 +125,7 @@ files = [ prog = env.add_program("#bin/godot", files) if env["debug_symbols"] and env["separate_debug_symbols"]: - env.AddPostAction(prog, run_in_subprocess(platform_macos_builders.make_debug_macos)) + env.AddPostAction(prog, env.Run(platform_macos_builders.make_debug_macos)) if env["generate_bundle"]: generate_bundle_command = env.Command("generate_bundle", [], generate_bundle) diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 9150a527a5..3c8b1ebee1 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -208,7 +208,9 @@ def configure(env: "SConsEnvironment"): "-framework", "IOKit", "-framework", - "ForceFeedback", + "GameController", + "-framework", + "CoreHaptics", "-framework", "CoreVideo", "-framework", @@ -240,10 +242,17 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "IOSurface"]) if not env["use_volk"]: env.Append(LINKFLAGS=["-lMoltenVK"]) - mvk_path = detect_mvk(env, "macos-arm64_x86_64") + + mvk_path = "" + arch_variants = ["macos-arm64_x86_64", "macos-" + env["arch"]] + for arch in arch_variants: + mvk_path = detect_mvk(env, arch) + if mvk_path != "": + mvk_path = os.path.join(mvk_path, arch) + break if mvk_path != "": - env.Append(LINKFLAGS=["-L" + os.path.join(mvk_path, "macos-arm64_x86_64")]) + env.Append(LINKFLAGS=["-L" + mvk_path]) else: print( "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path." diff --git a/platform/macos/dir_access_macos.mm b/platform/macos/dir_access_macos.mm index 66d81f2687..37f717c9de 100644 --- a/platform/macos/dir_access_macos.mm +++ b/platform/macos/dir_access_macos.mm @@ -41,9 +41,10 @@ String DirAccessMacOS::fix_unicode_name(const char *p_name) const { String fname; - NSString *nsstr = [[NSString stringWithUTF8String:p_name] precomposedStringWithCanonicalMapping]; - - fname.parse_utf8([nsstr UTF8String]); + if (p_name != nullptr) { + NSString *nsstr = [[NSString stringWithUTF8String:p_name] precomposedStringWithCanonicalMapping]; + fname.parse_utf8([nsstr UTF8String]); + } return fname; } diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index 7373a40237..5d38bf55ea 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -39,6 +39,8 @@ #include "gl_manager_macos_legacy.h" #endif // GLES3_ENABLED +#include "native_menu_macos.h" + #if defined(RD_ENABLED) #include "servers/rendering/rendering_device.h" @@ -142,19 +144,6 @@ private: #endif String rendering_driver; - NSMenu *apple_menu = nullptr; - NSMenu *window_menu = nullptr; - NSMenu *help_menu = nullptr; - NSMenu *dock_menu = nullptr; - struct MenuData { - Callable open; - Callable close; - NSMenu *menu = nullptr; - bool is_open = false; - }; - HashMap<String, MenuData> submenu; - HashMap<NSMenu *, String> submenu_inv; - struct WarpEvent { NSTimeInterval timestamp; NSPoint delta; @@ -168,6 +157,7 @@ private: id tts = nullptr; id menu_delegate = nullptr; + NativeMenuMacOS *native_menu = nullptr; Point2i im_selection; String im_text; @@ -222,15 +212,10 @@ private: Callable system_theme_changed; - const NSMenu *_get_menu_root(const String &p_menu_root) const; - NSMenu *_get_menu_root(const String &p_menu_root); - bool _is_menu_opened(NSMenu *p_menu) const; - WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect); void _update_window_style(WindowData p_wd); void _update_displays_arrangement(); - Point2i _get_screens_origin() const; Point2i _get_native_screen_position(int p_screen) const; static void _displays_arrangement_changed(CGDirectDisplayID display_id, CGDisplayChangeSummaryFlags flags, void *user_info); @@ -240,27 +225,24 @@ private: void _process_key_events(); void _update_keyboard_layouts(); static void _keyboard_layout_changed(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef user_info); - NSImage *_convert_to_nsimg(Ref<Image> &p_image) const; static NSCursor *_cursor_from_selector(SEL p_selector, SEL p_fallback = nil); - int _get_system_menu_start(const NSMenu *p_menu) const; - int _get_system_menu_count(const NSMenu *p_menu) const; - NSMenuItem *_menu_add_item(const String &p_menu_root, const String &p_label, Key p_accel, int p_index, int *r_out); - Error _file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb); public: - NSMenu *get_dock_menu() const; void menu_callback(id p_sender); - void menu_open(NSMenu *p_menu); - void menu_close(NSMenu *p_menu); void emit_system_theme_changed(); bool has_window(WindowID p_window) const; WindowData &get_window(WindowID p_window); + 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(); @@ -293,63 +275,6 @@ public: Callable _help_get_search_callback() const; Callable _help_get_action_callback() const; - virtual void global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback = Callable(), const Callable &p_close_callback = Callable()) override; - - virtual int global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index = -1) override; - virtual int global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; - virtual int global_menu_add_separator(const String &p_menu_root, int p_index = -1) override; - - virtual int global_menu_get_item_index_from_text(const String &p_menu_root, const String &p_text) const override; - virtual int global_menu_get_item_index_from_tag(const String &p_menu_root, const Variant &p_tag) const override; - - virtual bool global_menu_is_item_checked(const String &p_menu_root, int p_idx) const override; - virtual bool global_menu_is_item_checkable(const String &p_menu_root, int p_idx) const override; - virtual bool global_menu_is_item_radio_checkable(const String &p_menu_root, int p_idx) const override; - virtual Callable global_menu_get_item_callback(const String &p_menu_root, int p_idx) const override; - virtual Callable global_menu_get_item_key_callback(const String &p_menu_root, int p_idx) const override; - virtual Variant global_menu_get_item_tag(const String &p_menu_root, int p_idx) const override; - virtual String global_menu_get_item_text(const String &p_menu_root, int p_idx) const override; - virtual String global_menu_get_item_submenu(const String &p_menu_root, int p_idx) const override; - virtual Key global_menu_get_item_accelerator(const String &p_menu_root, int p_idx) const override; - virtual bool global_menu_is_item_disabled(const String &p_menu_root, int p_idx) const override; - virtual bool global_menu_is_item_hidden(const String &p_menu_root, int p_idx) const override; - virtual String global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const override; - virtual int global_menu_get_item_state(const String &p_menu_root, int p_idx) const override; - virtual int global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const override; - virtual Ref<Texture2D> global_menu_get_item_icon(const String &p_menu_root, int p_idx) const override; - virtual int global_menu_get_item_indentation_level(const String &p_menu_root, int p_idx) const override; - - virtual void global_menu_set_item_checked(const String &p_menu_root, int p_idx, bool p_checked) override; - virtual void global_menu_set_item_checkable(const String &p_menu_root, int p_idx, bool p_checkable) override; - virtual void global_menu_set_item_radio_checkable(const String &p_menu_root, int p_idx, bool p_checkable) override; - virtual void global_menu_set_item_callback(const String &p_menu_root, int p_idx, const Callable &p_callback) override; - virtual void global_menu_set_item_key_callback(const String &p_menu_root, int p_idx, const Callable &p_key_callback) override; - virtual void global_menu_set_item_hover_callbacks(const String &p_menu_root, int p_idx, const Callable &p_callback) override; - virtual void global_menu_set_item_tag(const String &p_menu_root, int p_idx, const Variant &p_tag) override; - virtual void global_menu_set_item_text(const String &p_menu_root, int p_idx, const String &p_text) override; - virtual void global_menu_set_item_submenu(const String &p_menu_root, int p_idx, const String &p_submenu) override; - virtual void global_menu_set_item_accelerator(const String &p_menu_root, int p_idx, Key p_keycode) override; - virtual void global_menu_set_item_disabled(const String &p_menu_root, int p_idx, bool p_disabled) override; - virtual void global_menu_set_item_hidden(const String &p_menu_root, int p_idx, bool p_hidden) override; - virtual void global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) override; - virtual void global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state) override; - virtual void global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states) override; - virtual void global_menu_set_item_icon(const String &p_menu_root, int p_idx, const Ref<Texture2D> &p_icon) override; - virtual void global_menu_set_item_indentation_level(const String &p_menu_root, int p_idx, int p_level) override; - - virtual int global_menu_get_item_count(const String &p_menu_root) const override; - - virtual void global_menu_remove_item(const String &p_menu_root, int p_idx) override; - virtual void global_menu_clear(const String &p_menu_root) override; - - virtual Dictionary global_menu_get_system_menu_roots() const override; - virtual bool tts_is_speaking() const override; virtual bool tts_is_paused() const override; virtual TypedArray<Dictionary> tts_get_voices() const override; @@ -501,7 +426,6 @@ 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; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index d8e546f571..cfe925e79b 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -48,7 +48,6 @@ #include "core/os/keyboard.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" -#include "scene/resources/atlas_texture.h" #include "scene/resources/image_texture.h" #if defined(GLES3_ENABLED) @@ -66,66 +65,6 @@ #import <IOKit/hid/IOHIDKeys.h> #import <IOKit/hid/IOHIDLib.h> -#define MENU_TAG_START 0x00004447 -#define MENU_TAG_END 0xFFFF4447 - -const NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) const { - const NSMenu *menu = nullptr; - if (p_menu_root == "" || p_menu_root.to_lower() == "_main") { - // Main menu. - menu = [NSApp mainMenu]; - } else if (p_menu_root.to_lower() == "_dock") { - // macOS dock menu. - menu = dock_menu; - } else if (p_menu_root.to_lower() == "_apple") { - // macOS Apple menu. - menu = apple_menu; - } else if (p_menu_root.to_lower() == "_window") { - // macOS Window menu. - menu = window_menu; - } else if (p_menu_root.to_lower() == "_help") { - // macOS Help menu. - menu = help_menu; - } else { - // Submenu. - if (submenu.has(p_menu_root)) { - menu = submenu[p_menu_root].menu; - } - } - return menu; -} - -NSMenu *DisplayServerMacOS::_get_menu_root(const String &p_menu_root) { - NSMenu *menu = nullptr; - if (p_menu_root == "" || p_menu_root.to_lower() == "_main") { - // Main menu. - menu = [NSApp mainMenu]; - } else if (p_menu_root.to_lower() == "_dock") { - // macOS dock menu. - menu = dock_menu; - } else if (p_menu_root.to_lower() == "_apple") { - // macOS Apple menu. - menu = apple_menu; - } else if (p_menu_root.to_lower() == "_window") { - // macOS Window menu. - menu = window_menu; - } else if (p_menu_root.to_lower() == "_help") { - // macOS Help menu. - menu = help_menu; - } else { - // Submenu. - if (!submenu.has(p_menu_root)) { - NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; - [n_menu setAutoenablesItems:NO]; - [n_menu setDelegate:menu_delegate]; - submenu[p_menu_root].menu = n_menu; - submenu_inv[n_menu] = p_menu_root; - } - menu = submenu[p_menu_root].menu; - } - return menu; -} - DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, const Rect2i &p_rect) { WindowID id; const float scale = screen_get_max_scale(); @@ -144,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. @@ -352,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(), @@ -416,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; @@ -576,7 +517,7 @@ void DisplayServerMacOS::_keyboard_layout_changed(CFNotificationCenterRef center NSImage *DisplayServerMacOS::_convert_to_nsimg(Ref<Image> &p_image) const { p_image->convert(Image::FORMAT_RGBA8); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] - initWithBitmapDataPlanes:NULL + initWithBitmapDataPlanes:nullptr pixelsWide:p_image->get_width() pixelsHigh:p_image->get_height() bitsPerSample:8 @@ -621,42 +562,6 @@ NSCursor *DisplayServerMacOS::_cursor_from_selector(SEL p_selector, SEL p_fallba return [NSCursor arrowCursor]; } -NSMenu *DisplayServerMacOS::get_dock_menu() const { - return dock_menu; -} - -void DisplayServerMacOS::menu_open(NSMenu *p_menu) { - if (submenu_inv.has(p_menu)) { - MenuData &md = submenu[submenu_inv[p_menu]]; - md.is_open = true; - if (md.open.is_valid()) { - Variant ret; - Callable::CallError ce; - - md.open.callp(nullptr, 0, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute menu open callback: %s.", Variant::get_callable_error_text(md.open, nullptr, 0, ce))); - } - } - } -} - -void DisplayServerMacOS::menu_close(NSMenu *p_menu) { - if (submenu_inv.has(p_menu)) { - MenuData &md = submenu[submenu_inv[p_menu]]; - md.is_open = false; - if (md.close.is_valid()) { - Variant ret; - Callable::CallError ce; - - md.close.callp(nullptr, 0, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute menu close callback: %s.", Variant::get_callable_error_text(md.close, nullptr, 0, ce))); - } - } - } -} - void DisplayServerMacOS::menu_callback(id p_sender) { if (![p_sender representedObject]) { return; @@ -840,7 +745,11 @@ void DisplayServerMacOS::window_resize(WindowID p_window, int p_width, int p_hei bool DisplayServerMacOS::has_feature(Feature p_feature) const { switch (p_feature) { - case FEATURE_GLOBAL_MENU: +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif case FEATURE_SUBWINDOWS: //case FEATURE_TOUCHSCREEN: case FEATURE_MOUSE: @@ -849,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: @@ -885,1135 +796,6 @@ Callable DisplayServerMacOS::_help_get_action_callback() const { return help_action_callback; } -bool DisplayServerMacOS::_is_menu_opened(NSMenu *p_menu) const { - if (submenu_inv.has(p_menu)) { - const MenuData &md = submenu[submenu_inv[p_menu]]; - if (md.is_open) { - return true; - } - } - for (NSInteger i = (p_menu == [NSApp mainMenu]) ? 1 : 0; i < [p_menu numberOfItems]; i++) { - const NSMenuItem *menu_item = [p_menu itemAtIndex:i]; - if ([menu_item submenu]) { - if (_is_menu_opened([menu_item submenu])) { - return true; - } - } - } - return false; -} - -int DisplayServerMacOS::_get_system_menu_start(const NSMenu *p_menu) const { - if (p_menu == [NSApp mainMenu]) { // Skip Apple menu. - return 1; - } - if (p_menu == apple_menu || p_menu == window_menu || p_menu == help_menu) { - int count = [p_menu numberOfItems]; - for (int i = 0; i < count; i++) { - NSMenuItem *menu_item = [p_menu itemAtIndex:i]; - if (menu_item.tag == MENU_TAG_START) { - return i + 1; - } - } - } - return 0; -} - -int DisplayServerMacOS::_get_system_menu_count(const NSMenu *p_menu) const { - if (p_menu == [NSApp mainMenu]) { // Skip Apple, Window and Help menu. - return [p_menu numberOfItems] - 3; - } - if (p_menu == apple_menu || p_menu == window_menu || p_menu == help_menu) { - int start = 0; - int count = [p_menu numberOfItems]; - for (int i = 0; i < count; i++) { - NSMenuItem *menu_item = [p_menu itemAtIndex:i]; - if (menu_item.tag == MENU_TAG_START) { - start = i + 1; - } - if (menu_item.tag == MENU_TAG_END) { - return i - start; - } - } - } - return [p_menu numberOfItems]; -} - -NSMenuItem *DisplayServerMacOS::_menu_add_item(const String &p_menu_root, const String &p_label, Key p_accel, int p_index, int *r_out) { - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); - NSMenuItem *menu_item; - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - if (p_index < 0) { - p_index = item_start + item_count; - } else { - p_index += item_start; - p_index = CLAMP(p_index, item_start, item_start + item_count); - } - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; - *r_out = p_index - item_start; - return menu_item; - } - return nullptr; -} - -int DisplayServerMacOS::global_menu_add_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_icon_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; - if (p_icon.is_valid()) { - obj->img = p_icon->get_image(); - obj->img = obj->img->duplicate(); - if (obj->img->is_compressed()) { - obj->img->decompress(); - } - obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); - [menu_item setImage:_convert_to_nsimg(obj->img)]; - } - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_icon_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; - if (p_icon.is_valid()) { - obj->img = p_icon->get_image(); - obj->img = obj->img->duplicate(); - if (obj->img->is_compressed()) { - obj->img->decompress(); - } - obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); - [menu_item setImage:_convert_to_nsimg(obj->img)]; - } - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_radio_check_item(const String &p_menu_root, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_icon_radio_check_item(const String &p_menu_root, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; - if (p_icon.is_valid()) { - obj->img = p_icon->get_image(); - obj->img = obj->img->duplicate(); - if (obj->img->is_compressed()) { - obj->img->decompress(); - } - obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); - [menu_item setImage:_convert_to_nsimg(obj->img)]; - } - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_multistate_item(const String &p_menu_root, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { - _THREAD_SAFE_METHOD_ - - int out = -1; - NSMenuItem *menu_item = _menu_add_item(p_menu_root, p_label, p_accel, p_index, &out); - if (menu_item) { - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = p_callback; - obj->key_callback = p_key_callback; - obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = p_max_states; - obj->state = p_default_state; - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; - [menu_item setRepresentedObject:obj]; - } - return out; -} - -void DisplayServerMacOS::global_menu_set_popup_callbacks(const String &p_menu_root, const Callable &p_open_callback, const Callable &p_close_callback) { - _THREAD_SAFE_METHOD_ - - if (p_menu_root != "" && p_menu_root.to_lower() != "_main" && p_menu_root.to_lower() != "_dock") { - // Submenu. - if (!submenu.has(p_menu_root)) { - NSMenu *n_menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:p_menu_root.utf8().get_data()]]; - [n_menu setAutoenablesItems:NO]; - [n_menu setDelegate:menu_delegate]; - submenu[p_menu_root].menu = n_menu; - submenu_inv[n_menu] = p_menu_root; - } - submenu[p_menu_root].open = p_open_callback; - submenu[p_menu_root].close = p_close_callback; - } -} - -int DisplayServerMacOS::global_menu_add_submenu_item(const String &p_menu_root, const String &p_label, const String &p_submenu, int p_index) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - NSMenu *sub_menu = _get_menu_root(p_submenu); - int out = -1; - if (menu && sub_menu) { - if (sub_menu == menu) { - ERR_PRINT("Can't set submenu to self!"); - return -1; - } - if ([sub_menu supermenu]) { - ERR_PRINT("Can't set submenu to menu that is already a submenu of some other menu!"); - return -1; - } - NSMenuItem *menu_item; - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - if (p_index < 0) { - p_index = item_start + item_count; - } else { - p_index += item_start; - p_index = CLAMP(p_index, item_start, item_start + item_count); - } - menu_item = [menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; - out = p_index - item_start; - - GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = Callable(); - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; - [menu_item setRepresentedObject:obj]; - - [sub_menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; - [menu setSubmenu:sub_menu forItem:menu_item]; - } - return out; -} - -int DisplayServerMacOS::global_menu_add_separator(const String &p_menu_root, int p_index) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - if (menu == [NSApp mainMenu]) { // Do not add separators into main menu. - return -1; - } - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - if (p_index < 0) { - p_index = item_start + item_count; - } else { - p_index += item_start; - p_index = CLAMP(p_index, item_start, item_start + item_count); - } - [menu insertItem:[NSMenuItem separatorItem] atIndex:p_index]; - return p_index - item_start; - } - return -1; -} - -int DisplayServerMacOS::global_menu_get_item_index_from_text(const String &p_menu_root, const String &p_text) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - int item_start = _get_system_menu_start(menu); - return [menu indexOfItemWithTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]] - item_start; - } - - return -1; -} - -int DisplayServerMacOS::global_menu_get_item_index_from_tag(const String &p_menu_root, const Variant &p_tag) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - for (NSInteger i = item_start; i < item_start + item_count; i++) { - const NSMenuItem *menu_item = [menu itemAtIndex:i]; - if (menu_item) { - const GodotMenuItem *obj = [menu_item representedObject]; - if (obj && obj->meta == p_tag) { - return i - item_start; - } - } - } - } - - return -1; -} - -bool DisplayServerMacOS::global_menu_is_item_checked(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, false); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - return ([menu_item state] == NSControlStateValueOn); - } - } - return false; -} - -bool DisplayServerMacOS::global_menu_is_item_checkable(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, false); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->checkable_type == CHECKABLE_TYPE_CHECK_BOX; - } - } - } - return false; -} - -bool DisplayServerMacOS::global_menu_is_item_radio_checkable(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, false); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->checkable_type == CHECKABLE_TYPE_RADIO_BUTTON; - } - } - } - return false; -} - -Callable DisplayServerMacOS::global_menu_get_item_callback(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, Callable()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, Callable()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->callback; - } - } - } - return Callable(); -} - -Callable DisplayServerMacOS::global_menu_get_item_key_callback(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, Callable()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, Callable()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->key_callback; - } - } - } - return Callable(); -} - -Variant DisplayServerMacOS::global_menu_get_item_tag(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, Variant()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, Variant()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->meta; - } - } - } - return Variant(); -} - -String DisplayServerMacOS::global_menu_get_item_text(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, String()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, String()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - return String::utf8([[menu_item title] UTF8String]); - } - } - return String(); -} - -String DisplayServerMacOS::global_menu_get_item_submenu(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, String()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, String()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - NSMenu *sub_menu = [menu_item submenu]; - if (sub_menu && submenu_inv.has(sub_menu)) { - return submenu_inv[sub_menu]; - } - } - } - return String(); -} - -Key DisplayServerMacOS::global_menu_get_item_accelerator(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, Key::NONE); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, Key::NONE); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - String ret = String::utf8([[menu_item keyEquivalent] UTF8String]); - Key keycode = find_keycode(ret); - NSUInteger mask = [menu_item keyEquivalentModifierMask]; - if (mask & NSEventModifierFlagControl) { - keycode |= KeyModifierMask::CTRL; - } - if (mask & NSEventModifierFlagOption) { - keycode |= KeyModifierMask::ALT; - } - if (mask & NSEventModifierFlagShift) { - keycode |= KeyModifierMask::SHIFT; - } - if (mask & NSEventModifierFlagCommand) { - keycode |= KeyModifierMask::META; - } - if (mask & NSEventModifierFlagNumericPad) { - keycode |= KeyModifierMask::KPAD; - } - return keycode; - } - } - return Key::NONE; -} - -bool DisplayServerMacOS::global_menu_is_item_disabled(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, false); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - return ![menu_item isEnabled]; - } - } - return false; -} - -bool DisplayServerMacOS::global_menu_is_item_hidden(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, false); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - return [menu_item isHidden]; - } - } - return false; -} - -String DisplayServerMacOS::global_menu_get_item_tooltip(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, String()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, String()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - return String::utf8([[menu_item toolTip] UTF8String]); - } - } - return String(); -} - -int DisplayServerMacOS::global_menu_get_item_state(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, 0); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->state; - } - } - } - return 0; -} - -int DisplayServerMacOS::global_menu_get_item_max_states(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, 0); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - return obj->max_states; - } - } - } - return 0; -} - -Ref<Texture2D> DisplayServerMacOS::global_menu_get_item_icon(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, Ref<Texture2D>()); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, Ref<Texture2D>()); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - if (obj) { - if (obj->img.is_valid()) { - return ImageTexture::create_from_image(obj->img); - } - } - } - } - return Ref<Texture2D>(); -} - -int DisplayServerMacOS::global_menu_get_item_indentation_level(const String &p_menu_root, int p_idx) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND_V(p_idx < 0, 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND_V(p_idx >= item_start + item_count, 0); - const NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - return [menu_item indentationLevel]; - } - } - return 0; -} - -void DisplayServerMacOS::global_menu_set_item_checked(const String &p_menu_root, int p_idx, bool p_checked) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - if (p_checked) { - [menu_item setState:NSControlStateValueOn]; - } else { - [menu_item setState:NSControlStateValueOff]; - } - } - } -} - -void DisplayServerMacOS::global_menu_set_item_checkable(const String &p_menu_root, int p_idx, bool p_checkable) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_CHECK_BOX : CHECKABLE_TYPE_NONE; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_radio_checkable(const String &p_menu_root, int p_idx, bool p_checkable) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_RADIO_BUTTON : CHECKABLE_TYPE_NONE; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_callback(const String &p_menu_root, int p_idx, const Callable &p_callback) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->callback = p_callback; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_hover_callbacks(const String &p_menu_root, int p_idx, const Callable &p_callback) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->hover_callback = p_callback; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_key_callback(const String &p_menu_root, int p_idx, const Callable &p_key_callback) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->key_callback = p_key_callback; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_tag(const String &p_menu_root, int p_idx, const Variant &p_tag) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->meta = p_tag; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_text(const String &p_menu_root, int p_idx, const String &p_text) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - [menu_item setTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; - NSMenu *sub_menu = [menu_item submenu]; - if (sub_menu) { - [sub_menu setTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; - } - } - } -} - -void DisplayServerMacOS::global_menu_set_item_submenu(const String &p_menu_root, int p_idx, const String &p_submenu) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu && p_submenu.is_empty()) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - if ([menu_item submenu] && _is_menu_opened([menu_item submenu])) { - ERR_PRINT("Can't remove open menu!"); - return; - } - [menu setSubmenu:nil forItem:menu_item]; - } - return; - } - - NSMenu *sub_menu = _get_menu_root(p_submenu); - if (menu && sub_menu) { - if (sub_menu == menu) { - ERR_PRINT("Can't set submenu to self!"); - return; - } - if ([sub_menu supermenu]) { - ERR_PRINT("Can't set submenu to menu that is already a submenu of some other menu!"); - return; - } - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - [menu setSubmenu:sub_menu forItem:menu_item]; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_accelerator(const String &p_menu_root, int p_idx, Key p_keycode) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - if (p_keycode == Key::NONE) { - [menu_item setKeyEquivalent:@""]; - } else { - [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_keycode)]; - String keycode = KeyMappingMacOS::keycode_get_native_string(p_keycode & KeyModifierMask::CODE_MASK); - [menu_item setKeyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; - } - } - } -} - -void DisplayServerMacOS::global_menu_set_item_disabled(const String &p_menu_root, int p_idx, bool p_disabled) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - [menu_item setEnabled:(!p_disabled)]; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_hidden(const String &p_menu_root, int p_idx, bool p_hidden) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - [menu_item setHidden:p_hidden]; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_tooltip(const String &p_menu_root, int p_idx, const String &p_tooltip) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - [menu_item setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_state(const String &p_menu_root, int p_idx, int p_state) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->state = p_state; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_max_states(const String &p_menu_root, int p_idx, int p_max_states) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - obj->max_states = p_max_states; - } - } -} - -void DisplayServerMacOS::global_menu_set_item_icon(const String &p_menu_root, int p_idx, const Ref<Texture2D> &p_icon) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - GodotMenuItem *obj = [menu_item representedObject]; - ERR_FAIL_NULL(obj); - if (p_icon.is_valid()) { - obj->img = p_icon->get_image(); - obj->img = obj->img->duplicate(); - if (obj->img->is_compressed()) { - obj->img->decompress(); - } - obj->img->resize(16, 16, Image::INTERPOLATE_LANCZOS); - [menu_item setImage:_convert_to_nsimg(obj->img)]; - } else { - obj->img = Ref<Image>(); - [menu_item setImage:nil]; - } - } - } -} - -void DisplayServerMacOS::global_menu_set_item_indentation_level(const String &p_menu_root, int p_idx, int p_level) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if (menu_item) { - [menu_item setIndentationLevel:p_level]; - } - } -} - -int DisplayServerMacOS::global_menu_get_item_count(const String &p_menu_root) const { - _THREAD_SAFE_METHOD_ - - const NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - return _get_system_menu_count(menu); - } else { - return 0; - } -} - -void DisplayServerMacOS::global_menu_remove_item(const String &p_menu_root, int p_idx) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - ERR_FAIL_COND(p_idx < 0); - int item_start = _get_system_menu_start(menu); - int item_count = _get_system_menu_count(menu); - p_idx += item_start; - ERR_FAIL_COND(p_idx >= item_start + item_count); - NSMenuItem *menu_item = [menu itemAtIndex:p_idx]; - if ([menu_item submenu] && _is_menu_opened([menu_item submenu])) { - ERR_PRINT("Can't remove open menu!"); - return; - } - [menu removeItemAtIndex:p_idx]; - } -} - -void DisplayServerMacOS::global_menu_clear(const String &p_menu_root) { - _THREAD_SAFE_METHOD_ - - NSMenu *menu = _get_menu_root(p_menu_root); - if (menu) { - if (_is_menu_opened(menu)) { - ERR_PRINT("Can't remove open menu!"); - return; - } - - if (menu == apple_menu) { - int start = _get_system_menu_start(apple_menu); - int count = _get_system_menu_count(apple_menu); - for (int i = start + count - 1; i >= start; i--) { - [apple_menu removeItemAtIndex:i]; - } - } else if (menu == window_menu) { - int start = _get_system_menu_start(window_menu); - int count = _get_system_menu_count(window_menu); - for (int i = start + count - 1; i >= start; i--) { - [window_menu removeItemAtIndex:i]; - } - } else if (menu == help_menu) { - int start = _get_system_menu_start(help_menu); - int count = _get_system_menu_count(help_menu); - for (int i = start + count - 1; i >= start; i--) { - [help_menu removeItemAtIndex:i]; - } - } else { - [menu removeAllItems]; - } - - // Restore Apple, Window and Help menu. - if (menu == [NSApp mainMenu]) { - NSMenuItem *menu_item = [menu addItemWithTitle:@"" action:nil keyEquivalent:@""]; - [menu setSubmenu:apple_menu forItem:menu_item]; - - menu_item = [menu addItemWithTitle:@"Window" action:nil keyEquivalent:@""]; - [menu setSubmenu:window_menu forItem:menu_item]; - - menu_item = [menu addItemWithTitle:@"Help" action:nil keyEquivalent:@""]; - [menu setSubmenu:help_menu forItem:menu_item]; - } - - if (submenu.has(p_menu_root)) { - submenu_inv.erase(submenu[p_menu_root].menu); - submenu.erase(p_menu_root); - } - } -} - -Dictionary DisplayServerMacOS::global_menu_get_system_menu_roots() const { - Dictionary out; - out["_dock"] = "@Dock"; - out["_apple"] = "@Apple"; - out["_window"] = "Window"; - out["_help"] = "Help"; - return out; -} - bool DisplayServerMacOS::tts_is_speaking() const { ERR_FAIL_NULL_V_MSG(tts, false, "Enable the \"audio/general/text_to_speech\" project setting to use text-to-speech."); return [tts isSpeaking]; @@ -2686,7 +1468,7 @@ Ref<Image> DisplayServerMacOS::clipboard_get_image() const { return image; } NSBitmapImageRep *bitmap = [NSBitmapImageRep imageRepWithData:data]; - NSData *pngData = [bitmap representationUsingType:NSPNGFileType properties:@{}]; + NSData *pngData = [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; image.instantiate(); PNGDriverCommon::png_to_image((const uint8_t *)pngData.bytes, pngData.length, false, image); return image; @@ -3099,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) { @@ -3213,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; } @@ -3302,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) { @@ -3531,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.max(Vector2i(12, 12)); if (wd.window_button_view) { [wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)]; } @@ -4037,39 +2826,10 @@ void DisplayServerMacOS::cursor_set_custom_image(const Ref<Resource> &p_cursor, cursors_cache.erase(p_shape); } - Ref<Texture2D> texture = p_cursor; - ERR_FAIL_COND(!texture.is_valid()); - Ref<AtlasTexture> atlas_texture = p_cursor; - Size2 texture_size; Rect2 atlas_rect; - - if (atlas_texture.is_valid()) { - texture = atlas_texture->get_atlas(); - - atlas_rect.size.width = texture->get_width(); - atlas_rect.size.height = texture->get_height(); - atlas_rect.position.x = atlas_texture->get_region().position.x; - atlas_rect.position.y = atlas_texture->get_region().position.y; - - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - - Ref<Image> image = texture->get_image(); - - ERR_FAIL_COND(!image.is_valid()); - if (image->is_compressed()) { - image = image->duplicate(true); - Error err = image->decompress(); - ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); - } + Ref<Image> image = _get_cursor_image_from_resource(p_cursor, p_hotspot, atlas_rect); + ERR_FAIL_COND(image.is_null()); + Vector2i texture_size = image->get_size(); NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nullptr @@ -4092,7 +2852,7 @@ void DisplayServerMacOS::cursor_set_custom_image(const Ref<Resource> &p_cursor, int row_index = floor(i / texture_size.width) + atlas_rect.position.y; int column_index = (i % int(texture_size.width)) + atlas_rect.position.x; - if (atlas_texture.is_valid()) { + if (atlas_rect.has_area()) { column_index = MIN(column_index, atlas_rect.size.width - 1); row_index = MIN(row_index, atlas_rect.size.height - 1); } @@ -4102,7 +2862,7 @@ void DisplayServerMacOS::cursor_set_custom_image(const Ref<Resource> &p_cursor, uint8_t alpha = (color >> 24) & 0xFF; pixels[i * 4 + 0] = ((color >> 16) & 0xFF) * alpha / 255; pixels[i * 4 + 1] = ((color >> 8) & 0xFF) * alpha / 255; - pixels[i * 4 + 2] = ((color)&0xFF) * alpha / 255; + pixels[i * 4 + 2] = ((color) & 0xFF) * alpha / 255; pixels[i * 4 + 3] = alpha; } @@ -4225,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 @@ -4258,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) { @@ -4284,6 +3046,8 @@ void DisplayServerMacOS::process_events() { } } } + + _THREAD_SAFE_UNLOCK_ } void DisplayServerMacOS::force_process_and_drop_events() { @@ -4295,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() { @@ -4720,6 +3489,8 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM tts = [[TTS_MacOS alloc] init]; } + native_menu = memnew(NativeMenuMacOS); + NSMenuItem *menu_item; NSString *title; @@ -4731,47 +3502,47 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM menu_delegate = [[GodotMenuDelegate alloc] init]; // Setup Dock menu. - dock_menu = [[NSMenu alloc] initWithTitle:@"_dock"]; + NSMenu *dock_menu = [[NSMenu alloc] initWithTitle:@"_dock"]; [dock_menu setAutoenablesItems:NO]; [dock_menu setDelegate:menu_delegate]; // Setup Apple menu. - apple_menu = [[NSMenu alloc] initWithTitle:@""]; + NSMenu *application_menu = [[NSMenu alloc] initWithTitle:@""]; title = [NSString stringWithFormat:NSLocalizedString(@"About %@", nil), nsappname]; - [apple_menu addItemWithTitle:title action:@selector(showAbout:) keyEquivalent:@""]; - [apple_menu setAutoenablesItems:NO]; - [apple_menu setDelegate:menu_delegate]; + [application_menu addItemWithTitle:title action:@selector(showAbout:) keyEquivalent:@""]; + [application_menu setAutoenablesItems:NO]; + [application_menu setDelegate:menu_delegate]; - [apple_menu addItem:[NSMenuItem separatorItem]]; + [application_menu addItem:[NSMenuItem separatorItem]]; - menu_item = [apple_menu addItemWithTitle:@"_start_" action:nil keyEquivalent:@""]; + menu_item = [application_menu addItemWithTitle:@"_start_" action:nil keyEquivalent:@""]; menu_item.hidden = YES; menu_item.tag = MENU_TAG_START; - menu_item = [apple_menu addItemWithTitle:@"_end_" action:nil keyEquivalent:@""]; + menu_item = [application_menu addItemWithTitle:@"_end_" action:nil keyEquivalent:@""]; menu_item.hidden = YES; menu_item.tag = MENU_TAG_END; NSMenu *services = [[NSMenu alloc] initWithTitle:@""]; - menu_item = [apple_menu addItemWithTitle:NSLocalizedString(@"Services", nil) action:nil keyEquivalent:@""]; - [apple_menu setSubmenu:services forItem:menu_item]; + menu_item = [application_menu addItemWithTitle:NSLocalizedString(@"Services", nil) action:nil keyEquivalent:@""]; + [application_menu setSubmenu:services forItem:menu_item]; [NSApp setServicesMenu:services]; - [apple_menu addItem:[NSMenuItem separatorItem]]; + [application_menu addItem:[NSMenuItem separatorItem]]; title = [NSString stringWithFormat:NSLocalizedString(@"Hide %@", nil), nsappname]; - [apple_menu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; + [application_menu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@"h"]; - menu_item = [apple_menu addItemWithTitle:NSLocalizedString(@"Hide Others", nil) action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; + menu_item = [application_menu addItemWithTitle:NSLocalizedString(@"Hide Others", nil) action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; [menu_item setKeyEquivalentModifierMask:(NSEventModifierFlagOption | NSEventModifierFlagCommand)]; - [apple_menu addItemWithTitle:NSLocalizedString(@"Show all", nil) action:@selector(unhideAllApplications:) keyEquivalent:@""]; + [application_menu addItemWithTitle:NSLocalizedString(@"Show all", nil) action:@selector(unhideAllApplications:) keyEquivalent:@""]; - [apple_menu addItem:[NSMenuItem separatorItem]]; + [application_menu addItem:[NSMenuItem separatorItem]]; title = [NSString stringWithFormat:NSLocalizedString(@"Quit %@", nil), nsappname]; - [apple_menu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; + [application_menu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"]; - window_menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Window", nil)]; + NSMenu *window_menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Window", nil)]; [window_menu addItemWithTitle:NSLocalizedString(@"Minimize", nil) action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [window_menu addItemWithTitle:NSLocalizedString(@"Zoom", nil) action:@selector(performZoom:) keyEquivalent:@""]; [window_menu addItem:[NSMenuItem separatorItem]]; @@ -4784,7 +3555,7 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM menu_item.hidden = YES; menu_item.tag = MENU_TAG_END; - help_menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Help", nil)]; + NSMenu *help_menu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Help", nil)]; menu_item = [help_menu addItemWithTitle:@"_start_" action:nil keyEquivalent:@""]; menu_item.hidden = YES; menu_item.tag = MENU_TAG_START; @@ -4798,7 +3569,7 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM // Add items to the menu bar. NSMenu *main_menu = [NSApp mainMenu]; menu_item = [main_menu addItemWithTitle:@"" action:nil keyEquivalent:@""]; - [main_menu setSubmenu:apple_menu forItem:menu_item]; + [main_menu setSubmenu:application_menu forItem:menu_item]; menu_item = [main_menu addItemWithTitle:NSLocalizedString(@"Window", nil) action:nil keyEquivalent:@""]; [main_menu setSubmenu:window_menu forItem:menu_item]; @@ -4808,6 +3579,8 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM [main_menu setAutoenablesItems:NO]; + native_menu->_register_system_menus(main_menu, application_menu, window_menu, help_menu, dock_menu); + //!!!!!!!!!!!!!!!!!!!!!!!!!! //TODO - do Vulkan and OpenGL support checks, driver selection and fallback rendering_driver = p_rendering_driver; @@ -4905,10 +3678,16 @@ DisplayServerMacOS::~DisplayServerMacOS() { } // Destroy all status indicators. - for (HashMap<IndicatorID, IndicatorData>::Iterator E = indicators.begin(); E;) { + for (HashMap<IndicatorID, IndicatorData>::Iterator E = indicators.begin(); E; ++E) { [[NSStatusBar systemStatusBar] removeStatusItem:E->value.item]; } + // Destroy native menu. + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } + // Destroy all windows. for (HashMap<WindowID, WindowData>::Iterator E = windows.begin(); E;) { HashMap<WindowID, WindowData>::Iterator F = E; 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 9cc57e4066..d75def9b50 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; } 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_application.mm b/platform/macos/godot_application.mm index e3a744caa2..f5e1bb43bf 100644 --- a/platform/macos/godot_application.mm +++ b/platform/macos/godot_application.mm @@ -100,7 +100,7 @@ } - (void)sendEvent:(NSEvent *)event { - if ([event type] == NSSystemDefined && [event subtype] == 8) { + if ([event type] == NSEventTypeSystemDefined && [event subtype] == 8) { int keyCode = (([event data1] & 0xFFFF0000) >> 16); int keyFlags = ([event data1] & 0x0000FFFF); int keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA; diff --git a/platform/macos/godot_application_delegate.mm b/platform/macos/godot_application_delegate.mm index 2e76d4aa97..02466bab97 100644 --- a/platform/macos/godot_application_delegate.mm +++ b/platform/macos/godot_application_delegate.mm @@ -31,6 +31,7 @@ #include "godot_application_delegate.h" #include "display_server_macos.h" +#include "native_menu_macos.h" #include "os_macos.h" @implementation GodotApplicationDelegate @@ -125,7 +126,8 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notice { NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; - NSString *nsbundleid_env = [NSString stringWithUTF8String:getenv("__CFBundleIdentifier")]; + const char *bundled_id = getenv("__CFBundleIdentifier"); + NSString *nsbundleid_env = [NSString stringWithUTF8String:(bundled_id != nullptr) ? bundled_id : ""]; NSString *nsbundleid = [[NSBundle mainBundle] bundleIdentifier]; if (nsappname == nil || isatty(STDOUT_FILENO) || isatty(STDIN_FILENO) || isatty(STDERR_FILENO) || ![nsbundleid isEqualToString:nsbundleid_env]) { // If the executable is started from terminal or is not bundled, macOS WindowServer won't register and activate app window correctly (menu and title bar are grayed out and input ignored). @@ -210,9 +212,9 @@ } - (NSMenu *)applicationDockMenu:(NSApplication *)sender { - DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); - if (ds) { - return ds->get_dock_menu(); + if (NativeMenu::get_singleton()) { + NativeMenuMacOS *nmenu = (NativeMenuMacOS *)NativeMenu::get_singleton(); + return nmenu->_get_dock_menu(); } else { return nullptr; } 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_menu_delegate.mm b/platform/macos/godot_menu_delegate.mm index dd57d9f251..5c1e849715 100644 --- a/platform/macos/godot_menu_delegate.mm +++ b/platform/macos/godot_menu_delegate.mm @@ -33,6 +33,7 @@ #include "display_server_macos.h" #include "godot_menu_item.h" #include "key_mapping_macos.h" +#include "native_menu_macos.h" @implementation GodotMenuDelegate @@ -40,16 +41,16 @@ } - (void)menuNeedsUpdate:(NSMenu *)menu { - if (DisplayServer::get_singleton()) { - DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); - ds->menu_open(menu); + if (NativeMenu::get_singleton()) { + NativeMenuMacOS *nmenu = (NativeMenuMacOS *)NativeMenu::get_singleton(); + nmenu->_menu_open(menu); } } - (void)menuDidClose:(NSMenu *)menu { - if (DisplayServer::get_singleton()) { - DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); - ds->menu_close(menu); + if (NativeMenu::get_singleton()) { + NativeMenuMacOS *nmenu = (NativeMenuMacOS *)NativeMenu::get_singleton(); + nmenu->_menu_close(menu); } } diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h index b816ea1b3a..b6e2d41c08 100644 --- a/platform/macos/godot_menu_item.h +++ b/platform/macos/godot_menu_item.h @@ -36,6 +36,9 @@ #import <AppKit/AppKit.h> #import <Foundation/Foundation.h> +#define MENU_TAG_START 0x00004447 +#define MENU_TAG_END 0xFFFF4447 + enum GlobalMenuCheckType { CHECKABLE_TYPE_NONE, CHECKABLE_TYPE_CHECK_BOX, 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/joypad_macos.cpp b/platform/macos/joypad_macos.cpp deleted file mode 100644 index 1fcd636a4b..0000000000 --- a/platform/macos/joypad_macos.cpp +++ /dev/null @@ -1,619 +0,0 @@ -/**************************************************************************/ -/* joypad_macos.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 "joypad_macos.h" - -#include <machine/endian.h> - -#define GODOT_JOY_LOOP_RUN_MODE CFSTR("GodotJoypad") - -static JoypadMacOS *self = nullptr; - -joypad::joypad() { - ff_constant_force.lMagnitude = 10000; - ff_effect.dwDuration = 0; - ff_effect.dwSamplePeriod = 0; - ff_effect.dwGain = 10000; - ff_effect.dwFlags = FFEFF_OBJECTOFFSETS; - ff_effect.dwTriggerButton = FFEB_NOTRIGGER; - ff_effect.dwStartDelay = 0; - ff_effect.dwTriggerRepeatInterval = 0; - ff_effect.lpEnvelope = nullptr; - ff_effect.cbTypeSpecificParams = sizeof(FFCONSTANTFORCE); - ff_effect.lpvTypeSpecificParams = &ff_constant_force; - ff_effect.dwSize = sizeof(ff_effect); -} - -void joypad::free() { - if (device_ref) { - IOHIDDeviceUnscheduleFromRunLoop(device_ref, CFRunLoopGetCurrent(), GODOT_JOY_LOOP_RUN_MODE); - } - if (ff_device) { - FFDeviceReleaseEffect(ff_device, ff_object); - FFReleaseDevice(ff_device); - ff_device = nullptr; - memfree(ff_axes); - memfree(ff_directions); - } -} - -bool joypad::has_element(IOHIDElementCookie p_cookie, Vector<rec_element> *p_list) const { - for (int i = 0; i < p_list->size(); i++) { - if (p_cookie == p_list->get(i).cookie) { - return true; - } - } - return false; -} - -int joypad::get_hid_element_state(rec_element *p_element) const { - int value = 0; - if (p_element && p_element->ref) { - IOHIDValueRef valueRef; - if (IOHIDDeviceGetValue(device_ref, p_element->ref, &valueRef) == kIOReturnSuccess) { - value = (SInt32)IOHIDValueGetIntegerValue(valueRef); - - // Record min and max for auto calibration. - if (value < p_element->min) { - p_element->min = value; - } - if (value > p_element->max) { - p_element->max = value; - } - } - } - return value; -} - -void joypad::add_hid_element(IOHIDElementRef p_element) { - const CFTypeID elementTypeID = p_element ? CFGetTypeID(p_element) : 0; - - if (p_element && (elementTypeID == IOHIDElementGetTypeID())) { - const IOHIDElementCookie cookie = IOHIDElementGetCookie(p_element); - const uint32_t usagePage = IOHIDElementGetUsagePage(p_element); - const uint32_t usage = IOHIDElementGetUsage(p_element); - Vector<rec_element> *list = nullptr; - - switch (IOHIDElementGetType(p_element)) { - case kIOHIDElementTypeInput_Misc: - case kIOHIDElementTypeInput_Button: - case kIOHIDElementTypeInput_Axis: { - switch (usagePage) { - case kHIDPage_GenericDesktop: - switch (usage) { - case kHIDUsage_GD_X: - case kHIDUsage_GD_Y: - case kHIDUsage_GD_Z: - case kHIDUsage_GD_Rx: - case kHIDUsage_GD_Ry: - case kHIDUsage_GD_Rz: - case kHIDUsage_GD_Slider: - case kHIDUsage_GD_Dial: - case kHIDUsage_GD_Wheel: - if (!has_element(cookie, &axis_elements)) { - list = &axis_elements; - } - break; - - case kHIDUsage_GD_Hatswitch: - if (!has_element(cookie, &hat_elements)) { - list = &hat_elements; - } - break; - case kHIDUsage_GD_DPadUp: - case kHIDUsage_GD_DPadDown: - case kHIDUsage_GD_DPadRight: - case kHIDUsage_GD_DPadLeft: - case kHIDUsage_GD_Start: - case kHIDUsage_GD_Select: - if (!has_element(cookie, &button_elements)) { - list = &button_elements; - } - break; - } - break; - - case kHIDPage_Simulation: - switch (usage) { - case kHIDUsage_Sim_Rudder: - case kHIDUsage_Sim_Throttle: - case kHIDUsage_Sim_Accelerator: - case kHIDUsage_Sim_Brake: - if (!has_element(cookie, &axis_elements)) { - list = &axis_elements; - } - break; - - default: - break; - } - break; - - case kHIDPage_Button: - case kHIDPage_Consumer: - if (!has_element(cookie, &button_elements)) { - list = &button_elements; - } - break; - - default: - break; - } - } break; - - case kIOHIDElementTypeCollection: { - CFArrayRef array = IOHIDElementGetChildren(p_element); - if (array) { - add_hid_elements(array); - } - } break; - - default: - break; - } - - if (list) { // Add to list. - rec_element element; - - element.ref = p_element; - element.usage = usage; - - element.min = (SInt32)IOHIDElementGetLogicalMin(p_element); - element.max = (SInt32)IOHIDElementGetLogicalMax(p_element); - element.cookie = IOHIDElementGetCookie(p_element); - list->push_back(element); - list->sort_custom<rec_element::Comparator>(); - } - } -} - -static void hid_element_added(const void *p_value, void *p_parameter) { - joypad *joy = static_cast<joypad *>(p_parameter); - joy->add_hid_element((IOHIDElementRef)p_value); -} - -void joypad::add_hid_elements(CFArrayRef p_array) { - CFRange range = { 0, CFArrayGetCount(p_array) }; - CFArrayApplyFunction(p_array, range, hid_element_added, this); -} - -static void joypad_removed_callback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject) { - self->_device_removed(res, ioHIDDeviceObject); -} - -static void joypad_added_callback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject) { - self->_device_added(res, ioHIDDeviceObject); -} - -static bool is_joypad(IOHIDDeviceRef p_device_ref) { - int usage_page = 0; - int usage = 0; - CFTypeRef refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDPrimaryUsagePageKey)); - if (refCF) { - CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &usage_page); - } - if (usage_page != kHIDPage_GenericDesktop) { - return false; - } - - refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDPrimaryUsageKey)); - if (refCF) { - CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &usage); - } - if ((usage != kHIDUsage_GD_Joystick && - usage != kHIDUsage_GD_GamePad && - usage != kHIDUsage_GD_MultiAxisController)) { - return false; - } - return true; -} - -void JoypadMacOS::_device_added(IOReturn p_res, IOHIDDeviceRef p_device) { - if (p_res != kIOReturnSuccess || have_device(p_device)) { - return; - } - - joypad new_joypad; - if (is_joypad(p_device)) { - configure_joypad(p_device, &new_joypad); -#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060 - if (IOHIDDeviceGetService) { -#endif - const io_service_t ioservice = IOHIDDeviceGetService(p_device); - if ((ioservice) && (FFIsForceFeedback(ioservice) == FF_OK) && new_joypad.config_force_feedback(ioservice)) { - new_joypad.ffservice = ioservice; - } -#if MAC_OS_X_VERSION_MIN_REQUIRED < 1060 - } -#endif - device_list.push_back(new_joypad); - } - IOHIDDeviceScheduleWithRunLoop(p_device, CFRunLoopGetCurrent(), GODOT_JOY_LOOP_RUN_MODE); -} - -void JoypadMacOS::_device_removed(IOReturn p_res, IOHIDDeviceRef p_device) { - int device = get_joy_ref(p_device); - ERR_FAIL_COND(device == -1); - - input->joy_connection_changed(device_list[device].id, false, ""); - device_list.write[device].free(); - device_list.remove_at(device); -} - -static String _hex_str(uint8_t p_byte) { - static const char *dict = "0123456789abcdef"; - char ret[3]; - ret[2] = 0; - - ret[0] = dict[p_byte >> 4]; - ret[1] = dict[p_byte & 0xF]; - - return ret; -} - -bool JoypadMacOS::configure_joypad(IOHIDDeviceRef p_device_ref, joypad *p_joy) { - p_joy->device_ref = p_device_ref; - // Get device name. - String name; - char c_name[256]; - CFTypeRef refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDProductKey)); - if (!refCF) { - refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDManufacturerKey)); - } - if ((!refCF) || (!CFStringGetCString((CFStringRef)refCF, c_name, sizeof(c_name), kCFStringEncodingUTF8))) { - name = "Unidentified Joypad"; - } else { - name = c_name; - } - - int id = input->get_unused_joy_id(); - ERR_FAIL_COND_V(id == -1, false); - p_joy->id = id; - int vendor = 0; - refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDVendorIDKey)); - if (refCF) { - CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &vendor); - } - - int product_id = 0; - refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDProductIDKey)); - if (refCF) { - CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &product_id); - } - - int version = 0; - refCF = IOHIDDeviceGetProperty(p_device_ref, CFSTR(kIOHIDVersionNumberKey)); - if (refCF) { - CFNumberGetValue((CFNumberRef)refCF, kCFNumberSInt32Type, &version); - } - - if (vendor && product_id) { - char uid[128]; - snprintf(uid, 128, "%08x%08x%08x%08x", OSSwapHostToBigInt32(3), OSSwapHostToBigInt32(vendor), OSSwapHostToBigInt32(product_id), OSSwapHostToBigInt32(version)); - input->joy_connection_changed(id, true, name, uid); - } else { - // Bluetooth device. - String guid = "05000000"; - for (int i = 0; i < 12; i++) { - if (i < name.size()) { - guid += _hex_str(name[i]); - } else { - guid += "00"; - } - } - input->joy_connection_changed(id, true, name, guid); - } - - CFArrayRef array = IOHIDDeviceCopyMatchingElements(p_device_ref, nullptr, kIOHIDOptionsTypeNone); - if (array) { - p_joy->add_hid_elements(array); - CFRelease(array); - } - // Xbox controller hat values start at 1 rather than 0. - p_joy->offset_hat = vendor == 0x45e && - (product_id == 0x0b05 || - product_id == 0x02e0 || - product_id == 0x02fd || - product_id == 0x0b13); - - return true; -} - -#define FF_ERR() \ - { \ - if (ret != FF_OK) { \ - FFReleaseDevice(ff_device); \ - ff_device = nullptr; \ - return false; \ - } \ - } -bool joypad::config_force_feedback(io_service_t p_service) { - HRESULT ret = FFCreateDevice(p_service, &ff_device); - ERR_FAIL_COND_V(ret != FF_OK, false); - - ret = FFDeviceSendForceFeedbackCommand(ff_device, FFSFFC_RESET); - FF_ERR(); - - ret = FFDeviceSendForceFeedbackCommand(ff_device, FFSFFC_SETACTUATORSON); - FF_ERR(); - - if (check_ff_features()) { - ret = FFDeviceCreateEffect(ff_device, kFFEffectType_ConstantForce_ID, &ff_effect, &ff_object); - FF_ERR(); - return true; - } - FFReleaseDevice(ff_device); - ff_device = nullptr; - return false; -} -#undef FF_ERR - -#define TEST_FF(ff) (features.supportedEffects & (ff)) -bool joypad::check_ff_features() { - FFCAPABILITIES features; - HRESULT ret = FFDeviceGetForceFeedbackCapabilities(ff_device, &features); - if (ret == FF_OK && (features.supportedEffects & FFCAP_ET_CONSTANTFORCE)) { - uint32_t val; - ret = FFDeviceGetForceFeedbackProperty(ff_device, FFPROP_FFGAIN, &val, sizeof(val)); - if (ret != FF_OK) { - return false; - } - int num_axes = features.numFfAxes; - ff_axes = (DWORD *)memalloc(sizeof(DWORD) * num_axes); - ff_directions = (LONG *)memalloc(sizeof(LONG) * num_axes); - - for (int i = 0; i < num_axes; i++) { - ff_axes[i] = features.ffAxes[i]; - ff_directions[i] = 0; - } - - ff_effect.cAxes = num_axes; - ff_effect.rgdwAxes = ff_axes; - ff_effect.rglDirection = ff_directions; - return true; - } - return false; -} - -static BitField<HatMask> process_hat_value(int p_min, int p_max, int p_value, bool p_offset_hat) { - int range = (p_max - p_min + 1); - int value = p_value - p_min; - BitField<HatMask> hat_value; - if (range == 4) { - value *= 2; - } - if (p_offset_hat) { - value -= 1; - } - - switch (value) { - case 0: - hat_value.set_flag(HatMask::UP); - break; - case 1: - hat_value.set_flag(HatMask::UP); - hat_value.set_flag(HatMask::RIGHT); - break; - case 2: - hat_value.set_flag(HatMask::RIGHT); - break; - case 3: - hat_value.set_flag(HatMask::DOWN); - hat_value.set_flag(HatMask::RIGHT); - break; - case 4: - hat_value.set_flag(HatMask::DOWN); - break; - case 5: - hat_value.set_flag(HatMask::DOWN); - hat_value.set_flag(HatMask::LEFT); - break; - case 6: - hat_value.set_flag(HatMask::LEFT); - break; - case 7: - hat_value.set_flag(HatMask::UP); - hat_value.set_flag(HatMask::LEFT); - break; - default: - break; - } - return hat_value; -} - -void JoypadMacOS::poll_joypads() const { - while (CFRunLoopRunInMode(GODOT_JOY_LOOP_RUN_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) { - // No-op. Pending callbacks will fire. - } -} - -static float axis_correct(int p_value, int p_min, int p_max) { - // Convert to a value between -1.0f and 1.0f. - return 2.0f * (p_value - p_min) / (p_max - p_min) - 1.0f; -} - -void JoypadMacOS::process_joypads() { - poll_joypads(); - - for (int i = 0; i < device_list.size(); i++) { - joypad &joy = device_list.write[i]; - - for (int j = 0; j < joy.axis_elements.size(); j++) { - rec_element &elem = joy.axis_elements.write[j]; - int value = joy.get_hid_element_state(&elem); - input->joy_axis(joy.id, (JoyAxis)j, axis_correct(value, elem.min, elem.max)); - } - for (int j = 0; j < joy.button_elements.size(); j++) { - int value = joy.get_hid_element_state(&joy.button_elements.write[j]); - input->joy_button(joy.id, (JoyButton)j, (value >= 1)); - } - for (int j = 0; j < joy.hat_elements.size(); j++) { - rec_element &elem = joy.hat_elements.write[j]; - int value = joy.get_hid_element_state(&elem); - BitField<HatMask> hat_value = process_hat_value(elem.min, elem.max, value, joy.offset_hat); - input->joy_hat(joy.id, hat_value); - } - - if (joy.ffservice) { - uint64_t timestamp = input->get_joy_vibration_timestamp(joy.id); - if (timestamp > joy.ff_timestamp) { - Vector2 strength = input->get_joy_vibration_strength(joy.id); - float duration = input->get_joy_vibration_duration(joy.id); - if (strength.x == 0 && strength.y == 0) { - joypad_vibration_stop(joy.id, timestamp); - } else { - float gain = MAX(strength.x, strength.y); - joypad_vibration_start(joy.id, gain, duration, timestamp); - } - } - } - } -} - -void JoypadMacOS::joypad_vibration_start(int p_id, float p_magnitude, float p_duration, uint64_t p_timestamp) { - joypad *joy = &device_list.write[get_joy_index(p_id)]; - joy->ff_timestamp = p_timestamp; - joy->ff_effect.dwDuration = p_duration * FF_SECONDS; - joy->ff_effect.dwGain = p_magnitude * FF_FFNOMINALMAX; - FFEffectSetParameters(joy->ff_object, &joy->ff_effect, FFEP_DURATION | FFEP_GAIN); - FFEffectStart(joy->ff_object, 1, 0); -} - -void JoypadMacOS::joypad_vibration_stop(int p_id, uint64_t p_timestamp) { - joypad *joy = &device_list.write[get_joy_index(p_id)]; - joy->ff_timestamp = p_timestamp; - FFEffectStop(joy->ff_object); -} - -int JoypadMacOS::get_joy_index(int p_id) const { - for (int i = 0; i < device_list.size(); i++) { - if (device_list[i].id == p_id) { - return i; - } - } - return -1; -} - -int JoypadMacOS::get_joy_ref(IOHIDDeviceRef p_device) const { - for (int i = 0; i < device_list.size(); i++) { - if (device_list[i].device_ref == p_device) { - return i; - } - } - return -1; -} - -bool JoypadMacOS::have_device(IOHIDDeviceRef p_device) const { - for (int i = 0; i < device_list.size(); i++) { - if (device_list[i].device_ref == p_device) { - return true; - } - } - return false; -} - -static CFDictionaryRef create_match_dictionary(const UInt32 page, const UInt32 usage, int *okay) { - CFDictionaryRef retval = nullptr; - CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page); - CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage); - - if (pageNumRef && usageNumRef) { - const void *keys[2] = { (void *)CFSTR(kIOHIDDeviceUsagePageKey), (void *)CFSTR(kIOHIDDeviceUsageKey) }; - const void *vals[2] = { (void *)pageNumRef, (void *)usageNumRef }; - retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - } - - if (pageNumRef) { - CFRelease(pageNumRef); - } - if (usageNumRef) { - CFRelease(usageNumRef); - } - - if (!retval) { - *okay = 0; - } - - return retval; -} - -void JoypadMacOS::config_hid_manager(CFArrayRef p_matching_array) const { - CFRunLoopRef runloop = CFRunLoopGetCurrent(); - IOReturn ret = IOHIDManagerOpen(hid_manager, kIOHIDOptionsTypeNone); - ERR_FAIL_COND(ret != kIOReturnSuccess); - - IOHIDManagerSetDeviceMatchingMultiple(hid_manager, p_matching_array); - IOHIDManagerRegisterDeviceMatchingCallback(hid_manager, joypad_added_callback, nullptr); - IOHIDManagerRegisterDeviceRemovalCallback(hid_manager, joypad_removed_callback, nullptr); - IOHIDManagerScheduleWithRunLoop(hid_manager, runloop, GODOT_JOY_LOOP_RUN_MODE); - - while (CFRunLoopRunInMode(GODOT_JOY_LOOP_RUN_MODE, 0, TRUE) == kCFRunLoopRunHandledSource) { - // No-op. Callback fires once per existing device. - } -} - -JoypadMacOS::JoypadMacOS(Input *in) { - self = this; - input = in; - - int okay = 1; - const void *vals[] = { - (void *)create_match_dictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay), - (void *)create_match_dictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay), - (void *)create_match_dictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay), - }; - const size_t n_elements = sizeof(vals) / sizeof(vals[0]); - CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, n_elements, &kCFTypeArrayCallBacks) : nullptr; - - for (size_t i = 0; i < n_elements; i++) { - if (vals[i]) { - CFRelease((CFTypeRef)vals[i]); - } - } - - if (array) { - hid_manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); - if (hid_manager) { - config_hid_manager(array); - } - CFRelease(array); - } -} - -JoypadMacOS::~JoypadMacOS() { - for (int i = 0; i < device_list.size(); i++) { - device_list.write[i].free(); - } - - IOHIDManagerUnscheduleFromRunLoop(hid_manager, CFRunLoopGetCurrent(), GODOT_JOY_LOOP_RUN_MODE); - IOHIDManagerClose(hid_manager, kIOHIDOptionsTypeNone); - CFRelease(hid_manager); - hid_manager = nullptr; -} diff --git a/platform/macos/joypad_macos.h b/platform/macos/joypad_macos.h index 6a2af11b51..b37a1b24f3 100644 --- a/platform/macos/joypad_macos.h +++ b/platform/macos/joypad_macos.h @@ -28,93 +28,62 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -#ifndef JOYPAD_MACOS_H -#define JOYPAD_MACOS_H - #include "core/input/input.h" -#import <ForceFeedback/ForceFeedback.h> -#import <ForceFeedback/ForceFeedbackConstants.h> -#import <IOKit/hid/IOHIDLib.h> -#import <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> - -struct rec_element { - IOHIDElementRef ref; - IOHIDElementCookie cookie; +#define Key _QKey +#import <CoreHaptics/CoreHaptics.h> +#import <GameController/GameController.h> +#undef Key - uint32_t usage = 0; +@interface JoypadMacOSObserver : NSObject - int min = 0; - int max = 0; +- (void)startObserving; +- (void)startProcessing; +- (void)finishObserving; - struct Comparator { - bool operator()(const rec_element p_a, const rec_element p_b) const { return p_a.usage < p_b.usage; } - }; -}; +@end -struct joypad { - IOHIDDeviceRef device_ref = nullptr; +API_AVAILABLE(macosx(11)) +@interface RumbleMotor : NSObject +@property(strong, nonatomic) CHHapticEngine *engine; +@property(strong, nonatomic) id<CHHapticPatternPlayer> player; +@end - Vector<rec_element> axis_elements; - Vector<rec_element> button_elements; - Vector<rec_element> hat_elements; +API_AVAILABLE(macosx(11)) +@interface RumbleContext : NSObject +// High frequency motor, it's usually the right engine. +@property(strong, nonatomic) RumbleMotor *weak_motor; +// Low frequency motor, it's usually the left engine. +@property(strong, nonatomic) RumbleMotor *strong_motor; +@end - int id = 0; - bool offset_hat = false; +// Controller support for macOS begins with macOS 10.9+, +// however haptics (vibrations) are only supported in macOS 11+. +@interface Joypad : NSObject - io_service_t ffservice = 0; // Interface for force feedback, 0 = no ff. - FFCONSTANTFORCE ff_constant_force; - FFDeviceObjectReference ff_device = nullptr; - FFEffectObjectReference ff_object = nullptr; - uint64_t ff_timestamp = 0; - LONG *ff_directions = nullptr; - FFEFFECT ff_effect; - DWORD *ff_axes = nullptr; +@property(assign, nonatomic) BOOL force_feedback; +@property(assign, nonatomic) NSInteger ff_effect_timestamp; +@property(strong, nonatomic) GCController *controller; +@property(strong, nonatomic) RumbleContext *rumble_context API_AVAILABLE(macosx(11)); - void add_hid_elements(CFArrayRef p_array); - void add_hid_element(IOHIDElementRef p_element); +- (instancetype)init; +- (instancetype)init:(GCController *)controller; - bool has_element(IOHIDElementCookie p_cookie, Vector<rec_element> *p_list) const; - bool config_force_feedback(io_service_t p_service); - bool check_ff_features(); - - int get_hid_element_state(rec_element *p_element) const; - - void free(); - joypad(); -}; +@end class JoypadMacOS { - enum { - JOYPADS_MAX = 16, - }; - private: - Input *input = nullptr; - IOHIDManagerRef hid_manager; - - Vector<joypad> device_list; - - bool have_device(IOHIDDeviceRef p_device) const; - bool configure_joypad(IOHIDDeviceRef p_device_ref, joypad *p_joy); - - int get_joy_index(int p_id) const; - int get_joy_ref(IOHIDDeviceRef p_device) const; - - void poll_joypads() const; - void config_hid_manager(CFArrayRef p_matching_array) const; - - void joypad_vibration_start(int p_id, float p_magnitude, float p_duration, uint64_t p_timestamp); - void joypad_vibration_stop(int p_id, uint64_t p_timestamp); + JoypadMacOSObserver *observer; public: - void process_joypads(); + JoypadMacOS(); + ~JoypadMacOS(); - void _device_added(IOReturn p_res, IOHIDDeviceRef p_device); - void _device_removed(IOReturn p_res, IOHIDDeviceRef p_device); + API_AVAILABLE(macosx(11)) + void joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp); + API_AVAILABLE(macosx(11)) + void joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp); - JoypadMacOS(Input *in); - ~JoypadMacOS(); + void start_processing(); + void process_joypads(); }; - -#endif // JOYPAD_MACOS_H diff --git a/platform/macos/joypad_macos.mm b/platform/macos/joypad_macos.mm new file mode 100644 index 0000000000..8cd5cdd9f2 --- /dev/null +++ b/platform/macos/joypad_macos.mm @@ -0,0 +1,612 @@ +/**************************************************************************/ +/* joypad_macos.mm */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#import "joypad_macos.h" + +#include <Foundation/Foundation.h> + +#import "os_macos.h" + +#include "core/config/project_settings.h" +#include "core/os/keyboard.h" +#include "core/string/ustring.h" +#include "main/main.h" + +@implementation RumbleMotor + +- (instancetype)initWithController:(GCController *)controller locality:(GCHapticsLocality)locality { + self = [super init]; + self.engine = [controller.haptics createEngineWithLocality:locality]; + self.player = nil; + return self; +} + +- (void)execute_pattern:(CHHapticPattern *)pattern { + NSError *error; + id<CHHapticPatternPlayer> player = [self.engine createPlayerWithPattern:pattern error:&error]; + + // When all players have stopped for an engine, stop the engine. + [self.engine notifyWhenPlayersFinished:^CHHapticEngineFinishedAction(NSError *_Nullable error) { + return CHHapticEngineFinishedActionStopEngine; + }]; + + self.player = player; + + // Starts the engine and returns if an error was encountered. + if (![self.engine startAndReturnError:&error]) { + print_verbose("Couldn't start controller haptic engine"); + return; + } + if (![self.player startAtTime:0 error:&error]) { + print_verbose("Couldn't execute controller haptic pattern"); + } +} + +- (void)stop { + NSError *error; + [self.player stopAtTime:0 error:&error]; + self.player = nil; +} + +@end + +@implementation RumbleContext + +- (instancetype)init { + self = [super init]; + self.weak_motor = nil; + self.strong_motor = nil; + return self; +} + +- (bool)hasMotors { + return self.weak_motor != nil && self.strong_motor != nil; +} +- (bool)hasActivePlayers { + if (![self hasMotors]) { + return NO; + } + return self.weak_motor.player != nil && self.strong_motor.player != nil; +} + +@end + +@implementation Joypad + +- (instancetype)init { + self = [super init]; + return self; +} +- (instancetype)init:(GCController *)controller { + self = [super init]; + self.controller = controller; + + if (@available(macOS 11, *)) { + // Haptics within the controller is only available in macOS 11+. + self.rumble_context = [[RumbleContext alloc] init]; + + // Create Weak and Strong motors for controller. + self.rumble_context.weak_motor = [[RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityRightHandle]; + self.rumble_context.strong_motor = [[RumbleMotor alloc] initWithController:controller locality:GCHapticsLocalityLeftHandle]; + + // If the rumble motors aren't available, disable force feedback. + if (![self.rumble_context hasMotors]) { + self.force_feedback = NO; + } else { + self.force_feedback = YES; + } + } else { + self.force_feedback = NO; + } + + self.ff_effect_timestamp = 0; + + return self; +} + +@end + +JoypadMacOS::JoypadMacOS() { + observer = [[JoypadMacOSObserver alloc] init]; + [observer startObserving]; + + if (@available(macOS 11.3, *)) { + GCController.shouldMonitorBackgroundEvents = YES; + } +} + +JoypadMacOS::~JoypadMacOS() { + if (observer) { + [observer finishObserving]; + observer = nil; + } +} + +void JoypadMacOS::start_processing() { + if (observer) { + [observer startProcessing]; + } + process_joypads(); +} + +API_AVAILABLE(macosx(10.15)) +CHHapticPattern *get_vibration_pattern(float p_magnitude, float p_duration) { + // Creates a vibration pattern with an intensity and duration. + NSDictionary *hapticDict = @{ + CHHapticPatternKeyPattern : @[ + @{ + CHHapticPatternKeyEvent : @{ + CHHapticPatternKeyEventType : CHHapticEventTypeHapticContinuous, + CHHapticPatternKeyTime : @(CHHapticTimeImmediate), + CHHapticPatternKeyEventDuration : [NSNumber numberWithFloat:p_duration], + + CHHapticPatternKeyEventParameters : @[ + @{ + CHHapticPatternKeyParameterID : CHHapticEventParameterIDHapticIntensity, + CHHapticPatternKeyParameterValue : [NSNumber numberWithFloat:p_magnitude] + }, + ], + }, + }, + ], + }; + NSError *error; + CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithDictionary:hapticDict error:&error]; + return pattern; +} + +void JoypadMacOS::joypad_vibration_start(Joypad *p_joypad, float p_weak_magnitude, float p_strong_magnitude, float p_duration, uint64_t p_timestamp) { + if (!p_joypad.force_feedback || p_weak_magnitude < 0.f || p_weak_magnitude > 1.f || p_strong_magnitude < 0.f || p_strong_magnitude > 1.f) { + return; + } + + // If there is active vibration players, stop them. + if ([p_joypad.rumble_context hasActivePlayers]) { + joypad_vibration_stop(p_joypad, p_timestamp); + } + + // Gets the default vibration pattern and creates a player for each motor. + CHHapticPattern *weak_pattern = get_vibration_pattern(p_weak_magnitude, p_duration); + CHHapticPattern *strong_pattern = get_vibration_pattern(p_strong_magnitude, p_duration); + + RumbleMotor *weak_motor = p_joypad.rumble_context.weak_motor; + RumbleMotor *strong_motor = p_joypad.rumble_context.strong_motor; + + [weak_motor execute_pattern:weak_pattern]; + [strong_motor execute_pattern:strong_pattern]; + + p_joypad.ff_effect_timestamp = p_timestamp; +} + +void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp) { + if (!p_joypad.force_feedback) { + return; + } + // If there is no active vibration players, exit. + if (![p_joypad.rumble_context hasActivePlayers]) { + return; + } + + RumbleMotor *weak_motor = p_joypad.rumble_context.weak_motor; + RumbleMotor *strong_motor = p_joypad.rumble_context.strong_motor; + + [weak_motor stop]; + [strong_motor stop]; + + p_joypad.ff_effect_timestamp = p_timestamp; +} + +@interface JoypadMacOSObserver () + +@property(assign, nonatomic) BOOL isObserving; +@property(assign, nonatomic) BOOL isProcessing; +@property(strong, nonatomic) NSMutableDictionary<NSNumber *, Joypad *> *connectedJoypads; +@property(strong, nonatomic) NSMutableArray<Joypad *> *joypadsQueue; + +@end + +@implementation JoypadMacOSObserver + +- (instancetype)init { + self = [super init]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.isObserving = NO; + self.isProcessing = NO; +} + +- (void)startProcessing { + self.isProcessing = YES; + + for (GCController *controller in self.joypadsQueue) { + [self addMacOSJoypad:controller]; + } + + [self.joypadsQueue removeAllObjects]; +} + +- (void)startObserving { + if (self.isObserving) { + return; + } + + self.isObserving = YES; + + self.connectedJoypads = [NSMutableDictionary dictionary]; + self.joypadsQueue = [NSMutableArray array]; + + // Get told when controllers connect, this will be called right away for + // already connected controllers. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(controllerWasConnected:) + name:GCControllerDidConnectNotification + object:nil]; + + // Get told when controllers disconnect. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(controllerWasDisconnected:) + name:GCControllerDidDisconnectNotification + object:nil]; +} + +- (void)finishObserving { + if (self.isObserving) { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + } + + self.isObserving = NO; + self.isProcessing = NO; + + self.connectedJoypads = nil; + self.joypadsQueue = nil; +} + +- (void)dealloc { + [self finishObserving]; +} + +- (NSArray<NSNumber *> *)getAllKeysForController:(GCController *)controller { + NSArray *keys = [self.connectedJoypads allKeys]; + NSMutableArray *final_keys = [NSMutableArray array]; + + for (NSNumber *key in keys) { + Joypad *joypad = [self.connectedJoypads objectForKey:key]; + if (joypad.controller == controller) { + [final_keys addObject:key]; + } + } + + return final_keys; +} + +- (int)getJoyIdForController:(GCController *)controller { + NSArray *keys = [self getAllKeysForController:controller]; + + for (NSNumber *key in keys) { + int joy_id = [key intValue]; + return joy_id; + } + + return -1; +} + +- (void)addMacOSJoypad:(GCController *)controller { + // Get a new id for our controller. + int joy_id = Input::get_singleton()->get_unused_joy_id(); + + if (joy_id == -1) { + print_verbose("Couldn't retrieve new joy ID."); + return; + } + + // Assign our player index. + if (controller.playerIndex == GCControllerPlayerIndexUnset) { + controller.playerIndex = [self getFreePlayerIndex]; + } + + // Tell Godot about our new controller. + Input::get_singleton()->joy_connection_changed(joy_id, true, String::utf8([controller.vendorName UTF8String])); + + Joypad *joypad = [[Joypad alloc] init:controller]; + + // Add it to our dictionary, this will retain our controllers. + [self.connectedJoypads setObject:joypad forKey:[NSNumber numberWithInt:joy_id]]; + + // Set our input handler. + [self setControllerInputHandler:controller]; +} + +- (void)controllerWasConnected:(NSNotification *)notification { + // Get our controller. + GCController *controller = (GCController *)notification.object; + + if (!controller) { + print_verbose("Couldn't retrieve new controller."); + return; + } + + if ([[self getAllKeysForController:controller] count] > 0) { + print_verbose("Controller is already registered."); + } else if (!self.isProcessing) { + Joypad *joypad = [[Joypad alloc] init:controller]; + [self.joypadsQueue addObject:joypad]; + } else { + [self addMacOSJoypad:controller]; + } +} + +- (void)controllerWasDisconnected:(NSNotification *)notification { + // Find our joystick, there should be only one in our dictionary. + GCController *controller = (GCController *)notification.object; + + if (!controller) { + return; + } + + NSArray *keys = [self getAllKeysForController:controller]; + for (NSNumber *key in keys) { + // Tell Godot this joystick is no longer there. + int joy_id = [key intValue]; + Input::get_singleton()->joy_connection_changed(joy_id, false, ""); + + // And remove it from our dictionary. + [self.connectedJoypads removeObjectForKey:key]; + } +} + +- (GCControllerPlayerIndex)getFreePlayerIndex { + bool have_player_1 = false; + bool have_player_2 = false; + bool have_player_3 = false; + bool have_player_4 = false; + + if (self.connectedJoypads == nil) { + NSArray *keys = [self.connectedJoypads allKeys]; + for (NSNumber *key in keys) { + Joypad *joypad = [self.connectedJoypads objectForKey:key]; + GCController *controller = joypad.controller; + if (controller.playerIndex == GCControllerPlayerIndex1) { + have_player_1 = true; + } else if (controller.playerIndex == GCControllerPlayerIndex2) { + have_player_2 = true; + } else if (controller.playerIndex == GCControllerPlayerIndex3) { + have_player_3 = true; + } else if (controller.playerIndex == GCControllerPlayerIndex4) { + have_player_4 = true; + } + } + } + + if (!have_player_1) { + return GCControllerPlayerIndex1; + } else if (!have_player_2) { + return GCControllerPlayerIndex2; + } else if (!have_player_3) { + return GCControllerPlayerIndex3; + } else if (!have_player_4) { + return GCControllerPlayerIndex4; + } else { + return GCControllerPlayerIndexUnset; + } +} + +- (void)setControllerInputHandler:(GCController *)controller { + // Hook in the callback handler for the correct gamepad profile. + // This is a bit of a weird design choice on Apples part. + // You need to select the most capable gamepad profile for the + // gamepad attached. + if (controller.extendedGamepad != nil) { + // The extended gamepad profile has all the input you could possibly find on + // a gamepad but will only be active if your gamepad actually has all of + // these... + _weakify(self); + _weakify(controller); + + controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { + _strongify(self); + _strongify(controller); + + int joy_id = [self getJoyIdForController:controller]; + + if (element == gamepad.buttonA) { + Input::get_singleton()->joy_button(joy_id, JoyButton::A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonB) { + Input::get_singleton()->joy_button(joy_id, JoyButton::B, + gamepad.buttonB.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JoyButton::X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.buttonY) { + Input::get_singleton()->joy_button(joy_id, JoyButton::Y, + gamepad.buttonY.isPressed); + } else if (element == gamepad.leftShoulder) { + Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_SHOULDER, + gamepad.leftShoulder.isPressed); + } else if (element == gamepad.rightShoulder) { + Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_SHOULDER, + gamepad.rightShoulder.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, + gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, + gamepad.dpad.right.isPressed); + } + + if (element == gamepad.leftThumbstick) { + float value = gamepad.leftThumbstick.xAxis.value; + Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_X, value); + value = -gamepad.leftThumbstick.yAxis.value; + Input::get_singleton()->joy_axis(joy_id, JoyAxis::LEFT_Y, value); + } else if (element == gamepad.rightThumbstick) { + float value = gamepad.rightThumbstick.xAxis.value; + Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_X, value); + value = -gamepad.rightThumbstick.yAxis.value; + Input::get_singleton()->joy_axis(joy_id, JoyAxis::RIGHT_Y, value); + } else if (element == gamepad.leftTrigger) { + float value = gamepad.leftTrigger.value; + Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_LEFT, value); + } else if (element == gamepad.rightTrigger) { + float value = gamepad.rightTrigger.value; + Input::get_singleton()->joy_axis(joy_id, JoyAxis::TRIGGER_RIGHT, value); + } + + if (@available(macOS 10.14.1, *)) { + if (element == gamepad.leftThumbstickButton) { + Input::get_singleton()->joy_button(joy_id, JoyButton::LEFT_STICK, + gamepad.leftThumbstickButton.isPressed); + } else if (element == gamepad.rightThumbstickButton) { + Input::get_singleton()->joy_button(joy_id, JoyButton::RIGHT_STICK, + gamepad.rightThumbstickButton.isPressed); + } + } + + if (@available(macOS 10.15, *)) { + if (element == gamepad.buttonOptions) { + Input::get_singleton()->joy_button(joy_id, JoyButton::BACK, + gamepad.buttonOptions.isPressed); + } else if (element == gamepad.buttonMenu) { + Input::get_singleton()->joy_button(joy_id, JoyButton::START, + gamepad.buttonMenu.isPressed); + } + } + + if (@available(macOS 11, *)) { + if (element == gamepad.buttonHome) { + Input::get_singleton()->joy_button(joy_id, JoyButton::GUIDE, + gamepad.buttonHome.isPressed); + } + if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { + GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; + if (element == xboxGamepad.paddleButton1) { + Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE1, + xboxGamepad.paddleButton1.isPressed); + } else if (element == xboxGamepad.paddleButton2) { + Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE2, + xboxGamepad.paddleButton2.isPressed); + } else if (element == xboxGamepad.paddleButton3) { + Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE3, + xboxGamepad.paddleButton3.isPressed); + } else if (element == xboxGamepad.paddleButton4) { + Input::get_singleton()->joy_button(joy_id, JoyButton::PADDLE4, + xboxGamepad.paddleButton4.isPressed); + } + } + } + + if (@available(macOS 12, *)) { + if ([gamepad isKindOfClass:[GCXboxGamepad class]]) { + GCXboxGamepad *xboxGamepad = (GCXboxGamepad *)gamepad; + if (element == xboxGamepad.buttonShare) { + Input::get_singleton()->joy_button(joy_id, JoyButton::MISC1, + xboxGamepad.buttonShare.isPressed); + } + } + } + }; + } else if (controller.microGamepad != nil) { + // Micro gamepads were added in macOS 10.11 and feature just 2 buttons and a d-pad. + _weakify(self); + _weakify(controller); + + controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { + _strongify(self); + _strongify(controller); + + int joy_id = [self getJoyIdForController:controller]; + + if (element == gamepad.buttonA) { + Input::get_singleton()->joy_button(joy_id, JoyButton::A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JoyButton::X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_LEFT, + gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JoyButton::DPAD_RIGHT, + gamepad.dpad.right.isPressed); + } + }; + } + + // TODO: Need to add support for controller.motion which gives us access to + // the orientation of the device (if supported). +} + +@end + +void JoypadMacOS::process_joypads() { + if (@available(macOS 11, *)) { + // Process vibrations in macOS 11+. + NSArray *keys = [observer.connectedJoypads allKeys]; + + for (NSNumber *key in keys) { + int id = key.intValue; + Joypad *joypad = [observer.connectedJoypads objectForKey:key]; + + if (joypad.force_feedback) { + Input *input = Input::get_singleton(); + uint64_t timestamp = input->get_joy_vibration_timestamp(id); + + if (timestamp > (unsigned)joypad.ff_effect_timestamp) { + Vector2 strength = input->get_joy_vibration_strength(id); + float duration = input->get_joy_vibration_duration(id); + if (duration == 0) { + duration = GCHapticDurationInfinite; + } + + if (strength.x == 0 && strength.y == 0) { + joypad_vibration_stop(joypad, timestamp); + } else { + joypad_vibration_start(joypad, strength.x, strength.y, duration, timestamp); + } + } + } + } + } +} diff --git a/platform/macos/native_menu_macos.h b/platform/macos/native_menu_macos.h new file mode 100644 index 0000000000..1d9feb64a7 --- /dev/null +++ b/platform/macos/native_menu_macos.h @@ -0,0 +1,157 @@ +/**************************************************************************/ +/* native_menu_macos.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 NATIVE_MENU_MACOS_H +#define NATIVE_MENU_MACOS_H + +#include "core/templates/hash_map.h" +#include "core/templates/rid_owner.h" +#include "servers/display/native_menu.h" + +#import <AppKit/AppKit.h> +#import <ApplicationServices/ApplicationServices.h> + +class NativeMenuMacOS : public NativeMenu { + GDCLASS(NativeMenuMacOS, NativeMenu) + + struct MenuData { + NSMenu *menu = nullptr; + + Callable open_cb; + Callable close_cb; + bool is_open = false; + bool is_system = false; + }; + + mutable RID_PtrOwner<MenuData> menus; + HashMap<NSMenu *, RID> menu_lookup; + + NSMenu *main_menu_ns = nullptr; + NSMenu *application_menu_ns = nullptr; + NSMenu *window_menu_ns = nullptr; + NSMenu *help_menu_ns = nullptr; + NSMenu *dock_menu_ns = nullptr; + + RID main_menu; + RID application_menu; + RID window_menu; + RID help_menu; + RID dock_menu; + + int _get_system_menu_start(const NSMenu *p_menu) const; + int _get_system_menu_count(const NSMenu *p_menu) const; + bool _is_menu_opened(NSMenu *p_menu) const; + NSMenuItem *_menu_add_item(NSMenu *p_menu, const String &p_label, Key p_accel, int p_index, int *r_out); + +public: + void _register_system_menus(NSMenu *p_main_menu, NSMenu *p_application_menu, NSMenu *p_window_menu, NSMenu *p_help_menu, NSMenu *p_dock_menu); + NSMenu *_get_dock_menu(); + void _menu_open(NSMenu *p_menu); + void _menu_close(NSMenu *p_menu); + + virtual bool has_feature(Feature p_feature) const override; + + virtual bool has_system_menu(SystemMenus p_menu_id) const override; + virtual RID get_system_menu(SystemMenus p_menu_id) const override; + + virtual RID create_menu() override; + virtual bool has_menu(const RID &p_rid) const override; + virtual void free_menu(const RID &p_rid) override; + + virtual Size2 get_size(const RID &p_rid) const override; + virtual void popup(const RID &p_rid, const Vector2i &p_position) override; + + virtual void set_interface_direction(const RID &p_rid, bool p_is_rtl) override; + virtual void set_popup_open_callback(const RID &p_rid, const Callable &p_callback) override; + virtual Callable get_popup_open_callback(const RID &p_rid) const override; + virtual void set_popup_close_callback(const RID &p_rid, const Callable &p_callback) override; + virtual Callable get_popup_close_callback(const RID &p_rid) const override; + virtual void set_minimum_width(const RID &p_rid, float p_width) override; + virtual float get_minimum_width(const RID &p_rid) const override; + + virtual int add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag = Variant(), int p_index = -1) override; + virtual int add_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_icon_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_radio_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_icon_radio_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_multistate_item(const RID &p_rid, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_separator(const RID &p_rid, int p_index = -1) override; + + virtual int find_item_index_with_text(const RID &p_rid, const String &p_text) const override; + virtual int find_item_index_with_tag(const RID &p_rid, const Variant &p_tag) const override; + + virtual bool is_item_checked(const RID &p_rid, int p_idx) const override; + virtual bool is_item_checkable(const RID &p_rid, int p_idx) const override; + virtual bool is_item_radio_checkable(const RID &p_rid, int p_idx) const override; + virtual Callable get_item_callback(const RID &p_rid, int p_idx) const override; + virtual Callable get_item_key_callback(const RID &p_rid, int p_idx) const override; + virtual Variant get_item_tag(const RID &p_rid, int p_idx) const override; + virtual String get_item_text(const RID &p_rid, int p_idx) const override; + virtual RID get_item_submenu(const RID &p_rid, int p_idx) const override; + virtual Key get_item_accelerator(const RID &p_rid, int p_idx) const override; + virtual bool is_item_disabled(const RID &p_rid, int p_idx) const override; + virtual bool is_item_hidden(const RID &p_rid, int p_idx) const override; + virtual String get_item_tooltip(const RID &p_rid, int p_idx) const override; + virtual int get_item_state(const RID &p_rid, int p_idx) const override; + virtual int get_item_max_states(const RID &p_rid, int p_idx) const override; + virtual Ref<Texture2D> get_item_icon(const RID &p_rid, int p_idx) const override; + virtual int get_item_indentation_level(const RID &p_rid, int p_idx) const override; + + virtual void set_item_checked(const RID &p_rid, int p_idx, bool p_checked) override; + virtual void set_item_checkable(const RID &p_rid, int p_idx, bool p_checkable) override; + virtual void set_item_radio_checkable(const RID &p_rid, int p_idx, bool p_checkable) override; + virtual void set_item_callback(const RID &p_rid, int p_idx, const Callable &p_callback) override; + virtual void set_item_key_callback(const RID &p_rid, int p_idx, const Callable &p_key_callback) override; + virtual void set_item_hover_callbacks(const RID &p_rid, int p_idx, const Callable &p_callback) override; + virtual void set_item_tag(const RID &p_rid, int p_idx, const Variant &p_tag) override; + virtual void set_item_text(const RID &p_rid, int p_idx, const String &p_text) override; + virtual void set_item_submenu(const RID &p_rid, int p_idx, const RID &p_submenu_rid) override; + virtual void set_item_accelerator(const RID &p_rid, int p_idx, Key p_keycode) override; + virtual void set_item_disabled(const RID &p_rid, int p_idx, bool p_disabled) override; + virtual void set_item_hidden(const RID &p_rid, int p_idx, bool p_hidden) override; + virtual void set_item_tooltip(const RID &p_rid, int p_idx, const String &p_tooltip) override; + virtual void set_item_state(const RID &p_rid, int p_idx, int p_state) override; + virtual void set_item_max_states(const RID &p_rid, int p_idx, int p_max_states) override; + virtual void set_item_icon(const RID &p_rid, int p_idx, const Ref<Texture2D> &p_icon) override; + virtual void set_item_indentation_level(const RID &p_rid, int p_idx, int p_level) override; + + virtual int get_item_count(const RID &p_rid) const override; + virtual bool is_system_menu(const RID &p_rid) const override; + + virtual void remove_item(const RID &p_rid, int p_idx) override; + virtual void clear(const RID &p_rid) override; + + NativeMenuMacOS(); + ~NativeMenuMacOS(); +}; + +#endif // NATIVE_MENU_MACOS_H diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm new file mode 100644 index 0000000000..8c2dd98862 --- /dev/null +++ b/platform/macos/native_menu_macos.mm @@ -0,0 +1,1363 @@ +/**************************************************************************/ +/* native_menu_macos.mm */ +/**************************************************************************/ +/* 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 "native_menu_macos.h" + +#include "display_server_macos.h" +#include "godot_menu_item.h" +#include "key_mapping_macos.h" + +#include "scene/resources/image_texture.h" + +void NativeMenuMacOS::_register_system_menus(NSMenu *p_main_menu, NSMenu *p_application_menu, NSMenu *p_window_menu, NSMenu *p_help_menu, NSMenu *p_dock_menu) { + { + MenuData *md = memnew(MenuData); + md->menu = p_main_menu; + md->is_system = true; + main_menu = menus.make_rid(md); + main_menu_ns = p_main_menu; + menu_lookup[md->menu] = main_menu; + } + { + MenuData *md = memnew(MenuData); + md->menu = p_application_menu; + md->is_system = true; + application_menu = menus.make_rid(md); + application_menu_ns = p_application_menu; + menu_lookup[md->menu] = application_menu; + } + { + MenuData *md = memnew(MenuData); + md->menu = p_window_menu; + md->is_system = true; + window_menu = menus.make_rid(md); + window_menu_ns = p_window_menu; + menu_lookup[md->menu] = window_menu; + } + { + MenuData *md = memnew(MenuData); + md->menu = p_help_menu; + md->is_system = true; + help_menu = menus.make_rid(md); + help_menu_ns = p_help_menu; + menu_lookup[md->menu] = help_menu; + } + { + MenuData *md = memnew(MenuData); + md->menu = p_dock_menu; + md->is_system = true; + dock_menu = menus.make_rid(md); + dock_menu_ns = p_dock_menu; + menu_lookup[md->menu] = dock_menu; + } +} + +NSMenu *NativeMenuMacOS::_get_dock_menu() { + MenuData *md = menus.get_or_null(dock_menu); + if (md) { + return md->menu; + } + return nullptr; +} + +void NativeMenuMacOS::_menu_open(NSMenu *p_menu) { + if (menu_lookup.has(p_menu)) { + MenuData *md = menus.get_or_null(menu_lookup[p_menu]); + if (md) { + md->is_open = true; + if (md->open_cb.is_valid()) { + Variant ret; + Callable::CallError ce; + + md->open_cb.callp(nullptr, 0, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute menu open callback: %s.", Variant::get_callable_error_text(md->open_cb, nullptr, 0, ce))); + } + } + } + } +} + +void NativeMenuMacOS::_menu_close(NSMenu *p_menu) { + if (menu_lookup.has(p_menu)) { + MenuData *md = menus.get_or_null(menu_lookup[p_menu]); + if (md) { + md->is_open = false; + if (md->close_cb.is_valid()) { + Variant ret; + Callable::CallError ce; + + md->close_cb.callp(nullptr, 0, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute menu close callback: %s.", Variant::get_callable_error_text(md->close_cb, nullptr, 0, ce))); + } + } + } + } +} + +bool NativeMenuMacOS::_is_menu_opened(NSMenu *p_menu) const { + if (menu_lookup.has(p_menu)) { + const MenuData *md = menus.get_or_null(menu_lookup[p_menu]); + if (md && md->is_open) { + return true; + } + } + for (NSInteger i = (p_menu == [NSApp mainMenu]) ? 1 : 0; i < [p_menu numberOfItems]; i++) { + const NSMenuItem *menu_item = [p_menu itemAtIndex:i]; + if ([menu_item submenu]) { + if (_is_menu_opened([menu_item submenu])) { + return true; + } + } + } + return false; +} + +int NativeMenuMacOS::_get_system_menu_start(const NSMenu *p_menu) const { + if (p_menu == [NSApp mainMenu]) { // Skip Apple menu. + return 1; + } + if (p_menu == application_menu_ns || p_menu == window_menu_ns || p_menu == help_menu_ns) { + int count = [p_menu numberOfItems]; + for (int i = 0; i < count; i++) { + NSMenuItem *menu_item = [p_menu itemAtIndex:i]; + if (menu_item.tag == MENU_TAG_START) { + return i + 1; + } + } + } + return 0; +} + +int NativeMenuMacOS::_get_system_menu_count(const NSMenu *p_menu) const { + if (p_menu == [NSApp mainMenu]) { // Skip Apple, Window and Help menu. + return [p_menu numberOfItems] - 3; + } + if (p_menu == application_menu_ns || p_menu == window_menu_ns || p_menu == help_menu_ns) { + int start = 0; + int count = [p_menu numberOfItems]; + for (int i = 0; i < count; i++) { + NSMenuItem *menu_item = [p_menu itemAtIndex:i]; + if (menu_item.tag == MENU_TAG_START) { + start = i + 1; + } + if (menu_item.tag == MENU_TAG_END) { + return i - start; + } + } + } + return [p_menu numberOfItems]; +} + +bool NativeMenuMacOS::has_feature(Feature p_feature) const { + switch (p_feature) { + case FEATURE_GLOBAL_MENU: + case FEATURE_POPUP_MENU: + case FEATURE_OPEN_CLOSE_CALLBACK: + case FEATURE_HOVER_CALLBACK: + case FEATURE_KEY_CALLBACK: + return true; + default: + return false; + } +} + +bool NativeMenuMacOS::has_system_menu(SystemMenus p_menu_id) const { + switch (p_menu_id) { + case MAIN_MENU_ID: + case APPLICATION_MENU_ID: + case WINDOW_MENU_ID: + case HELP_MENU_ID: + case DOCK_MENU_ID: + return true; + default: + return false; + } +} + +RID NativeMenuMacOS::get_system_menu(SystemMenus p_menu_id) const { + switch (p_menu_id) { + case MAIN_MENU_ID: + return main_menu; + case APPLICATION_MENU_ID: + return application_menu; + case WINDOW_MENU_ID: + return window_menu; + case HELP_MENU_ID: + return help_menu; + case DOCK_MENU_ID: + return dock_menu; + default: + return RID(); + } +} + +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; +} + +bool NativeMenuMacOS::has_menu(const RID &p_rid) const { + return menus.owns(p_rid); +} + +void NativeMenuMacOS::free_menu(const RID &p_rid) { + MenuData *md = menus.get_or_null(p_rid); + if (md && !md->is_system) { + clear(p_rid); + menus.free(p_rid); + menu_lookup.erase(md->menu); + md->menu = nullptr; + memdelete(md); + } +} + +Size2 NativeMenuMacOS::get_size(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Size2()); + + return Size2(md->menu.size.width, md->menu.size.height) * DisplayServer::get_singleton()->screen_get_max_scale(); +} + +void NativeMenuMacOS::popup(const RID &p_rid, const Vector2i &p_position) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds) { + Point2i position = p_position; + // macOS native y-coordinate relative to _get_screens_origin() is negative, + // Godot passes a positive value. + position.y *= -1; + position += ds->_get_screens_origin(); + position /= ds->screen_get_max_scale(); + + [md->menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(position.x, position.y) inView:nil]; + } +} + +void NativeMenuMacOS::set_interface_direction(const RID &p_rid, bool p_is_rtl) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + md->menu.userInterfaceLayoutDirection = p_is_rtl ? NSUserInterfaceLayoutDirectionRightToLeft : NSUserInterfaceLayoutDirectionLeftToRight; +} + +void NativeMenuMacOS::set_popup_open_callback(const RID &p_rid, const Callable &p_callback) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + md->open_cb = p_callback; +} + +Callable NativeMenuMacOS::get_popup_open_callback(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Callable()); + + return md->open_cb; +} + +void NativeMenuMacOS::set_popup_close_callback(const RID &p_rid, const Callable &p_callback) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + md->close_cb = p_callback; +} + +Callable NativeMenuMacOS::get_popup_close_callback(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Callable()); + + return md->close_cb; +} + +void NativeMenuMacOS::set_minimum_width(const RID &p_rid, float p_width) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + md->menu.minimumWidth = p_width / DisplayServer::get_singleton()->screen_get_max_scale(); +} + +float NativeMenuMacOS::get_minimum_width(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, 0.0); + + return md->menu.minimumWidth * DisplayServer::get_singleton()->screen_get_max_scale(); +} + +int NativeMenuMacOS::add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + MenuData *md_sub = menus.get_or_null(p_submenu_rid); + ERR_FAIL_NULL_V(md, -1); + ERR_FAIL_NULL_V(md_sub, -1); + ERR_FAIL_COND_V_MSG(md->menu == md_sub->menu, -1, "Can't set submenu to self!"); + ERR_FAIL_COND_V_MSG([md_sub->menu supermenu], -1, "Can't set submenu to menu that is already a submenu of some other menu!"); + + NSMenuItem *menu_item; + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + if (p_index < 0) { + p_index = item_start + item_count; + } else { + p_index += item_start; + p_index = CLAMP(p_index, item_start, item_start + item_count); + } + menu_item = [md->menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; + + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = Callable(); + obj->key_callback = Callable(); + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = 0; + obj->state = 0; + [menu_item setRepresentedObject:obj]; + + [md_sub->menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; + [md->menu setSubmenu:md_sub->menu forItem:menu_item]; + + return p_index - item_start; +} + +NSMenuItem *NativeMenuMacOS::_menu_add_item(NSMenu *p_menu, const String &p_label, Key p_accel, int p_index, int *r_out) { + if (p_menu) { + String keycode = KeyMappingMacOS::keycode_get_native_string(p_accel & KeyModifierMask::CODE_MASK); + NSMenuItem *menu_item; + int item_start = _get_system_menu_start(p_menu); + int item_count = _get_system_menu_count(p_menu); + if (p_index < 0) { + p_index = item_start + item_count; + } else { + p_index += item_start; + p_index = CLAMP(p_index, item_start, item_start + item_count); + } + menu_item = [p_menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:@selector(globalMenuCallback:) keyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()] atIndex:p_index]; + *r_out = p_index - item_start; + return menu_item; + } + return nullptr; +} + +int NativeMenuMacOS::add_item(const RID &p_rid, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = 0; + obj->state = 0; + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + obj->max_states = 0; + obj->state = 0; + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = 0; + obj->state = 0; + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds && p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + NSImage *image = ds->_convert_to_nsimg(obj->img); + [image setSize:NSMakeSize(16, 16)]; + [menu_item setImage:image]; + } + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_icon_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + obj->max_states = 0; + obj->state = 0; + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds && p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + NSImage *image = ds->_convert_to_nsimg(obj->img); + [image setSize:NSMakeSize(16, 16)]; + [menu_item setImage:image]; + } + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_radio_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; + obj->max_states = 0; + obj->state = 0; + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_icon_radio_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; + obj->max_states = 0; + obj->state = 0; + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds && p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + NSImage *image = ds->_convert_to_nsimg(obj->img); + [image setSize:NSMakeSize(16, 16)]; + [menu_item setImage:image]; + } + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_multistate_item(const RID &p_rid, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int out = -1; + NSMenuItem *menu_item = _menu_add_item(md->menu, p_label, p_accel, p_index, &out); + if (menu_item) { + GodotMenuItem *obj = [[GodotMenuItem alloc] init]; + obj->callback = p_callback; + obj->key_callback = p_key_callback; + obj->meta = p_tag; + obj->checkable_type = CHECKABLE_TYPE_NONE; + obj->max_states = p_max_states; + obj->state = p_default_state; + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; + [menu_item setRepresentedObject:obj]; + } + return out; +} + +int NativeMenuMacOS::add_separator(const RID &p_rid, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (md->menu == [NSApp mainMenu]) { // Do not add separators into main menu. + return -1; + } + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + if (p_index < 0) { + p_index = item_start + item_count; + } else { + p_index += item_start; + p_index = CLAMP(p_index, item_start, item_start + item_count); + } + [md->menu insertItem:[NSMenuItem separatorItem] atIndex:p_index]; + return p_index - item_start; +} + +int NativeMenuMacOS::find_item_index_with_text(const RID &p_rid, const String &p_text) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int item_start = _get_system_menu_start(md->menu); + int index = [md->menu indexOfItemWithTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; + if (index >= 0) { + return index - item_start; + } + return -1; +} + +int NativeMenuMacOS::find_item_index_with_tag(const RID &p_rid, const Variant &p_tag) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + for (NSInteger i = item_start; i < item_start + item_count; i++) { + const NSMenuItem *menu_item = [md->menu itemAtIndex:i]; + if (menu_item) { + const GodotMenuItem *obj = [menu_item representedObject]; + if (obj && obj->meta == p_tag) { + return i - item_start; + } + } + } + return -1; +} + +bool NativeMenuMacOS::is_item_checked(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + return ([menu_item state] == NSControlStateValueOn); + } + return false; +} + +bool NativeMenuMacOS::is_item_checkable(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->checkable_type == CHECKABLE_TYPE_CHECK_BOX; + } + } + return false; +} + +bool NativeMenuMacOS::is_item_radio_checkable(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->checkable_type == CHECKABLE_TYPE_RADIO_BUTTON; + } + } + return false; +} + +Callable NativeMenuMacOS::get_item_callback(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Callable()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Callable()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, Callable()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->callback; + } + } + return Callable(); +} + +Callable NativeMenuMacOS::get_item_key_callback(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Callable()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Callable()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, Callable()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->key_callback; + } + } + return Callable(); +} + +Variant NativeMenuMacOS::get_item_tag(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Variant()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Variant()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, Variant()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->meta; + } + } + return Variant(); +} + +String NativeMenuMacOS::get_item_text(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, String()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, String()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, String()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + return String::utf8([[menu_item title] UTF8String]); + } + return String(); +} + +RID NativeMenuMacOS::get_item_submenu(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, RID()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, RID()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, RID()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + NSMenu *sub_menu = [menu_item submenu]; + if (sub_menu && menu_lookup.has(sub_menu)) { + return menu_lookup[sub_menu]; + } + } + return RID(); +} + +Key NativeMenuMacOS::get_item_accelerator(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Key::NONE); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Key::NONE); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, Key::NONE); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + String ret = String::utf8([[menu_item keyEquivalent] UTF8String]); + Key keycode = find_keycode(ret); + NSUInteger mask = [menu_item keyEquivalentModifierMask]; + if (mask & NSEventModifierFlagControl) { + keycode |= KeyModifierMask::CTRL; + } + if (mask & NSEventModifierFlagOption) { + keycode |= KeyModifierMask::ALT; + } + if (mask & NSEventModifierFlagShift) { + keycode |= KeyModifierMask::SHIFT; + } + if (mask & NSEventModifierFlagCommand) { + keycode |= KeyModifierMask::META; + } + if (mask & NSEventModifierFlagNumericPad) { + keycode |= KeyModifierMask::KPAD; + } + return keycode; + } + return Key::NONE; +} + +bool NativeMenuMacOS::is_item_disabled(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + return ![menu_item isEnabled]; + } + return false; +} + +bool NativeMenuMacOS::is_item_hidden(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + return [menu_item isHidden]; + } + return false; +} + +String NativeMenuMacOS::get_item_tooltip(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, String()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, String()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, String()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + return String::utf8([[menu_item toolTip] UTF8String]); + } + return String(); +} + +int NativeMenuMacOS::get_item_state(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, 0); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, 0); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, 0); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->state; + } + } + return 0; +} + +int NativeMenuMacOS::get_item_max_states(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, 0); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, 0); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, 0); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->max_states; + } + } + return 0; +} + +Ref<Texture2D> NativeMenuMacOS::get_item_icon(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Ref<Texture2D>()); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Ref<Texture2D>()); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, Ref<Texture2D>()); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + if (obj->img.is_valid()) { + return ImageTexture::create_from_image(obj->img); + } + } + } + return Ref<Texture2D>(); +} + +int NativeMenuMacOS::get_item_indentation_level(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, 0); + + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, 0); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND_V(p_idx >= item_start + item_count, 0); + const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + return [menu_item indentationLevel]; + } + return 0; +} + +void NativeMenuMacOS::set_item_checked(const RID &p_rid, int p_idx, bool p_checked) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + if (p_checked) { + [menu_item setState:NSControlStateValueOn]; + } else { + [menu_item setState:NSControlStateValueOff]; + } + } +} + +void NativeMenuMacOS::set_item_checkable(const RID &p_rid, int p_idx, bool p_checkable) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_CHECK_BOX : CHECKABLE_TYPE_NONE; + } +} + +void NativeMenuMacOS::set_item_radio_checkable(const RID &p_rid, int p_idx, bool p_checkable) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->checkable_type = (p_checkable) ? CHECKABLE_TYPE_RADIO_BUTTON : CHECKABLE_TYPE_NONE; + } +} + +void NativeMenuMacOS::set_item_callback(const RID &p_rid, int p_idx, const Callable &p_callback) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->callback = p_callback; + } +} + +void NativeMenuMacOS::set_item_key_callback(const RID &p_rid, int p_idx, const Callable &p_key_callback) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->key_callback = p_key_callback; + } +} + +void NativeMenuMacOS::set_item_hover_callbacks(const RID &p_rid, int p_idx, const Callable &p_callback) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->hover_callback = p_callback; + } +} + +void NativeMenuMacOS::set_item_tag(const RID &p_rid, int p_idx, const Variant &p_tag) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->meta = p_tag; + } +} + +void NativeMenuMacOS::set_item_text(const RID &p_rid, int p_idx, const String &p_text) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; + NSMenu *sub_menu = [menu_item submenu]; + if (sub_menu) { + [sub_menu setTitle:[NSString stringWithUTF8String:p_text.utf8().get_data()]]; + } + } +} + +void NativeMenuMacOS::set_item_submenu(const RID &p_rid, int p_idx, const RID &p_submenu_rid) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + if (p_submenu_rid.is_valid()) { + MenuData *md_sub = menus.get_or_null(p_submenu_rid); + ERR_FAIL_NULL(md_sub); + ERR_FAIL_COND_MSG(md->menu == md_sub->menu, "Can't set submenu to self!"); + ERR_FAIL_COND_MSG([md_sub->menu supermenu], "Can't set submenu to menu that is already a submenu of some other menu!"); + + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + [md->menu setSubmenu:md_sub->menu forItem:menu_item]; + } + } else { + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + if ([menu_item submenu] && _is_menu_opened([menu_item submenu])) { + ERR_PRINT("Can't remove open menu!"); + return; + } + [md->menu setSubmenu:nil forItem:menu_item]; + } + } +} + +void NativeMenuMacOS::set_item_accelerator(const RID &p_rid, int p_idx, Key p_keycode) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + if (p_keycode == Key::NONE) { + [menu_item setKeyEquivalent:@""]; + } else { + [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_keycode)]; + String keycode = KeyMappingMacOS::keycode_get_native_string(p_keycode & KeyModifierMask::CODE_MASK); + [menu_item setKeyEquivalent:[NSString stringWithUTF8String:keycode.utf8().get_data()]]; + } + } +} + +void NativeMenuMacOS::set_item_disabled(const RID &p_rid, int p_idx, bool p_disabled) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setEnabled:(!p_disabled)]; + } +} + +void NativeMenuMacOS::set_item_hidden(const RID &p_rid, int p_idx, bool p_hidden) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setHidden:p_hidden]; + } +} + +void NativeMenuMacOS::set_item_tooltip(const RID &p_rid, int p_idx, const String &p_tooltip) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setToolTip:[NSString stringWithUTF8String:p_tooltip.utf8().get_data()]]; + } +} + +void NativeMenuMacOS::set_item_state(const RID &p_rid, int p_idx, int p_state) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->state = p_state; + } +} + +void NativeMenuMacOS::set_item_max_states(const RID &p_rid, int p_idx, int p_max_states) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + obj->max_states = p_max_states; + } +} + +void NativeMenuMacOS::set_item_icon(const RID &p_rid, int p_idx, const Ref<Texture2D> &p_icon) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + GodotMenuItem *obj = [menu_item representedObject]; + ERR_FAIL_NULL(obj); + DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); + if (ds && p_icon.is_valid()) { + obj->img = p_icon->get_image(); + obj->img = obj->img->duplicate(); + if (obj->img->is_compressed()) { + obj->img->decompress(); + } + NSImage *image = ds->_convert_to_nsimg(obj->img); + [image setSize:NSMakeSize(16, 16)]; + [menu_item setImage:image]; + } else { + obj->img = Ref<Image>(); + [menu_item setImage:nil]; + } + } +} + +void NativeMenuMacOS::set_item_indentation_level(const RID &p_rid, int p_idx, int p_level) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if (menu_item) { + [menu_item setIndentationLevel:p_level]; + } +} + +int NativeMenuMacOS::get_item_count(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, 0); + + return _get_system_menu_count(md->menu); +} + +bool NativeMenuMacOS::is_system_menu(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + + return md->is_system; +} + +void NativeMenuMacOS::remove_item(const RID &p_rid, int p_idx) { + ERR_FAIL_COND(p_idx < 0); + + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int item_start = _get_system_menu_start(md->menu); + int item_count = _get_system_menu_count(md->menu); + p_idx += item_start; + ERR_FAIL_COND(p_idx >= item_start + item_count); + NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; + if ([menu_item submenu] && _is_menu_opened([menu_item submenu])) { + ERR_PRINT("Can't remove open menu!"); + return; + } + [md->menu removeItemAtIndex:p_idx]; +} + +void NativeMenuMacOS::clear(const RID &p_rid) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + ERR_FAIL_COND_MSG(_is_menu_opened(md->menu), "Can't remove open menu!"); + + if (p_rid == application_menu || p_rid == window_menu || p_rid == help_menu) { + int start = _get_system_menu_start(md->menu); + int count = _get_system_menu_count(md->menu); + for (int i = start + count - 1; i >= start; i--) { + [md->menu removeItemAtIndex:i]; + } + } else { + [md->menu removeAllItems]; + } + + if (p_rid == main_menu) { + // Restore Apple, Window and Help menu. + MenuData *md_app = menus.get_or_null(application_menu); + if (md_app) { + NSMenuItem *menu_item = [md->menu addItemWithTitle:@"" action:nil keyEquivalent:@""]; + [md->menu setSubmenu:md_app->menu forItem:menu_item]; + } + MenuData *md_win = menus.get_or_null(window_menu); + if (md_win) { + NSMenuItem *menu_item = [md->menu addItemWithTitle:@"Window" action:nil keyEquivalent:@""]; + [md->menu setSubmenu:md_win->menu forItem:menu_item]; + } + MenuData *md_hlp = menus.get_or_null(help_menu); + if (md_hlp) { + NSMenuItem *menu_item = [md->menu addItemWithTitle:@"Help" action:nil keyEquivalent:@""]; + [md->menu setSubmenu:md_hlp->menu forItem:menu_item]; + } + } +} + +NativeMenuMacOS::NativeMenuMacOS() {} + +NativeMenuMacOS::~NativeMenuMacOS() { + if (main_menu.is_valid()) { + MenuData *md = menus.get_or_null(main_menu); + if (md) { + clear(main_menu); + menus.free(main_menu); + menu_lookup.erase(md->menu); + md->menu = nullptr; + main_menu_ns = nullptr; + memdelete(md); + } + } + if (application_menu.is_valid()) { + MenuData *md = menus.get_or_null(application_menu); + if (md) { + clear(application_menu); + menus.free(application_menu); + menu_lookup.erase(md->menu); + md->menu = nullptr; + application_menu_ns = nullptr; + memdelete(md); + } + } + if (window_menu.is_valid()) { + MenuData *md = menus.get_or_null(window_menu); + if (md) { + clear(window_menu); + menus.free(window_menu); + menu_lookup.erase(md->menu); + md->menu = nullptr; + window_menu_ns = nullptr; + memdelete(md); + } + } + if (help_menu.is_valid()) { + MenuData *md = menus.get_or_null(help_menu); + if (md) { + clear(help_menu); + menus.free(help_menu); + menu_lookup.erase(md->menu); + md->menu = nullptr; + help_menu_ns = nullptr; + memdelete(md); + } + } + if (dock_menu.is_valid()) { + MenuData *md = menus.get_or_null(dock_menu); + if (md) { + clear(dock_menu); + menus.free(dock_menu); + menu_lookup.erase(md->menu); + md->menu = nullptr; + dock_menu_ns = nullptr; + memdelete(md); + } + } +} diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index 9b20e51f47..912a682a6b 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -32,7 +32,7 @@ #define OS_MACOS_H #include "crash_handler_macos.h" -#include "joypad_macos.h" +#import "joypad_macos.h" #include "core/input/input.h" #import "drivers/coreaudio/audio_driver_coreaudio.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 934767db74..9f0bea5951 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -70,7 +70,7 @@ void OS_MacOS::initialize() { String OS_MacOS::get_processor_name() const { char buffer[256]; size_t buffer_len = 256; - if (sysctlbyname("machdep.cpu.brand_string", &buffer, &buffer_len, NULL, 0) == 0) { + if (sysctlbyname("machdep.cpu.brand_string", &buffer, &buffer_len, nullptr, 0) == 0) { return String::utf8(buffer, buffer_len); } ERR_FAIL_V_MSG("", String("Couldn't get the CPU model name. Returning an empty string.")); @@ -139,7 +139,7 @@ void OS_MacOS::finalize() { } void OS_MacOS::initialize_joypads() { - joypad_macos = memnew(JoypadMacOS(Input::get_singleton())); + joypad_macos = memnew(JoypadMacOS()); } void OS_MacOS::set_main_loop(MainLoop *p_main_loop) { @@ -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; @@ -601,7 +601,9 @@ Error OS_MacOS::create_process(const String &p_path, const List<String> &p_argum for (const String &arg : p_arguments) { [arguments addObject:[NSString stringWithUTF8String:arg.utf8().get_data()]]; } +#if defined(__x86_64__) if (@available(macOS 10.15, *)) { +#endif NSWorkspaceOpenConfiguration *configuration = [[NSWorkspaceOpenConfiguration alloc] init]; [configuration setArguments:arguments]; [configuration setCreatesNewApplicationInstance:YES]; @@ -630,6 +632,7 @@ Error OS_MacOS::create_process(const String &p_path, const List<String> &p_argum } return err; +#if defined(__x86_64__) } else { Error err = ERR_TIMEOUT; NSError *error = nullptr; @@ -645,6 +648,7 @@ Error OS_MacOS::create_process(const String &p_path, const List<String> &p_argum } return err; } +#endif } else { return OS_Unix::create_process(p_path, p_arguments, r_child_id, p_open_console); } @@ -769,7 +773,7 @@ void OS_MacOS::run() { if (DisplayServer::get_singleton()) { DisplayServer::get_singleton()->process_events(); // Get rid of pending events. } - joypad_macos->process_joypads(); + joypad_macos->start_processing(); if (Main::iteration()) { quit = true; diff --git a/platform/macos/platform_config.h b/platform/macos/platform_config.h index 1a571b689a..01b0a12b5d 100644 --- a/platform/macos/platform_config.h +++ b/platform/macos/platform_config.h @@ -31,3 +31,10 @@ #include <alloca.h> #define PTHREAD_RENAME_SELF + +#define _weakify(var) __weak typeof(var) GDWeak_##var = var; +#define _strongify(var) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong typeof(var) var = GDWeak_##var; \ + _Pragma("clang diagnostic pop") diff --git a/platform/macos/platform_macos_builders.py b/platform/macos/platform_macos_builders.py index 3a1cc92bd2..eabe5aae2f 100644 --- a/platform/macos/platform_macos_builders.py +++ b/platform/macos/platform_macos_builders.py @@ -1,21 +1,14 @@ -"""Functions used to generate source files during build time +"""Functions used to generate source files during build time""" -All such functions are invoked in a subprocess on Windows to prevent build flakiness. - -""" import os -from platform_methods import subprocess_main def make_debug_macos(target, source, env): + dst = str(target[0]) if env["macports_clang"] != "no": mpprefix = os.environ.get("MACPORTS_PREFIX", "/opt/local") mpclangver = env["macports_clang"] - os.system(mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-dsymutil {0} -o {0}.dSYM".format(target[0])) + os.system(mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-dsymutil {0} -o {0}.dSYM".format(dst)) else: - os.system("dsymutil {0} -o {0}.dSYM".format(target[0])) - os.system("strip -u -r {0}".format(target[0])) - - -if __name__ == "__main__": - subprocess_main(globals()) + os.system("dsymutil {0} -o {0}.dSYM".format(dst)) + os.system("strip -u -r {0}".format(dst)) diff --git a/platform/web/detect.py b/platform/web/detect.py index 7d9b1de6b7..2d2cc288a1 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -86,7 +86,7 @@ def configure(env: "SConsEnvironment"): supported_arches = ["wasm32"] if env["arch"] not in supported_arches: print( - 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' + 'Unsupported CPU architecture "%s" for Web. Supported architectures are: %s.' % (env["arch"], ", ".join(supported_arches)) ) sys.exit() @@ -253,6 +253,9 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-fvisibility=hidden"]) env.extra_suffix = ".dlink" + env.extra_suffix + # WASM_BIGINT is needed since emscripten ≥ 3.1.41 + needs_wasm_bigint = cc_semver >= (3, 1, 41) + # Run the main application in a web worker if env["proxy_to_pthread"]: env.Append(LINKFLAGS=["-s", "PROXY_TO_PTHREAD=1"]) @@ -261,6 +264,9 @@ def configure(env: "SConsEnvironment"): # https://github.com/emscripten-core/emscripten/issues/18034#issuecomment-1277561925 env.Append(LINKFLAGS=["-s", "TEXTDECODER=0"]) # BigInt support to pass object pointers between contexts + needs_wasm_bigint = True + + if needs_wasm_bigint: env.Append(LINKFLAGS=["-s", "WASM_BIGINT"]) # Reduce code size by generating less support code (e.g. skip NodeJS support). diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index bc4c0d22f0..06f5eb82f7 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -36,7 +36,6 @@ #include "core/config/project_settings.h" #include "core/object/callable_method_pointer.h" -#include "scene/resources/atlas_texture.h" #include "servers/rendering/dummy/rasterizer_dummy.h" #ifdef GLES3_ENABLED @@ -511,43 +510,12 @@ DisplayServer::CursorShape DisplayServerWeb::cursor_get_shape() const { void DisplayServerWeb::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); if (p_cursor.is_valid()) { - Ref<Texture2D> texture = p_cursor; - ERR_FAIL_COND(!texture.is_valid()); - Ref<AtlasTexture> atlas_texture = p_cursor; - Size2 texture_size; Rect2 atlas_rect; + Ref<Image> image = _get_cursor_image_from_resource(p_cursor, p_hotspot, atlas_rect); + ERR_FAIL_COND(image.is_null()); + Vector2i texture_size = image->get_size(); - if (atlas_texture.is_valid()) { - texture = atlas_texture->get_atlas(); - - atlas_rect.size.width = texture->get_width(); - atlas_rect.size.height = texture->get_height(); - atlas_rect.position.x = atlas_texture->get_region().position.x; - atlas_rect.position.y = atlas_texture->get_region().position.y; - - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - - Ref<Image> image = texture->get_image(); - - ERR_FAIL_COND(!image.is_valid()); - - image = image->duplicate(true); - - if (image->is_compressed()) { - Error err = image->decompress(); - ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); - } - - if (atlas_texture.is_valid()) { + if (atlas_rect.has_area()) { image->crop_from_point( atlas_rect.position.x, atlas_rect.position.y, @@ -1062,6 +1030,7 @@ DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode r_error = OK; // Always succeeds for now. tts = GLOBAL_GET("audio/general/text_to_speech"); + native_menu = memnew(NativeMenu); // Dummy native menu. // Ensure the canvas ID. godot_js_config_canvas_id_get(canvas_id, 256); @@ -1130,6 +1099,10 @@ DisplayServerWeb::DisplayServerWeb(const String &p_rendering_driver, WindowMode } DisplayServerWeb::~DisplayServerWeb() { + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } #ifdef GLES3_ENABLED if (webgl_ctx) { emscripten_webgl_commit_frame(); @@ -1140,7 +1113,11 @@ DisplayServerWeb::~DisplayServerWeb() { bool DisplayServerWeb::has_feature(Feature p_feature) const { switch (p_feature) { - //case FEATURE_GLOBAL_MENU: +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif //case FEATURE_HIDPI: case FEATURE_ICON: case FEATURE_CLIPBOARD: @@ -1151,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/display_server_web.h b/platform/web/display_server_web.h index 682d10704f..49a017015a 100644 --- a/platform/web/display_server_web.h +++ b/platform/web/display_server_web.h @@ -104,6 +104,7 @@ private: bool swap_cancel_ok = false; bool tts = false; + NativeMenu *native_menu = nullptr; // utilities static void dom2godot_mod(Ref<InputEventWithModifiers> ev, int p_mod, Key p_keycode); diff --git a/platform/web/doc_classes/EditorExportPlatformWeb.xml b/platform/web/doc_classes/EditorExportPlatformWeb.xml index 75125e2708..755308de9a 100644 --- a/platform/web/doc_classes/EditorExportPlatformWeb.xml +++ b/platform/web/doc_classes/EditorExportPlatformWeb.xml @@ -28,9 +28,8 @@ The custom HTML page that wraps the exported web build. If left empty, the default HTML shell is used. For more information, see the [url=$DOCS_URL/tutorials/platform/web/customizing_html5_shell.html]Customizing HTML5 Shell[/url] tutorial. </member> - <member name="html/experimental_virtual_keyboard" type="bool" setter="" getter=""> + <member name="html/experimental_virtual_keyboard" type="bool" setter="" getter="" experimental=""> If [code]true[/code], embeds support for a virtual keyboard into the web page, which is shown when necessary on touchscreen devices. - [b]Warning:[/b] This feature is experimental and may be changed in a future release. </member> <member name="html/export_icon" type="bool" setter="" getter=""> If [code]true[/code], the project icon will be used as the favicon for this application's web page. @@ -84,8 +83,8 @@ If [code]true[/code] enables [GDExtension] support for this web build. </member> <member name="variant/thread_support" type="bool" setter="" getter=""> - If enabled, the exported game will support threads. It requires [url=https://web.dev/articles/coop-coep]a "cross-origin isolated" website[/url], which can be difficult to setup and brings some limitations (e.g. not being able to communicate with third-party websites). - If disabled, the exported game will not support threads. As a result, it is more prone to performance and audio issues, but will only require to be run on a HTTPS website. + If [code]true[/code], the exported game will support threads. It requires [url=https://web.dev/articles/coop-coep]a "cross-origin isolated" website[/url], which may be difficult to set up and is limited for security reasons (such as not being able to communicate with third-party websites). + If [code]false[/code], the exported game will not support threads. As a result, it is more prone to performance and audio issues, but will only require to be run on an HTTPS website. </member> <member name="vram_texture_compression/for_desktop" type="bool" setter="" getter=""> If [code]true[/code], allows textures to be optimized for desktop through the S3TC algorithm. diff --git a/platform/web/os_web.cpp b/platform/web/os_web.cpp index 45671ca491..ab4e7f8470 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(); } @@ -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..a825938e96 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; } @@ -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 34c8f8e7a1..159a273e70 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -4,7 +4,6 @@ Import("env") import os from pathlib import Path -from platform_methods import run_in_subprocess import platform_windows_builders sources = [] @@ -18,6 +17,8 @@ common_win = [ "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", "wgl_detect_version.cpp", @@ -135,8 +136,8 @@ if env["d3d12"]: if not os.getenv("VCINSTALLDIR"): if env["debug_symbols"]: - env.AddPostAction(prog, run_in_subprocess(platform_windows_builders.make_debug_mingw)) + env.AddPostAction(prog, env.Run(platform_windows_builders.make_debug_mingw)) if env["windows_subsystem"] == "gui": - env.AddPostAction(prog_wrap, run_in_subprocess(platform_windows_builders.make_debug_mingw)) + env.AddPostAction(prog_wrap, env.Run(platform_windows_builders.make_debug_mingw)) env.platform_sources += sources diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows.cpp index e32fbd47dd..133d36aa0d 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows.cpp @@ -109,7 +109,7 @@ public: ret.module_name = temp; std::vector<char> img(ret.image_name.begin(), ret.image_name.end()); std::vector<char> mod(ret.module_name.begin(), ret.module_name.end()); - SymLoadModule64(process, 0, &img[0], &mod[0], (DWORD64)ret.base_address, ret.load_size); + SymLoadModule64(process, nullptr, &img[0], &mod[0], (DWORD64)ret.base_address, ret.load_size); return ret; } }; diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 4585884859..f34d479345 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -202,6 +202,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 cl/link stdout bloat, redirecting any errors to stderr.", True), ("angle_libs", "Path to the ANGLE static libraries", ""), # Direct3D 12 support. ( @@ -392,6 +393,39 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): ## Compile/link flags + env["MAXLINELENGTH"] = 8192 # Windows Vista and beyond, so always applicable. + + if env["silence_msvc"]: + from tempfile import mkstemp + + old_spawn = env["SPAWN"] + + 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) + + 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. env.AppendUnique(CCFLAGS=["/MDd"]) @@ -422,6 +456,10 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): else: print("Missing environment variable: WindowsSdkDir") + if int(env["target_win_version"], 16) < 0x0601: + print("`target_win_version` should be 0x0601 or higher (Windows 7).") + sys.exit(255) + env.AppendUnique( CPPDEFINES=[ "WINDOWS_ENABLED", @@ -486,7 +524,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): sys.exit(255) env.AppendUnique(CPPDEFINES=["D3D12_ENABLED", "RD_ENABLED"]) - LIBS += ["d3d12", "dxgi", "dxguid"] + LIBS += ["dxgi", "dxguid"] LIBS += ["version"] # Mesa dependency. # Needed for avoiding C1128. @@ -651,6 +689,10 @@ 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).") + sys.exit(255) + if not env["use_llvm"]: env.Append(CCFLAGS=["-mwindows"]) @@ -711,7 +753,7 @@ def configure_mingw(env: "SConsEnvironment"): sys.exit(255) env.AppendUnique(CPPDEFINES=["D3D12_ENABLED", "RD_ENABLED"]) - env.Append(LIBS=["d3d12", "dxgi", "dxguid"]) + env.Append(LIBS=["dxgi", "dxguid"]) # PIX if not env["arch"] in ["x86_64", "arm64"] or env["pix_path"] == "" or not os.path.exists(env["pix_path"]): diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 16eabf6855..b6b713687f 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -35,9 +35,9 @@ #include "core/config/project_settings.h" #include "core/io/marshalls.h" +#include "core/version.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" -#include "scene/resources/atlas_texture.h" #if defined(VULKAN_ENABLED) #include "rendering_context_driver_vulkan_windows.h" @@ -51,6 +51,9 @@ #include <avrt.h> #include <dwmapi.h> +#include <propkey.h> +#include <propvarutil.h> +#include <shellapi.h> #include <shlwapi.h> #include <shobjidl.h> #include <wbemcli.h> @@ -93,6 +96,11 @@ static void track_mouse_leave_event(HWND hWnd) { bool DisplayServerWindows::has_feature(Feature p_feature) const { switch (p_feature) { +#ifndef DISABLE_DEPRECATED + case FEATURE_GLOBAL_MENU: { + return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU)); + } break; +#endif case FEATURE_SUBWINDOWS: case FEATURE_TOUCHSCREEN: case FEATURE_MOUSE: @@ -106,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: @@ -194,8 +204,8 @@ void DisplayServerWindows::_register_raw_input_devices(WindowID p_target_window) rid[1].hwndTarget = windows[p_target_window].hWnd; } else { // Follow the keyboard focus - rid[0].hwndTarget = 0; - rid[1].hwndTarget = 0; + rid[0].hwndTarget = nullptr; + rid[1].hwndTarget = nullptr; } if (RegisterRawInputDevices(rid, 2, sizeof(rid[0])) == FALSE) { @@ -264,7 +274,7 @@ public: QITABENT(FileDialogEventHandler, IFileDialogEvents), QITABENT(FileDialogEventHandler, IFileDialogControlEvents), #endif - { 0, 0 }, + { nullptr, 0 }, }; return QISearch(this, qit, riid, ppv); } @@ -754,15 +764,15 @@ Ref<Image> DisplayServerWindows::clipboard_get_image() const { } } else if (IsClipboardFormatAvailable(CF_DIB)) { HGLOBAL mem = GetClipboardData(CF_DIB); - if (mem != NULL) { + if (mem != nullptr) { BITMAPINFO *ptr = static_cast<BITMAPINFO *>(GlobalLock(mem)); - if (ptr != NULL) { + if (ptr != nullptr) { BITMAPINFOHEADER *info = &ptr->bmiHeader; void *dib_bits = (void *)(ptr->bmiColors); // Draw DIB image to temporary DC surface and read it back as BGRA8. - HDC dc = GetDC(0); + HDC dc = GetDC(nullptr); if (dc) { HDC hdc = CreateCompatibleDC(dc); if (hdc) { @@ -796,7 +806,7 @@ Ref<Image> DisplayServerWindows::clipboard_get_image() const { } DeleteDC(hdc); } - ReleaseDC(NULL, dc); + ReleaseDC(nullptr, dc); } GlobalUnlock(mem); } @@ -860,7 +870,7 @@ int DisplayServerWindows::get_screen_count() const { } int DisplayServerWindows::get_primary_screen() const { - EnumScreenData data = { 0, 0, 0 }; + EnumScreenData data = { 0, 0, nullptr }; EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcPrim, (LPARAM)&data); return data.screen; } @@ -895,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; } @@ -1108,16 +1117,16 @@ Color DisplayServerWindows::screen_get_pixel(const Point2i &p_position) const { p.x = pos.x; p.y = pos.y; if (win81p_LogicalToPhysicalPointForPerMonitorDPI) { - win81p_LogicalToPhysicalPointForPerMonitorDPI(0, &p); + win81p_LogicalToPhysicalPointForPerMonitorDPI(nullptr, &p); } - HDC dc = GetDC(0); + HDC dc = GetDC(nullptr); if (dc) { COLORREF col = GetPixel(dc, p.x, p.y); if (col != CLR_INVALID) { - ReleaseDC(NULL, dc); + ReleaseDC(nullptr, dc); return Color(float(col & 0x000000FF) / 255.0f, float((col & 0x0000FF00) >> 8) / 255.0f, float((col & 0x00FF0000) >> 16) / 255.0f, 1.0f); } - ReleaseDC(NULL, dc); + ReleaseDC(nullptr, dc); } return Color(); @@ -1148,12 +1157,12 @@ Ref<Image> DisplayServerWindows::screen_get_image(int p_screen) const { p2.x = pos.x + size.x; p2.y = pos.y + size.y; if (win81p_LogicalToPhysicalPointForPerMonitorDPI) { - win81p_LogicalToPhysicalPointForPerMonitorDPI(0, &p1); - win81p_LogicalToPhysicalPointForPerMonitorDPI(0, &p2); + win81p_LogicalToPhysicalPointForPerMonitorDPI(nullptr, &p1); + win81p_LogicalToPhysicalPointForPerMonitorDPI(nullptr, &p2); } Ref<Image> img; - HDC dc = GetDC(0); + HDC dc = GetDC(nullptr); if (dc) { HDC hdc = CreateCompatibleDC(dc); int width = p2.x - p1.x; @@ -1186,7 +1195,7 @@ Ref<Image> DisplayServerWindows::screen_get_image(int p_screen) const { } DeleteDC(hdc); } - ReleaseDC(NULL, dc); + ReleaseDC(nullptr, dc); } return img; @@ -1375,6 +1384,15 @@ void DisplayServerWindows::delete_sub_window(WindowID p_window) { WindowData &wd = windows[p_window]; + IPropertyStore *prop_store; + HRESULT hr = SHGetPropertyStoreForWindow(wd.hWnd, IID_IPropertyStore, (void **)&prop_store); + if (hr == S_OK) { + PROPVARIANT val; + PropVariantInit(&val); + prop_store->SetValue(PKEY_AppUserModel_ID, val); + prop_store->Release(); + } + while (wd.transient_children.size()) { window_set_transient(*wd.transient_children.begin(), INVALID_WINDOW_ID); } @@ -1403,7 +1421,7 @@ void DisplayServerWindows::delete_sub_window(WindowID p_window) { if ((tablet_get_current_driver() == "wintab") && wintab_available && windows[p_window].wtctx) { wintab_WTClose(windows[p_window].wtctx); - windows[p_window].wtctx = 0; + windows[p_window].wtctx = nullptr; } DestroyWindow(windows[p_window].hWnd); windows.erase(p_window); @@ -1524,7 +1542,7 @@ Size2i DisplayServerWindows::window_get_title_size(const String &p_title, Window return size; } - HDC hdc = GetDCEx(wd.hWnd, NULL, DCX_WINDOW); + HDC hdc = GetDCEx(wd.hWnd, nullptr, DCX_WINDOW); if (hdc) { Char16String s = p_title.utf16(); SIZE text_size; @@ -1542,8 +1560,8 @@ Size2i DisplayServerWindows::window_get_title_size(const String &p_title, Window ClientToScreen(wd.hWnd, (POINT *)&rect.right); if (win81p_PhysicalToLogicalPointForPerMonitorDPI) { - win81p_PhysicalToLogicalPointForPerMonitorDPI(0, (POINT *)&rect.left); - win81p_PhysicalToLogicalPointForPerMonitorDPI(0, (POINT *)&rect.right); + win81p_PhysicalToLogicalPointForPerMonitorDPI(nullptr, (POINT *)&rect.left); + win81p_PhysicalToLogicalPointForPerMonitorDPI(nullptr, (POINT *)&rect.right); } size.x += (rect.right - rect.left); @@ -1620,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); } } @@ -1878,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 @@ -1892,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 { @@ -1928,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); @@ -1971,10 +1998,11 @@ 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) { - SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, 0, 0); + SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, nullptr, 0); restore_mouse_trails = 0; } } @@ -2006,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; @@ -2031,7 +2059,7 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) // Save number of trails so we can restore when exiting, then turn off mouse trails SystemParametersInfoA(SPI_GETMOUSETRAILS, 0, &restore_mouse_trails, 0); if (restore_mouse_trails > 1) { - SystemParametersInfoA(SPI_SETMOUSETRAILS, 0, 0, 0); + SystemParametersInfoA(SPI_SETMOUSETRAILS, 0, nullptr, 0); } } } @@ -2286,10 +2314,10 @@ void DisplayServerWindows::window_set_ime_active(const bool p_active, WindowID p if (p_active) { wd.ime_active = true; ImmAssociateContext(wd.hWnd, wd.im_himc); - CreateCaret(wd.hWnd, NULL, 1, 1); + CreateCaret(wd.hWnd, nullptr, 1, 1); window_set_ime_position(wd.im_position, p_window); } else { - ImmAssociateContext(wd.hWnd, (HIMC)0); + ImmAssociateContext(wd.hWnd, (HIMC) nullptr); DestroyCaret(); wd.ime_active = false; } @@ -2304,7 +2332,7 @@ void DisplayServerWindows::window_set_ime_position(const Point2i &p_pos, WindowI wd.im_position = p_pos; HIMC himc = ImmGetContext(wd.hWnd); - if (himc == (HIMC)0) { + if (himc == (HIMC) nullptr) { return; } @@ -2380,38 +2408,10 @@ void DisplayServerWindows::cursor_set_custom_image(const Ref<Resource> &p_cursor cursors_cache.erase(p_shape); } - Ref<Texture2D> texture = p_cursor; - ERR_FAIL_COND(!texture.is_valid()); - Ref<AtlasTexture> atlas_texture = p_cursor; - Size2 texture_size; Rect2 atlas_rect; - - if (atlas_texture.is_valid()) { - texture = atlas_texture->get_atlas(); - - atlas_rect.size.width = texture->get_width(); - atlas_rect.size.height = texture->get_height(); - atlas_rect.position.x = atlas_texture->get_region().position.x; - atlas_rect.position.y = atlas_texture->get_region().position.y; - texture_size.width = atlas_texture->get_region().size.x; - texture_size.height = atlas_texture->get_region().size.y; - } else { - texture_size.width = texture->get_width(); - texture_size.height = texture->get_height(); - } - - ERR_FAIL_COND(p_hotspot.x < 0 || p_hotspot.y < 0); - ERR_FAIL_COND(texture_size.width > 256 || texture_size.height > 256); - ERR_FAIL_COND(p_hotspot.x > texture_size.width || p_hotspot.y > texture_size.height); - - Ref<Image> image = texture->get_image(); - - ERR_FAIL_COND(!image.is_valid()); - if (image->is_compressed()) { - image = image->duplicate(true); - Error err = image->decompress(); - ERR_FAIL_COND_MSG(err != OK, "Couldn't decompress VRAM-compressed custom mouse cursor image. Switch to a lossless compression mode in the Import dock."); - } + Ref<Image> image = _get_cursor_image_from_resource(p_cursor, p_hotspot, atlas_rect); + ERR_FAIL_COND(image.is_null()); + Vector2i texture_size = image->get_size(); UINT image_size = texture_size.width * texture_size.height; @@ -2440,7 +2440,7 @@ void DisplayServerWindows::cursor_set_custom_image(const Ref<Resource> &p_cursor int row_index = floor(index / texture_size.width) + atlas_rect.position.y; int column_index = (index % int(texture_size.width)) + atlas_rect.position.x; - if (atlas_texture.is_valid()) { + if (atlas_rect.has_area()) { column_index = MIN(column_index, atlas_rect.size.width - 1); row_index = MIN(row_index, atlas_rect.size.height - 1); } @@ -2506,6 +2506,298 @@ void DisplayServerWindows::enable_for_stealing_focus(OS::ProcessID pid) { AllowSetForegroundWindow(pid); } +static HRESULT CALLBACK win32_task_dialog_callback(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, LONG_PTR lpRefData) { + if (msg == TDN_CREATED) { + // To match the input text dialog. + SendMessageW(hwnd, WM_SETICON, ICON_BIG, 0); + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, 0); + } + + return 0; +} + +Error DisplayServerWindows::dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) { + _THREAD_SAFE_METHOD_ + + TASKDIALOGCONFIG config; + ZeroMemory(&config, sizeof(TASKDIALOGCONFIG)); + config.cbSize = sizeof(TASKDIALOGCONFIG); + + Char16String title = p_title.utf16(); + Char16String message = p_description.utf16(); + List<Char16String> buttons; + for (String s : p_buttons) { + buttons.push_back(s.utf16()); + } + + config.pszWindowTitle = (LPCWSTR)(title.get_data()); + config.pszContent = (LPCWSTR)(message.get_data()); + + const int button_count = MIN(buttons.size(), 8); + config.cButtons = button_count; + + // No dynamic stack array size :( + TASKDIALOG_BUTTON *tbuttons = button_count != 0 ? (TASKDIALOG_BUTTON *)alloca(sizeof(TASKDIALOG_BUTTON) * button_count) : nullptr; + if (tbuttons) { + for (int i = 0; i < button_count; i++) { + tbuttons[i].nButtonID = i; + tbuttons[i].pszButtonText = (LPCWSTR)(buttons[i].get_data()); + } + } + config.pButtons = tbuttons; + config.pfCallback = win32_task_dialog_callback; + + Error result = FAILED; + HMODULE comctl = LoadLibraryW(L"comctl32.dll"); + if (comctl) { + typedef HRESULT(WINAPI * TaskDialogIndirectPtr)(const TASKDIALOGCONFIG *pTaskConfig, int *pnButton, int *pnRadioButton, BOOL *pfVerificationFlagChecked); + + TaskDialogIndirectPtr task_dialog_indirect = (TaskDialogIndirectPtr)GetProcAddress(comctl, "TaskDialogIndirect"); + int button_pressed; + + if (task_dialog_indirect && SUCCEEDED(task_dialog_indirect(&config, &button_pressed, nullptr, nullptr))) { + if (!p_callback.is_null()) { + Variant button = button_pressed; + const Variant *args[1] = { &button }; + Variant ret; + Callable::CallError ce; + p_callback.callp(args, 1, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute dialog callback: %s.", Variant::get_callable_error_text(p_callback, args, 1, ce))); + } + } + + result = OK; + } + FreeLibrary(comctl); + } else { + ERR_PRINT("Unable to create native dialog."); + } + + return result; +} + +struct Win32InputTextDialogInit { + const char16_t *title; + const char16_t *description; + const char16_t *partial; + const Callable &callback; +}; + +static int scale_with_dpi(int p_pos, int p_dpi) { + return IsProcessDPIAware() ? (p_pos * p_dpi / 96) : p_pos; +} + +static INT_PTR input_text_dialog_init(HWND hWnd, UINT code, WPARAM wParam, LPARAM lParam) { + Win32InputTextDialogInit init = *(Win32InputTextDialogInit *)lParam; + SetWindowLongPtrW(hWnd, GWLP_USERDATA, (LONG_PTR)&init.callback); // Set dialog callback. + + SetWindowTextW(hWnd, (LPCWSTR)init.title); + + const int dpi = DisplayServerWindows::get_singleton()->screen_get_dpi(); + + const int margin = scale_with_dpi(7, dpi); + const SIZE dlg_size = { scale_with_dpi(300, dpi), scale_with_dpi(50, dpi) }; + + int str_len = lstrlenW((LPCWSTR)init.description); + SIZE str_size = { dlg_size.cx, 0 }; + if (str_len > 0) { + HDC hdc = GetDC(nullptr); + RECT trect = { margin, margin, margin + dlg_size.cx, margin + dlg_size.cy }; + SelectObject(hdc, (HFONT)SendMessageW(hWnd, WM_GETFONT, 0, 0)); + + // `+ margin` adds some space between the static text and the edit field. + // Don't scale this with DPI because DPI is already handled by DrawText. + str_size.cy = DrawTextW(hdc, (LPCWSTR)init.description, str_len, &trect, DT_LEFT | DT_WORDBREAK | DT_CALCRECT) + margin; + + ReleaseDC(nullptr, hdc); + } + + RECT crect, wrect; + GetClientRect(hWnd, &crect); + GetWindowRect(hWnd, &wrect); + int sw = GetSystemMetrics(SM_CXSCREEN); + int sh = GetSystemMetrics(SM_CYSCREEN); + int new_width = dlg_size.cx + margin * 2 + wrect.right - wrect.left - crect.right; + int new_height = dlg_size.cy + margin * 2 + wrect.bottom - wrect.top - crect.bottom + str_size.cy; + + MoveWindow(hWnd, (sw - new_width) / 2, (sh - new_height) / 2, new_width, new_height, true); + + HWND ok_button = GetDlgItem(hWnd, 1); + MoveWindow(ok_button, + dlg_size.cx + margin - scale_with_dpi(65, dpi), + dlg_size.cy + str_size.cy + margin - scale_with_dpi(20, dpi), + scale_with_dpi(65, dpi), scale_with_dpi(20, dpi), true); + + HWND description = GetDlgItem(hWnd, 3); + MoveWindow(description, margin, margin, dlg_size.cx, str_size.cy, true); + SetWindowTextW(description, (LPCWSTR)init.description); + + HWND text_edit = GetDlgItem(hWnd, 2); + MoveWindow(text_edit, margin, str_size.cy + margin, dlg_size.cx, scale_with_dpi(20, dpi), true); + SetWindowTextW(text_edit, (LPCWSTR)init.partial); + + return TRUE; +} + +static INT_PTR input_text_dialog_cmd_proc(HWND hWnd, UINT code, WPARAM wParam, LPARAM lParam) { + if (LOWORD(wParam) == 1) { + HWND text_edit = GetDlgItem(hWnd, 2); + ERR_FAIL_NULL_V(text_edit, false); + + Char16String text; + text.resize(GetWindowTextLengthW(text_edit) + 1); + GetWindowTextW(text_edit, (LPWSTR)text.get_data(), text.size()); + + const Callable *callback = (const Callable *)GetWindowLongPtrW(hWnd, GWLP_USERDATA); + if (callback && callback->is_valid()) { + Variant v_result = String((const wchar_t *)text.get_data()); + Variant ret; + Callable::CallError ce; + const Variant *args[1] = { &v_result }; + + callback->callp(args, 1, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute input dialog callback: %s.", Variant::get_callable_error_text(*callback, args, 1, ce))); + } + } + + return EndDialog(hWnd, 0); + } + + return false; +} + +static INT_PTR CALLBACK input_text_dialog_proc(HWND hWnd, UINT code, WPARAM wParam, LPARAM lParam) { + switch (code) { + case WM_INITDIALOG: + return input_text_dialog_init(hWnd, code, wParam, lParam); + + case WM_COMMAND: + return input_text_dialog_cmd_proc(hWnd, code, wParam, lParam); + + default: + return FALSE; + } +} + +Error DisplayServerWindows::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) { +#pragma pack(push, 1) + + // NOTE: Use default/placeholder coordinates here. Windows uses its own coordinate system + // specifically for dialogs which relies on font sizes instead of pixels. + const struct { + WORD dlgVer; // must be 1 + WORD signature; // must be 0xFFFF + DWORD helpID; + DWORD exStyle; + DWORD style; + WORD cDlgItems; + short x; + short y; + short cx; + short cy; + WCHAR menu[1]; // must be 0 + WCHAR windowClass[7]; // must be "#32770" -- the default window class for dialogs + WCHAR title[1]; // must be 0 + WORD pointsize; + WORD weight; + BYTE italic; + BYTE charset; + WCHAR font[13]; // must be "MS Shell Dlg" + } template_base = { + 1, 0xFFFF, 0, 0, + DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | DS_3DLOOK | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU, + 3, 0, 0, 20, 20, L"", L"#32770", L"", 8, FW_NORMAL, 0, DEFAULT_CHARSET, L"MS Shell Dlg" + }; + + const struct { + DWORD helpID; + DWORD exStyle; + DWORD style; + short x; + short y; + short cx; + short cy; + DWORD id; + WCHAR windowClass[7]; // must be "Button" + WCHAR title[3]; // must be "OK" + WORD extraCount; + } ok_button = { + 0, 0, WS_VISIBLE | BS_DEFPUSHBUTTON, 0, 0, 50, 14, 1, WC_BUTTONW, L"OK", 0 + }; + const struct { + DWORD helpID; + DWORD exStyle; + DWORD style; + short x; + short y; + short cx; + short cy; + DWORD id; + WCHAR windowClass[5]; // must be "Edit" + WCHAR title[1]; // must be 0 + WORD extraCount; + } text_field = { + 0, 0, WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 0, 0, 250, 14, 2, WC_EDITW, L"", 0 + }; + const struct { + DWORD helpID; + DWORD exStyle; + DWORD style; + short x; + short y; + short cx; + short cy; + DWORD id; + WCHAR windowClass[7]; // must be "Static" + WCHAR title[1]; // must be 0 + WORD extraCount; + } static_text = { + 0, 0, WS_VISIBLE, 0, 0, 250, 14, 3, WC_STATICW, L"", 0 + }; + +#pragma pack(pop) + + // Dialog template + const size_t data_size = sizeof(template_base) + (sizeof(template_base) % 4) + + sizeof(ok_button) + (sizeof(ok_button) % 4) + + sizeof(text_field) + (sizeof(text_field) % 4) + + sizeof(static_text) + (sizeof(static_text) % 4); + + void *data_template = memalloc(data_size); + ERR_FAIL_NULL_V_MSG(data_template, FAILED, "Unable to allocate memory for the dialog template."); + ZeroMemory(data_template, data_size); + + char *current_block = (char *)data_template; + CopyMemory(current_block, &template_base, sizeof(template_base)); + current_block += sizeof(template_base) + (sizeof(template_base) % 4); + CopyMemory(current_block, &ok_button, sizeof(ok_button)); + current_block += sizeof(ok_button) + (sizeof(ok_button) % 4); + CopyMemory(current_block, &text_field, sizeof(text_field)); + current_block += sizeof(text_field) + (sizeof(text_field) % 4); + CopyMemory(current_block, &static_text, sizeof(static_text)); + + Char16String title16 = p_title.utf16(); + Char16String description16 = p_description.utf16(); + Char16String partial16 = p_partial.utf16(); + + Win32InputTextDialogInit init = { + title16.get_data(), description16.get_data(), partial16.get_data(), p_callback + }; + + // No modal dialogs for specific windows? Assume main window here. + INT_PTR ret = DialogBoxIndirectParamW(hInstance, (LPDLGTEMPLATEW)data_template, nullptr, (DLGPROC)input_text_dialog_proc, (LPARAM)(&init)); + + Error result = ret != -1 ? OK : FAILED; + memfree(data_template); + + if (result == FAILED) { + ERR_PRINT("Unable to create native dialog."); + } + return result; +} + int DisplayServerWindows::keyboard_get_layout_count() const { return GetKeyboardLayoutList(0, nullptr); } @@ -2670,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; @@ -2685,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_ } } @@ -2698,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() { @@ -3081,9 +3381,9 @@ void DisplayServerWindows::set_context(Context p_context) { #define SIGNATURE_MASK 0xFFFFFF00 // Keeping the name suggested by Microsoft, but this macro really answers: // Is this mouse event emulated from touch or pen input? -#define IsPenEvent(dw) (((dw)&SIGNATURE_MASK) == MI_WP_SIGNATURE) +#define IsPenEvent(dw) (((dw) & SIGNATURE_MASK) == MI_WP_SIGNATURE) // This one tells whether the event comes from touchscreen (and not from pen). -#define IsTouchEvent(dw) (IsPenEvent(dw) && ((dw)&0x80)) +#define IsTouchEvent(dw) (IsPenEvent(dw) && ((dw) & 0x80)) void DisplayServerWindows::_touch_event(WindowID p_window, bool p_pressed, float p_x, float p_y, int idx) { if (touch_state.has(idx) == p_pressed) { @@ -3141,7 +3441,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; } @@ -3383,6 +3682,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA // Process window messages. switch (uMsg) { + case WM_MENUCOMMAND: { + native_menu->_menu_activate(HMENU(lParam), (int)wParam); + } break; case WM_CREATE: { if (is_dark_mode_supported() && dark_title_available) { BOOL value = is_dark_mode(); @@ -3425,63 +3727,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); - } - touch_state.clear(); - - bool self_steal = false; - HWND new_hwnd = (HWND)wParam; - if (IsWindow(new_hwnd)) { - self_steal = true; + case WM_ACTIVATEAPP: { + bool new_app_focused = (bool)wParam; + if (new_app_focused == app_focused) { + break; } - - 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: { @@ -3498,6 +3755,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; @@ -3549,9 +3815,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: { @@ -3580,9 +3852,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. @@ -3694,7 +3963,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); } } @@ -3742,7 +4011,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; } @@ -3792,7 +4061,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); } } @@ -3878,7 +4147,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; } @@ -3941,7 +4210,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); } @@ -3993,7 +4262,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; } @@ -4285,10 +4554,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; @@ -4341,10 +4623,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: @@ -4493,7 +4771,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); @@ -4550,20 +4828,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) { @@ -4731,7 +5014,7 @@ void DisplayServerWindows::_update_tablet_ctx(const String &p_old_driver, const if ((p_old_driver == "wintab") && wintab_available && wd.wtctx) { wintab_WTEnable(wd.wtctx, false); wintab_WTClose(wd.wtctx); - wd.wtctx = 0; + wd.wtctx = nullptr; } if ((p_new_driver == "wintab") && wintab_available) { wintab_WTInfo(WTI_DEFSYSCTX, 0, &wd.wtlc); @@ -4767,7 +5050,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; @@ -4792,8 +5075,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; @@ -4820,8 +5102,6 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, dwExStyle, L"Engine", L"", dwStyle, - // (GetSystemMetrics(SM_CXSCREEN) - WindowRect.right) / 2, - // (GetSystemMetrics(SM_CYSCREEN) - WindowRect.bottom) / 2, WindowRect.left, WindowRect.top, WindowRect.right - WindowRect.left, @@ -4939,7 +5219,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, print_verbose("WinTab context creation failed."); } } else { - wd.wtctx = 0; + wd.wtctx = nullptr; } if (p_mode == WINDOW_MODE_MAXIMIZED) { @@ -4956,9 +5236,36 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, wd.last_pressure_update = 0; wd.last_tilt = Vector2(); + IPropertyStore *prop_store; + HRESULT hr = SHGetPropertyStoreForWindow(wd.hWnd, IID_IPropertyStore, (void **)&prop_store); + if (hr == S_OK) { + PROPVARIANT val; + String appname; + if (Engine::get_singleton()->is_editor_hint()) { + appname = "Godot.GodotEditor." + String(VERSION_BRANCH); + } else { + String name = GLOBAL_GET("application/config/name"); + String version = GLOBAL_GET("application/config/version"); + if (version.is_empty()) { + version = "0"; + } + String clean_app_name = name.to_pascal_case(); + for (int i = 0; i < clean_app_name.length(); i++) { + if (!is_ascii_alphanumeric_char(clean_app_name[i]) && clean_app_name[i] != '_' && clean_app_name[i] != '.') { + clean_app_name[i] = '_'; + } + } + clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix("."); + appname = "Godot." + clean_app_name + "." + version; + } + InitPropVariantFromString((PCWSTR)appname.utf16().get_data(), &val); + prop_store->SetValue(PKEY_AppUserModel_ID, val); + prop_store->Release(); + } + // IME. wd.im_himc = ImmGetContext(wd.hWnd); - ImmAssociateContext(wd.hWnd, (HIMC)0); + ImmAssociateContext(wd.hWnd, (HIMC) nullptr); wd.im_position = Vector2(); @@ -4976,6 +5283,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++; } @@ -5013,17 +5326,17 @@ Vector2i _get_device_ids(const String &p_device_name) { REFCLSID clsid = CLSID_WbemLocator; // Unmarshaler CLSID REFIID uuid = IID_IWbemLocator; // Interface UUID - IWbemLocator *wbemLocator = NULL; // to get the services - IWbemServices *wbemServices = NULL; // to get the class - IEnumWbemClassObject *iter = NULL; + IWbemLocator *wbemLocator = nullptr; // to get the services + IWbemServices *wbemServices = nullptr; // to get the class + IEnumWbemClassObject *iter = nullptr; IWbemClassObject *pnpSDriverObject[1]; // contains driver name, version, etc. - HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, uuid, (LPVOID *)&wbemLocator); + HRESULT hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, uuid, (LPVOID *)&wbemLocator); if (hr != S_OK) { return Vector2i(); } BSTR resource_name = SysAllocString(L"root\\CIMV2"); - hr = wbemLocator->ConnectServer(resource_name, NULL, NULL, NULL, 0, NULL, NULL, &wbemServices); + hr = wbemLocator->ConnectServer(resource_name, nullptr, nullptr, nullptr, 0, nullptr, nullptr, &wbemServices); SysFreeString(resource_name); SAFE_RELEASE(wbemLocator) // from now on, use `wbemServices` @@ -5037,7 +5350,7 @@ Vector2i _get_device_ids(const String &p_device_name) { const String gpu_device_class_query = vformat("SELECT * FROM Win32_PnPSignedDriver WHERE DeviceName = \"%s\"", p_device_name); BSTR query = SysAllocString((const WCHAR *)gpu_device_class_query.utf16().get_data()); BSTR query_lang = SysAllocString(L"WQL"); - hr = wbemServices->ExecQuery(query_lang, query, WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, NULL, &iter); + hr = wbemServices->ExecQuery(query_lang, query, WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, nullptr, &iter); SysFreeString(query_lang); SysFreeString(query); if (hr == S_OK) { @@ -5048,7 +5361,7 @@ Vector2i _get_device_ids(const String &p_device_name) { VARIANT did; VariantInit(&did); BSTR object_name = SysAllocString(L"DeviceID"); - hr = pnpSDriverObject[0]->Get(object_name, 0, &did, NULL, NULL); + hr = pnpSDriverObject[0]->Get(object_name, 0, &did, nullptr, nullptr); SysFreeString(object_name); if (hr == S_OK) { String device_id = String(V_BSTR(&did)); @@ -5162,6 +5475,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win if (tts_enabled) { tts = memnew(TTS_Windows); } + native_menu = memnew(NativeMenuWindows); // Enforce default keep screen on value. screen_set_keep_on(GLOBAL_GET("display/window/energy_saving/keep_screen_on")); @@ -5187,6 +5501,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) { @@ -5245,6 +5585,23 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } + HMODULE comctl32 = LoadLibraryW(L"comctl32.dll"); + if (comctl32) { + typedef BOOL(WINAPI * InitCommonControlsExPtr)(_In_ const INITCOMMONCONTROLSEX *picce); + InitCommonControlsExPtr init_common_controls_ex = (InitCommonControlsExPtr)GetProcAddress(comctl32, "InitCommonControlsEx"); + + // Fails if the incorrect version was loaded. Probably not a big enough deal to print an error about. + if (init_common_controls_ex) { + INITCOMMONCONTROLSEX icc = {}; + icc.dwICC = ICC_STANDARD_CLASSES; + icc.dwSize = sizeof(INITCOMMONCONTROLSEX); + if (!init_common_controls_ex(&icc)) { + WARN_PRINT("Unable to initialize Windows common controls. Native dialogs may not work properly."); + } + } + FreeLibrary(comctl32); + } + memset(&wc, 0, sizeof(WNDCLASSEXW)); wc.cbSize = sizeof(WNDCLASSEXW); wc.style = CS_OWNDC | CS_DBLCLKS; @@ -5295,7 +5652,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win if (rendering_driver == "opengl3") { rendering_driver = "opengl3_angle"; } -#else +#elif defined(EGL_STATIC) bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_angle"); if (fallback && (rendering_driver == "opengl3")) { Dictionary gl_info = detect_wgl(); @@ -5354,6 +5711,26 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } #endif + String appname; + if (Engine::get_singleton()->is_editor_hint()) { + appname = "Godot.GodotEditor." + String(VERSION_BRANCH); + } else { + String name = GLOBAL_GET("application/config/name"); + String version = GLOBAL_GET("application/config/version"); + if (version.is_empty()) { + version = "0"; + } + String clean_app_name = name.to_pascal_case(); + for (int i = 0; i < clean_app_name.length(); i++) { + if (!is_ascii_alphanumeric_char(clean_app_name[i]) && clean_app_name[i] != '_' && clean_app_name[i] != '.') { + clean_app_name[i] = '_'; + } + } + clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix("."); + appname = "Godot." + clean_app_name + "." + version; + } + SetCurrentProcessExplicitAppUserModelID((PCWSTR)appname.utf16().get_data()); + mouse_monitor = SetWindowsHookEx(WH_MOUSE, ::MouseProc, nullptr, GetCurrentThreadId()); Point2i window_position; @@ -5479,7 +5856,7 @@ DisplayServerWindows::~DisplayServerWindows() { cursors_cache.clear(); // Destroy all status indicators. - for (HashMap<IndicatorID, IndicatorData>::Iterator E = indicators.begin(); E;) { + for (HashMap<IndicatorID, IndicatorData>::Iterator E = indicators.begin(); E; ++E) { NOTIFYICONDATAW ndat; ZeroMemory(&ndat, sizeof(NOTIFYICONDATAW)); ndat.cbSize = sizeof(NOTIFYICONDATAW); @@ -5501,6 +5878,11 @@ DisplayServerWindows::~DisplayServerWindows() { // Close power request handle. screen_set_keep_on(false); + if (native_menu) { + memdelete(native_menu); + native_menu = nullptr; + } + #ifdef GLES3_ENABLED // destroy windows .. NYI? // FIXME wglDeleteContext is never called @@ -5518,7 +5900,7 @@ DisplayServerWindows::~DisplayServerWindows() { #endif if (wintab_available && windows[MAIN_WINDOW_ID].wtctx) { wintab_WTClose(windows[MAIN_WINDOW_ID].wtctx); - windows[MAIN_WINDOW_ID].wtctx = 0; + windows[MAIN_WINDOW_ID].wtctx = nullptr; } DestroyWindow(windows[MAIN_WINDOW_ID].hWnd); } @@ -5536,7 +5918,7 @@ DisplayServerWindows::~DisplayServerWindows() { #endif if (restore_mouse_trails > 1) { - SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, 0, 0); + SystemParametersInfoA(SPI_SETMOUSETRAILS, restore_mouse_trails, nullptr, 0); } #ifdef GLES3_ENABLED if (gl_manager_angle) { diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 81cddec49f..2fe1b0733d 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -61,6 +61,8 @@ #include "gl_manager_windows_native.h" #endif // GLES3_ENABLED +#include "native_menu_windows.h" + #include <io.h> #include <stdio.h> @@ -152,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 @@ -192,6 +206,7 @@ typedef UINT32 PEN_MASK; #define POINTER_MESSAGE_FLAG_FIRSTBUTTON 0x00000010 #endif +#if WINVER < 0x0602 enum tagPOINTER_INPUT_TYPE { PT_POINTER = 0x00000001, PT_TOUCH = 0x00000002, @@ -242,6 +257,7 @@ typedef struct tagPOINTER_PEN_INFO { INT32 tiltX; INT32 tiltY; } POINTER_PEN_INFO; +#endif #endif //POINTER_STRUCTURES @@ -356,6 +372,7 @@ class DisplayServerWindows : public DisplayServer { HANDLE power_request; TTS_Windows *tts = nullptr; + NativeMenuWindows *native_menu = nullptr; struct WindowData { HWND hWnd; @@ -365,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; @@ -374,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; @@ -385,7 +402,6 @@ class DisplayServerWindows : public DisplayServer { // Timers. uint32_t move_timer_id = 0U; - uint32_t focus_timer_id = 0U; HANDLE wtctx; LOGCONTEXTW wtlc; @@ -455,7 +471,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; @@ -642,6 +658,9 @@ public: virtual void enable_for_stealing_focus(OS::ProcessID pid) override; + virtual Error dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) override; + virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; + virtual int keyboard_get_layout_count() const override; virtual int keyboard_get_current_layout() const override; virtual void keyboard_set_current_layout(int p_index) override; @@ -660,7 +679,6 @@ 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; 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 0c6721e118..a3c86611a4 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -285,15 +285,15 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> if (p_preset->get("codesign/enable")) { _code_sign(p_preset, pck_path); - String wrapper_path = p_path.get_basename() + ".console.exe"; + String wrapper_path = path.get_basename() + ".console.exe"; if (FileAccess::exists(wrapper_path)) { _code_sign(p_preset, wrapper_path); } } if (embedded) { - Ref<DirAccess> tmp_dir = DirAccess::create_for_path(p_path.get_base_dir()); - err = tmp_dir->rename(pck_path, p_path); + Ref<DirAccess> tmp_dir = DirAccess::create_for_path(path.get_base_dir()); + err = tmp_dir->rename(pck_path, path); if (err != OK) { add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), vformat(TTR("Failed to rename temporary file \"%s\"."), pck_path)); } @@ -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 8af32395b7..80cf818199 100644 --- a/platform/windows/gl_manager_windows_native.cpp +++ b/platform/windows/gl_manager_windows_native.cpp @@ -107,22 +107,22 @@ static bool nvapi_err_check(const char *msg, int status) { // to avoid stuttering, see https://stackoverflow.com/questions/36959508/nvidia-graphics-driver-causing-noticeable-frame-stuttering/37632948 // also see https://github.com/Ryujinx/Ryujinx/blob/master/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { - HMODULE nvapi = 0; + HMODULE nvapi = nullptr; #ifdef _WIN64 nvapi = LoadLibraryA("nvapi64.dll"); #else nvapi = LoadLibraryA("nvapi.dll"); #endif - if (nvapi == NULL) { + if (nvapi == nullptr) { return; } - void *(__cdecl * NvAPI_QueryInterface)(unsigned int interface_id) = 0; + void *(__cdecl * NvAPI_QueryInterface)(unsigned int interface_id) = nullptr; NvAPI_QueryInterface = (void *(__cdecl *)(unsigned int))(void *)GetProcAddress(nvapi, "nvapi_QueryInterface"); - if (NvAPI_QueryInterface == NULL) { + if (NvAPI_QueryInterface == nullptr) { print_verbose("Error getting NVAPI NvAPI_QueryInterface"); return; } @@ -176,7 +176,7 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { Char16String app_executable_name_u16 = app_executable_name.utf16(); Char16String app_friendly_name_u16 = app_friendly_name.utf16(); - NvDRSProfileHandle profile_handle = 0; + NvDRSProfileHandle profile_handle = nullptr; int profile_status = NvAPI_DRS_FindProfileByName(session_handle, (NvU16 *)(app_profile_name_u16.ptrw()), &profile_handle); @@ -195,7 +195,7 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { } } - NvDRSProfileHandle app_profile_handle = 0; + NvDRSProfileHandle app_profile_handle = nullptr; NVDRS_APPLICATION_V4 app; app.version = NVDRS_APPLICATION_VER_V4; @@ -362,14 +362,14 @@ Error GLManagerNative_Windows::_create_context(GLWindow &win, GLDisplay &gl_disp if (wglCreateContextAttribsARB == nullptr) //OpenGL 3.0 is not supported { gd_wglDeleteContext(gl_display.hRC); - gl_display.hRC = 0; + gl_display.hRC = nullptr; return ERR_CANT_CREATE; } - HGLRC new_hRC = wglCreateContextAttribsARB(win.hDC, 0, attribs); + HGLRC new_hRC = wglCreateContextAttribsARB(win.hDC, nullptr, attribs); if (!new_hRC) { gd_wglDeleteContext(gl_display.hRC); - gl_display.hRC = 0; + gl_display.hRC = nullptr; return ERR_CANT_CREATE; } @@ -384,7 +384,7 @@ Error GLManagerNative_Windows::_create_context(GLWindow &win, GLDisplay &gl_disp { ERR_PRINT("Could not attach OpenGL context to newly created window with replaced OpenGL context: " + format_error_message(GetLastError())); gd_wglDeleteContext(gl_display.hRC); - gl_display.hRC = 0; + gl_display.hRC = nullptr; return ERR_CANT_CREATE; } @@ -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_res.rc b/platform/windows/godot_res.rc index 8187c0c936..86191ad9d9 100644 --- a/platform/windows/godot_res.rc +++ b/platform/windows/godot_res.rc @@ -1,6 +1,11 @@ #include "core/version.h" +#ifndef RT_MANIFEST +#define RT_MANIFEST 24 +#endif + GODOT_ICON ICON platform/windows/godot.ico +1 RT_MANIFEST "godot.manifest" 1 VERSIONINFO FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0 diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index f86ecd87fb..5f41b4e568 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(); diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp new file mode 100644 index 0000000000..40a08f87df --- /dev/null +++ b/platform/windows/native_menu_windows.cpp @@ -0,0 +1,1165 @@ +/**************************************************************************/ +/* native_menu_windows.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 "native_menu_windows.h" + +#include "display_server_windows.h" + +#include "scene/resources/image_texture.h" + +HBITMAP NativeMenuWindows::_make_bitmap(const Ref<Image> &p_img) const { + Vector2i texture_size = p_img->get_size(); + UINT image_size = texture_size.width * texture_size.height; + + COLORREF *buffer = nullptr; + + BITMAPV5HEADER bi; + ZeroMemory(&bi, sizeof(bi)); + bi.bV5Size = sizeof(bi); + bi.bV5Width = texture_size.width; + bi.bV5Height = -texture_size.height; + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00ff0000; + bi.bV5GreenMask = 0x0000ff00; + bi.bV5BlueMask = 0x000000ff; + bi.bV5AlphaMask = 0xff000000; + + HDC dc = GetDC(nullptr); + HBITMAP bitmap = CreateDIBSection(dc, reinterpret_cast<BITMAPINFO *>(&bi), DIB_RGB_COLORS, reinterpret_cast<void **>(&buffer), nullptr, 0); + for (UINT index = 0; index < image_size; index++) { + int row_index = floor(index / texture_size.width); + int column_index = (index % int(texture_size.width)); + const Color &c = p_img->get_pixel(column_index, row_index); + *(buffer + index) = c.to_argb32(); + } + ReleaseDC(nullptr, dc); + + return bitmap; +} + +void NativeMenuWindows::_menu_activate(HMENU p_menu, int p_index) const { + if (menu_lookup.has(p_menu)) { + MenuData *md = menus.get_or_null(menu_lookup[p_menu]); + if (md) { + int count = GetMenuItemCount(md->menu); + if (p_index >= 0 && p_index < count) { + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STATE | MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_index, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + if (item_data->max_states > 0) { + item_data->state++; + if (item_data->state >= item_data->max_states) { + item_data->state = 0; + } + } + + if (item_data->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { + if ((item.fState & MFS_CHECKED) == MFS_CHECKED) { + item.fState &= ~MFS_CHECKED; + } else { + item.fState |= MFS_CHECKED; + } + SetMenuItemInfoW(md->menu, p_index, true, &item); + } + + if (item_data->callback.is_valid()) { + Variant ret; + Callable::CallError ce; + const Variant *args[1] = { &item_data->meta }; + + item_data->callback.callp(args, 1, ret, ce); + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute menu callback: %s.", Variant::get_callable_error_text(item_data->callback, args, 1, ce))); + } + } + } + } + } + } + } +} + +bool NativeMenuWindows::has_feature(Feature p_feature) const { + switch (p_feature) { + // case FEATURE_GLOBAL_MENU: + // case FEATURE_OPEN_CLOSE_CALLBACK: + // case FEATURE_HOVER_CALLBACK: + // case FEATURE_KEY_CALLBACK: + case FEATURE_POPUP_MENU: + return true; + default: + return false; + } +} + +bool NativeMenuWindows::has_system_menu(SystemMenus p_menu_id) const { + return false; +} + +RID NativeMenuWindows::get_system_menu(SystemMenus p_menu_id) const { + return RID(); +} + +RID NativeMenuWindows::create_menu() { + MenuData *md = memnew(MenuData); + md->menu = CreatePopupMenu(); + + MENUINFO menu_info; + ZeroMemory(&menu_info, sizeof(menu_info)); + menu_info.cbSize = sizeof(menu_info); + menu_info.fMask = MIM_STYLE; + menu_info.dwStyle = MNS_NOTIFYBYPOS; + SetMenuInfo(md->menu, &menu_info); + + RID rid = menus.make_rid(md); + menu_lookup[md->menu] = rid; + return rid; +} + +bool NativeMenuWindows::has_menu(const RID &p_rid) const { + return menus.owns(p_rid); +} + +void NativeMenuWindows::free_menu(const RID &p_rid) { + MenuData *md = menus.get_or_null(p_rid); + if (md) { + clear(p_rid); + DestroyMenu(md->menu); + menus.free(p_rid); + menu_lookup.erase(md->menu); + memdelete(md); + } +} + +Size2 NativeMenuWindows::get_size(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Size2()); + + Size2 size; + int count = GetMenuItemCount(md->menu); + for (int i = 0; i < count; i++) { + RECT rect; + if (GetMenuItemRect(NULL, md->menu, i, &rect)) { + size.x = MAX(size.x, rect.right - rect.left); + size.y += rect.bottom - rect.top; + } + } + return size; +} + +void NativeMenuWindows::popup(const RID &p_rid, const Vector2i &p_position) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + HWND hwnd = (HWND)DisplayServer::get_singleton()->window_get_native_handle(DisplayServer::WINDOW_HANDLE, DisplayServer::MAIN_WINDOW_ID); + UINT flags = TPM_HORIZONTAL | TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON | TPM_VERPOSANIMATION; + 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) { + MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + if (md->is_rtl == p_is_rtl) { + return; + } + md->is_rtl = p_is_rtl; +} + +void NativeMenuWindows::set_popup_open_callback(const RID &p_rid, const Callable &p_callback) { + // Not supported. +} + +Callable NativeMenuWindows::get_popup_open_callback(const RID &p_rid) const { + // Not supported. + return Callable(); +} + +void NativeMenuWindows::set_popup_close_callback(const RID &p_rid, const Callable &p_callback) { + // Not supported. +} + +Callable NativeMenuWindows::get_popup_close_callback(const RID &p_rid) const { + // Not supported. + return Callable(); +} + +void NativeMenuWindows::set_minimum_width(const RID &p_rid, float p_width) { + // Not supported. +} + +float NativeMenuWindows::get_minimum_width(const RID &p_rid) const { + // Not supported. + return 0.f; +} + +int NativeMenuWindows::add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag, int p_index) { + MenuData *md = menus.get_or_null(p_rid); + MenuData *md_sub = menus.get_or_null(p_submenu_rid); + ERR_FAIL_NULL_V(md, -1); + ERR_FAIL_NULL_V(md_sub, -1); + ERR_FAIL_COND_V_MSG(md->menu == md_sub->menu, -1, "Can't set submenu to self!"); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_NONE; + item_data->max_states = 0; + item_data->state = 0; + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA | MIIM_SUBMENU; + item.fType = MFT_STRING; + item.hSubMenu = md_sub->menu; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_item(const RID &p_rid, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_NONE; + item_data->max_states = 0; + item_data->state = 0; + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA; + item.fType = MFT_STRING; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + item_data->max_states = 0; + item_data->state = 0; + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA; + item.fType = MFT_STRING; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_NONE; + item_data->max_states = 0; + item_data->state = 0; + item_data->img = p_icon->get_image(); + item_data->img = item_data->img->duplicate(); + if (item_data->img->is_compressed()) { + item_data->img->decompress(); + } + item_data->bmp = _make_bitmap(item_data->img); + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA | MIIM_BITMAP; + item.fType = MFT_STRING; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + item.hbmpItem = item_data->bmp; + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_icon_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + item_data->max_states = 0; + item_data->state = 0; + item_data->img = p_icon->get_image(); + item_data->img = item_data->img->duplicate(); + if (item_data->img->is_compressed()) { + item_data->img->decompress(); + } + item_data->bmp = _make_bitmap(item_data->img); + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA | MIIM_BITMAP; + item.fType = MFT_STRING; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + item.hbmpItem = item_data->bmp; + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_radio_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; + item_data->max_states = 0; + item_data->state = 0; + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA; + item.fType = MFT_STRING | MFT_RADIOCHECK; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_icon_radio_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; + item_data->max_states = 0; + item_data->state = 0; + item_data->img = p_icon->get_image(); + item_data->img = item_data->img->duplicate(); + if (item_data->img->is_compressed()) { + item_data->img->decompress(); + } + item_data->bmp = _make_bitmap(item_data->img); + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA | MIIM_BITMAP; + item.fType = MFT_STRING | MFT_RADIOCHECK; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + item.hbmpItem = item_data->bmp; + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_multistate_item(const RID &p_rid, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback, const Callable &p_key_callback, const Variant &p_tag, Key p_accel, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->callback = p_callback; + item_data->meta = p_tag; + item_data->checkable_type = CHECKABLE_TYPE_NONE; + item_data->max_states = p_max_states; + item_data->state = p_default_state; + + Char16String label = p_label.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA; + item.fType = MFT_STRING; + item.dwItemData = (ULONG_PTR)item_data; + item.dwTypeData = (LPWSTR)label.get_data(); + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::add_separator(const RID &p_rid, int p_index) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + if (p_index == -1) { + p_index = GetMenuItemCount(md->menu); + } else { + p_index = CLAMP(p_index, 0, GetMenuItemCount(md->menu)); + } + + MenuItemData *item_data = memnew(MenuItemData); + item_data->checkable_type = CHECKABLE_TYPE_NONE; + item_data->max_states = 0; + item_data->state = 0; + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_DATA; + item.fType = MFT_SEPARATOR; + item.dwItemData = (ULONG_PTR)item_data; + + if (!InsertMenuItemW(md->menu, p_index, true, &item)) { + memdelete(item_data); + return -1; + } + return p_index; +} + +int NativeMenuWindows::find_item_index_with_text(const RID &p_rid, const String &p_text) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + MENUITEMINFOW item; + int count = GetMenuItemCount(md->menu); + for (int i = 0; i < count; i++) { + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STRING; + item.dwTypeData = nullptr; + if (GetMenuItemInfoW(md->menu, i, true, &item)) { + 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; + } + } + } + } + return -1; +} + +int NativeMenuWindows::find_item_index_with_tag(const RID &p_rid, const Variant &p_tag) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + + MENUITEMINFOW item; + int count = GetMenuItemCount(md->menu); + for (int i = 0; i < count; i++) { + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, i, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + if (item_data->meta == p_tag) { + return i; + } + } + } + } + return -1; +} + +bool NativeMenuWindows::is_item_checked(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, false); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STATE; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + return (item.fState & MFS_CHECKED) == MFS_CHECKED; + } + return false; +} + +bool NativeMenuWindows::is_item_checkable(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, false); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->checkable_type == CHECKABLE_TYPE_CHECK_BOX; + } + } + return false; +} + +bool NativeMenuWindows::is_item_radio_checkable(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, false); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->checkable_type == CHECKABLE_TYPE_RADIO_BUTTON; + } + } + return false; +} + +Callable NativeMenuWindows::get_item_callback(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Callable()); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Callable()); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, Callable()); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->callback; + } + } + return Callable(); +} + +Callable NativeMenuWindows::get_item_key_callback(const RID &p_rid, int p_idx) const { + // Not supported. + return Callable(); +} + +Variant NativeMenuWindows::get_item_tag(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Variant()); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Variant()); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, Variant()); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->meta; + } + } + return Variant(); +} + +String NativeMenuWindows::get_item_text(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, String()); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, String()); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, String()); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STRING; + item.dwTypeData = nullptr; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + 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(); +} + +RID NativeMenuWindows::get_item_submenu(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, RID()); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, RID()); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, RID()); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_SUBMENU; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + if (menu_lookup.has(item.hSubMenu)) { + return menu_lookup[item.hSubMenu]; + } + } + return RID(); +} + +Key NativeMenuWindows::get_item_accelerator(const RID &p_rid, int p_idx) const { + // Not supported. + return Key::NONE; +} + +bool NativeMenuWindows::is_item_disabled(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, false); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, false); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, false); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STATE; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + return (item.fState & MFS_DISABLED) == MFS_DISABLED; + } + return false; +} + +bool NativeMenuWindows::is_item_hidden(const RID &p_rid, int p_idx) const { + // Not supported. + return false; +} + +String NativeMenuWindows::get_item_tooltip(const RID &p_rid, int p_idx) const { + // Not supported. + return String(); +} + +int NativeMenuWindows::get_item_state(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, -1); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, -1); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->state; + } + } + return -1; +} + +int NativeMenuWindows::get_item_max_states(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, -1); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, -1); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, -1); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->max_states; + } + } + return -1; +} + +Ref<Texture2D> NativeMenuWindows::get_item_icon(const RID &p_rid, int p_idx) const { + ERR_FAIL_COND_V(p_idx < 0, Ref<Texture2D>()); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, Ref<Texture2D>()); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND_V(p_idx >= count, Ref<Texture2D>()); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return ImageTexture::create_from_image(item_data->img); + } + } + return Ref<Texture2D>(); +} + +int NativeMenuWindows::get_item_indentation_level(const RID &p_rid, int p_idx) const { + // Not supported. + return 0; +} + +void NativeMenuWindows::set_item_checked(const RID &p_rid, int p_idx, bool p_checked) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STATE; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + if (p_checked) { + item.fState |= MFS_CHECKED; + } else { + item.fState &= ~MFS_CHECKED; + } + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } +} + +void NativeMenuWindows::set_item_checkable(const RID &p_rid, int p_idx, bool p_checkable) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item.fType &= ~MFT_RADIOCHECK; + item_data->checkable_type = (p_checkable) ? CHECKABLE_TYPE_CHECK_BOX : CHECKABLE_TYPE_NONE; + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } + } +} + +void NativeMenuWindows::set_item_radio_checkable(const RID &p_rid, int p_idx, bool p_checkable) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + if (p_checkable) { + item.fType |= MFT_RADIOCHECK; + item_data->checkable_type = CHECKABLE_TYPE_CHECK_BOX; + } else { + item.fType &= ~MFT_RADIOCHECK; + item_data->checkable_type = CHECKABLE_TYPE_NONE; + } + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } + } +} + +void NativeMenuWindows::set_item_callback(const RID &p_rid, int p_idx, const Callable &p_callback) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->callback = p_callback; + } + } +} + +void NativeMenuWindows::set_item_key_callback(const RID &p_rid, int p_idx, const Callable &p_key_callback) { + // Not supported. +} + +void NativeMenuWindows::set_item_hover_callbacks(const RID &p_rid, int p_idx, const Callable &p_callback) { + // Not supported. +} + +void NativeMenuWindows::set_item_tag(const RID &p_rid, int p_idx, const Variant &p_tag) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->meta = p_tag; + } + } +} + +void NativeMenuWindows::set_item_text(const RID &p_rid, int p_idx, const String &p_text) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + Char16String label = p_text.utf16(); + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_FTYPE | MIIM_STRING | MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + item.dwTypeData = (LPWSTR)label.get_data(); + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } +} + +void NativeMenuWindows::set_item_submenu(const RID &p_rid, int p_idx, const RID &p_submenu_rid) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MenuData *md_sub = menus.get_or_null(p_submenu_rid); + ERR_FAIL_COND_MSG(md->menu == md_sub->menu, "Can't set submenu to self!"); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_SUBMENU; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + if (p_submenu_rid.is_valid()) { + item.hSubMenu = md_sub->menu; + } else { + item.hSubMenu = 0; + } + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } +} + +void NativeMenuWindows::set_item_accelerator(const RID &p_rid, int p_idx, Key p_keycode) { + // Not supported. +} + +void NativeMenuWindows::set_item_disabled(const RID &p_rid, int p_idx, bool p_disabled) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_STATE; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + if (p_disabled) { + item.fState |= MFS_DISABLED; + } else { + item.fState &= ~MFS_DISABLED; + } + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } +} + +void NativeMenuWindows::set_item_hidden(const RID &p_rid, int p_idx, bool p_hidden) { + // Not supported. +} + +void NativeMenuWindows::set_item_tooltip(const RID &p_rid, int p_idx, const String &p_tooltip) { + // Not supported. +} + +void NativeMenuWindows::set_item_state(const RID &p_rid, int p_idx, int p_state) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->state = p_state; + } + } +} + +void NativeMenuWindows::set_item_max_states(const RID &p_rid, int p_idx, int p_max_states) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->max_states = p_max_states; + } + } +} + +void NativeMenuWindows::set_item_icon(const RID &p_rid, int p_idx, const Ref<Texture2D> &p_icon) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA | MIIM_BITMAP; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + if (item_data->bmp) { + DeleteObject(item_data->bmp); + } + if (p_icon.is_valid()) { + item_data->img = p_icon->get_image(); + item_data->img = item_data->img->duplicate(); + if (item_data->img->is_compressed()) { + item_data->img->decompress(); + } + item_data->bmp = _make_bitmap(item_data->img); + } else { + item_data->img = Ref<Image>(); + item_data->bmp = 0; + } + item.hbmpItem = item_data->bmp; + SetMenuItemInfoW(md->menu, p_idx, true, &item); + } + } +} + +void NativeMenuWindows::set_item_indentation_level(const RID &p_rid, int p_idx, int p_level) { + // Not supported. +} + +int NativeMenuWindows::get_item_count(const RID &p_rid) const { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL_V(md, 0); + + return GetMenuItemCount(md->menu); +} + +bool NativeMenuWindows::is_system_menu(const RID &p_rid) const { + return false; +} + +void NativeMenuWindows::remove_item(const RID &p_rid, int p_idx) { + ERR_FAIL_COND(p_idx < 0); + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + int count = GetMenuItemCount(md->menu); + ERR_FAIL_COND(p_idx >= count); + + MENUITEMINFOW item; + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + if (item_data->bmp) { + DeleteObject(item_data->bmp); + } + memdelete(item_data); + } + } + RemoveMenu(md->menu, p_idx, MF_BYPOSITION); +} + +void NativeMenuWindows::clear(const RID &p_rid) { + const MenuData *md = menus.get_or_null(p_rid); + ERR_FAIL_NULL(md); + + MENUITEMINFOW item; + int count = GetMenuItemCount(md->menu); + for (int i = 0; i < count; i++) { + ZeroMemory(&item, sizeof(item)); + item.cbSize = sizeof(item); + item.fMask = MIIM_DATA; + if (GetMenuItemInfoW(md->menu, 0, true, &item)) { + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + if (item_data->bmp) { + DeleteObject(item_data->bmp); + } + memdelete(item_data); + } + } + RemoveMenu(md->menu, 0, MF_BYPOSITION); + } +} + +NativeMenuWindows::NativeMenuWindows() {} + +NativeMenuWindows::~NativeMenuWindows() {} diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h new file mode 100644 index 0000000000..74fd231903 --- /dev/null +++ b/platform/windows/native_menu_windows.h @@ -0,0 +1,151 @@ +/**************************************************************************/ +/* native_menu_windows.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 NATIVE_MENU_WINDOWS_H +#define NATIVE_MENU_WINDOWS_H + +#include "core/templates/hash_map.h" +#include "core/templates/rid_owner.h" +#include "servers/display/native_menu.h" + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +class NativeMenuWindows : public NativeMenu { + GDCLASS(NativeMenuWindows, NativeMenu) + + enum GlobalMenuCheckType { + CHECKABLE_TYPE_NONE, + CHECKABLE_TYPE_CHECK_BOX, + CHECKABLE_TYPE_RADIO_BUTTON, + }; + + struct MenuItemData { + Callable callback; + Variant meta; + GlobalMenuCheckType checkable_type; + int max_states = 0; + int state = 0; + Ref<Image> img; + HBITMAP bmp = 0; + }; + + struct MenuData { + HMENU menu = 0; + bool is_rtl = false; + }; + + mutable RID_PtrOwner<MenuData> menus; + HashMap<HMENU, RID> menu_lookup; + + HBITMAP _make_bitmap(const Ref<Image> &p_img) const; + +public: + void _menu_activate(HMENU p_menu, int p_index) const; + + virtual bool has_feature(Feature p_feature) const override; + + virtual bool has_system_menu(SystemMenus p_menu_id) const override; + virtual RID get_system_menu(SystemMenus p_menu_id) const override; + + virtual RID create_menu() override; + virtual bool has_menu(const RID &p_rid) const override; + virtual void free_menu(const RID &p_rid) override; + + virtual Size2 get_size(const RID &p_rid) const override; + virtual void popup(const RID &p_rid, const Vector2i &p_position) override; + + virtual void set_interface_direction(const RID &p_rid, bool p_is_rtl) override; + virtual void set_popup_open_callback(const RID &p_rid, const Callable &p_callback) override; + virtual Callable get_popup_open_callback(const RID &p_rid) const override; + virtual void set_popup_close_callback(const RID &p_rid, const Callable &p_callback) override; + virtual Callable get_popup_close_callback(const RID &p_rid) const override; + virtual void set_minimum_width(const RID &p_rid, float p_width) override; + virtual float get_minimum_width(const RID &p_rid) const override; + + virtual int add_submenu_item(const RID &p_rid, const String &p_label, const RID &p_submenu_rid, const Variant &p_tag = Variant(), int p_index = -1) override; + virtual int add_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_icon_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_radio_check_item(const RID &p_rid, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_icon_radio_check_item(const RID &p_rid, const Ref<Texture2D> &p_icon, const String &p_label, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_multistate_item(const RID &p_rid, const String &p_label, int p_max_states, int p_default_state, const Callable &p_callback = Callable(), const Callable &p_key_callback = Callable(), const Variant &p_tag = Variant(), Key p_accel = Key::NONE, int p_index = -1) override; + virtual int add_separator(const RID &p_rid, int p_index = -1) override; + + virtual int find_item_index_with_text(const RID &p_rid, const String &p_text) const override; + virtual int find_item_index_with_tag(const RID &p_rid, const Variant &p_tag) const override; + + virtual bool is_item_checked(const RID &p_rid, int p_idx) const override; + virtual bool is_item_checkable(const RID &p_rid, int p_idx) const override; + virtual bool is_item_radio_checkable(const RID &p_rid, int p_idx) const override; + virtual Callable get_item_callback(const RID &p_rid, int p_idx) const override; + virtual Callable get_item_key_callback(const RID &p_rid, int p_idx) const override; + virtual Variant get_item_tag(const RID &p_rid, int p_idx) const override; + virtual String get_item_text(const RID &p_rid, int p_idx) const override; + virtual RID get_item_submenu(const RID &p_rid, int p_idx) const override; + virtual Key get_item_accelerator(const RID &p_rid, int p_idx) const override; + virtual bool is_item_disabled(const RID &p_rid, int p_idx) const override; + virtual bool is_item_hidden(const RID &p_rid, int p_idx) const override; + virtual String get_item_tooltip(const RID &p_rid, int p_idx) const override; + virtual int get_item_state(const RID &p_rid, int p_idx) const override; + virtual int get_item_max_states(const RID &p_rid, int p_idx) const override; + virtual Ref<Texture2D> get_item_icon(const RID &p_rid, int p_idx) const override; + virtual int get_item_indentation_level(const RID &p_rid, int p_idx) const override; + + virtual void set_item_checked(const RID &p_rid, int p_idx, bool p_checked) override; + virtual void set_item_checkable(const RID &p_rid, int p_idx, bool p_checkable) override; + virtual void set_item_radio_checkable(const RID &p_rid, int p_idx, bool p_checkable) override; + virtual void set_item_callback(const RID &p_rid, int p_idx, const Callable &p_callback) override; + virtual void set_item_key_callback(const RID &p_rid, int p_idx, const Callable &p_key_callback) override; + virtual void set_item_hover_callbacks(const RID &p_rid, int p_idx, const Callable &p_callback) override; + virtual void set_item_tag(const RID &p_rid, int p_idx, const Variant &p_tag) override; + virtual void set_item_text(const RID &p_rid, int p_idx, const String &p_text) override; + virtual void set_item_submenu(const RID &p_rid, int p_idx, const RID &p_submenu_rid) override; + virtual void set_item_accelerator(const RID &p_rid, int p_idx, Key p_keycode) override; + virtual void set_item_disabled(const RID &p_rid, int p_idx, bool p_disabled) override; + virtual void set_item_hidden(const RID &p_rid, int p_idx, bool p_hidden) override; + virtual void set_item_tooltip(const RID &p_rid, int p_idx, const String &p_tooltip) override; + virtual void set_item_state(const RID &p_rid, int p_idx, int p_state) override; + virtual void set_item_max_states(const RID &p_rid, int p_idx, int p_max_states) override; + virtual void set_item_icon(const RID &p_rid, int p_idx, const Ref<Texture2D> &p_icon) override; + virtual void set_item_indentation_level(const RID &p_rid, int p_idx, int p_level) override; + + virtual int get_item_count(const RID &p_rid) const override; + virtual bool is_system_menu(const RID &p_rid) const override; + + virtual void remove_item(const RID &p_rid, int p_idx) override; + virtual void clear(const RID &p_rid) override; + + NativeMenuWindows(); + ~NativeMenuWindows(); +}; + +#endif // NATIVE_MENU_WINDOWS_H diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 93d1ffeac1..abed93d414 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" @@ -178,6 +180,7 @@ void OS_Windows::initialize() { 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 +270,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); @@ -329,7 +336,7 @@ void debug_dynamic_library_check_dependencies(const String &p_root_path, const S if (import_desc) { for (; import_desc->Name && import_desc->FirstThunk; import_desc++) { char16_t full_name_wc[MAX_PATH]; - const char *name_cs = (const char *)ImageRvaToVa(loaded_image.FileHeader, loaded_image.MappedAddress, import_desc->Name, 0); + const char *name_cs = (const char *)ImageRvaToVa(loaded_image.FileHeader, loaded_image.MappedAddress, import_desc->Name, nullptr); String name = String(name_cs); if (name.begins_with("api-ms-win-")) { r_checked.insert(name); @@ -352,7 +359,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 +369,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 +407,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 +435,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 +446,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 +461,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) { @@ -463,9 +522,9 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { REFCLSID clsid = CLSID_WbemLocator; // Unmarshaler CLSID REFIID uuid = IID_IWbemLocator; // Interface UUID - IWbemLocator *wbemLocator = NULL; // to get the services - IWbemServices *wbemServices = NULL; // to get the class - IEnumWbemClassObject *iter = NULL; + IWbemLocator *wbemLocator = nullptr; // to get the services + IWbemServices *wbemServices = nullptr; // to get the class + IEnumWbemClassObject *iter = nullptr; IWbemClassObject *pnpSDriverObject[1]; // contains driver name, version, etc. String driver_name; String driver_version; @@ -475,12 +534,12 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { return Vector<String>(); } - HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, uuid, (LPVOID *)&wbemLocator); + HRESULT hr = CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, uuid, (LPVOID *)&wbemLocator); if (hr != S_OK) { return Vector<String>(); } BSTR resource_name = SysAllocString(L"root\\CIMV2"); - hr = wbemLocator->ConnectServer(resource_name, NULL, NULL, NULL, 0, NULL, NULL, &wbemServices); + hr = wbemLocator->ConnectServer(resource_name, nullptr, nullptr, nullptr, 0, nullptr, nullptr, &wbemServices); SysFreeString(resource_name); SAFE_RELEASE(wbemLocator) // from now on, use `wbemServices` @@ -492,7 +551,7 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { const String gpu_device_class_query = vformat("SELECT * FROM Win32_PnPSignedDriver WHERE DeviceName = \"%s\"", device_name); BSTR query = SysAllocString((const WCHAR *)gpu_device_class_query.utf16().get_data()); BSTR query_lang = SysAllocString(L"WQL"); - hr = wbemServices->ExecQuery(query_lang, query, WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, NULL, &iter); + hr = wbemServices->ExecQuery(query_lang, query, WBEM_FLAG_RETURN_IMMEDIATELY | WBEM_FLAG_FORWARD_ONLY, nullptr, &iter); SysFreeString(query_lang); SysFreeString(query); if (hr == S_OK) { @@ -504,13 +563,13 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { VariantInit(&dn); BSTR object_name = SysAllocString(L"DriverName"); - hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, NULL, NULL); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, nullptr, nullptr); SysFreeString(object_name); if (hr == S_OK) { String d_name = String(V_BSTR(&dn)); if (d_name.is_empty()) { object_name = SysAllocString(L"DriverProviderName"); - hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, NULL, NULL); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, nullptr, nullptr); SysFreeString(object_name); if (hr == S_OK) { driver_name = String(V_BSTR(&dn)); @@ -520,7 +579,7 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { } } else { object_name = SysAllocString(L"DriverProviderName"); - hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, NULL, NULL); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dn, nullptr, nullptr); SysFreeString(object_name); if (hr == S_OK) { driver_name = String(V_BSTR(&dn)); @@ -530,7 +589,7 @@ Vector<String> OS_Windows::get_video_adapter_driver_info() const { VARIANT dv; VariantInit(&dv); object_name = SysAllocString(L"DriverVersion"); - hr = pnpSDriverObject[0]->Get(object_name, 0, &dv, NULL, NULL); + hr = pnpSDriverObject[0]->Get(object_name, 0, &dv, nullptr, nullptr); SysFreeString(object_name); if (hr == S_OK) { driver_version = String(V_BSTR(&dv)); @@ -727,6 +786,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); @@ -783,7 +943,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, DWORD read = 0; for (;;) { // Read StdOut and StdErr from pipe. bytes.resize(bytes_in_buffer + CHUNK_SIZE); - const bool success = ReadFile(pipe[0], bytes.ptr() + bytes_in_buffer, CHUNK_SIZE, &read, NULL); + const bool success = ReadFile(pipe[0], bytes.ptr() + bytes_in_buffer, CHUNK_SIZE, &read, nullptr); if (!success || read == 0) { break; } @@ -856,13 +1016,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); @@ -873,7 +1036,7 @@ Error OS_Windows::kill(const ProcessID &p_pid) { CloseHandle(pi.hThread); } else { HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, false, (DWORD)p_pid); - if (hProcess != NULL) { + if (hProcess != nullptr) { ret = TerminateProcess(hProcess, 0); CloseHandle(hProcess); @@ -888,24 +1051,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; @@ -1455,7 +1652,7 @@ String OS_Windows::get_processor_name() const { WCHAR buffer[256]; DWORD buffer_len = 256; DWORD vtype = REG_SZ; - if (RegQueryValueExW(hkey, L"ProcessorNameString", NULL, &vtype, (LPBYTE)buffer, &buffer_len) == ERROR_SUCCESS) { + if (RegQueryValueExW(hkey, L"ProcessorNameString", nullptr, &vtype, (LPBYTE)buffer, &buffer_len) == ERROR_SUCCESS) { RegCloseKey(hkey); return String::utf16((const char16_t *)buffer, buffer_len).strip_edges(); } else { diff --git a/platform/windows/os_windows.h b/platform/windows/os_windows.h index 7e7d23a2a8..b6a21ed42d 100644 --- a/platform/windows/os_windows.h +++ b/platform/windows/os_windows.h @@ -69,7 +69,7 @@ #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x4 #endif -template <class T> +template <typename T> class ComAutoreleaseRef { public: T *reference = nullptr; @@ -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/platform_windows_builders.py b/platform/windows/platform_windows_builders.py index 652dc06acf..729d55cea6 100644 --- a/platform/windows/platform_windows_builders.py +++ b/platform/windows/platform_windows_builders.py @@ -1,31 +1,24 @@ -"""Functions used to generate source files during build time +"""Functions used to generate source files during build time""" -All such functions are invoked in a subprocess on Windows to prevent build flakiness. - -""" import os from detect import get_mingw_bin_prefix from detect import try_cmd -from platform_methods import subprocess_main def make_debug_mingw(target, source, env): + dst = str(target[0]) # Force separate debug symbols if executable size is larger than 1.9 GB. - if env["separate_debug_symbols"] or os.stat(target[0]).st_size >= 2040109465: + if env["separate_debug_symbols"] or os.stat(dst).st_size >= 2040109465: mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) if try_cmd("objcopy --version", env["mingw_prefix"], env["arch"]): - os.system(mingw_bin_prefix + "objcopy --only-keep-debug {0} {0}.debugsymbols".format(target[0])) + os.system(mingw_bin_prefix + "objcopy --only-keep-debug {0} {0}.debugsymbols".format(dst)) else: - os.system("objcopy --only-keep-debug {0} {0}.debugsymbols".format(target[0])) + os.system("objcopy --only-keep-debug {0} {0}.debugsymbols".format(dst)) if try_cmd("strip --version", env["mingw_prefix"], env["arch"]): - os.system(mingw_bin_prefix + "strip --strip-debug --strip-unneeded {0}".format(target[0])) + os.system(mingw_bin_prefix + "strip --strip-debug --strip-unneeded {0}".format(dst)) else: - os.system("strip --strip-debug --strip-unneeded {0}".format(target[0])) + os.system("strip --strip-debug --strip-unneeded {0}".format(dst)) if try_cmd("objcopy --version", env["mingw_prefix"], env["arch"]): - os.system(mingw_bin_prefix + "objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(target[0])) + os.system(mingw_bin_prefix + "objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(dst)) else: - os.system("objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(target[0])) - - -if __name__ == "__main__": - subprocess_main(globals()) + os.system("objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(dst)) diff --git a/platform/windows/tts_windows.cpp b/platform/windows/tts_windows.cpp index 11d63d85ee..39a8f3e120 100644 --- a/platform/windows/tts_windows.cpp +++ b/platform/windows/tts_windows.cpp @@ -35,7 +35,7 @@ TTS_Windows *TTS_Windows::singleton = nullptr; void __stdcall TTS_Windows::speech_event_callback(WPARAM wParam, LPARAM lParam) { TTS_Windows *tts = TTS_Windows::get_singleton(); SPEVENT event; - while (tts->synth->GetEvents(1, &event, NULL) == S_OK) { + while (tts->synth->GetEvents(1, &event, nullptr) == S_OK) { uint32_t stream_num = (uint32_t)event.ulStreamNum; if (tts->ids.has(stream_num)) { if (event.eEventId == SPEI_START_INPUT_STREAM) { @@ -82,7 +82,7 @@ void TTS_Windows::_update_tts() { if (SUCCEEDED(hr)) { hr = cpEnum->GetCount(&ulCount); while (SUCCEEDED(hr) && ulCount--) { - wchar_t *w_id = 0L; + wchar_t *w_id = nullptr; hr = cpEnum->Next(1, &cpVoiceToken, nullptr); cpVoiceToken->GetId(&w_id); if (String::utf16((const char16_t *)w_id) == message.voice) { diff --git a/platform/windows/wgl_detect_version.cpp b/platform/windows/wgl_detect_version.cpp index 49da4b58c7..12dd6f6ee6 100644 --- a/platform/windows/wgl_detect_version.cpp +++ b/platform/windows/wgl_detect_version.cpp @@ -140,7 +140,7 @@ Dictionary detect_wgl() { PFNWGLCREATECONTEXTATTRIBSARBPROC gd_wglCreateContextAttribsARB = nullptr; gd_wglCreateContextAttribsARB = (PFNWGLCREATECONTEXTATTRIBSARBPROC)gd_wglGetProcAddress("wglCreateContextAttribsARB"); if (gd_wglCreateContextAttribsARB) { - HGLRC new_hRC = gd_wglCreateContextAttribsARB(hDC, 0, attribs); + HGLRC new_hRC = gd_wglCreateContextAttribsARB(hDC, nullptr, attribs); if (new_hRC) { if (gd_wglMakeCurrent(hDC, new_hRC)) { PFNWGLGETSTRINGPROC gd_wglGetString = (PFNWGLGETSTRINGPROC)GetProcAddress(module, "glGetString"); 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 |