diff options
Diffstat (limited to 'platform')
60 files changed, 1382 insertions, 311 deletions
diff --git a/platform/SCsub b/platform/SCsub index 7c9d07f6ef..248b4b88dd 100644 --- a/platform/SCsub +++ b/platform/SCsub @@ -60,7 +60,7 @@ register_platform_apis = env.CommandNoCache( ) env.add_source_files(env.platform_sources, register_platform_apis) for platform in env.platform_apis: - env.add_source_files(env.platform_sources, f"{platform}/api/api.cpp") + env.add_source_files(env.platform_sources, f"{platform}/api/*.cpp") lib = env.add_library("platform", env.platform_sources) env.Prepend(LIBS=[lib]) diff --git a/platform/android/detect.py b/platform/android/detect.py index 0a10754e24..4bc7e9474b 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -5,6 +5,7 @@ import sys from typing import TYPE_CHECKING from methods import print_error, print_warning +from platform_methods import validate_arch if TYPE_CHECKING: from SCons.Script.SConscript import SConsEnvironment @@ -95,15 +96,19 @@ def install_ndk_if_needed(env: "SConsEnvironment"): env["ANDROID_NDK_ROOT"] = get_android_ndk_root(env) +def detect_swappy(): + archs = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"] + has_swappy = True + for arch in archs: + if not os.path.isfile("thirdparty/swappy-frame-pacing/" + arch + "/libswappy_static.a"): + has_swappy = False + return has_swappy + + def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] - if env["arch"] not in supported_arches: - print_error( - 'Unsupported CPU architecture "%s" for Android. Supported architectures are: %s.' - % (env["arch"], ", ".join(supported_arches)) - ) - sys.exit(255) + validate_arch(env["arch"], get_name(), supported_arches) if get_min_sdk_version(env["ndk_platform"]) < get_min_target_api(): print_warning( @@ -171,24 +176,45 @@ def configure(env: "SConsEnvironment"): env["AS"] = compiler_path + "/clang" env.Append( - CCFLAGS=( - "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() - ) + CCFLAGS=("-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden".split()) ) + has_swappy = detect_swappy() + if not has_swappy: + print_warning( + "Swappy Frame Pacing not detected! It is strongly recommended you download it from https://github.com/darksylinc/godot-swappy/releases and extract it so that the following files can be found:\n" + + " thirdparty/swappy-frame-pacing/arm64-v8a/libswappy_static.a\n" + + " thirdparty/swappy-frame-pacing/armeabi-v7a/libswappy_static.a\n" + + " thirdparty/swappy-frame-pacing/x86/libswappy_static.a\n" + + " thirdparty/swappy-frame-pacing/x86_64/libswappy_static.a\n" + + "Without Swappy, Godot apps on Android will inevitable suffer stutter and struggle to keep consistent 30/60/90/120 fps. Though Swappy cannot guarantee your app will be stutter-free, not having Swappy will guarantee there will be stutter even on the best phones and the most simple of scenes." + ) + if env["swappy"]: + print_error("Use build option `swappy=no` to ignore missing Swappy dependency and build without it.") + sys.exit(255) + if get_min_sdk_version(env["ndk_platform"]) >= 24: env.Append(CPPDEFINES=[("_FILE_OFFSET_BITS", 64)]) if env["arch"] == "x86_32": # The NDK adds this if targeting API < 24, so we can drop it when Godot targets it at least env.Append(CCFLAGS=["-mstackrealign"]) + if has_swappy: + env.Append(LIBPATH=["../../thirdparty/swappy-frame-pacing/x86"]) + elif env["arch"] == "x86_64": + if has_swappy: + env.Append(LIBPATH=["../../thirdparty/swappy-frame-pacing/x86_64"]) elif env["arch"] == "arm32": env.Append(CCFLAGS="-march=armv7-a -mfloat-abi=softfp".split()) env.Append(CPPDEFINES=["__ARM_ARCH_7__", "__ARM_ARCH_7A__"]) env.Append(CPPDEFINES=["__ARM_NEON__"]) + if has_swappy: + env.Append(LIBPATH=["../../thirdparty/swappy-frame-pacing/armeabi-v7a"]) elif env["arch"] == "arm64": env.Append(CCFLAGS=["-mfix-cortex-a53-835769"]) env.Append(CPPDEFINES=["__ARM_ARCH_8A__"]) + if has_swappy: + env.Append(LIBPATH=["../../thirdparty/swappy-frame-pacing/arm64-v8a"]) env.Append(CCFLAGS=["-ffp-contract=off"]) @@ -203,6 +229,9 @@ def configure(env: "SConsEnvironment"): if env["vulkan"]: env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) + if has_swappy: + env.Append(CPPDEFINES=["SWAPPY_FRAME_PACING_ENABLED"]) + env.Append(LIBS=["swappy_static"]) if not env["use_volk"]: env.Append(LIBS=["vulkan"]) diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index fa5b970a96..38f6931c8a 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -71,8 +71,9 @@ bool DisplayServerAndroid::has_feature(Feature p_feature) const { case FEATURE_MOUSE: //case FEATURE_MOUSE_WARP: //case FEATURE_NATIVE_DIALOG: - //case FEATURE_NATIVE_DIALOG_INPUT: - //case FEATURE_NATIVE_DIALOG_FILE: + case FEATURE_NATIVE_DIALOG_INPUT: + case FEATURE_NATIVE_DIALOG_FILE: + //case FEATURE_NATIVE_DIALOG_FILE_EXTRA: //case FEATURE_NATIVE_ICON: //case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_CLIPBOARD: @@ -176,6 +177,38 @@ bool DisplayServerAndroid::clipboard_has() const { } } +Error DisplayServerAndroid::dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + ERR_FAIL_NULL_V(godot_java, FAILED); + input_dialog_callback = p_callback; + return godot_java->show_input_dialog(p_title, p_description, p_partial); +} + +void DisplayServerAndroid::emit_input_dialog_callback(String p_text) { + if (input_dialog_callback.is_valid()) { + input_dialog_callback.call_deferred(p_text); + } +} + +Error DisplayServerAndroid::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + ERR_FAIL_NULL_V(godot_java, FAILED); + file_picker_callback = p_callback; + return godot_java->show_file_picker(p_current_directory, p_filename, p_mode, p_filters); +} + +void DisplayServerAndroid::emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths) { + if (file_picker_callback.is_valid()) { + file_picker_callback.call_deferred(p_ok, p_selected_paths, 0); + } +} + +Color DisplayServerAndroid::get_accent_color() const { + GodotJavaWrapper *godot_java = OS_Android::get_singleton()->get_godot_java(); + ERR_FAIL_NULL_V(godot_java, Color(0, 0, 0, 0)); + return godot_java->get_accent_color(); +} + TypedArray<Rect2> DisplayServerAndroid::get_display_cutouts() const { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); ERR_FAIL_NULL_V(godot_io_java, Array()); @@ -389,6 +422,14 @@ int64_t DisplayServerAndroid::window_get_native_handle(HandleType p_handle_type, } return 0; } + case EGL_DISPLAY: { + // @todo Find a way to get this from the Java side. + return 0; + } + case EGL_CONFIG: { + // @todo Find a way to get this from the Java side. + return 0; + } #endif default: { return 0; @@ -596,12 +637,6 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis native_menu = memnew(NativeMenu); -#if defined(GLES3_ENABLED) - if (rendering_driver == "opengl3") { - RasterizerGLES3::make_current(false); - } -#endif - #if defined(RD_ENABLED) rendering_context = nullptr; rendering_device = nullptr; @@ -616,19 +651,24 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis if (rendering_context->initialize() != OK) { memdelete(rendering_context); rendering_context = nullptr; +#if defined(GLES3_ENABLED) bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); if (fallback_to_opengl3 && rendering_driver != "opengl3") { WARN_PRINT("Your device seem not to support Vulkan, switching to OpenGL 3."); rendering_driver = "opengl3"; OS::get_singleton()->set_current_rendering_method("gl_compatibility"); OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); - } else { + } else +#endif + { ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver)); r_error = ERR_UNAVAILABLE; return; } } + } + if (rendering_context) { union { #ifdef VULKAN_ENABLED RenderingContextDriverVulkanAndroid::WindowPlatformData vulkan; @@ -668,6 +708,12 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis } #endif +#if defined(GLES3_ENABLED) + if (rendering_driver == "opengl3") { + RasterizerGLES3::make_current(false); + } +#endif + Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); r_error = OK; diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 65c6a53446..1744ad3069 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -87,6 +87,9 @@ class DisplayServerAndroid : public DisplayServer { Callable system_theme_changed; + Callable input_dialog_callback; + Callable file_picker_callback; + void _window_callback(const Callable &p_callable, const Variant &p_arg, bool p_deferred = false) const; static void _dispatch_input_events(const Ref<InputEvent> &p_event); @@ -116,6 +119,14 @@ public: virtual String clipboard_get() const override; virtual bool clipboard_has() const override; + virtual Error dialog_input_text(String p_title, String p_description, String p_partial, const Callable &p_callback) override; + void emit_input_dialog_callback(String p_text); + + virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, const FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override; + void emit_file_picker_callback(bool p_ok, const Vector<String> &p_selected_paths); + + virtual Color get_accent_color() const override; + virtual TypedArray<Rect2> get_display_cutouts() const override; virtual Rect2i get_display_safe_area() const override; diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml index 2fe5539e56..8c8bca2b7c 100644 --- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml +++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml @@ -577,7 +577,7 @@ Allows an application to write to the user dictionary. </member> <member name="screen/immersive_mode" type="bool" setter="" getter=""> - If [code]true[/code], hides navigation and status bar. + If [code]true[/code], hides navigation and status bar. See [method DisplayServer.window_set_mode] to toggle it at runtime. </member> <member name="screen/support_large" type="bool" setter="" getter=""> Indicates whether the application supports larger screen form-factors. diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 93ac498ab3..d2b64c74a4 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -49,7 +49,9 @@ void register_android_exporter() { EDITOR_DEF_BASIC("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)); -#ifndef ANDROID_ENABLED +#ifdef ANDROID_ENABLED + EDITOR_DEF_BASIC("export/android/install_exported_apk", true); +#else EDITOR_DEF_BASIC("export/android/java_sdk_path", OS::get_singleton()->get_environment("JAVA_HOME")); EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/android/java_sdk_path", PROPERTY_HINT_GLOBAL_DIR)); EDITOR_DEF_BASIC("export/android/android_sdk_path", OS::get_singleton()->get_environment("ANDROID_HOME")); diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 41f460ca8f..aea09583b7 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -1693,7 +1693,7 @@ void EditorExportPlatformAndroid::load_icon_refs(const Ref<EditorExportPreset> & path = static_cast<String>(p_preset->get(launcher_adaptive_icon_monochrome_option)).strip_edges(); if (!path.is_empty()) { print_verbose("Loading adaptive monochrome icon from " + path); - ImageLoader::load_image(path, background); + ImageLoader::load_image(path, monochrome); } } @@ -1778,6 +1778,12 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport if (!is_package_name_valid(pn, &pn_err)) { return TTR("Invalid package name:") + " " + pn_err; } + } else if (p_name == launcher_adaptive_icon_monochrome_option) { + String monochrome_icon_path = p_preset->get(launcher_adaptive_icon_monochrome_option); + + if (monochrome_icon_path.is_empty()) { + return TTR("No adaptive monochrome icon specified; default Godot monochrome icon will be used."); + } } else if (p_name == "gradle_build/use_gradle_build") { bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build"); String enabled_plugins_names = _get_plugins_names(Ref<EditorExportPreset>(p_preset)); @@ -2887,6 +2893,14 @@ Error EditorExportPlatformAndroid::sign_apk(const Ref<EditorExportPreset> &p_pre #endif print_verbose("Successfully completed signing build."); + +#ifdef ANDROID_ENABLED + bool prompt_apk_install = EDITOR_GET("export/android/install_exported_apk"); + if (prompt_apk_install) { + OS_Android::get_singleton()->shell_open(apk_path); + } +#endif + return OK; } diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 7e1d626486..15e80f824d 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -35,6 +35,7 @@ #include "godot_plugin_config.h" #endif // DISABLE_DEPRECATED +#include "core/io/image.h" #include "core/io/zip_io.h" #include "core/os/os.h" #include "editor/export/editor_export_platform.h" diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index a875745860..c1eb03b31f 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <application android:allowBackup="false" diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt index 7b6d1f6bd1..6aa2ba7195 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt @@ -390,7 +390,7 @@ abstract class BaseGodotEditor : GodotActivity() { * If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the * editor setting or device and screen metrics. * - * If the launch policy is [LaunchPolicy.PIP] but PIP is not supported, fallback to the default + * If the launch policy is [LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE] but PIP is not supported, fallback to the default * launch policy. */ private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy { @@ -453,9 +453,9 @@ abstract class BaseGodotEditor : GodotActivity() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) // Check if we got the MANAGE_EXTERNAL_STORAGE permission - if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { + when (requestCode) { + PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { Toast.makeText( this, R.string.denied_storage_permission_error_msg, @@ -463,6 +463,16 @@ abstract class BaseGodotEditor : GodotActivity() { ).show() } } + + PermissionsUtil.REQUEST_INSTALL_PACKAGES_REQ_CODE -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !packageManager.canRequestPackageInstalls()) { + Toast.makeText( + this, + R.string.denied_install_packages_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } } } @@ -514,7 +524,7 @@ abstract class BaseGodotEditor : GodotActivity() { override fun supportsFeature(featureTag: String): Boolean { if (featureTag == "xr_editor") { - return isNativeXRDevice(); + return isNativeXRDevice() } if (featureTag == "horizonos") { diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index 0ad54ac3a1..a25b6c0a2d 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -2,5 +2,6 @@ <resources> <string name="godot_game_activity_name">Godot Play window</string> <string name="denied_storage_permission_error_msg">Missing storage access permission!</string> + <string name="denied_install_packages_permission_error_msg">Missing install packages permission!</string> <string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string> </resources> diff --git a/platform/android/java/lib/res/values/dimens.xml b/platform/android/java/lib/res/values/dimens.xml index 9034dbbcc1..287d1c8920 100644 --- a/platform/android/java/lib/res/values/dimens.xml +++ b/platform/android/java/lib/res/values/dimens.xml @@ -1,4 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="text_edit_height">48dp</dimen> + <dimen name="input_dialog_padding_horizontal">10dp</dimen> + <dimen name="input_dialog_padding_vertical">5dp</dimen> </resources> diff --git a/platform/android/java/lib/res/values/strings.xml b/platform/android/java/lib/res/values/strings.xml index 03752e092e..e44addadd0 100644 --- a/platform/android/java/lib/res/values/strings.xml +++ b/platform/android/java/lib/res/values/strings.xml @@ -55,4 +55,7 @@ <string name="kilobytes_per_second">%1$s KB/s</string> <string name="time_remaining">Time remaining: %1$s</string> <string name="time_remaining_notification">%1$s left</string> + + <!-- Labels for the dialog action buttons --> + <string name="dialog_ok">OK</string> </resources> 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 567b134234..3ad8e6bc9e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -44,6 +44,7 @@ import android.os.* import android.util.Log import android.util.TypedValue import android.view.* +import android.widget.EditText import android.widget.FrameLayout import androidx.annotation.Keep import androidx.annotation.StringRes @@ -56,6 +57,7 @@ import com.google.android.vending.expansion.downloader.* import org.godotengine.godot.error.Error import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.input.GodotInputHandler +import org.godotengine.godot.io.FilePicker import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.plugin.AndroidRuntimePlugin @@ -81,6 +83,7 @@ import java.util.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference + /** * Core component used to interface with the native layer of the engine. * @@ -477,12 +480,17 @@ class Godot(private val context: Context) { // ...add to FrameLayout containerLayout?.addView(editText) renderView = if (usesVulkan()) { - if (!meetsVulkanRequirements(activity.packageManager)) { + if (meetsVulkanRequirements(activity.packageManager)) { + GodotVulkanRenderView(host, this, godotInputHandler) + } else if (canFallbackToOpenGL()) { + // Fallback to OpenGl. + GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl) + } else { throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message)) } - GodotVulkanRenderView(host, this, godotInputHandler) + } else { - // Fallback to openGl + // Fallback to OpenGl. GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl) } @@ -670,6 +678,9 @@ class Godot(private val context: Context) { for (plugin in pluginRegistry.allPlugins) { plugin.onMainActivityResult(requestCode, resultCode, data) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + FilePicker.handleActivityResult(context, requestCode, resultCode, data) + } } /** @@ -772,7 +783,7 @@ class Godot(private val context: Context) { val builder = AlertDialog.Builder(activity) builder.setMessage(message).setTitle(title) builder.setPositiveButton( - "OK" + R.string.dialog_ok ) { dialog: DialogInterface, id: Int -> okCallback?.run() dialog.cancel() @@ -817,6 +828,13 @@ class Godot(private val context: Context) { } /** + * Returns true if can fallback to OpenGL. + */ + private fun canFallbackToOpenGL(): Boolean { + return java.lang.Boolean.parseBoolean(GodotLib.getGlobal("rendering/rendering_device/fallback_to_opengl3")) + } + + /** * Returns true if the device meets the base requirements for Vulkan support, false otherwise. */ private fun meetsVulkanRequirements(packageManager: PackageManager?): Boolean { @@ -876,6 +894,44 @@ class Godot(private val context: Context) { mClipboard.setPrimaryClip(clip) } + @Keep + private fun showFilePicker(currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + FilePicker.showFilePicker(context, getActivity(), currentDirectory, filename, fileMode, filters) + } + } + + /** + * Popup a dialog to input text. + */ + @Keep + private fun showInputDialog(title: String, message: String, existingText: String) { + val activity: Activity = getActivity() ?: return + val inputField = EditText(activity) + val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.input_dialog_padding_horizontal) + val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.input_dialog_padding_vertical) + inputField.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical) + inputField.setText(existingText) + runOnUiThread { + val builder = AlertDialog.Builder(activity) + builder.setMessage(message).setTitle(title).setView(inputField) + builder.setPositiveButton(R.string.dialog_ok) { + dialog: DialogInterface, id: Int -> + GodotLib.inputDialogCallback(inputField.text.toString()) + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + } + + @Keep + private fun getAccentColor(): Int { + val value = TypedValue() + context.theme.resolveAttribute(android.R.attr.colorAccent, value, true) + return value.data + } + /** * Destroys the Godot Engine and kill the process it's running in. */ diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index f060c7aaff..79751dd58f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -47,6 +47,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.DisplayCutout; +import android.view.Surface; import android.view.WindowInsets; import androidx.core.content.FileProvider; diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 295a4a6340..13ae2150d7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -225,6 +225,16 @@ public class GodotLib { public static native void onNightModeChanged(); /** + * Invoked on the input dialog submitted. + */ + public static native void inputDialogCallback(String p_text); + + /** + * Invoked on the file picker closed. + */ + public static native void filePickerCallback(boolean p_ok, String[] p_selected_paths); + + /** * Invoked on the GL thread to configure the height of the virtual keyboard. */ public static native void setVirtualKeyboardHeight(int p_height); diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt b/platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt new file mode 100644 index 0000000000..2befe0583b --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/FilePicker.kt @@ -0,0 +1,160 @@ +/**************************************************************************/ +/* FilePicker.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 + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import android.util.Log +import androidx.annotation.RequiresApi +import org.godotengine.godot.GodotLib +import org.godotengine.godot.io.file.MediaStoreData + +/** + * Utility class for managing file selection and file picker activities. + * + * It provides methods to launch a file picker and handle the result, supporting various file modes, + * including opening files, directories, and saving files. + */ +internal class FilePicker { + companion object { + private const val FILE_PICKER_REQUEST = 1000 + private val TAG = FilePicker::class.java.simpleName + + // Constants for fileMode values + private const val FILE_MODE_OPEN_FILE = 0 + private const val FILE_MODE_OPEN_FILES = 1 + private const val FILE_MODE_OPEN_DIR = 2 + private const val FILE_MODE_OPEN_ANY = 3 + private const val FILE_MODE_SAVE_FILE = 4 + + /** + * Handles the result from a file picker activity and processes the selected file(s) or directory. + * + * @param context The context from which the file picker was launched. + * @param requestCode The request code used when starting the file picker activity. + * @param resultCode The result code returned by the activity. + * @param data The intent data containing the selected file(s) or directory. + */ + @RequiresApi(Build.VERSION_CODES.Q) + fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == FILE_PICKER_REQUEST) { + if (resultCode == Activity.RESULT_CANCELED) { + Log.d(TAG, "File picker canceled") + GodotLib.filePickerCallback(false, emptyArray()) + return + } + if (resultCode == Activity.RESULT_OK) { + val selectedPaths: MutableList<String> = mutableListOf() + // Handle multiple file selection. + val clipData = data?.clipData + if (clipData != null) { + for (i in 0 until clipData.itemCount) { + val uri = clipData.getItemAt(i).uri + uri?.let { + val filepath = MediaStoreData.getFilePathFromUri(context, uri) + if (filepath != null) { + selectedPaths.add(filepath) + } else { + Log.d(TAG, "null filepath URI: $it") + } + } + } + } else { + val uri: Uri? = data?.data + uri?.let { + val filepath = MediaStoreData.getFilePathFromUri(context, uri) + if (filepath != null) { + selectedPaths.add(filepath) + } else { + Log.d(TAG, "null filepath URI: $it") + } + } + } + + if (selectedPaths.isNotEmpty()) { + GodotLib.filePickerCallback(true, selectedPaths.toTypedArray()) + } else { + GodotLib.filePickerCallback(false, emptyArray()) + } + } + } + } + + /** + * Launches a file picker activity with specified settings based on the mode, initial directory, + * file type filters, and other parameters. + * + * @param context The context from which to start the file picker. + * @param activity The activity instance used to initiate the picker. Required for activity results. + * @param currentDirectory The directory path to start the file picker in. + * @param filename The name of the file when using save mode. + * @param fileMode The mode to operate in, specifying open, save, or directory select. + * @param filters Array of MIME types to filter file selection. + */ + @RequiresApi(Build.VERSION_CODES.Q) + fun showFilePicker(context: Context, activity: Activity?, currentDirectory: String, filename: String, fileMode: Int, filters: Array<String>) { + val intent = when (fileMode) { + FILE_MODE_OPEN_DIR -> Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + FILE_MODE_SAVE_FILE -> Intent(Intent.ACTION_CREATE_DOCUMENT) + else -> Intent(Intent.ACTION_OPEN_DOCUMENT) + } + val initialDirectory = MediaStoreData.getUriFromDirectoryPath(context, currentDirectory) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && initialDirectory != null) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectory) + } else { + Log.d(TAG, "Error cannot set initial directory") + } + if (fileMode == FILE_MODE_OPEN_FILES) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Set multi select for FILE_MODE_OPEN_FILES + } else if (fileMode == FILE_MODE_SAVE_FILE) { + intent.putExtra(Intent.EXTRA_TITLE, filename) // Set filename for FILE_MODE_SAVE_FILE + } + // ACTION_OPEN_DOCUMENT_TREE does not support intent type + if (fileMode != FILE_MODE_OPEN_DIR) { + intent.type = "*/*" + if (filters.isNotEmpty()) { + if (filters.size == 1) { + intent.type = filters[0] + } else { + intent.putExtra(Intent.EXTRA_MIME_TYPES, filters) + } + } + intent.addCategory(Intent.CATEGORY_OPENABLE) + } + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true) + activity?.startActivityForResult(intent, FILE_PICKER_REQUEST) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt index 97362e2542..46bd465e90 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt @@ -38,6 +38,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore +import android.util.Log import androidx.annotation.RequiresApi import java.io.File @@ -46,6 +47,7 @@ import java.io.FileNotFoundException import java.io.FileOutputStream import java.nio.channels.FileChannel + /** * Implementation of [DataAccess] which handles access and interactions with file and data * under scoped storage via the MediaStore API. @@ -81,6 +83,10 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " + " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?" + private const val AUTHORITY_MEDIA_DOCUMENTS = "com.android.providers.media.documents" + private const val AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS = "com.android.externalstorage.documents" + private const val AUTHORITY_DOWNLOADS_DOCUMENTS = "com.android.providers.downloads.documents" + private fun getSelectionByPathArguments(path: String): Array<String> { return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path)) } @@ -230,6 +236,72 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi ) return updated > 0 } + + fun getUriFromDirectoryPath(context: Context, directoryPath: String): Uri? { + if (!directoryExists(directoryPath)) { + return null + } + // Check if the path is under external storage. + val externalStorageRoot = Environment.getExternalStorageDirectory().absolutePath + if (directoryPath.startsWith(externalStorageRoot)) { + val relativePath = directoryPath.replaceFirst(externalStorageRoot, "").trim('/') + val uri = Uri.Builder() + .scheme("content") + .authority(AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) + .appendPath("document") + .appendPath("primary:$relativePath") + .build() + return uri + } + return null + } + + fun getFilePathFromUri(context: Context, uri: Uri): String? { + // Converts content uri to filepath. + val id = getIdFromUri(uri) ?: return null + + if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS) { + val split = id.split(":") + val fileName = split.last() + val relativePath = split.dropLast(1).joinToString("/") + val fullPath = File(Environment.getExternalStorageDirectory(), "$relativePath/$fileName").absolutePath + return fullPath + } else { + val id = id.toLongOrNull() ?: return null + val dataItems = queryById(context, id) + return if (dataItems.isNotEmpty()) { + val dataItem = dataItems[0] + File(Environment.getExternalStorageDirectory(), File(dataItem.relativePath, dataItem.displayName).toString()).absolutePath + } else { + null + } + } + } + + private fun getIdFromUri(uri: Uri): String? { + return try { + if (uri.authority == AUTHORITY_EXTERNAL_STORAGE_DOCUMENTS || uri.authority == AUTHORITY_MEDIA_DOCUMENTS || uri.authority == AUTHORITY_DOWNLOADS_DOCUMENTS) { + val documentId = uri.lastPathSegment ?: throw IllegalArgumentException("Invalid URI: $uri") + documentId.substringAfter(":") + } else { + throw IllegalArgumentException("Unsupported URI format: $uri") + } + } catch (e: Exception) { + Log.d(TAG, "Failed to parse ID from URI: $uri", e) + null + } + } + + private fun directoryExists(path: String): Boolean { + return try { + val file = File(path) + file.isDirectory && file.exists() + } catch (e: SecurityException) { + Log.d(TAG, "Failed to check directoryExists: $path", e) + false + } + } + } private val id: Long 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 4e8e82a70a..885873e46d 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 @@ -49,7 +49,6 @@ 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; @@ -66,6 +65,7 @@ public final class PermissionsUtil { public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; public static final int REQUEST_SINGLE_PERMISSION_REQ_CODE = 1002; public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002; + public static final int REQUEST_INSTALL_PACKAGES_REQ_CODE = 3002; private PermissionsUtil() { } @@ -105,10 +105,20 @@ public final class PermissionsUtil { activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); } } + } else if (permission.equals(Manifest.permission.REQUEST_INSTALL_PACKAGES)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !activity.getPackageManager().canRequestPackageInstalls()) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); + intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); + activity.startActivityForResult(intent, REQUEST_INSTALL_PACKAGES_REQ_CODE); + } catch (Exception e) { + Log.e(TAG, "Unable to request permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES); + } + } } 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) { + if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "Requesting permission " + permission); requestedPermissions.add(permission); } @@ -174,7 +184,7 @@ public final class PermissionsUtil { try { PermissionInfo permissionInfo = getPermissionInfo(activity, permissionName); int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) { + if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, permissionName) != PackageManager.PERMISSION_GRANTED) { activity.requestPermissions(new String[] { permissionName }, REQUEST_SINGLE_PERMISSION_REQ_CODE); return false; } @@ -215,7 +225,7 @@ public final class PermissionsUtil { try { manifestPermissions = getManifestPermissions(activity); } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + Log.e(TAG, "Unable to retrieve manifest permissions", e); return false; } @@ -242,7 +252,7 @@ public final class PermissionsUtil { try { manifestPermissions = getManifestPermissions(context); } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + Log.e(TAG, "Unable to retrieve manifest permissions", e); return new String[0]; } if (manifestPermissions.isEmpty()) { @@ -259,7 +269,7 @@ public final class PermissionsUtil { } else { PermissionInfo permissionInfo = getPermissionInfo(context, manifestPermission); int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) { + if ((protectionLevel & PermissionInfo.PROTECTION_DANGEROUS) == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(context, manifestPermission) == PackageManager.PERMISSION_GRANTED) { grantedPermissions.add(manifestPermission); } } diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index a7d2774db5..a5ecafeb09 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -21,4 +21,4 @@ target_include_directories(${PROJECT_NAME} ${ANDROID_ROOT_DIR} ${OPENXR_INCLUDE_DIR}) -add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED) +add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED -DDEBUG_ENABLED) diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 1a256959cd..5c1e78dcc4 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -540,6 +540,30 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JN } } +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text) { + DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton(); + if (ds) { + String text = jstring_to_string(p_text, env); + ds->emit_input_dialog_callback(text); + } +} + +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths) { + DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton(); + if (ds) { + Vector<String> selected_paths; + + jint length = env->GetArrayLength(p_selected_paths); + for (jint i = 0; i < length; ++i) { + jstring java_string = (jstring)env->GetObjectArrayElement(p_selected_paths, i); + String path = jstring_to_string(java_string, env); + selected_paths.push_back(path); + env->DeleteLocalRef(java_string); + } + ds->emit_file_picker_callback(p_ok, selected_paths); + } +} + JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result) { String permission = jstring_to_string(p_permission, env); if (permission == "android.permission.RECORD_AUDIO" && p_result) { diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 2165ce264b..31a7598a7b 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -67,6 +67,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv * JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_inputDialogCallback(JNIEnv *env, jclass clazz, jstring p_text); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_filePickerCallback(JNIEnv *env, jclass clazz, jboolean p_ok, jobjectArray p_selected_paths); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz); diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index d3b30e4589..5ecd789d43 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -64,9 +64,12 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _alert = p_env->GetMethodID(godot_class, "alert", "(Ljava/lang/String;Ljava/lang/String;)V"); _is_dark_mode_supported = p_env->GetMethodID(godot_class, "isDarkModeSupported", "()Z"); _is_dark_mode = p_env->GetMethodID(godot_class, "isDarkMode", "()Z"); + _get_accent_color = p_env->GetMethodID(godot_class, "getAccentColor", "()I"); _get_clipboard = p_env->GetMethodID(godot_class, "getClipboard", "()Ljava/lang/String;"); _set_clipboard = p_env->GetMethodID(godot_class, "setClipboard", "(Ljava/lang/String;)V"); _has_clipboard = p_env->GetMethodID(godot_class, "hasClipboard", "()Z"); + _show_input_dialog = p_env->GetMethodID(godot_class, "showInputDialog", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + _show_file_picker = p_env->GetMethodID(godot_class, "showFilePicker", "(Ljava/lang/String;Ljava/lang/String;I[Ljava/lang/String;)V"); _request_permission = p_env->GetMethodID(godot_class, "requestPermission", "(Ljava/lang/String;)Z"); _request_permissions = p_env->GetMethodID(godot_class, "requestPermissions", "()Z"); _get_granted_permissions = p_env->GetMethodID(godot_class, "getGrantedPermissions", "()[Ljava/lang/String;"); @@ -212,6 +215,23 @@ bool GodotJavaWrapper::is_dark_mode() { } } +Color GodotJavaWrapper::get_accent_color() { + if (_get_accent_color) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, Color(0, 0, 0, 0)); + int accent_color = env->CallIntMethod(godot_instance, _get_accent_color); + + // Convert ARGB to RGBA. + int alpha = (accent_color >> 24) & 0xFF; + int red = (accent_color >> 16) & 0xFF; + int green = (accent_color >> 8) & 0xFF; + int blue = accent_color & 0xFF; + return Color(red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f); + } else { + return Color(0, 0, 0, 0); + } +} + bool GodotJavaWrapper::has_get_clipboard() { return _get_clipboard != nullptr; } @@ -268,6 +288,46 @@ bool GodotJavaWrapper::has_clipboard() { } } +Error GodotJavaWrapper::show_input_dialog(const String &p_title, const String &p_message, const String &p_existing_text) { + if (_show_input_dialog) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); + jstring jStrTitle = env->NewStringUTF(p_title.utf8().get_data()); + jstring jStrMessage = env->NewStringUTF(p_message.utf8().get_data()); + jstring jStrExistingText = env->NewStringUTF(p_existing_text.utf8().get_data()); + env->CallVoidMethod(godot_instance, _show_input_dialog, jStrTitle, jStrMessage, jStrExistingText); + env->DeleteLocalRef(jStrTitle); + env->DeleteLocalRef(jStrMessage); + env->DeleteLocalRef(jStrExistingText); + return OK; + } else { + return ERR_UNCONFIGURED; + } +} + +Error GodotJavaWrapper::show_file_picker(const String &p_current_directory, const String &p_filename, int p_mode, const Vector<String> &p_filters) { + if (_show_file_picker) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, ERR_UNCONFIGURED); + jstring j_current_directory = env->NewStringUTF(p_current_directory.utf8().get_data()); + jstring j_filename = env->NewStringUTF(p_filename.utf8().get_data()); + jint j_mode = p_mode; + jobjectArray j_filters = env->NewObjectArray(p_filters.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < p_filters.size(); ++i) { + jstring j_filter = env->NewStringUTF(p_filters[i].utf8().get_data()); + env->SetObjectArrayElement(j_filters, i, j_filter); + env->DeleteLocalRef(j_filter); + } + env->CallVoidMethod(godot_instance, _show_file_picker, j_current_directory, j_filename, j_mode, j_filters); + env->DeleteLocalRef(j_current_directory); + env->DeleteLocalRef(j_filename); + env->DeleteLocalRef(j_filters); + return OK; + } else { + return ERR_UNCONFIGURED; + } +} + bool GodotJavaWrapper::request_permission(const String &p_name) { if (_request_permission) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index 51d7f98541..512779169a 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -34,6 +34,7 @@ #include "java_godot_view_wrapper.h" #include "string_android.h" +#include "core/math/color.h" #include "core/templates/list.h" #include <android/log.h> @@ -55,9 +56,12 @@ private: jmethodID _alert = nullptr; jmethodID _is_dark_mode_supported = nullptr; jmethodID _is_dark_mode = nullptr; + jmethodID _get_accent_color = nullptr; jmethodID _get_clipboard = nullptr; jmethodID _set_clipboard = nullptr; jmethodID _has_clipboard = nullptr; + jmethodID _show_input_dialog = nullptr; + jmethodID _show_file_picker = nullptr; jmethodID _request_permission = nullptr; jmethodID _request_permissions = nullptr; jmethodID _get_granted_permissions = nullptr; @@ -97,12 +101,15 @@ public: void alert(const String &p_message, const String &p_title); bool is_dark_mode_supported(); bool is_dark_mode(); + Color get_accent_color(); bool has_get_clipboard(); String get_clipboard(); bool has_set_clipboard(); void set_clipboard(const String &p_text); bool has_has_clipboard(); bool has_clipboard(); + Error show_input_dialog(const String &p_title, const String &p_message, const String &p_existing_text); + Error show_file_picker(const String &p_current_directory, const String &p_filename, int p_mode, const Vector<String> &p_filters); bool request_permission(const String &p_name); bool request_permissions(); Vector<String> get_granted_permissions() const; diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 20a3a996bc..0f7f938852 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -3,6 +3,7 @@ import sys from typing import TYPE_CHECKING from methods import detect_darwin_sdk_path, print_error, print_warning +from platform_methods import validate_arch if TYPE_CHECKING: from SCons.Script.SConscript import SConsEnvironment @@ -60,12 +61,7 @@ def get_flags(): def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_64", "arm64"] - if env["arch"] not in supported_arches: - print_error( - 'Unsupported CPU architecture "%s" for iOS. Supported architectures are: %s.' - % (env["arch"], ", ".join(supported_arches)) - ) - sys.exit(255) + validate_arch(env["arch"], get_name(), supported_arches) ## LTO @@ -134,7 +130,7 @@ def configure(env: "SConsEnvironment"): elif env["arch"] == "arm64": env.Append( CCFLAGS=( - "-fobjc-arc -arch arm64 -fmessage-length=0 -fno-strict-aliasing" + "-fobjc-arc -arch arm64 -fmessage-length=0" " -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits" " -fpascal-strings -fblocks -fvisibility=hidden -MMD -MT dependencies" " -isysroot $IOS_SDK_PATH".split() diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index dcc6ce9218..5d9179bd9a 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -111,19 +111,24 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode if (rendering_context->initialize() != OK) { memdelete(rendering_context); rendering_context = nullptr; +#if defined(GLES3_ENABLED) bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); if (fallback_to_opengl3 && rendering_driver != "opengl3") { WARN_PRINT("Your device seem not to support MoltenVK or Metal, switching to OpenGL 3."); rendering_driver = "opengl3"; OS::get_singleton()->set_current_rendering_method("gl_compatibility"); OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); - } else { + } else +#endif + { ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver)); r_error = ERR_UNAVAILABLE; return; } } + } + if (rendering_context) { if (rendering_context->window_create(MAIN_WINDOW_ID, &wpd) != OK) { ERR_PRINT(vformat("Failed to create %s window.", rendering_driver)); memdelete(rendering_context); @@ -365,6 +370,7 @@ bool DisplayServerIOS::has_feature(Feature p_feature) const { // case FEATURE_NATIVE_DIALOG: // case FEATURE_NATIVE_DIALOG_INPUT: // case FEATURE_NATIVE_DIALOG_FILE: + // case FEATURE_NATIVE_DIALOG_FILE_EXTRA: // case FEATURE_NATIVE_ICON: // case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_CLIPBOARD: diff --git a/platform/ios/ios.mm b/platform/ios/ios.mm index 6943de5ac8..26067b94bb 100644 --- a/platform/ios/ios.mm +++ b/platform/ios/ios.mm @@ -42,7 +42,7 @@ void iOS::_bind_methods() { ClassDB::bind_method(D_METHOD("supports_haptic_engine"), &iOS::supports_haptic_engine); ClassDB::bind_method(D_METHOD("start_haptic_engine"), &iOS::start_haptic_engine); ClassDB::bind_method(D_METHOD("stop_haptic_engine"), &iOS::stop_haptic_engine); -}; +} bool iOS::supports_haptic_engine() { if (@available(iOS 13, *)) { diff --git a/platform/linuxbsd/crash_handler_linuxbsd.h b/platform/linuxbsd/crash_handler_linuxbsd.h index 684f62b249..a14b93e11b 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.h +++ b/platform/linuxbsd/crash_handler_linuxbsd.h @@ -38,7 +38,7 @@ public: void initialize(); void disable(); - bool is_disabled() const { return disabled; }; + bool is_disabled() const { return disabled; } CrashHandler(); ~CrashHandler(); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index a67434527c..2fd573da75 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -4,7 +4,7 @@ import sys from typing import TYPE_CHECKING from methods import get_compiler_version, print_error, print_warning, using_gcc -from platform_methods import detect_arch +from platform_methods import detect_arch, validate_arch if TYPE_CHECKING: from SCons.Script.SConscript import SConsEnvironment @@ -74,12 +74,7 @@ def get_flags(): def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64"] - if env["arch"] not in supported_arches: - print_error( - 'Unsupported CPU architecture "%s" for Linux / *BSD. Supported architectures are: %s.' - % (env["arch"], ", ".join(supported_arches)) - ) - sys.exit(255) + validate_arch(env["arch"], get_name(), supported_arches) ## Build type diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h index 9e016bd4c3..c8842b3a17 100644 --- a/platform/linuxbsd/export/export_plugin.h +++ b/platform/linuxbsd/export/export_plugin.h @@ -55,7 +55,7 @@ class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC { ssh_args = p_ssh_arg; cmd_args = p_cmd_args; wait = p_wait; - }; + } }; Ref<ImageTexture> run_icon; diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 6355562feb..b309e8d8eb 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -771,11 +771,11 @@ Vector<String> OS_LinuxBSD::get_system_font_path_for_text(const String &p_font_n FcLangSetAdd(lang_set, reinterpret_cast<const FcChar8 *>(p_locale.utf8().get_data())); FcPatternAddLangSet(pattern, FC_LANG, lang_set); - FcConfigSubstitute(0, pattern, FcMatchPattern); + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); FcDefaultSubstitute(pattern); FcResult result; - FcPattern *match = FcFontMatch(0, pattern, &result); + FcPattern *match = FcFontMatch(nullptr, pattern, &result); if (match) { char *file_name = nullptr; if (FcPatternGetString(match, FC_FILE, 0, reinterpret_cast<FcChar8 **>(&file_name)) == FcResultMatch) { @@ -816,11 +816,11 @@ String OS_LinuxBSD::get_system_font_path(const String &p_font_name, int p_weight FcPatternAddInteger(pattern, FC_WIDTH, _stretch_to_fc(p_stretch)); FcPatternAddInteger(pattern, FC_SLANT, p_italic ? FC_SLANT_ITALIC : FC_SLANT_ROMAN); - FcConfigSubstitute(0, pattern, FcMatchPattern); + FcConfigSubstitute(nullptr, pattern, FcMatchPattern); FcDefaultSubstitute(pattern); FcResult result; - FcPattern *match = FcFontMatch(0, pattern, &result); + FcPattern *match = FcFontMatch(nullptr, pattern, &result); if (match) { if (!allow_substitutes) { char *family_name = nullptr; diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 3fbbc263a0..fe359532bb 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -216,7 +216,8 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const { //case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG_INPUT: #ifdef DBUS_ENABLED - case FEATURE_NATIVE_DIALOG_FILE: { + case FEATURE_NATIVE_DIALOG_FILE: + case FEATURE_NATIVE_DIALOG_FILE_EXTRA: { return true; } break; #endif @@ -627,6 +628,18 @@ int64_t DisplayServerWayland::window_get_native_handle(HandleType p_handle_type, } return 0; } break; + case EGL_DISPLAY: { + if (egl_manager) { + return (int64_t)egl_manager->get_display(p_window); + } + return 0; + } + case EGL_CONFIG: { + if (egl_manager) { + return (int64_t)egl_manager->get_config(p_window); + } + return 0; + } #endif // GLES3_ENABLED default: { @@ -1344,13 +1357,16 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win if (rendering_context->initialize() != OK) { memdelete(rendering_context); rendering_context = nullptr; +#if defined(GLES3_ENABLED) bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); if (fallback_to_opengl3 && rendering_driver != "opengl3") { WARN_PRINT("Your video card drivers seem not to support the required Vulkan version, switching to OpenGL 3."); rendering_driver = "opengl3"; OS::get_singleton()->set_current_rendering_method("gl_compatibility"); OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); - } else { + } else +#endif // GLES3_ENABLED + { r_error = ERR_CANT_CREATE; if (p_rendering_driver == "vulkan") { @@ -1479,12 +1495,12 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win driver_found = true; } } +#endif // GLES3_ENABLED if (!driver_found) { r_error = ERR_UNAVAILABLE; ERR_FAIL_MSG("Video driver not found."); } -#endif // GLES3_ENABLED cursor_set_shape(CURSOR_BUSY); diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 293623e594..a9c94bd823 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -129,6 +129,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const { case FEATURE_ICON: #ifdef DBUS_ENABLED case FEATURE_NATIVE_DIALOG_FILE: + case FEATURE_NATIVE_DIALOG_FILE_EXTRA: #endif //case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG_INPUT: @@ -1861,6 +1862,18 @@ int64_t DisplayServerX11::window_get_native_handle(HandleType p_handle_type, Win } return 0; } + case EGL_DISPLAY: { + if (gl_manager_egl) { + return (int64_t)gl_manager_egl->get_display(p_window); + } + return 0; + } + case EGL_CONFIG: { + if (gl_manager_egl) { + return (int64_t)gl_manager_egl->get_config(p_window); + } + return 0; + } #endif default: { return 0; @@ -6156,13 +6169,16 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode if (rendering_context->initialize() != OK) { memdelete(rendering_context); rendering_context = nullptr; +#if defined(GLES3_ENABLED) bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); if (fallback_to_opengl3 && rendering_driver != "opengl3") { WARN_PRINT("Your video card drivers seem not to support the required Vulkan version, switching to OpenGL 3."); rendering_driver = "opengl3"; OS::get_singleton()->set_current_rendering_method("gl_compatibility"); OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); - } else { + } else +#endif // GLES3_ENABLED + { r_error = ERR_CANT_CREATE; if (p_rendering_driver == "vulkan") { diff --git a/platform/macos/crash_handler_macos.h b/platform/macos/crash_handler_macos.h index f821283167..d52cb7234a 100644 --- a/platform/macos/crash_handler_macos.h +++ b/platform/macos/crash_handler_macos.h @@ -38,7 +38,7 @@ public: void initialize(); void disable(); - bool is_disabled() const { return disabled; }; + bool is_disabled() const { return disabled; } CrashHandler(); ~CrashHandler(); diff --git a/platform/macos/detect.py b/platform/macos/detect.py index a8968b592e..cab91fd33c 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -3,7 +3,7 @@ import sys from typing import TYPE_CHECKING from methods import detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang, print_error, print_warning -from platform_methods import detect_arch, detect_mvk +from platform_methods import detect_arch, detect_mvk, validate_arch if TYPE_CHECKING: from SCons.Script.SConscript import SConsEnvironment @@ -68,12 +68,7 @@ def get_flags(): def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_64", "arm64"] - if env["arch"] not in supported_arches: - print_error( - 'Unsupported CPU architecture "%s" for macOS. Supported architectures are: %s.' - % (env["arch"], ", ".join(supported_arches)) - ) - sys.exit(255) + validate_arch(env["arch"], get_name(), supported_arches) ## Build type diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index 48cc7bbba3..f1078d9868 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -752,6 +752,7 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const { case FEATURE_NATIVE_DIALOG: case FEATURE_NATIVE_DIALOG_INPUT: case FEATURE_NATIVE_DIALOG_FILE: + case FEATURE_NATIVE_DIALOG_FILE_EXTRA: case FEATURE_IME: case FEATURE_WINDOW_TRANSPARENCY: case FEATURE_HIDPI: @@ -2676,6 +2677,18 @@ int64_t DisplayServerMacOS::window_get_native_handle(HandleType p_handle_type, W } return 0; } + case EGL_DISPLAY: { + if (gl_manager_angle) { + return (int64_t)gl_manager_angle->get_display(p_window); + } + return 0; + } + case EGL_CONFIG: { + if (gl_manager_angle) { + return (int64_t)gl_manager_angle->get_config(p_window); + } + return 0; + } #endif default: { return 0; @@ -3611,6 +3624,39 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM //TODO - do Vulkan and OpenGL support checks, driver selection and fallback rendering_driver = p_rendering_driver; +#if defined(RD_ENABLED) +#if defined(VULKAN_ENABLED) + if (rendering_driver == "vulkan") { + rendering_context = memnew(RenderingContextDriverVulkanMacOS); + } +#endif +#if defined(METAL_ENABLED) + if (rendering_driver == "metal") { + rendering_context = memnew(RenderingContextDriverMetal); + } +#endif + + if (rendering_context) { + if (rendering_context->initialize() != OK) { + memdelete(rendering_context); + rendering_context = nullptr; +#if defined(GLES3_ENABLED) + bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); + if (fallback_to_opengl3 && rendering_driver != "opengl3") { + WARN_PRINT("Your device seem not to support MoltenVK or Metal, switching to OpenGL 3."); + rendering_driver = "opengl3"; + OS::get_singleton()->set_current_rendering_method("gl_compatibility"); + OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); + } else +#endif + { + r_error = ERR_CANT_CREATE; + ERR_FAIL_MSG("Could not initialize " + rendering_driver); + } + } + } +#endif + #if defined(GLES3_ENABLED) if (rendering_driver == "opengl3_angle") { gl_manager_angle = memnew(GLManagerANGLE_MacOS); @@ -3643,35 +3689,6 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM } } #endif -#if defined(RD_ENABLED) -#if defined(VULKAN_ENABLED) - if (rendering_driver == "vulkan") { - rendering_context = memnew(RenderingContextDriverVulkanMacOS); - } -#endif -#if defined(METAL_ENABLED) - if (rendering_driver == "metal") { - rendering_context = memnew(RenderingContextDriverMetal); - } -#endif - - if (rendering_context) { - if (rendering_context->initialize() != OK) { - memdelete(rendering_context); - rendering_context = nullptr; - bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); - if (fallback_to_opengl3 && rendering_driver != "opengl3") { - WARN_PRINT("Your device seem not to support MoltenVK or Metal, switching to OpenGL 3."); - rendering_driver = "opengl3"; - OS::get_singleton()->set_current_rendering_method("gl_compatibility"); - OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); - } else { - r_error = ERR_CANT_CREATE; - ERR_FAIL_MSG("Could not initialize " + rendering_driver); - } - } - } -#endif Point2i window_position; if (p_position != nullptr) { diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml index c261c252ba..dcaba9bbd2 100644 --- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml +++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml @@ -75,6 +75,13 @@ <member name="codesign/custom_options" type="PackedStringArray" setter="" getter=""> Array of the additional command line arguments passed to the code signing tool. </member> + <member name="codesign/entitlements/additional" type="String" setter="" getter=""> + Additional data added to the root [code]<dict>[/code] section of the [url=https://developer.apple.com/documentation/bundleresources/entitlements].entitlements[/url] file. The value should be an XML section with pairs of key-value elements, e.g.: + [codeblock lang=text] + <key>key_name</key> + <string>value</string> + [/codeblock] + </member> <member name="codesign/entitlements/address_book" type="bool" setter="" getter=""> Enable to allow access to contacts in the user's address book, if it's enabled you should also provide usage message in the [member privacy/address_book_usage_description] option. See [url=https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_personal-information_addressbook]com.apple.security.personal-information.addressbook[/url]. </member> diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index c99e9cdd0c..bf5645d9a6 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -65,6 +65,11 @@ void EditorExportPlatformMacOS::get_preset_features(const Ref<EditorExportPreset } else { ERR_PRINT("Invalid architecture"); } + + if (architecture == "universal") { + r_features->push_back("x86_64"); + r_features->push_back("arm64"); + } } String EditorExportPlatformMacOS::get_export_option_warning(const EditorExportPreset *p_preset, const StringName &p_name) const { @@ -327,7 +332,7 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP } bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); - if (p_option.begins_with("privacy")) { + if (p_option.begins_with("privacy") || p_option == "codesign/entitlements/additional") { return advanced_options_enabled; } } @@ -501,6 +506,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_movies", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "codesign/entitlements/app_sandbox/files_user_selected", PROPERTY_HINT_ENUM, "No,Read-only,Read-write"), 0)); r_options->push_back(ExportOption(PropertyInfo(Variant::ARRAY, "codesign/entitlements/app_sandbox/helper_executables", PROPERTY_HINT_ARRAY_TYPE, itos(Variant::STRING) + "/" + itos(PROPERTY_HINT_GLOBAL_FILE) + ":"), Array())); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "codesign/entitlements/additional", PROPERTY_HINT_MULTILINE_TEXT), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::PACKED_STRING_ARRAY, "codesign/custom_options"), PackedStringArray())); #ifdef MACOS_ENABLED @@ -2126,6 +2132,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } } + const String &additional_entitlements = p_preset->get("codesign/entitlements/additional"); + if (!additional_entitlements.is_empty()) { + ent_f->store_line(additional_entitlements); + } + ent_f->store_line("</dict>"); ent_f->store_line("</plist>"); } else { @@ -2288,6 +2299,14 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p } } + if (FileAccess::exists(ent_path)) { + print_verbose("entitlements:\n" + FileAccess::get_file_as_string(ent_path)); + } + + if (FileAccess::exists(hlp_ent_path)) { + print_verbose("helper entitlements:\n" + FileAccess::get_file_as_string(hlp_ent_path)); + } + // Clean up temporary entitlements files. if (FileAccess::exists(hlp_ent_path)) { DirAccess::remove_file_or_error(hlp_ent_path); diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index d88d347359..ef8d1bb8ab 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -34,6 +34,7 @@ #include "core/config/project_settings.h" #include "core/io/dir_access.h" #include "core/io/file_access.h" +#include "core/io/image.h" #include "core/io/marshalls.h" #include "core/io/resource_saver.h" #include "core/os/os.h" @@ -75,7 +76,7 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { ssh_args = p_ssh_arg; cmd_args = p_cmd_args; wait = p_wait; - }; + } }; Ref<ImageTexture> run_icon; diff --git a/platform/web/detect.py b/platform/web/detect.py index 735e2eaf4f..26bbbccffa 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -14,6 +14,7 @@ from emscripten_helpers import ( from SCons.Util import WhereIs from methods import get_compiler_version, print_error, print_warning +from platform_methods import validate_arch if TYPE_CHECKING: from SCons.Script.SConscript import SConsEnvironment @@ -86,12 +87,7 @@ def get_flags(): def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["wasm32"] - if env["arch"] not in supported_arches: - print_error( - 'Unsupported CPU architecture "%s" for Web. Supported architectures are: %s.' - % (env["arch"], ", ".join(supported_arches)) - ) - sys.exit(255) + validate_arch(env["arch"], get_name(), supported_arches) try: env["initial_memory"] = int(env["initial_memory"]) diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index 4e55cc137a..b2db62ea2f 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -1133,6 +1133,7 @@ bool DisplayServerWeb::has_feature(Feature p_feature) const { //case FEATURE_NATIVE_DIALOG: //case FEATURE_NATIVE_DIALOG_INPUT: //case FEATURE_NATIVE_DIALOG_FILE: + //case FEATURE_NATIVE_DIALOG_FILE_EXTRA: //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 352b3fe523..c28a6fd082 100644 --- a/platform/web/display_server_web.h +++ b/platform/web/display_server_web.h @@ -209,6 +209,7 @@ public: virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + virtual void screen_set_keep_on(bool p_enable) override {} virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), VirtualKeyboardType p_type = KEYBOARD_TYPE_DEFAULT, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1) override; virtual void virtual_keyboard_hide() override; @@ -265,6 +266,7 @@ public: virtual bool can_any_window_draw() const override; + virtual void window_set_vsync_mode(VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override {} virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override; // events diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py index 8fcabb21c7..3122271a71 100644 --- a/platform/web/emscripten_helpers.py +++ b/platform/web/emscripten_helpers.py @@ -3,6 +3,8 @@ import os from SCons.Util import WhereIs +from platform_methods import get_build_version + def run_closure_compiler(target, source, env, for_signature): closure_bin = os.path.join( @@ -21,22 +23,6 @@ def run_closure_compiler(target, source, env, for_signature): return " ".join(cmd) -def get_build_version(): - import version - - name = "custom_build" - if os.getenv("BUILD_NAME") is not None: - name = os.getenv("BUILD_NAME") - v = "%d.%d" % (version.major, version.minor) - if version.patch > 0: - v += ".%d" % version.patch - status = version.status - if os.getenv("GODOT_VERSION_STATUS") is not None: - status = str(os.getenv("GODOT_VERSION_STATUS")) - v += ".%s.%s" % (status, name) - return v - - def create_engine_file(env, target, source, externs, threads_enabled): if env["use_closure_compiler"]: return env.BuildJS(target, source, JSEXTERNS=externs) @@ -84,7 +70,7 @@ def create_template_zip(env, js, wasm, worker, side): cache.append("godot.editor.worker.js") opt_cache = ["godot.editor.wasm"] subst_dict = { - "___GODOT_VERSION___": get_build_version(), + "___GODOT_VERSION___": get_build_version(False), "___GODOT_NAME___": "GodotEngine", "___GODOT_CACHE___": json.dumps(cache), "___GODOT_OPT_CACHE___": json.dumps(opt_cache), diff --git a/platform/web/js/libs/library_godot_fetch.js b/platform/web/js/libs/library_godot_fetch.js index 00616bc1a5..eeb3978256 100644 --- a/platform/web/js/libs/library_godot_fetch.js +++ b/platform/web/js/libs/library_godot_fetch.js @@ -59,7 +59,12 @@ const GodotFetch = { }); obj.status = response.status; obj.response = response; - obj.reader = response.body.getReader(); + // `body` can be null per spec (for example, in cases where the request method is HEAD). + // As of the time of writing, Chromium (127.0.6533.72) does not follow the spec but Firefox (131.0.3) does. + // See godotengine/godot#76825 for more information. + // See Chromium revert (of the change to follow the spec): + // https://chromium.googlesource.com/chromium/src/+/135354b7bdb554cd03c913af7c90aceead03c4d4 + obj.reader = response.body?.getReader(); obj.chunked = chunked; }, @@ -121,6 +126,10 @@ const GodotFetch = { } obj.reading = true; obj.reader.read().then(GodotFetch.onread.bind(null, id)).catch(GodotFetch.onerror.bind(null, id)); + } else if (obj.reader == null && obj.response.body == null) { + // Emulate a stream closure to maintain the request lifecycle. + obj.reading = true; + GodotFetch.onread(id, { value: undefined, done: true }); } }, }, @@ -159,7 +168,10 @@ const GodotFetch = { if (!obj.response) { return 0; } - if (obj.reader) { + // If the reader is nullish, but there is no body, and the request is not marked as done, + // the same status should be returned as though the request is currently being read + // so that the proper lifecycle closure can be handled in `read()`. + if (obj.reader || (obj.response.body == null && !obj.done)) { return 1; } if (obj.done) { diff --git a/platform/windows/SCsub b/platform/windows/SCsub index 1d17e7b325..30df9df809 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -24,6 +24,7 @@ common_win = [ "gl_manager_windows_angle.cpp", "wgl_detect_version.cpp", "rendering_context_driver_vulkan_windows.cpp", + "drop_target_windows.cpp", ] if env.msvc: diff --git a/platform/windows/crash_handler_windows.h b/platform/windows/crash_handler_windows.h index a0a0b610d0..f47d9ec66e 100644 --- a/platform/windows/crash_handler_windows.h +++ b/platform/windows/crash_handler_windows.h @@ -51,7 +51,7 @@ public: void initialize(); void disable(); - bool is_disabled() const { return disabled; }; + bool is_disabled() const { return disabled; } CrashHandler(); ~CrashHandler(); diff --git a/platform/windows/crash_handler_windows_signal.cpp b/platform/windows/crash_handler_windows_signal.cpp index e11a60bdc7..c3a0d08ad6 100644 --- a/platform/windows/crash_handler_windows_signal.cpp +++ b/platform/windows/crash_handler_windows_signal.cpp @@ -159,7 +159,7 @@ extern void CrashHandlerException(int signal) { // Load process and image info to determine ASLR addresses offset. MODULEINFO mi; - GetModuleInformation(GetCurrentProcess(), GetModuleHandle(NULL), &mi, sizeof(mi)); + GetModuleInformation(GetCurrentProcess(), GetModuleHandle(nullptr), &mi, sizeof(mi)); int64_t image_mem_base = reinterpret_cast<int64_t>(mi.lpBaseOfDll); int64_t image_file_base = get_image_base(_execpath); data.offset = image_mem_base - image_file_base; diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 4043f3a8c2..e1109db24f 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING import methods from methods import print_error, print_warning -from platform_methods import detect_arch +from platform_methods import detect_arch, validate_arch if TYPE_CHECKING: from SCons.Script.SConscript import SConsEnvironment @@ -68,23 +68,23 @@ def can_build(): def get_mingw_bin_prefix(prefix, arch): - if not prefix: - mingw_bin_prefix = "" - elif prefix[-1] != "/": - mingw_bin_prefix = prefix + "/bin/" - else: - mingw_bin_prefix = prefix + "bin/" + bin_prefix = (os.path.normpath(os.path.join(prefix, "bin")) + os.sep) if prefix else "" + ARCH_PREFIXES = { + "x86_64": "x86_64-w64-mingw32-", + "x86_32": "i686-w64-mingw32-", + "arm32": "armv7-w64-mingw32-", + "arm64": "aarch64-w64-mingw32-", + } + arch_prefix = ARCH_PREFIXES[arch] if arch else "" + return bin_prefix + arch_prefix - if arch == "x86_64": - mingw_bin_prefix += "x86_64-w64-mingw32-" - elif arch == "x86_32": - mingw_bin_prefix += "i686-w64-mingw32-" - elif arch == "arm32": - mingw_bin_prefix += "armv7-w64-mingw32-" - elif arch == "arm64": - mingw_bin_prefix += "aarch64-w64-mingw32-" - return mingw_bin_prefix +def get_detected(env: "SConsEnvironment", tool: str) -> str: + checks = [ + get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) + tool, + get_mingw_bin_prefix(env["mingw_prefix"], "") + tool, + ] + return str(env.Detect(checks)) def detect_build_env_arch(): @@ -245,41 +245,6 @@ def get_flags(): } -def build_res_file(target, source, env: "SConsEnvironment"): - arch_aliases = { - "x86_32": "pe-i386", - "x86_64": "pe-x86-64", - "arm32": "armv7-w64-mingw32", - "arm64": "aarch64-w64-mingw32", - } - cmdbase = "windres --include-dir . --target=" + arch_aliases[env["arch"]] - - mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) - - for x in range(len(source)): - ok = True - # Try prefixed executable (MinGW on Linux). - cmd = mingw_bin_prefix + cmdbase + " -i " + str(source[x]) + " -o " + str(target[x]) - try: - out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() - if len(out[1]): - ok = False - except Exception: - ok = False - - # Try generic executable (MSYS2). - if not ok: - cmd = cmdbase + " -i " + str(source[x]) + " -o " + str(target[x]) - try: - out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() - if len(out[1]): - return -1 - except Exception: - return -1 - - return 0 - - def setup_msvc_manual(env: "SConsEnvironment"): """Running from VCVARS environment""" @@ -361,6 +326,10 @@ def setup_mingw(env: "SConsEnvironment"): print_error("No valid compilers found, use MINGW_PREFIX environment variable to set MinGW path.") sys.exit(255) + env.Tool("mingw") + env.AppendUnique(CCFLAGS=env.get("ccflags", "").split()) + env.AppendUnique(RCFLAGS=env.get("rcflags", "").split()) + print("Using MinGW, arch %s" % (env["arch"])) @@ -483,9 +452,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): else: print_warning("Missing environment variable: WindowsSdkDir") - if int(env["target_win_version"], 16) < 0x0601: - print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") - sys.exit(255) + validate_win_version(env) env.AppendUnique( CPPDEFINES=[ @@ -549,15 +516,7 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): LIBS += ["vulkan"] if env["d3d12"]: - # Check whether we have d3d12 dependencies installed. - if not os.path.exists(env["mesa_libs"]): - print_error( - "The Direct3D 12 rendering driver requires dependencies to be installed.\n" - "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" - "See the documentation for more information:\n\t" - "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" - ) - sys.exit(255) + check_d3d12_installed(env) env.AppendUnique(CPPDEFINES=["D3D12_ENABLED", "RD_ENABLED"]) LIBS += ["dxgi", "dxguid"] @@ -662,7 +621,7 @@ def get_ar_version(env): print_warning("Couldn't check version of `ar`.") return ret - match = re.search(r"GNU ar \(GNU Binutils\) (\d+)\.(\d+)(?:\.(\d+))?", output) + match = re.search(r"GNU ar(?: \(GNU Binutils\)| version) (\d+)\.(\d+)(?:\.(\d+))?", output) if match: ret["major"] = int(match[1]) ret["minor"] = int(match[2]) @@ -716,6 +675,13 @@ def configure_mingw(env: "SConsEnvironment"): # https://www.scons.org/wiki/LongCmdLinesOnWin32 env.use_windows_spawn_fix() + # HACK: For some reason, Windows-native shells have their MinGW tools + # frequently fail as a result of parsing path separators incorrectly. + # For some other reason, this issue is circumvented entirely if the + # `mingw_prefix` bin is prepended to PATH. + if os.sep == "\\": + env.PrependENVPath("PATH", os.path.join(env["mingw_prefix"], "bin")) + # In case the command line to AR is too long, use a response file. env["ARCOM_ORIG"] = env["ARCOM"] env["ARCOM"] = "${TEMPFILE('$ARCOM_ORIG', '$ARCOMSTR')}" @@ -753,9 +719,6 @@ def configure_mingw(env: "SConsEnvironment"): ## Compiler configuration - if os.name != "nt": - env["PROGSUFFIX"] = env["PROGSUFFIX"] + ".exe" # for linux cross-compilation - if env["arch"] == "x86_32": if env["use_static_cpp"]: env.Append(LINKFLAGS=["-static"]) @@ -770,28 +733,31 @@ def configure_mingw(env: "SConsEnvironment"): env.Append(CCFLAGS=["-ffp-contract=off"]) - mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) - if env["use_llvm"]: - env["CC"] = mingw_bin_prefix + "clang" - env["CXX"] = mingw_bin_prefix + "clang++" - if try_cmd("as --version", env["mingw_prefix"], env["arch"]): - env["AS"] = mingw_bin_prefix + "as" - env.Append(ASFLAGS=["-c"]) - if try_cmd("ar --version", env["mingw_prefix"], env["arch"]): - env["AR"] = mingw_bin_prefix + "ar" - if try_cmd("ranlib --version", env["mingw_prefix"], env["arch"]): - env["RANLIB"] = mingw_bin_prefix + "ranlib" + env["CC"] = get_detected(env, "clang") + env["CXX"] = get_detected(env, "clang++") + env["AR"] = get_detected(env, "ar") + env["RANLIB"] = get_detected(env, "ranlib") + env.Append(ASFLAGS=["-c"]) env.extra_suffix = ".llvm" + env.extra_suffix else: - env["CC"] = mingw_bin_prefix + "gcc" - env["CXX"] = mingw_bin_prefix + "g++" - if try_cmd("as --version", env["mingw_prefix"], env["arch"]): - env["AS"] = mingw_bin_prefix + "as" - if try_cmd("gcc-ar --version", env["mingw_prefix"], env["arch"]): - env["AR"] = mingw_bin_prefix + "gcc-ar" - if try_cmd("gcc-ranlib --version", env["mingw_prefix"], env["arch"]): - env["RANLIB"] = mingw_bin_prefix + "gcc-ranlib" + env["CC"] = get_detected(env, "gcc") + env["CXX"] = get_detected(env, "g++") + env["AR"] = get_detected(env, "gcc-ar" if os.name != "nt" else "ar") + env["RANLIB"] = get_detected(env, "gcc-ranlib") + + env["RC"] = get_detected(env, "windres") + ARCH_TARGETS = { + "x86_32": "pe-i386", + "x86_64": "pe-x86-64", + "arm32": "armv7-w64-mingw32", + "arm64": "aarch64-w64-mingw32", + } + env.AppendUnique(RCFLAGS=f"--target={ARCH_TARGETS[env['arch']]}") + + env["AS"] = get_detected(env, "as") + env["OBJCOPY"] = get_detected(env, "objcopy") + env["STRIP"] = get_detected(env, "strip") ## LTO @@ -819,9 +785,7 @@ def configure_mingw(env: "SConsEnvironment"): ## Compile flags - if int(env["target_win_version"], 16) < 0x0601: - print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") - sys.exit(255) + validate_win_version(env) if not env["use_llvm"]: env.Append(CCFLAGS=["-mwindows"]) @@ -899,15 +863,7 @@ def configure_mingw(env: "SConsEnvironment"): env.Append(LIBS=["vulkan"]) if env["d3d12"]: - # Check whether we have d3d12 dependencies installed. - if not os.path.exists(env["mesa_libs"]): - print_error( - "The Direct3D 12 rendering driver requires dependencies to be installed.\n" - "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" - "See the documentation for more information:\n\t" - "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" - ) - sys.exit(255) + check_d3d12_installed(env) env.AppendUnique(CPPDEFINES=["D3D12_ENABLED", "RD_ENABLED"]) env.Append(LIBS=["dxgi", "dxguid"]) @@ -943,19 +899,11 @@ def configure_mingw(env: "SConsEnvironment"): env.Append(CPPDEFINES=["MINGW_ENABLED", ("MINGW_HAS_SECURE_API", 1)]) - # resrc - env.Append(BUILDERS={"RES": env.Builder(action=build_res_file, suffix=".o", src_suffix=".rc")}) - def configure(env: "SConsEnvironment"): # Validate arch. supported_arches = ["x86_32", "x86_64", "arm32", "arm64"] - if env["arch"] not in supported_arches: - print_error( - 'Unsupported CPU architecture "%s" for Windows. Supported architectures are: %s.' - % (env["arch"], ", ".join(supported_arches)) - ) - sys.exit(255) + validate_arch(env["arch"], get_name(), supported_arches) # At this point the env has been set up with basic tools/compilers. env.Prepend(CPPPATH=["#platform/windows"]) @@ -983,3 +931,20 @@ def configure(env: "SConsEnvironment"): else: # MinGW configure_mingw(env) + + +def check_d3d12_installed(env): + if not os.path.exists(env["mesa_libs"]): + print_error( + "The Direct3D 12 rendering driver requires dependencies to be installed.\n" + "You can install them by running `python misc\\scripts\\install_d3d12_sdk_windows.py`.\n" + "See the documentation for more information:\n\t" + "https://docs.godotengine.org/en/latest/contributing/development/compiling/compiling_for_windows.html" + ) + sys.exit(255) + + +def validate_win_version(env): + if int(env["target_win_version"], 16) < 0x0601: + print_error("`target_win_version` should be 0x0601 or higher (Windows 7).") + sys.exit(255) diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index ffa3840181..a6eab1bd29 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -30,6 +30,7 @@ #include "display_server_windows.h" +#include "drop_target_windows.h" #include "os_windows.h" #include "wgl_detect_version.h" @@ -67,6 +68,18 @@ #define DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 19 #endif +#ifndef DWMWA_WINDOW_CORNER_PREFERENCE +#define DWMWA_WINDOW_CORNER_PREFERENCE 33 +#endif + +#ifndef DWMWCP_DEFAULT +#define DWMWCP_DEFAULT 0 +#endif + +#ifndef DWMWCP_DONOTROUND +#define DWMWCP_DONOTROUND 1 +#endif + #define WM_INDICATOR_CALLBACK_MESSAGE (WM_USER + 1) #if defined(__GNUC__) @@ -117,6 +130,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_NATIVE_DIALOG: case FEATURE_NATIVE_DIALOG_INPUT: case FEATURE_NATIVE_DIALOG_FILE: + case FEATURE_NATIVE_DIALOG_FILE_EXTRA: case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: case FEATURE_TEXT_TO_SPEECH: @@ -304,8 +318,8 @@ public: } // IFileDialogEvents methods - HRESULT STDMETHODCALLTYPE OnFileOk(IFileDialog *) { return S_OK; }; - HRESULT STDMETHODCALLTYPE OnFolderChange(IFileDialog *) { return S_OK; }; + HRESULT STDMETHODCALLTYPE OnFileOk(IFileDialog *) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnFolderChange(IFileDialog *) { return S_OK; } HRESULT STDMETHODCALLTYPE OnFolderChanging(IFileDialog *p_pfd, IShellItem *p_item) { if (root.is_empty()) { @@ -324,11 +338,11 @@ public: return S_OK; } - HRESULT STDMETHODCALLTYPE OnHelp(IFileDialog *) { return S_OK; }; - HRESULT STDMETHODCALLTYPE OnSelectionChange(IFileDialog *) { return S_OK; }; - HRESULT STDMETHODCALLTYPE OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; }; - HRESULT STDMETHODCALLTYPE OnTypeChange(IFileDialog *pfd) { return S_OK; }; - HRESULT STDMETHODCALLTYPE OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; }; + HRESULT STDMETHODCALLTYPE OnHelp(IFileDialog *) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnSelectionChange(IFileDialog *) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnShareViolation(IFileDialog *, IShellItem *, FDE_SHAREVIOLATION_RESPONSE *) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnTypeChange(IFileDialog *pfd) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnOverwrite(IFileDialog *, IShellItem *, FDE_OVERWRITE_RESPONSE *) { return S_OK; } // IFileDialogControlEvents methods HRESULT STDMETHODCALLTYPE OnItemSelected(IFileDialogCustomize *p_pfdc, DWORD p_ctl_id, DWORD p_item_idx) { @@ -338,14 +352,14 @@ public: return S_OK; } - HRESULT STDMETHODCALLTYPE OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; }; + HRESULT STDMETHODCALLTYPE OnButtonClicked(IFileDialogCustomize *, DWORD) { return S_OK; } HRESULT STDMETHODCALLTYPE OnCheckButtonToggled(IFileDialogCustomize *p_pfdc, DWORD p_ctl_id, BOOL p_checked) { if (ctls.has(p_ctl_id)) { selected[ctls[p_ctl_id]] = (bool)p_checked; } return S_OK; } - HRESULT STDMETHODCALLTYPE OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; }; + HRESULT STDMETHODCALLTYPE OnControlActivating(IFileDialogCustomize *, DWORD) { return S_OK; } Dictionary get_selected() { return selected; @@ -722,7 +736,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title GetWindowRect(fd->hwnd_owner, &crect); fd->wrect = Rect2i(crect.left, crect.top, crect.right - crect.left, crect.bottom - crect.top); } else { - fd->hwnd_owner = 0; + fd->hwnd_owner = nullptr; fd->wrect = Rect2i(CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT); } fd->appid = appname; @@ -1483,6 +1497,9 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod if (p_flags & WINDOW_FLAG_ALWAYS_ON_TOP_BIT && p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { wd.always_on_top = true; } + if (p_flags & WINDOW_FLAG_SHARP_CORNERS_BIT) { + wd.sharp_corners = true; + } if (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) { wd.no_focus = true; } @@ -1601,11 +1618,17 @@ void DisplayServerWindows::delete_sub_window(WindowID p_window) { } #endif - if ((tablet_get_current_driver() == "wintab") && wintab_available && windows[p_window].wtctx) { - wintab_WTClose(windows[p_window].wtctx); - windows[p_window].wtctx = nullptr; + if ((tablet_get_current_driver() == "wintab") && wintab_available && wd.wtctx) { + wintab_WTClose(wd.wtctx); + wd.wtctx = nullptr; + } + + if (wd.drop_target != nullptr) { + RevokeDragDrop(wd.hWnd); + wd.drop_target->Release(); } - DestroyWindow(windows[p_window].hWnd); + + DestroyWindow(wd.hWnd); windows.erase(p_window); if (last_focused_window == p_window) { @@ -1650,6 +1673,18 @@ int64_t DisplayServerWindows::window_get_native_handle(HandleType p_handle_type, } return 0; } + case EGL_DISPLAY: { + if (gl_manager_angle) { + return (int64_t)gl_manager_angle->get_display(p_window); + } + return 0; + } + case EGL_CONFIG: { + if (gl_manager_angle) { + return (int64_t)gl_manager_angle->get_config(p_window); + } + return 0; + } #endif default: { return 0; @@ -1703,7 +1738,14 @@ void DisplayServerWindows::window_set_drop_files_callback(const Callable &p_call _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - windows[p_window].drop_files_callback = p_callable; + WindowData &window_data = windows[p_window]; + + window_data.drop_files_callback = p_callable; + + if (window_data.drop_target == nullptr) { + window_data.drop_target = memnew(DropTargetWindows(&window_data)); + ERR_FAIL_COND(RegisterDragDrop(window_data.hWnd, window_data.drop_target) != S_OK); + } } void DisplayServerWindows::window_set_title(const String &p_title, WindowID p_window) { @@ -2297,6 +2339,12 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W wd.always_on_top = p_enabled; _update_window_style(p_window); } break; + case WINDOW_FLAG_SHARP_CORNERS: { + wd.sharp_corners = p_enabled; + DWORD value = wd.sharp_corners ? DWMWCP_DONOTROUND : DWMWCP_DEFAULT; + ::DwmSetWindowAttribute(wd.hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(value)); + _update_window_style(p_window); + } break; case WINDOW_FLAG_TRANSPARENT: { if (p_enabled) { // Enable per-pixel alpha. @@ -3994,6 +4042,10 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA native_menu->_menu_activate(HMENU(lParam), (int)wParam); } break; case WM_CREATE: { + { + DWORD value = windows[window_id].sharp_corners ? DWMWCP_DONOTROUND : DWMWCP_DEFAULT; + ::DwmSetWindowAttribute(windows[window_id].hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(value)); + } if (is_dark_mode_supported() && dark_title_available) { BOOL value = is_dark_mode(); @@ -5282,32 +5334,6 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } } break; - case WM_DROPFILES: { - HDROP hDropInfo = (HDROP)wParam; - const int buffsize = 4096; - WCHAR buf[buffsize]; - - int fcount = DragQueryFileW(hDropInfo, 0xFFFFFFFF, nullptr, 0); - - Vector<String> files; - - for (int i = 0; i < fcount; i++) { - DragQueryFileW(hDropInfo, i, buf, buffsize); - String file = String::utf16((const char16_t *)buf); - files.push_back(file); - } - - if (files.size() && windows[window_id].drop_files_callback.is_valid()) { - Variant v_files = files; - const Variant *v_args[1] = { &v_files }; - Variant ret; - Callable::CallError ce; - windows[window_id].drop_files_callback.callp((const Variant **)&v_args, 1, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute drop files callback: %s.", Variant::get_callable_error_text(windows[window_id].drop_files_callback, v_args, 1, ce))); - } - } - } break; default: { if (user_proc) { return CallWindowProcW(user_proc, hWnd, uMsg, wParam, lParam); @@ -5412,7 +5438,7 @@ void DisplayServerWindows::_process_key_events() { k->set_physical_keycode(physical_keycode); k->set_key_label(key_label); k->set_unicode(fix_unicode(unicode)); - if (k->get_unicode() && ke.altgr) { + if (k->get_unicode() && ke.altgr && windows[ke.window_id].ime_active) { k->set_alt_pressed(false); k->set_ctrl_pressed(false); } @@ -5488,7 +5514,7 @@ void DisplayServerWindows::_process_key_events() { } k->set_unicode(fix_unicode(unicode)); } - if (k->get_unicode() && ke.altgr) { + if (k->get_unicode() && ke.altgr && windows[ke.window_id].ime_active) { k->set_alt_pressed(false); k->set_ctrl_pressed(false); } @@ -5645,6 +5671,12 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, wd_transient_parent->transient_children.insert(id); } + wd.sharp_corners = p_flags & WINDOW_FLAG_SHARP_CORNERS_BIT; + { + DWORD value = wd.sharp_corners ? DWMWCP_DONOTROUND : DWMWCP_DEFAULT; + ::DwmSetWindowAttribute(wd.hWnd, DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(value)); + } + if (is_dark_mode_supported() && dark_title_available) { BOOL value = is_dark_mode(); ::DwmSetWindowAttribute(wd.hWnd, use_legacy_dark_mode_before_20H1 ? DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 : DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value)); @@ -6130,6 +6162,8 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win FreeLibrary(comctl32); } + OleInitialize(nullptr); + memset(&wc, 0, sizeof(WNDCLASSEXW)); wc.cbSize = sizeof(WNDCLASSEXW); wc.style = CS_OWNDC | CS_DBLCLKS; @@ -6195,6 +6229,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } #endif +#if defined(GLES3_ENABLED) bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3"); if (failed && fallback_to_opengl3 && rendering_driver != "opengl3") { memdelete(rendering_context); @@ -6206,6 +6241,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win OS::get_singleton()->set_current_rendering_driver_name(rendering_driver); failed = false; } +#endif if (failed) { memdelete(rendering_context); rendering_context = nullptr; @@ -6369,7 +6405,10 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } WindowID main_window = _create_window(p_mode, p_vsync_mode, p_flags, Rect2i(window_position, p_resolution), false, INVALID_WINDOW_ID); - ERR_FAIL_COND_MSG(main_window == INVALID_WINDOW_ID, "Failed to create main window."); + if (main_window == INVALID_WINDOW_ID) { + r_error = ERR_UNAVAILABLE; + ERR_FAIL_MSG("Failed to create main window."); + } joypad = new JoypadWindows(&windows[MAIN_WINDOW_ID].hWnd); @@ -6557,6 +6596,12 @@ DisplayServerWindows::~DisplayServerWindows() { wintab_WTClose(windows[MAIN_WINDOW_ID].wtctx); windows[MAIN_WINDOW_ID].wtctx = nullptr; } + + if (windows[MAIN_WINDOW_ID].drop_target != nullptr) { + RevokeDragDrop(windows[MAIN_WINDOW_ID].hWnd); + windows[MAIN_WINDOW_ID].drop_target->Release(); + } + DestroyWindow(windows[MAIN_WINDOW_ID].hWnd); } @@ -6588,4 +6633,6 @@ DisplayServerWindows::~DisplayServerWindows() { if (tts) { memdelete(tts); } + + OleUninitialize(); } diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 7d6a3e96a6..0462d3f8fa 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -38,6 +38,7 @@ #include "core/config/project_settings.h" #include "core/input/input.h" +#include "core/io/image.h" #include "core/os/os.h" #include "drivers/unix/ip_unix.h" #include "drivers/wasapi/audio_driver_wasapi.h" @@ -356,9 +357,13 @@ typedef enum _SHC_PROCESS_DPI_AWARENESS { SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2, } SHC_PROCESS_DPI_AWARENESS; +class DropTargetWindows; + class DisplayServerWindows : public DisplayServer { // No need to register with GDCLASS, it's platform-specific and nothing is added. + friend class DropTargetWindows; + _THREAD_SAFE_CLASS_ // UXTheme API @@ -473,6 +478,7 @@ class DisplayServerWindows : public DisplayServer { bool exclusive = false; bool context_created = false; bool mpass = false; + bool sharp_corners = false; // Used to transfer data between events using timer. WPARAM saved_wparam; @@ -519,6 +525,9 @@ class DisplayServerWindows : public DisplayServer { Callable input_text_callback; Callable drop_files_callback; + // OLE API + DropTargetWindows *drop_target = nullptr; + WindowID transient_parent = INVALID_WINDOW_ID; HashSet<WindowID> transient_children; diff --git a/platform/windows/drop_target_windows.cpp b/platform/windows/drop_target_windows.cpp new file mode 100644 index 0000000000..d04924a9cf --- /dev/null +++ b/platform/windows/drop_target_windows.cpp @@ -0,0 +1,375 @@ +/**************************************************************************/ +/* drop_target_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 "drop_target_windows.h" + +#include "core/io/dir_access.h" +#include "core/math/random_pcg.h" +#include "core/os/time.h" + +#include <fileapi.h> + +// Helpers + +static String create_temp_dir() { + Char16String buf; + int bufsize = GetTempPathW(0, nullptr) + 1; + buf.resize(bufsize); + if (GetTempPathW(bufsize, (LPWSTR)buf.ptrw()) == 0) { + return ""; + } + + String tmp_dir = String::utf16((const char16_t *)buf.ptr()); + RandomPCG gen(Time::get_singleton()->get_ticks_usec()); + + const int attempts = 4; + + for (int i = 0; i < attempts; ++i) { + uint32_t rnd = gen.rand(); + String dirname = "godot_tmp_" + String::num_uint64(rnd); + String res_dir = tmp_dir.path_join(dirname); + Char16String res_dir16 = res_dir.utf16(); + + if (CreateDirectoryW((LPCWSTR)res_dir16.ptr(), nullptr)) { + return res_dir; + } + } + + return ""; +} + +static bool remove_dir_recursive(const String &p_dir) { + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + if (dir_access->change_dir(p_dir) != OK) { + return false; + } + return dir_access->erase_contents_recursive() == OK; +} + +static bool stream2file(IStream *p_stream, FILEDESCRIPTORW *p_desc, const String &p_path) { + if (DirAccess::make_dir_recursive_absolute(p_path.get_base_dir()) != OK) { + return false; + } + + Char16String path16 = p_path.utf16(); + DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; + + if (p_desc->dwFlags & FD_ATTRIBUTES) { + dwFlagsAndAttributes = p_desc->dwFileAttributes; + } + + HANDLE file = CreateFileW( + (LPCWSTR)path16.ptr(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_NEW, + dwFlagsAndAttributes, + nullptr); + + if (!file) { + return false; + } + + const int bufsize = 4096; + char buf[bufsize]; + ULONG nread = 0; + DWORD nwritten = 0; + HRESULT read_result = S_OK; + bool result = true; + + while (true) { + read_result = p_stream->Read(buf, bufsize, &nread); + if (read_result != S_OK && read_result != S_FALSE) { + result = false; + goto cleanup; + } + + if (!nread) { + break; + } + + while (nread > 0) { + if (!WriteFile(file, buf, nread, &nwritten, nullptr) || !nwritten) { + result = false; + goto cleanup; + } + nread -= nwritten; + } + } + +cleanup: + CloseHandle(file); + return result; +} + +// DropTargetWindows + +bool DropTargetWindows::is_valid_filedescriptor() { + return cf_filedescriptor != 0 && cf_filecontents != 0; +} + +HRESULT DropTargetWindows::handle_hdrop_format(Vector<String> *p_files, IDataObject *pDataObj) { + FORMATETC fmt = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stg; + HRESULT res = S_OK; + + if (pDataObj->GetData(&fmt, &stg) != S_OK) { + return E_UNEXPECTED; + } + + HDROP hDropInfo = (HDROP)GlobalLock(stg.hGlobal); + + Char16String buf; + + if (hDropInfo == nullptr) { + ReleaseStgMedium(&stg); + return E_UNEXPECTED; + } + + int fcount = DragQueryFileW(hDropInfo, 0xFFFFFFFF, nullptr, 0); + + for (int i = 0; i < fcount; i++) { + int buffsize = DragQueryFileW(hDropInfo, i, nullptr, 0); + buf.resize(buffsize + 1); + if (DragQueryFileW(hDropInfo, i, (LPWSTR)buf.ptrw(), buffsize + 1) == 0) { + res = E_UNEXPECTED; + goto cleanup; + } + String file = String::utf16((const char16_t *)buf.ptr()); + p_files->push_back(file); + } + +cleanup: + GlobalUnlock(stg.hGlobal); + ReleaseStgMedium(&stg); + + return res; +} + +HRESULT DropTargetWindows::handle_filedescriptor_format(Vector<String> *p_files, IDataObject *pDataObj) { + FORMATETC fmt = { cf_filedescriptor, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stg; + HRESULT res = S_OK; + + if (pDataObj->GetData(&fmt, &stg) != S_OK) { + return E_UNEXPECTED; + } + + FILEGROUPDESCRIPTORW *filegroup_desc = (FILEGROUPDESCRIPTORW *)GlobalLock(stg.hGlobal); + + if (!filegroup_desc) { + ReleaseStgMedium(&stg); + return E_UNEXPECTED; + } + + tmp_path = create_temp_dir(); + Ref<DirAccess> dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + PackedStringArray copied; + + if (dir_access->change_dir(tmp_path) != OK) { + res = E_UNEXPECTED; + goto cleanup; + } + + for (int i = 0; i < (int)filegroup_desc->cItems; ++i) { + res = save_as_file(tmp_path, filegroup_desc->fgd + i, pDataObj, i); + if (res != S_OK) { + res = E_UNEXPECTED; + goto cleanup; + } + } + + copied = dir_access->get_files(); + for (const String &file : copied) { + p_files->push_back(tmp_path.path_join(file)); + } + + copied = dir_access->get_directories(); + for (const String &dir : copied) { + p_files->push_back(tmp_path.path_join(dir)); + } + +cleanup: + GlobalUnlock(filegroup_desc); + ReleaseStgMedium(&stg); + if (res != S_OK) { + remove_dir_recursive(tmp_path); + tmp_path.clear(); + } + return res; +} + +HRESULT DropTargetWindows::save_as_file(const String &p_out_dir, FILEDESCRIPTORW *p_file_desc, IDataObject *pDataObj, int p_file_idx) { + String relpath = String::utf16((const char16_t *)p_file_desc->cFileName); + String fullpath = p_out_dir.path_join(relpath); + + if (p_file_desc->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (DirAccess::make_dir_recursive_absolute(fullpath) != OK) { + return E_UNEXPECTED; + } + return S_OK; + } + + FORMATETC fmt = { cf_filecontents, nullptr, DVASPECT_CONTENT, p_file_idx, TYMED_ISTREAM }; + STGMEDIUM stg; + HRESULT res = S_OK; + + if (pDataObj->GetData(&fmt, &stg) != S_OK) { + return E_UNEXPECTED; + } + + IStream *stream = stg.pstm; + if (stream == nullptr) { + res = E_UNEXPECTED; + goto cleanup; + } + + if (!stream2file(stream, p_file_desc, fullpath)) { + res = E_UNEXPECTED; + goto cleanup; + } + +cleanup: + ReleaseStgMedium(&stg); + return res; +} + +DropTargetWindows::DropTargetWindows(DisplayServerWindows::WindowData *p_window_data) : + ref_count(1), window_data(p_window_data) { + cf_filedescriptor = RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); + cf_filecontents = RegisterClipboardFormat(CFSTR_FILECONTENTS); +} + +HRESULT STDMETHODCALLTYPE DropTargetWindows::QueryInterface(REFIID riid, void **ppvObject) { + if (riid == IID_IUnknown || riid == IID_IDropTarget) { + *ppvObject = static_cast<IDropTarget *>(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; +} + +ULONG STDMETHODCALLTYPE DropTargetWindows::AddRef() { + return InterlockedIncrement(&ref_count); +} + +ULONG STDMETHODCALLTYPE DropTargetWindows::Release() { + ULONG count = InterlockedDecrement(&ref_count); + if (count == 0) { + memfree(this); + } + return count; +} + +HRESULT STDMETHODCALLTYPE DropTargetWindows::DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) { + (void)grfKeyState; + (void)pt; + + FORMATETC hdrop_fmt = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + FORMATETC filedesc_fmt = { cf_filedescriptor, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + + if (!window_data->drop_files_callback.is_valid()) { + *pdwEffect = DROPEFFECT_NONE; + } else if (pDataObj->QueryGetData(&hdrop_fmt) == S_OK) { + *pdwEffect = DROPEFFECT_COPY; + } else if (is_valid_filedescriptor() && pDataObj->QueryGetData(&filedesc_fmt) == S_OK) { + *pdwEffect = DROPEFFECT_COPY; + } else { + *pdwEffect = DROPEFFECT_NONE; + } + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE DropTargetWindows::DragOver(DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) { + (void)grfKeyState; + (void)pt; + + *pdwEffect = DROPEFFECT_COPY; + return S_OK; +} + +HRESULT STDMETHODCALLTYPE DropTargetWindows::DragLeave() { + return S_OK; +} + +HRESULT STDMETHODCALLTYPE DropTargetWindows::Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) { + (void)grfKeyState; + (void)pt; + + *pdwEffect = DROPEFFECT_NONE; + + if (!window_data->drop_files_callback.is_valid()) { + return S_OK; + } + + FORMATETC hdrop_fmt = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + FORMATETC filedesc_fmt = { cf_filedescriptor, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + Vector<String> files; + + if (pDataObj->QueryGetData(&hdrop_fmt) == S_OK) { + HRESULT res = handle_hdrop_format(&files, pDataObj); + if (res != S_OK) { + return res; + } + } else if (pDataObj->QueryGetData(&filedesc_fmt) == S_OK && is_valid_filedescriptor()) { + HRESULT res = handle_filedescriptor_format(&files, pDataObj); + if (res != S_OK) { + return res; + } + } else { + return E_UNEXPECTED; + } + + if (!files.size()) { + return S_OK; + } + + Variant v_files = files; + const Variant *v_args[1] = { &v_files }; + Variant ret; + Callable::CallError ce; + window_data->drop_files_callback.callp((const Variant **)&v_args, 1, ret, ce); + + if (!tmp_path.is_empty()) { + remove_dir_recursive(tmp_path); + tmp_path.clear(); + } + + if (ce.error != Callable::CallError::CALL_OK) { + ERR_PRINT(vformat("Failed to execute drop files callback: %s.", Variant::get_callable_error_text(window_data->drop_files_callback, v_args, 1, ce))); + return E_UNEXPECTED; + } + + *pdwEffect = DROPEFFECT_COPY; + return S_OK; +} diff --git a/platform/windows/drop_target_windows.h b/platform/windows/drop_target_windows.h new file mode 100644 index 0000000000..bd0e0270c6 --- /dev/null +++ b/platform/windows/drop_target_windows.h @@ -0,0 +1,77 @@ +/**************************************************************************/ +/* drop_target_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 DROP_TARGET_WINDOWS_H +#define DROP_TARGET_WINDOWS_H + +#include "display_server_windows.h" + +#include <shlobj.h> + +// Silence warning due to a COM API weirdness. +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" +#endif + +// https://learn.microsoft.com/en-us/windows/win32/api/ole2/nf-ole2-dodragdrop#remarks +class DropTargetWindows : public IDropTarget { + LONG ref_count; + DisplayServerWindows::WindowData *window_data = nullptr; + CLIPFORMAT cf_filedescriptor = 0; + CLIPFORMAT cf_filecontents = 0; + String tmp_path; + + bool is_valid_filedescriptor(); + HRESULT handle_hdrop_format(Vector<String> *p_files, IDataObject *pDataObj); + HRESULT handle_filedescriptor_format(Vector<String> *p_files, IDataObject *pDataObj); + HRESULT save_as_file(const String &p_out_dir, FILEDESCRIPTORW *p_file_desc, IDataObject *pDataObj, int p_file_idx); + +public: + DropTargetWindows(DisplayServerWindows::WindowData *p_window_data); + virtual ~DropTargetWindows() {} + + // IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + + // IDropTarget + HRESULT STDMETHODCALLTYPE DragEnter(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) override; + HRESULT STDMETHODCALLTYPE DragOver(DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) override; + HRESULT STDMETHODCALLTYPE DragLeave() override; + HRESULT STDMETHODCALLTYPE Drop(IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect) override; +}; + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif // DROP_TARGET_WINDOWS_H diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index 1972b36845..89db449424 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -59,7 +59,7 @@ class EditorExportPlatformWindows : public EditorExportPlatformPC { ssh_args = p_ssh_arg; cmd_args = p_cmd_args; wait = p_wait; - }; + } }; Ref<ImageTexture> run_icon; diff --git a/platform/windows/godot.natvis b/platform/windows/godot.natvis index 14536fa130..fc34ad3cb3 100644 --- a/platform/windows/godot.natvis +++ b/platform/windows/godot.natvis @@ -1,5 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> + <Type Name="Ref<*>"> + <SmartPointer Usage="Minimal">reference</SmartPointer> + <DisplayString Condition="!reference">[empty]</DisplayString> + <DisplayString Condition="!!reference">{*reference}</DisplayString> + <Expand> + <Item Condition="!!reference" Name="[ptr]">reference</Item> + <Item Condition="!!reference" Name="[refcount]">reference->refcount.count.value</Item> + </Expand> + </Type> + <Type Name="Vector<*>"> <Expand> <Item Name="[size]">_cowdata._ptr ? (((const unsigned long long *)(_cowdata._ptr))[-1]) : 0</Item> @@ -91,6 +101,16 @@ <StringView Condition="_data && !_data->cname">_data->name,s32b</StringView> </Type> + <Type Name="HashSet<*,*,*>"> + <Expand> + <Item Name="[size]">num_elements</Item> + <ArrayItems> + <Size>num_elements</Size> + <ValuePointer>($T1 *) keys._cowdata._ptr</ValuePointer> + </ArrayItems> + </Expand> + </Type> + <Type Name="HashMapElement<*,*>"> <DisplayString>{{Key = {($T1 *) &data.key} Value = {($T2 *) &data.value}}}</DisplayString> <Expand> diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index a5f1629cf0..0f55cda5d7 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -122,8 +122,9 @@ bool JoypadWindows::is_xinput_device(const GUID *p_guid) { memcmp(p_guid, &IID_XOneSWirelessGamepad, sizeof(*p_guid)) == 0 || memcmp(p_guid, &IID_XOneSBluetoothGamepad, sizeof(*p_guid)) == 0 || memcmp(p_guid, &IID_XOneEliteWirelessGamepad, sizeof(*p_guid)) == 0 || - memcmp(p_guid, &IID_XOneElite2WirelessGamepad, sizeof(*p_guid)) == 0) + memcmp(p_guid, &IID_XOneElite2WirelessGamepad, sizeof(*p_guid)) == 0) { return true; + } PRAWINPUTDEVICELIST dev_list = nullptr; unsigned int dev_list_count = 0; diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp index fde55918e4..9fa2849e7b 100644 --- a/platform/windows/native_menu_windows.cpp +++ b/platform/windows/native_menu_windows.cpp @@ -158,7 +158,7 @@ Size2 NativeMenuWindows::get_size(const RID &p_rid) const { int count = GetMenuItemCount(md->menu); for (int i = 0; i < count; i++) { RECT rect; - if (GetMenuItemRect(NULL, md->menu, i, &rect)) { + if (GetMenuItemRect(nullptr, md->menu, i, &rect)) { size.x = MAX(size.x, rect.right - rect.left); size.y += rect.bottom - rect.top; } @@ -992,7 +992,7 @@ void NativeMenuWindows::set_item_submenu(const RID &p_rid, int p_idx, const RID if (p_submenu_rid.is_valid()) { item.hSubMenu = md_sub->menu; } else { - item.hSubMenu = 0; + item.hSubMenu = nullptr; } SetMenuItemInfoW(md->menu, p_idx, true, &item); } @@ -1095,7 +1095,7 @@ void NativeMenuWindows::set_item_icon(const RID &p_rid, int p_idx, const Ref<Tex item_data->bmp = _make_bitmap(item_data->img); } else { item_data->img = Ref<Image>(); - item_data->bmp = 0; + item_data->bmp = nullptr; } item.hbmpItem = item_data->bmp; SetMenuItemInfoW(md->menu, p_idx, true, &item); diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h index 235a4b332a..09e4640b40 100644 --- a/platform/windows/native_menu_windows.h +++ b/platform/windows/native_menu_windows.h @@ -31,6 +31,7 @@ #ifndef NATIVE_MENU_WINDOWS_H #define NATIVE_MENU_WINDOWS_H +#include "core/io/image.h" #include "core/templates/hash_map.h" #include "core/templates/rid_owner.h" #include "servers/display/native_menu.h" diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index adc72a79e9..bff3443214 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -139,13 +139,13 @@ void RedirectIOToConsole() { if (AttachConsole(ATTACH_PARENT_PROCESS)) { // Restore redirection (Note: if not redirected it's NULL handles not INVALID_HANDLE_VALUE). - if (h_stdin != 0) { + if (h_stdin != nullptr) { SetStdHandle(STD_INPUT_HANDLE, h_stdin); } - if (h_stdout != 0) { + if (h_stdout != nullptr) { SetStdHandle(STD_OUTPUT_HANDLE, h_stdout); } - if (h_stderr != 0) { + if (h_stderr != nullptr) { SetStdHandle(STD_ERROR_HANDLE, h_stderr); } @@ -908,9 +908,9 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String } // Create pipes. - HANDLE pipe_in[2] = { 0, 0 }; - HANDLE pipe_out[2] = { 0, 0 }; - HANDLE pipe_err[2] = { 0, 0 }; + HANDLE pipe_in[2] = { nullptr, nullptr }; + HANDLE pipe_out[2] = { nullptr, nullptr }; + HANDLE pipe_err[2] = { nullptr, nullptr }; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); @@ -981,7 +981,7 @@ Dictionary OS_Windows::execute_with_pipe(const String &p_path, const List<String Ref<FileAccessWindowsPipe> err_pipe; err_pipe.instantiate(); - err_pipe->open_existing(pipe_err[0], 0, p_blocking); + err_pipe->open_existing(pipe_err[0], nullptr, p_blocking); ret["stdio"] = main_pipe; ret["stderr"] = err_pipe; @@ -1363,7 +1363,7 @@ public: locale = p_locale; n_sub = p_nsub; rtl = p_rtl; - }; + } virtual ~FallbackTextAnalysisSource() {} }; diff --git a/platform/windows/platform_windows_builders.py b/platform/windows/platform_windows_builders.py index 3fd9e1a581..2020e68748 100644 --- a/platform/windows/platform_windows_builders.py +++ b/platform/windows/platform_windows_builders.py @@ -2,23 +2,11 @@ import os -from detect import get_mingw_bin_prefix, try_cmd - 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(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(dst)) - else: - 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(dst)) - else: - 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(dst)) - else: - os.system("objcopy --add-gnu-debuglink={0}.debugsymbols {0}".format(dst)) + os.system("{0} --only-keep-debug {1} {1}.debugsymbols".format(env["OBJCOPY"], dst)) + os.system("{0} --strip-debug --strip-unneeded {1}".format(env["STRIP"], dst)) + os.system("{0} --add-gnu-debuglink={1}.debugsymbols {1}".format(env["OBJCOPY"], dst)) diff --git a/platform/windows/windows_utils.cpp b/platform/windows/windows_utils.cpp index 30743c6900..3b9bfb50f7 100644 --- a/platform/windows/windows_utils.cpp +++ b/platform/windows/windows_utils.cpp @@ -67,11 +67,11 @@ Error WindowsUtils::copy_and_rename_pdb(const String &p_dll_path) { { // 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); + HMODULE library_ptr = LoadLibraryExW((LPCWSTR)(p_dll_path.utf16().get_data()), nullptr, 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); + IMAGE_DEBUG_DIRECTORY *dbg_dir = (IMAGE_DEBUG_DIRECTORY *)ImageDirectoryEntryToDataEx(library_ptr, FALSE, IMAGE_DIRECTORY_ENTRY_DEBUG, &dbg_info_size, nullptr); bool has_debug = dbg_dir && dbg_dir->Type == IMAGE_DEBUG_TYPE_CODEVIEW; if (has_debug) { |